浏览代码

fix: bug feat: substorejs different mode

Dew-OF-Aurora 16 小时之前
父节点
当前提交
877831cbe9
共有 9 个文件被更改,包括 199 次插入453 次删除
  1. 10 10
      CLAUDE.md
  2. 127 429
      README.md
  3. 2 2
      build_router_package.sh
  4. 0 0
      config.local.json
  5. 0 0
      config_router.conf
  6. 2 2
      scripts/router_local_http.sh
  7. 1 1
      scripts/router_local_update.sh
  8. 51 3
      substore/operator_template.js
  9. 6 6
      workflow.md

+ 10 - 10
CLAUDE.md

@@ -13,8 +13,8 @@ The repository supports two independent operating modes:
   - Optional git automation: yes, via `runtime-state`
 - Router mode
   - Source: local `cfst`
-  - Python config: `config.router.json`
-  - BusyBox shell config: `router_local.conf`
+  - Python config: `config.local.json`
+  - BusyBox shell config: `config_router.conf`
   - Main output: `cfip_runtime/`
   - Optional git automation: no
 
@@ -42,14 +42,14 @@ python3 scripts/domain_updater.py --config config.server.json
 ### Run local cfst mode once
 
 ```bash
-python3 scripts/domain_updater.py --config config.router.json
+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.router.json --print-output-settings
+python3 scripts/domain_updater.py --config config.local.json --print-output-settings
 ```
 
 ### Run server scheduler entrypoint
@@ -69,8 +69,8 @@ GIT_FORCE_COMMIT=1 bash scripts/run_update_and_commit.sh config.server.json
 ### BusyBox router mode
 
 ```bash
-sh scripts/router_local_update.sh ./router_local.conf
-sh scripts/router_local_http.sh ./router_local.conf
+sh scripts/router_local_update.sh ./config_router.conf
+sh scripts/router_local_http.sh ./config_router.conf
 ./build_router_package.sh
 ```
 
@@ -126,11 +126,11 @@ sudo bash scripts/uninstall_debian.sh --keep-auth-files
 
 There are two router-side entry styles:
 
-- Python local mode via `config.router.json`
+- 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 `router_local.conf`
+- 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
@@ -162,7 +162,7 @@ Main blocks:
 - `output`
 - `notify`
 
-### `config.router.json`
+### `config.local.json`
 
 Main blocks:
 
@@ -174,7 +174,7 @@ Main blocks:
 - `output`
 - `notify`
 
-### `router_local.conf`
+### `config_router.conf`
 
 Main groups:
 

+ 127 - 429
README.md

@@ -1,360 +1,145 @@
 # vmess-domain-rotator
 
-一个用于选择优选目标并写出运行时文件的工具集,当前支持两套独立模式:
+用于选择优选目标并产出运行时文件,支持三种模式:
 
-- 云服务器模式:调用远程 API,选择优选域名,写入 `runtime/`,可自动提交到 `runtime-state`
-- 本地路由器模式:调用本地 `cfst`,选择优选 IP,写入 `cfip_runtime/`;HTTP 暴露推荐使用项目目录下的完整 BusyBox `httpd`
+- 服务器模式(API -> 域名,支持自动提交 `runtime-state`)
+- 本地 Python `cfst` 模式(本机 `cfst` -> IP)
+- BusyBox 路由器模式(路由器 `sh` + `cfst` -> IP + HTTP 暴露)
 
-这两套模式已经彻底拆开:
-
-- 服务器模式使用 [`config.server.json`](./config.server.json)
-- 本地 `cfst` 模式使用 [`config.router.json`](./config.router.json)
-- BusyBox 路由器脚本使用 [`router_local.conf`](./router_local.conf)
-
-旧的 `config.json` / `config.example.json` 已废弃,不再使用。
+---
 
 ## 1. 目录与入口
 
 核心脚本:
 
-- `scripts/domain_updater.py`
-  统一的 Python 主入口,支持 `api` 和 `cfst_local` 两种 `source.type`
-- `scripts/run_update_and_commit.sh`
-  服务器模式入口,执行 updater 并按配置同步运行时文件到 `runtime-state`
-- `scripts/install_debian.sh`
-  Debian/Ubuntu 一键安装 systemd service + timer
-- `scripts/uninstall_debian.sh`
-  卸载 systemd service + timer
-- `scripts/router_local_update.sh`
-  BusyBox `sh` 路由器入口,执行 `cfst` 并写出 `cfip_runtime`
-- `scripts/router_local_http.sh`
-  BusyBox `sh` 路由器 HTTP 暴露入口,优先使用 BusyBox `httpd` 暴露运行时文件
-- `build_router_package.sh`
-  路由器部署包打包脚本,按 `/jffs/vmess` 推荐目录结构生成 `vmess.tar.gz`
+- `scripts/domain_updater.py`:Python 主入口(`api` / `cfst_local`)
+- `scripts/run_update_and_commit.sh`:服务器模式调度入口(含 git 同步)
+- `scripts/install_debian.sh` / `scripts/uninstall_debian.sh`:systemd 安装/卸载
+- `scripts/router_local_update.sh`:路由器更新入口(BusyBox `sh`)
+- `scripts/router_local_http.sh`:路由器 HTTP 入口(BusyBox `httpd`)
+- `build_router_package.sh`:路由器部署包打包脚本(生成 `vmess.tar.gz`)
 
 配置文件:
 
-- `config.server.json`
-  服务器模式配置,输出默认是:
-  - `runtime/current_domain.txt`
-  - `runtime/current_domain.json`
-  - `runtime/state.json`
-  - `runtime/substore_vars.json`
-- `config.router.json`
-  本地 `cfst` 模式配置,输出默认是:
-  - `cfip_runtime/current_ip.txt`
-  - `cfip_runtime/current_ip.json`
-  - `cfip_runtime/state.json`
-  - `cfip_runtime/substore_vars.json`
-- `router_local.conf`
-  BusyBox 路由器脚本配置,给 `router_local_update.sh` / `router_local_http.sh` 使用
-
-## 2. 共用设计
-
-两套模式共用同一套“输出抽象”:
-
-- 文本值文件:当前选中的值
-- JSON 文件:当前结果
-- state 文件:上次可用值和状态
-- export vars 文件:给外部系统消费
-
-关键点:
-
-- 输出文件名和输出目录都从配置读取,不再在 shell 脚本里硬编码 `runtime/current_domain.txt`
-- `run_update_and_commit.sh` 会先解析配置里的输出路径,再决定同步哪些文件
-- 只有服务器模式默认集成 git commit/push;路由器模式不做 git 操作
-- `domain_updater.py` 会按 `--config` 所在目录解析相对路径
-
-## 3. 模式选择
-
-### 3.1 云服务器模式
-
-适用场景:
-
-- 有 `python3`
-- 有网络访问目标 API
-- 需要 systemd 定时执行
-- 需要自动提交 `runtime-state`
-
-入口:
-
-- 手动执行:`python3 scripts/domain_updater.py --config config.server.json`
-- 定时执行:`bash scripts/run_update_and_commit.sh config.server.json`
-- Debian 安装:`sudo bash scripts/install_debian.sh --config config.server.json`
-
-### 3.2 本地 `cfst` 模式
-
-适用场景:
-
-- 在本机、Mac、Linux 上已有可执行的 `cfst`
-- 希望由 `domain_updater.py` 直接调用本地 `cfst`
-- 不需要 BusyBox 专用脚本
-
-入口:
-
-- `python3 scripts/domain_updater.py --config config.router.json`
-
-### 3.3 BusyBox 路由器模式
-
-适用场景:
+- `config.server.json`:服务器模式
+- `config.local.json`:本地 Python `cfst` 模式
+- `config_router.conf`:BusyBox 路由器模式
 
-- 路由器没有 Python
-- 只有 BusyBox `sh` / `awk` / `sed` 等基础工具即可执行更新脚本
-- HTTP 暴露推荐使用项目目录下的完整 BusyBox,例如 `BUSYBOX_BIN="./busybox_armv7l"`,脚本会调用它的 `httpd` applet;系统自带精简 `nc` 不够
-
-入口:
-
-- 更新结果:`sh scripts/router_local_update.sh ./router_local.conf`
-- 暴露 HTTP:`sh scripts/router_local_http.sh ./router_local.conf`
-
-## 4. 云服务器模式部署
-
-### 4.1 前置条件
+> 旧 `config.json` / `config.example.json` 已废弃。
 
-- Debian/Ubuntu
-- `git clone` 后在仓库目录执行
-- 远程仓库已配置,例如 `origin`
-- 运行用户对仓库有读写权限
-- 机器上可访问 `config.server.json` 中的 API
+---
 
-### 4.2 核心配置文件
+## 2. 输出文件约定
 
-服务器模式固定使用 [`config.server.json`](./config.server.json)。
+三种模式都围绕同类输出:
 
-它当前默认行为:
+- 文本值文件:当前选中值
+- JSON 文件:当前状态与候选信息
+- state 文件:上次可用值(fallback)
+- export vars 文件:给外部系统消费
 
-- `source.type = "api"`
-- 通过 `api.url` 拉取候选结果
-- 通过 `parser`、`record_mapping`、`record_filter`、`scoring` 选择目标
-- 写出到 `runtime/`
+默认目录:
 
-你通常只需要修改:
+- 服务器模式:`runtime/`
+- 路由器相关模式:`cfip_runtime/`
 
-- `api.url`
-- `api.headers`
-- `parser`
-- `record_mapping`
-- `record_filter`
-- `scoring`
-- `healthcheck`
+---
 
-### 4.3 本地手动跑通
+## 3. 服务器模式(API)
 
-先做语法检查:
+### 3.1 适用场景
 
-```bash
-env PYTHONPYCACHEPREFIX=/tmp/pycache python3 -m py_compile scripts/domain_updater.py
-```
+- 有 `python3`
+- 可访问目标 API
+- 需要自动提交到 `runtime-state`
 
-执行一次:
+### 3.2 快速执行
 
 ```bash
 python3 scripts/domain_updater.py --config config.server.json
-```
-
-查看结果:
-
-```bash
-cat runtime/current_domain.txt
-cat runtime/current_domain.json
-cat runtime/state.json
-cat runtime/substore_vars.json
-```
-
-如果你只想看脚本解析出的输出路径:
-
-```bash
-python3 scripts/domain_updater.py --config config.server.json --print-output-settings
-```
-
-### 4.4 自动提交到 runtime-state
-
-服务器模式提交脚本:
-
-```bash
 bash scripts/run_update_and_commit.sh config.server.json
 ```
 
-它会执行:
-
-1. 调用 `domain_updater.py`
-2. 从配置解析出运行时文件路径
-3. 比较本次“选中值”和 `runtime-state` 分支上次记录
-4. 值未变化时跳过 commit/push
-5. 值变化时同步配置定义的输出文件并提交
-
 强制提交:
 
 ```bash
 bash scripts/run_update_and_commit.sh --force-commit config.server.json
-```
-
-或:
-
-```bash
+# 或
 GIT_FORCE_COMMIT=1 bash scripts/run_update_and_commit.sh config.server.json
 ```
 
-### 4.5 Debian 一键安装
-
-推荐命令:
-
-```bash
-sudo bash scripts/install_debian.sh --config config.server.json
-```
-
-默认行为:
-
-- 使用当前 `sudo` 前的用户作为 service 用户
-- 定时周期 `1h`
-- 自动 push 开启
-- 目标分支 `runtime-state`
-
-常用参数:
+### 3.3 维护命令
 
 ```bash
-sudo bash scripts/install_debian.sh \
-  --config config.server.json \
-  --interval 10min \
-  --git-push 1 \
-  --git-push-remote origin
-```
-
-如果用 token:
-
-```bash
-sudo bash scripts/install_debian.sh \
-  --config config.server.json \
-  --git-http-username <your-user> \
-  --git-http-token-file /root/.config/vmess-token \
-  --git-use-credential-store 1
-```
+# 语法检查
+env PYTHONPYCACHEPREFIX=/tmp/pycache python3 -m py_compile scripts/domain_updater.py
+bash -n scripts/run_update_and_commit.sh
 
-### 4.6 安装后验证
+# 查看解析后的输出路径
+python3 scripts/domain_updater.py --config config.server.json --print-output-settings
 
-```bash
+# systemd(Debian/Ubuntu)
+sudo bash scripts/install_debian.sh --config config.server.json
 sudo systemctl status vmess-domain-rotator.timer
 sudo systemctl status vmess-domain-rotator.service
 sudo systemctl start vmess-domain-rotator.service
 sudo journalctl -u vmess-domain-rotator.service -n 120 --no-pager
-```
-
-成功时通常应看到:
-
-- updater 输出 JSON
-- `committed output changes on runtime-state`
-- `pushed to origin/runtime-state`
 
-### 4.7 卸载
-
-```bash
+# 卸载
 sudo bash scripts/uninstall_debian.sh
 ```
 
-保留认证文件:
-
-```bash
-sudo bash scripts/uninstall_debian.sh --keep-auth-files
-```
+---
 
-## 5. 本地 `cfst` 模式部署
+## 4. 本地 Python `cfst` 模式
 
-### 5.1 适用环境
+### 4.1 适用场景
 
 - 有 `python3`
-- 有可执行的 `cfst`
-- 不依赖 BusyBox
-- 希望继续复用 `domain_updater.py` 的统一输出逻辑
+- 本机可执行 `cfst`
+- 不需要 BusyBox 专用脚本
 
-### 5.2 核心配置文件
+### 4.2 快速执行
 
-本模式使用 [`config.router.json`](./config.router.json)。
+```bash
+python3 scripts/domain_updater.py --config config.local.json
+```
 
-它当前默认指向:
+默认配置当前指向:
 
-- `./cfst_linux_armv5/cfst`
-- 结果文件 `./cfst_linux_armv5/result.csv`
+- `cfst_local.work_dir = ./cfst_linux_armv5`
+- `cfst_local.binary = ./cfst`
 - 输出目录 `./cfip_runtime`
 
-你通常只需要修改:
-
-- `cfst_local.work_dir`
-- `cfst_local.binary`
-- `cfst_local.run_args`
-- `cfst_local.result_file`
-- `cfst_local.columns`
-- `output.*`
-
-### 5.3 手动执行
-
-```bash
-python3 scripts/domain_updater.py --config config.router.json
-```
-
-查看结果:
+### 4.3 维护命令
 
 ```bash
+# 输出文件检查
 cat cfip_runtime/current_ip.txt
 cat cfip_runtime/current_ip.json
 cat cfip_runtime/state.json
 cat cfip_runtime/substore_vars.json
-```
-
-查看解析出的输出路径:
 
-```bash
-python3 scripts/domain_updater.py --config config.router.json --print-output-settings
+# 仅检查输出路径解析
+python3 scripts/domain_updater.py --config config.local.json --print-output-settings
 ```
 
-### 5.4 配置说明
-
-`cfst_local` 关键项:
-
-- `work_dir`
-  `cfst` 工作目录
-- `binary`
-  `cfst` 可执行文件路径
-- `run_args`
-  执行参数数组,例如 `-f ip.txt -o result.csv -p 10`
-- `result_file`
-  `cfst` 输出结果文件
-- `skip_run`
-  为 `true` 时不执行 `cfst`,只解析现有结果文件
-- `columns`
-  CSV 列映射,默认按:
-  - `0` IP
-  - `1` 已发送
-  - `2` 已接收
-  - `3` 丢包率
-  - `4` 平均延迟
-  - `5` 下载速度
-  - `6` 地区
-
-## 6. BusyBox 路由器模式部署
-
-### 6.1 适用环境
-
-- 路由器没有 Python
-- 已准备对应架构的 `cfst`
-- 已准备完整 BusyBox,例如本项目默认使用 `busybox_armv7l`
-- HTTP 暴露使用完整 BusyBox 的 `httpd` applet,不使用 ASUS 固件自带 `/usr/sbin/httpd`
-
-架构以目标路由器实际输出为准:
+---
 
-```bash
-uname -m
-```
+## 5. BusyBox 路由器模式
 
-`cfst` 和完整 BusyBox 都必须匹配路由器架构。ASUS RT-AC68U 示例里,完整 BusyBox 文件名使用 `busybox_armv7l`。
+### 5.1 适用场景
 
-### 6.2 路由器目录建议
+- 路由器没有 Python
+- 有匹配架构的 `cfst`
+- 有完整 BusyBox(需要 `httpd` applet)
 
-示例(ASUS RT-AC68U 推荐放在 `/jffs/vmess`):
+### 5.2 推荐目录(ASUS RT-AC68U 示例)
 
 ```text
 /jffs/vmess/
 ├── busybox_armv7l
-├── router_local.conf
+├── config_router.conf
 ├── scripts/
 │   ├── router_local_update.sh
 │   └── router_local_http.sh
@@ -364,126 +149,33 @@ uname -m
     └── result.csv
 ```
 
-其中:
-
-- `router_local.conf` 指定 `CFST_WORK_DIR`、输出目录、HTTP 端口、完整 BusyBox 路径等
-- `busybox_armv7l` 是你下载的完整 BusyBox,HTTP 服务脚本会优先调用它,而不是系统自带 BusyBox/ASUS `httpd`
-- `cfst/` 放路由器架构对应的 `cfst`
-
-### 6.3 配置 router_local.conf
-
-当前仓库里的 [`router_local.conf`](./router_local.conf) 就是 BusyBox 配置文件。
-
-最重要的字段:
-
-- `CFST_WORK_DIR`
-  `cfst` 所在目录
-- `CFST_BIN`
-  `cfst` 可执行文件名,一般是 `./cfst`
-- `CFST_IP_FILE`
-  输入 IP 列表
-- `CFST_RESULT_FILE`
-  `cfst` 结果文件
-- `RUNTIME_DIR`
-  运行时输出目录,默认 `./cfip_runtime`
-- `VALUE_TEXT_FILE`
-  当前值文本文件,默认 `current_ip.txt`
-- `VALUE_JSON_FILE`
-  当前值 JSON 文件,默认 `current_ip.json`
-- `BUSYBOX_BIN`
-  完整 BusyBox 二进制路径,默认 `./busybox_armv7l`;HTTP 脚本会优先调用它的 `httpd` applet,避免误用 ASUS `/usr/sbin/httpd`
-- `HTTP_PORT`
-  局域网 HTTP 监听端口,默认 `8080`
-
-### 6.4 手动更新一次
-
-```bash
-sh scripts/router_local_update.sh ./router_local.conf
-```
-
-成功时会输出类似:
-
-```text
-[router-local] update_at=2026-05-08T08:00:00Z selected_ip=x.x.x.x candidates=10
-[router-local] candidate_1 ip=x.x.x.x sent=... recv=... loss=... latency=... speed=... region=...
-```
-
-日志行为说明:
-
-- 终端手动执行(stdout 是 TTY)时,会显示 `cfst` 原始输出,便于排错
-- 重定向到日志或后台执行时(例如 `>> /tmp/router_local_update.log 2>&1`),默认静默 `cfst` 原始输出,仅保留脚本摘要日志
-- 可通过 `CFST_SHOW_LOG_ON_TTY`、`CFST_QUIET_LOG`、`CFST_DEBUG` 组合控制日志详细程度
-
-生成文件默认在:
-
-- `cfip_runtime/current_ip.txt`
-- `cfip_runtime/index.html`(复制当前 IP 文本,供 `/` 访问)
-- `cfip_runtime/current_ip.json`
-- `cfip_runtime/state.json`
-- `cfip_runtime/substore_vars.json`
-
-### 6.5 暴露到局域网
-
-```bash
-sh scripts/router_local_http.sh ./router_local.conf
-```
-
-默认会监听:
-
-```text
-0.0.0.0:8080
-```
-
-这个脚本不再使用系统自带的精简 `nc` 或 ASUS `/usr/sbin/httpd`。它会读取 `router_local.conf` 里的 `BUSYBOX_BIN`,优先调用项目目录下完整 BusyBox 的 `httpd` applet:
-
-```bash
-./busybox_armv7l httpd -f -p 8080 -h ./cfip_runtime
-```
-
-如果你的 BusyBox applet 列表里没有 `httpd`,当前脚本会明确报错。ASUS 固件自带的 `/usr/sbin/httpd` 通常是路由器管理后台服务,不是 BusyBox 静态文件服务器;即使支持 `-p` 端口参数,也不代表支持 `-h ./cfip_runtime` 这类目录发布。
+### 5.3 关键配置(`config_router.conf`)
 
-可访问路径:
+- `CFST_WORK_DIR` / `CFST_BIN` / `CFST_RESULT_FILE`
+- `CFST_SPEED_LIMIT`(建议设置,如 `5`)
+- `RUNTIME_DIR` / `VALUE_TEXT_FILE` / `VALUE_JSON_FILE`
+- `BUSYBOX_BIN`(如 `./busybox_armv7l`)
+- `HTTP_PORT`(默认 `8080`)
 
-- `/`(由 `index.html` 返回当前 IP 文本)
-- `/current_ip.txt`
-- `/current_ip.json`
-- `/state.json`
-- `/substore_vars.json`
-
-例如局域网内访问:
+### 5.4 手动执行
 
 ```bash
-curl http://192.168.50.1:8080/current_ip.json
-curl http://192.168.50.1:8080/current_ip.txt
-```
-
-### 6.6 ASUS RT-AC68U services-start 自启动
-
-如果路由器使用 `/jffs/scripts/services-start` 统一启动任务,可以用 `cru` 注册定时更新和 watchdog。这个小节是 ASUS RT-AC68U / KoolShare 风格固件专用示例,假设项目放在 `/jffs/vmess`,完整 BusyBox 位于 `/jffs/vmess/busybox_armv7l`。
-
-部署前建议确认:
-
-```sh
-chmod +x /jffs/vmess/busybox_armv7l
-chmod +x /jffs/scripts/services-start
-/jffs/vmess/busybox_armv7l --list | grep '^httpd$'
+cd /jffs/vmess
+sh scripts/router_local_update.sh ./config_router.conf
+sh scripts/router_local_http.sh ./config_router.conf
 ```
 
-`router_local.conf` 至少需要包含
+说明:
 
-```sh
-BUSYBOX_BIN="./busybox_armv7l"
-HTTP_PORT="8080"
-```
+- 手动终端执行 update 时会显示 `cfst` 原始输出(便于排错)
+- 重定向日志/后台执行时默认静默 `cfst`,只保留 `[router-local]` 摘要
+- HTTP 脚本只使用 `BUSYBOX_BIN` 指向的 BusyBox `httpd`,不使用 ASUS 系统 `httpd`
 
-把 `/jffs/scripts/services-start` 写成:
+### 5.5 自启动(`/jffs/scripts/services-start`)
 
 ```sh
-#!/bin/sh
-/koolshare/bin/ks-services-start.sh
-
 VMESS_DIR="/jffs/vmess"
-CONFIG="./router_local.conf"
+CONFIG="./config_router.conf"
 UPDATE_LOG="/tmp/router_local_update.log"
 HTTP_LOG="/tmp/router_http.log"
 HTTP_PROCESS_PATTERN='[r]outer_local_http.sh'
@@ -492,16 +184,14 @@ start_vmess_http() {
   if ps | grep -q "$HTTP_PROCESS_PATTERN"; then
     return 0
   fi
-
   cd "$VMESS_DIR" || exit 1
   nohup sh scripts/router_local_http.sh "$CONFIG" >> "$HTTP_LOG" 2>&1 &
 }
 
 cd "$VMESS_DIR" || exit 1
 
-# 开机后先等待系统和网络稳定,避免 cfst 启动太早拿不到结果。
+# 避免开机过早导致 result.csv 为空
 sleep 60
-
 i=1
 while [ "$i" -le 5 ]; do
   if sh scripts/router_local_update.sh "$CONFIG" >> "$UPDATE_LOG" 2>&1; then
@@ -521,49 +211,49 @@ cru d vmess_watchdog
 cru a vmess_watchdog "*/5 * * * * if ! ps | grep -q '$HTTP_PROCESS_PATTERN'; then cd $VMESS_DIR && nohup sh scripts/router_local_http.sh $CONFIG >> $HTTP_LOG 2>&1 & fi"
 ```
 
-这里用 `[r]outer_local_http.sh` 作为 `grep` 关键字,是为了避免 watchdog 的 shell 命令在 `ps` 里匹配到自己。`router_local_http.sh` 会保留 wrapper 进程并等待子进程里的完整 BusyBox `httpd`,所以 watchdog 用这个关键字可以正常判断服务是否仍在运行。示例里先 `cru d` 再 `cru a`,避免 `services-start` 被重复触发后留下旧的定时任务定义;启动 HTTP 前也会先检查进程,避免重复监听同一个端口。开机首次更新前加入 `sleep + retry`,是为了避免网络尚未就绪时出现 `result file missing or empty`。
+### 5.6 维护命令
 
-如果你的固件没有 KoolShare,删除 `/koolshare/bin/ks-services-start.sh` 那一行即可;如果 Web UI 里有“启用 JFFS 自定义脚本”的选项,需要先开启,否则 `/jffs/scripts/services-start` 不会在开机时执行。
+```bash
+# 基础检查
+cd /jffs/vmess
+chmod +x busybox_armv7l
+./busybox_armv7l --list | grep '^httpd$'
 
-### 6.7 定时执行
+# 更新与结果检查
+sh scripts/router_local_update.sh ./config_router.conf
+cat cfip_runtime/current_ip.txt
+cat cfip_runtime/current_ip.json
+cat cfip_runtime/state.json
 
-可以用 BusyBox `crond` 定时更新,例如每 15 分钟执行一次:
+# HTTP 与端口检查
+sh scripts/router_local_http.sh ./config_router.conf
+ps | grep router_local_http.sh
+netstat -lntp | grep ':8080'
+curl http://127.0.0.1:8080/current_ip.txt
 
-```cron
-*/15 * * * * cd /jffs/vmess && sh scripts/router_local_update.sh ./router_local.conf >> /tmp/router_local_update.log 2>&1
+# 常见端口冲突处理(Address already in use)
+# 1) 改 config_router.conf 的 HTTP_PORT
+# 2) 或停止旧进程后重启
 ```
 
-### 6.8 手动常驻 HTTP
+---
 
-HTTP 服务如果需要手动后台启动:
+## 6. Sub-Store 脚本模式说明
 
-```bash
-cd /jffs/vmess
-nohup sh scripts/router_local_http.sh ./router_local.conf >/tmp/router_http.log 2>&1 &
-```
+`substore/operator_template.js` 支持:
 
-## 7. 常用运维命令
+- `VALUE_SOURCE_MODE = "default"`:路由器优先,低速时切服务器
+- `VALUE_SOURCE_MODE = "server_only"`:只用服务器
+- `VALUE_SOURCE_MODE = "router_only"`:只用路由器
 
-服务器模式:
+速度阈值使用 `MB/s`
 
-```bash
-sudo systemctl status vmess-domain-rotator.timer
-sudo systemctl status vmess-domain-rotator.service
-sudo systemctl start vmess-domain-rotator.service
-sudo journalctl -u vmess-domain-rotator.service -f
-git log runtime-state --oneline -n 20
-```
+- `ROUTER_MIN_SPEED_MB_PER_S = 5`
+- `download_speed` 纯数字按 `MB/s` 解释
 
-路由器模式:
+---
 
-```bash
-sh scripts/router_local_update.sh ./router_local.conf
-sh scripts/router_local_http.sh ./router_local.conf
-cat cfip_runtime/current_ip.txt
-cat cfip_runtime/current_ip.json
-```
-
-路由器包打包(按 `/jffs/vmess` 目录结构):
+## 7. 路由器部署包打包
 
 ```bash
 ./build_router_package.sh
@@ -578,11 +268,19 @@ OUT_TAR="/path/to/vmess.tar.gz" \
 ./build_router_package.sh
 ```
 
+打包内容默认为:
+
+- `vmess/busybox_armv7l`
+- `vmess/config_router.conf`
+- `vmess/scripts/router_local_update.sh`
+- `vmess/scripts/router_local_http.sh`
+- `vmess/cfst/*`
+
+---
+
 ## 8. 注意事项
 
-1. 服务器模式和路由器模式的配置不要混用。
-2. `run_update_and_commit.sh` 设计目标是服务器模式;路由器模式默认不走 git 提交。
-3. 服务器模式下,service 用户和 git 凭证用户必须一致,否则会出现 `terminal prompts disabled`。
-4. `credential.helper store` 是明文存储,只适合可控服务器。
-5. BusyBox 路由器模式下,`router_local_update.sh` 只依赖常见基础 applet;HTTP 暴露会优先调用 `BUSYBOX_BIN` 指向的完整 BusyBox `httpd`,不要依赖 ASUS `/usr/sbin/httpd` 或系统精简 `nc`。
-6. `state.json` 需要持久化,否则 fallback 不可用。
+1. 三种模式的配置不要混用。
+2. 只有服务器模式默认做 git 自动提交。
+3. `state.json` 需持久化,否则 fallback 不可用。
+4. 路由器模式请优先使用完整 BusyBox `httpd`,不要依赖系统精简 `httpd` / `nc`。

+ 2 - 2
build_router_package.sh

@@ -8,7 +8,7 @@ PKG_ROOT_NAME=${PKG_ROOT_NAME:-vmess}
 OUT_TAR=${OUT_TAR:-"$APP_DIR/vmess.tar.gz"}
 TMP_DIR=${TMP_DIR:-"/tmp/vmess_pkg.$$"}
 
-ROUTER_CONF=${ROUTER_CONF:-"$APP_DIR/router_local.conf"}
+ROUTER_CONF=${ROUTER_CONF:-"$APP_DIR/config_router.conf"}
 HTTP_SCRIPT=${HTTP_SCRIPT:-"$APP_DIR/scripts/router_local_http.sh"}
 UPDATE_SCRIPT=${UPDATE_SCRIPT:-"$APP_DIR/scripts/router_local_update.sh"}
 
@@ -106,7 +106,7 @@ fi
 
 mkdir -p "$TMP_DIR/$PKG_ROOT_NAME/scripts" "$TMP_DIR/$PKG_ROOT_NAME/cfst"
 
-cp "$ROUTER_CONF" "$TMP_DIR/$PKG_ROOT_NAME/router_local.conf"
+cp "$ROUTER_CONF" "$TMP_DIR/$PKG_ROOT_NAME/config_router.conf"
 cp "$HTTP_SCRIPT" "$TMP_DIR/$PKG_ROOT_NAME/scripts/router_local_http.sh"
 cp "$UPDATE_SCRIPT" "$TMP_DIR/$PKG_ROOT_NAME/scripts/router_local_update.sh"
 cp "$BUSYBOX_SRC" "$TMP_DIR/$PKG_ROOT_NAME/busybox_armv7l"

+ 0 - 0
config.router.json → config.local.json


+ 0 - 0
router_local.conf → config_router.conf


+ 2 - 2
scripts/router_local_http.sh

@@ -3,7 +3,7 @@ set -eu
 
 SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
 APP_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
-CONFIG_PATH=${1:-"$APP_DIR/router_local.conf"}
+CONFIG_PATH=${1:-"$APP_DIR/config_router.conf"}
 CONFIG_DIR=$(CDPATH= cd -- "$(dirname -- "$CONFIG_PATH")" && pwd)
 
 if [ ! -r "$CONFIG_PATH" ]; then
@@ -133,7 +133,7 @@ start_httpd() {
 
   print_busybox_debug
   echo "[router-http] usable BusyBox httpd applet not found" >&2
-  echo "[router-http] set BUSYBOX_BIN in router_local.conf to your full BusyBox binary, for example: BUSYBOX_BIN=\"./busybox_armv7l\"" >&2
+  echo "[router-http] set BUSYBOX_BIN in config_router.conf to your full BusyBox binary, for example: BUSYBOX_BIN=\"./busybox_armv7l\"" >&2
   exit 1
 }
 

+ 1 - 1
scripts/router_local_update.sh

@@ -3,7 +3,7 @@ set -eu
 
 SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
 APP_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
-CONFIG_PATH=${1:-"$APP_DIR/router_local.conf"}
+CONFIG_PATH=${1:-"$APP_DIR/config_router.conf"}
 
 if [ ! -r "$CONFIG_PATH" ]; then
   echo "[router-local] config not found: $CONFIG_PATH" >&2

+ 51 - 3
substore/operator_template.js

@@ -7,6 +7,7 @@
 const ROUTER_VALUE_JSON_URL = "http://192.168.50.1:8080/current_ip.json";
 const SERVER_VALUE_JSON_URL = "https://git.dewofaurora.de/aurora/vmess-domain-rotator/raw/runtime-state/runtime/current_domain.json";
 const ROUTER_MIN_SPEED_MB_PER_S = 5;
+const VALUE_SOURCE_MODE = "default"; // default | server_only | router_only
 
 const NODE_NAME_REGEX = /(argo|cf|vm|优选)/i;
 const CACHE_KEY = "vmess-domain-rotator:current";
@@ -157,6 +158,53 @@ async function selectValueRouterFirst() {
   return { value: telemetry.router.value, chosenSource: "router", reason: "router_preferred", telemetry };
 }
 
+async function selectValueByMode() {
+  if (VALUE_SOURCE_MODE === "default") {
+    return selectValueRouterFirst();
+  }
+
+  if (VALUE_SOURCE_MODE === "server_only") {
+    const serverObj = await fetchRuntimeJson(SERVER_VALUE_JSON_URL, "server");
+    const serverValue = extractValue(serverObj, "server");
+    return {
+      value: serverValue,
+      chosenSource: "server",
+      reason: "server_only_mode",
+      telemetry: {
+        router: { ok: false, value: "", status: "", sourceType: "", error: "skipped_by_mode", speedRaw: "", speedMBps: null, speedParse: "missing", speedUnit: "" },
+        server: { ok: true, value: serverValue, status: String(serverObj?.status || ""), sourceType: String(serverObj?.source_type || ""), error: "" }
+      }
+    };
+  }
+
+  if (VALUE_SOURCE_MODE === "router_only") {
+    const routerObj = await fetchRuntimeJson(ROUTER_VALUE_JSON_URL, "router");
+    const routerValue = extractValue(routerObj, "router");
+    const speed = parseRouterSpeedMBps(routerObj);
+    return {
+      value: routerValue,
+      chosenSource: "router",
+      reason: "router_only_mode",
+      telemetry: {
+        router: {
+          ok: true,
+          value: routerValue,
+          status: String(routerObj?.status || ""),
+          sourceType: String(routerObj?.source_type || ""),
+          error: "",
+          speedRaw: speed.raw,
+          speedMBps: speed.mbPerSec,
+          speedParse: speed.status,
+          speedUnit: speed.unit
+        },
+        server: { ok: false, value: "", status: "", sourceType: "", error: "skipped_by_mode" }
+      }
+    };
+  }
+
+  throw new Error(`invalid VALUE_SOURCE_MODE=${VALUE_SOURCE_MODE}, expected: default | server_only | router_only`);
+}
+
 async function operator(proxies = [], targetPlatform, context) {
   const cache = scriptResourceCache;
   let value = cache.get(CACHE_KEY);
@@ -164,7 +212,7 @@ async function operator(proxies = [], targetPlatform, context) {
 
   if (!value) {
     try {
-      decision = await selectValueRouterFirst();
+      decision = await selectValueByMode();
       value = decision.value;
       cache.set(CACHE_KEY, value, CACHE_TTL_MS);
     } catch (e) {
@@ -187,10 +235,10 @@ async function operator(proxies = [], targetPlatform, context) {
   if (decision) {
     const t = decision.telemetry;
     console.log(
-      `[vmess-domain-rotator] 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}`
+      `[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 {
-    console.log(`[vmess-domain-rotator] chosen=cache, value=${value}, updated=${updated}, total=${proxies.length}, target=${targetPlatform}`);
+    console.log(`[vmess-domain-rotator] mode=${VALUE_SOURCE_MODE}, chosen=cache, value=${value}, updated=${updated}, total=${proxies.length}, target=${targetPlatform}`);
   }
 
   return proxies;

+ 6 - 6
workflow.md

@@ -39,11 +39,11 @@ flowchart TD
 
 ```mermaid
 flowchart TD
-    A1[manual run] --> A2[domain_updater.py --config config.router.json]
+    A1[manual run] --> A2[domain_updater.py --config config.local.json]
 
     subgraph U2["domain_updater.py / cfst_local mode"]
       direction TB
-      B1[read config.router.json] --> B2[resolve output paths]
+      B1[read config.local.json] --> B2[resolve output paths]
       B2 --> B3[run local cfst]
       B3 --> B4[parse result.csv]
       B4 --> B5[optional filter / optional healthcheck]
@@ -59,12 +59,12 @@ flowchart TD
 
 ```mermaid
 flowchart TD
-    A1[crond / manual run] --> A2[router_local_update.sh router_local.conf]
-    A3[background service / manual run] --> A4[router_local_http.sh router_local.conf]
+    A1[crond / manual run] --> A2[router_local_update.sh config_router.conf]
+    A3[background service / manual run] --> A4[router_local_http.sh config_router.conf]
 
     subgraph R1["router_local_update.sh"]
       direction TB
-      B1[read router_local.conf] --> B2[run cfst]
+      B1[read config_router.conf] --> B2[run cfst]
       B2 --> B3[read result.csv]
       B3 --> B4[pick best ip]
       B4 --> B5[write cfip_runtime/current_ip.txt]
@@ -75,7 +75,7 @@ flowchart TD
 
     subgraph R2["router_local_http.sh"]
       direction TB
-      C1[read router_local.conf] --> C2[start configured BusyBox httpd]
+      C1[read config_router.conf] --> C2[start configured BusyBox httpd]
       C2 --> C3[serve current_ip.txt]
       C2 --> C4[serve current_ip.json]
       C2 --> C5[serve state.json]