Parcourir la source

feature: add domain overwrite

Dew-OF-Aurora il y a 1 semaine
Parent
commit
647403ccfe
3 fichiers modifiés avec 115 ajouts et 153 suppressions
  1. 61 152
      CLAUDE.md
  2. 17 0
      README.md
  3. 37 1
      substore/operator_template.js

+ 61 - 152
CLAUDE.md

@@ -1,28 +1,40 @@
 # CLAUDE.md
 
-This file provides guidance to Claude Code when working with this repository.
+## What This Repo Does
 
-## Project Overview
+自动选优 Cloudflare 优选域名/IP,写入运行时文件供 Sub-Store 等下游消费。
 
-The repository supports two independent operating modes:
+两种独立运行模式:
 
-- Server mode
-  - Source: remote API
-  - Main config: `config.server.json`
-  - Main output: `runtime/`
-  - Optional git automation: yes, via `runtime-state`
-- Router mode
-  - Source: local `cfst`
-  - Python config: `config.local.json`
-  - BusyBox shell config: `config_router.conf`
-  - Main output: `cfip_runtime/`
-  - Optional git automation: no
+| 模式 | 数据源 | 配置 | 输出目录 | Git 自动化 |
+|------|--------|------|----------|-----------|
+| Server | 远程 API | `config.server.json` | `runtime/` | `runtime-state` 分支 |
+| Router (Python) | 本地 `cfst` | `config.local.json` | `cfip_runtime/` | 无 |
+| Router (BusyBox) | 本地 `cfst` | `config_router.conf` | 由配置决定 | 无 |
 
-The old `config.json` and `config.example.json` are deprecated and should not be reintroduced.
+## File Map
+
+```
+scripts/
+├── domain_updater.py          # 核心:两种模式共用的 Python 更新器
+├── run_update_and_commit.sh   # Server 模式调度器 + git 提交逻辑
+├── router_local_update.sh     # BusyBox 路由器:运行 cfst 并写输出
+├── router_local_http.sh       # BusyBox 路由器:httpd 提供运行时文件
+├── install_debian.sh          # systemd timer/service 安装
+└── uninstall_debian.sh        # systemd 卸载
+
+substore/
+└── operator_template.js       # Sub-Store 脚本操作符,消费运行时 JSON
+
+build_router_package.sh        # 打包路由器部署 tarball → vmess.tar.gz
+config.server.json             # Server 模式完整配置
+config.local.json              # Python 本地 cfst 模式配置
+config_router.conf             # BusyBox shell 模式配置
+```
 
 ## Common Commands
 
-### Validate scripts
+### 语法校验
 
 ```bash
 env PYTHONPYCACHEPREFIX=/tmp/pycache python3 -m py_compile scripts/domain_updater.py
@@ -33,168 +45,65 @@ sh -n scripts/router_local_update.sh
 sh -n scripts/router_local_http.sh
 ```
 
-### Run server mode once
+### 运行
 
 ```bash
+# Server 模式单次运行
 python3 scripts/domain_updater.py --config config.server.json
-```
-
-### Run local cfst mode once
 
-```bash
+# 本地 cfst 模式单次运行
 python3 scripts/domain_updater.py --config config.local.json
-```
-
-### Print resolved output paths
 
-```bash
+# 查看解析后的输出路径(不执行更新)
 python3 scripts/domain_updater.py --config config.server.json --print-output-settings
-python3 scripts/domain_updater.py --config config.local.json --print-output-settings
-```
-
-### Run server scheduler entrypoint
 
-```bash
+# Server 调度器入口
 bash scripts/run_update_and_commit.sh config.server.json
-```
-
-### Force commit to runtime-state once
 
-```bash
+# 强制提交
 bash scripts/run_update_and_commit.sh --force-commit config.server.json
-# or
-GIT_FORCE_COMMIT=1 bash scripts/run_update_and_commit.sh config.server.json
-```
 
-### BusyBox router mode
-
-```bash
+# BusyBox 路由器
 sh scripts/router_local_update.sh ./config_router.conf
 sh scripts/router_local_http.sh ./config_router.conf
+
+# 打包路由器部署包
 ./build_router_package.sh
 ```
 
-### Debian systemd install/uninstall
+### systemd 安装/卸载
 
 ```bash
 sudo bash scripts/install_debian.sh --config config.server.json
-sudo bash scripts/install_debian.sh --config config.server.json --offpeak-interval 30min --peak-interval 10min
-sudo bash scripts/install_debian.sh --config config.server.json --peak-start 19 --peak-end 24 --peak-tz Asia/Shanghai
-sudo bash scripts/install_debian.sh --config config.server.json --git-push 0
 sudo bash scripts/uninstall_debian.sh
-sudo bash scripts/uninstall_debian.sh --keep-auth-files
 ```
 
-## Testing and Verification
-
-- There is no dedicated `tests/` directory yet.
-- Primary verification is syntax checks plus manual script runs.
-- `--print-output-settings` is the quickest way to verify output path resolution without performing a full update.
-
-## Architecture
-
-### 1) Unified Python updater
-
-`scripts/domain_updater.py` is the shared core:
-
-- Supports `source.type = "api"` and `source.type = "cfst_local"`
-- Resolves output file paths from config instead of hardcoded runtime paths
-- Writes four runtime artifacts:
-  - selected value text file
-  - selected value JSON file
-  - state file
-  - export vars file
-- Builds mode-aligned `top_candidates` with unified semantic field names
-- Uses sparse JSON output: only writes fields that have values (no blank placeholder fields)
-- Supports fallback to the last good value from the configured state file
-- On error with fallback, writes `vars_file` with `STATUS: error_use_last_good` so downstream consumers stay updated
-
-### 2) Server mode
-
-`config.server.json` defines:
-
-- API request settings
-- parser / record mapping / record filter
-- scoring behavior
-- output paths under `runtime/`
-
-`scripts/run_update_and_commit.sh`:
-
-- Resolves output paths by calling `domain_updater.py --print-output-settings`
-- Runs the updater and captures its JSON output
-- Detects `error_use_last_good` status and adjusts commit message prefix to `chore(fallback):`
-- Compares the selected value with `runtime-state`
-- Syncs configured repo-local output files into the target worktree
-- Commits and optionally pushes
-
-### 3) Router mode
-
-There are two router-side entry styles:
-
-- Python local mode via `config.local.json`
-  - still uses `domain_updater.py`
-  - executes local `cfst`
-  - parses CSV and writes `cfip_runtime/`
-- BusyBox shell mode via `config_router.conf`
-  - `scripts/router_local_update.sh`
-  - `scripts/router_local_http.sh` prefers the configured full BusyBox binary (`BUSYBOX_BIN`, default `./busybox_armv7l`) and uses its `httpd` applet; ASUS/system `httpd` is not used automatically
-  - intended for routers without Python
-
-### 4) Sub-Store consumer
-
-`substore/operator_template.js`:
-
-- Fetches a JSON runtime endpoint
-- Accepts either `domain` or `ip`
-- Rewrites VMess `server` for matched nodes
-- Uses `scriptResourceCache` with a short TTL
-- Cache key is scoped per `VALUE_SOURCE_MODE` to avoid cross-mode stale cache
-
-## Configuration Model
-
-### `config.server.json`
-
-Main blocks:
-
-- `source`
-- `api`
-- `parser`
-- `record_mapping`
-- `record_filter`
-- `domain_filter`
-- `scoring`
-- `selection`
-- `output`
-- `notify`
-
-### `config.local.json`
-
-Main blocks:
+`install_debian.sh --help` 查看完整参数(peak/offpeak 间隔、时区等)。
 
-- `source`
-- `cfst_local`
-- `domain_filter`
-- `selection`
-- `output`
-- `notify`
+## Testing
 
-### `config_router.conf`
+- 无专用测试框架,以语法校验 + 手动运行为主。
+- `--print-output-settings` 是最快的路径解析验证方式。
+- **本地测试服务器端时,仅运行 `python3 scripts/domain_updater.py --config config.server.json` 进行输出验证,不要运行 `run_update_and_commit.sh` 调度/Git 推送脚本。**
 
-Main groups:
+## Key Constraints
 
-- `CFST_*`
-- `TOP_N`
-- `RUNTIME_DIR`
-- `VALUE_*`
-- `STATE_*`
-- `EXPORT_*`
-- `HTTP_PORT`
+- `config.json` / `config.example.json` 已废弃,不要重新引入。
+- 输出路径由配置驱动,不要硬编码 `runtime/current_domain.txt` 之类的路径。
+- `runtime/` 和 `cfip_runtime/` 在 `main` 分支被 gitignore。
+- 只有 Server 模式向 `runtime-state` 分支提交。
+- `state.json` 用于 fallback(保存上次正常值),两种模式都依赖它。
+- `substore_vars.json` 是给下游消费者的轻量契约,不要把完整运行时数据塞进去。
+- BusyBox shell 脚本需兼容 POSIX sh,不能用 bash 特性。
+- `router_local_http.sh` 使用配置中的 BusyBox 二进制的 httpd applet,不使用系统 httpd。
+- SubStore 脚本的 `$arguments.domain` 参数为最高优先级覆盖,`$arguments.domain_override` 控制开关,不受任何模式和策略影响。
 
-## Operational Notes
+## Where to Look
 
-- Server mode is the only mode intended to update `runtime-state`.
-- Router mode does not use git automation by default.
-- `runtime/` and `cfip_runtime/` are ignored on `main`; runtime artifacts are meant to be ephemeral locally.
-- Persistent `state.json` matters for fallback behavior in both modes (stores last good selected value).
-- `substore_vars.json` is a lightweight downstream contract (`AUTO_*`, `UPDATED_AT`, `STATUS`) for consumers that should not parse full runtime JSON.
-- Avoid reintroducing hardcoded assumptions about `runtime/current_domain.txt`; use config-driven paths instead.
+- 想了解配置字段含义 → 直接读对应的 config JSON/conf 文件,字段名即文档
+- 想了解评分/筛选逻辑 → `scripts/domain_updater.py`
+- 想了解 git 提交/推送流程 → `scripts/run_update_and_commit.sh`
+- 想了解 systemd 部署细节 → `scripts/install_debian.sh`
+- 想了解下游消费方式 → `substore/operator_template.js`
+- 想了解完整工作流图 → `workflow.md`(Mermaid 流程图)
+- 想了解用户文档 → `README.md`

+ 17 - 0
README.md

@@ -496,6 +496,23 @@ STATE_FILE="/jffs/vmess/state.json"
 
 缓存按 `VALUE_SOURCE_MODE` 隔离,切换模式后不会命中旧缓存。
 
+### 6.1 URL 参数强制指定域名
+
+在 Sub-Store 的脚本操作符参数中支持以下参数:
+
+| 参数 | 说明 | 默认值 |
+|------|------|--------|
+| `domain` | 强制使用的域名或 IP | 空(不覆盖) |
+| `domain_override` | 是否启用域名覆盖 | `true` |
+
+- `domain` 拥有**最高优先级**,不受 `VALUE_SOURCE_MODE`、速度阈值、缓存的影响。
+- 设置 `domain_override=false` 可在不删除 `domain` 的情况下临时关闭覆盖,恢复正常逻辑。
+- 支持域名和 IPv4/IPv6 格式。
+
+示例参数:
+- 启用覆盖:`domain=my.override.com`(`domain_override` 默认 true)
+- 临时关闭:`domain=my.override.com` + `domain_override=false`
+
 ---
 
 ## 7. 路由器部署包打包

+ 37 - 1
substore/operator_template.js

@@ -210,10 +210,41 @@ async function selectValueByMode() {
   throw new Error(`invalid VALUE_SOURCE_MODE=${VALUE_SOURCE_MODE}, expected: default | server_only | router_only`);
 }
 
+function extractOverrideDomain() {
+  const args = typeof $arguments !== "undefined" ? $arguments : {};
+
+  // --- domain_override 开关:默认 true,显式设为 false/0/off/no 时禁用域名覆盖 ---
+  const overrideRaw = String(args?.domain_override ?? "").trim().toLowerCase();
+  if (overrideRaw && ["false", "0", "off", "no"].includes(overrideRaw)) {
+    logWithTime(`[vmess-domain-rotator] domain override disabled by domain_override=${overrideRaw}`);
+    return null;
+  }
+
+  // --- domain 参数 ---
+  const raw = String(args?.domain || "").trim().toLowerCase();
+  if (!raw) return null;
+
+  // 基本格式校验:域名或 IPv4/IPv6
+  const domainRe = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
+  const ipv4Re = /^(\d{1,3}\.){3}\d{1,3}$/;
+  const ipv6Re = /^\[?[0-9a-f:]+\]?$/;
+
+  if (!domainRe.test(raw) && !ipv4Re.test(raw) && !ipv6Re.test(raw)) {
+    logWithTime(`[vmess-domain-rotator] invalid override domain format: "${raw}", ignoring`);
+    return null;
+  }
+
+  return raw;
+}
+
 async function operator(proxies = [], targetPlatform, context) {
+  // --- URL parameter override (highest priority) ---
+  const overrideDomain = extractOverrideDomain();
+
   const cache = scriptResourceCache;
-  let value = cache.get(CACHE_KEY);
+  let value = overrideDomain || cache.get(CACHE_KEY);
   let decision = null;
+  let isOverride = !!overrideDomain;
 
   if (!value) {
     try {
@@ -242,9 +273,14 @@ async function operator(proxies = [], targetPlatform, context) {
     logWithTime(
       `[vmess-domain-rotator] mode=${VALUE_SOURCE_MODE}, chosen=${decision.chosenSource}, reason=${decision.reason}, router_status=${t.router.status || "n/a"}, server_status=${t.server.status || "n/a"}, router_speed_raw=${t.router.speedRaw || "n/a"}, router_speed_mb_per_s=${t.router.speedMBps == null ? "n/a" : t.router.speedMBps.toFixed(3)}, speed_parse=${t.router.speedParse}, threshold_mb_per_s=${ROUTER_MIN_SPEED_MB_PER_S}, value=${value}, updated=${updated}, total=${proxies.length}, target=${targetPlatform}`
     );
+  } else if (isOverride) {
+    logWithTime(
+      `[vmess-domain-rotator] mode=override, chosen=url_param, value=${value}, updated=${updated}, total=${proxies.length}, target=${targetPlatform}`
+    );
   } else {
     logWithTime(`[vmess-domain-rotator] mode=${VALUE_SOURCE_MODE}, chosen=cache, value=${value}, updated=${updated}, total=${proxies.length}, target=${targetPlatform}`);
   }
 
   return proxies;
 }
+