name: 脚本开发规范 description: entrypoint.sh 与 rename.js 脚本架构、编码规范、已知 Bug 速查和调试指南
Shell 脚本开发规范
⚠️ 重要提醒:修改 Shell 脚本前,请先阅读
.agents/skills/_shared/BUGS.md中entrypoint.sh分区的所有条目,避免重复踩坑。
修改 entrypoint.sh 中任何核心函数(测速、选路、变量缓存等)之前,必须:
- 先写测试,确认测试能暴露待修问题(红灯)
- 完成代码修改后运行测试,全部通过才算完成(绿灯)
- 禁止跳过测试直接提交代码
脚本文件概览
| 脚本 | 职责 |
|---|---|
scripts/entrypoint.sh |
核心入口脚本,16段 §N 架构,系统启动流水线 |
scripts/check_ip_type.sh |
IP 质量体检 (ASN/流媒体/风控检测) |
scripts/show-config.sh |
展示生成的配置与订阅链接 |
scripts/geo_update.sh |
GeoIP/Geosite 数据库更新 |
scripts/stop-supervisor.sh |
Supervisord 优雅停止 |
entrypoint.sh 架构概览
16段 §N 分层架构,分层原则与段落含义见 .agents/skills/project-overview/SKILL.md。
修改脚本时需了解的关键函数:
| 函数 | 所在段 | 关键点 |
|---|---|---|
ensure_var() |
§6 | 环境变量缓存系统核心,见下节详解 |
_apply_tpl() |
§5 | 模板渲染引擎(envsubst + jq 校验) |
_is_restricted_region() |
§8 | 地区限制判断,影响所有流媒体/AI检测分支 |
apply_isp_routing_logic() |
§10 | ISP 路由策略决策,核心选路逻辑 |
issueCertificate() |
§12 | ACME 证书申请/续期,acme.sh 集成 |
createConfig() |
§13 | 全量模板渲染总入口 |
main_init() |
§16 | 12步启动流水线总入口 |
环境变量设计规范
变量分类
| 类型 | 在 Dockerfile 中 | 在 entrypoint 中 | 优先级来源 |
|---|---|---|---|
| 用户配置变量 | 设置有意义的默认值 | 不处理(docker-compose 直接 override) | docker-compose > Dockerfile |
| auto-gen 变量 | 必须置空 ENV VAR="" |
analyze_base_env 中 ensure_var |
docker-compose > ENV_FILE缓存 > auto-gen |
| 远端密钥变量 | 不声明 | crypctl 解密写入 /.env/secret |
/.env/secret 文件 |
| 自动检测变量 | 不声明 | analyze_ai_routing_env 中 ensure_var --no-persist 至 STATUS_FILE |
/.env/status 文件 |
设计规则
规则 1:auto-gen 变量在 Dockerfile 中必须置空
# ✅ 正确:置空,entrypoint 会 auto-gen 随机值
ENV DUFS_PORT=""
ENV SUB_STORE_FRONTEND_BACKEND_PATH=""
# ❌ 错误:有默认值,ensure_var 分支 1 命中,auto-gen 永远不执行
ENV DUFS_PORT="8889"
规则 2:用户配置变量设置有意义的 Dockerfile 默认值
# ✅ 正确:用户可以通过 docker-compose 覆盖,不覆盖则用此默认值
ENV LISTENING_PORT="443"
ENV DEST_HOST="www.microsoft.com"
ENV XUI_WEBBASEPATH="xui"
规则 3:新增变量的判断标准
- 需要随机/动态生成 → auto-gen 变量,Dockerfile 置空,加入
analyze_base_env - 用户可能需要自定义 → 用户配置变量,Dockerfile 给有意义默认值,不加入
analyze_base_env - 敏感凭证 → 远端密钥变量,不放 Dockerfile,由
crypctl解密 - 网络探测结果 → 自动检测变量,不放 Dockerfile,缓存至
/.env/status
环境变量缓存系统
ensure_var() 工作原理
优先级设计(高→低):docker-compose > auto-gen (ENV_FILE) > Dockerfile 默认值
ensure_var "KEY" "command to generate value"
ensure_var "KEY" --no-persist "command"
逻辑流程(见 Bug #015):
- shell 中有值:docker-compose 显式设置 → 直接返回,无 I/O
- 在 ENV_FILE 不在 shell:从
/.env/sb-xray读取并export到当前 shell → 返回 - 两者均无:执行指定命令获取值,
export到当前环境,写入持久化文件(--no-persist跳过写文件)
⚠️ 不能只检查文件存在性就返回,必须同时 export 到当前 shell(Bug #015 根本原因)。
⚠️ auto-gen 变量(需要随机生成的)在 Dockerfile 中必须置空(
ENV VAR=""),否则 Dockerfile 默认值会进入 shell,ensure_var 分支 1 直接返回,auto-gen 永远不执行。
analyze_base_env 中的变量格式:
# 格式: "KEY|生成命令"
"XRAY_UUID|generateRandomStr uuid"
"SUB_STORE_FRONTEND_BACKEND_PATH|echo /$(generateRandomStr path 32)"
两级持久化文件
| 文件 | 用途 | 清除条件 |
|---|---|---|
/.env/sb-xray |
核心参数 (UUID/端口/密钥等),长期不变 | 需手动删除 |
/.env/status |
运行时状态 (ISP_TAG/流媒体检测结果),可能随网络变化 | 删除可触发重新检测 |
/.env/secret |
远端解密的敏感配置 | 拉取后长期缓存 |
ISP 测速与节点质量标签架构
⚠️ 修改测速或选路逻辑前必读:该模块不仅决定出口路由,还驱动客户端 OpenClash 的智能评分,影响范围远超 entrypoint.sh 本身。
核心设计原则
ISP SOCKS5 代理的目的是解锁 geo 限制(ChatGPT、Netflix 等),不是与直连竞速。只要有 ISP 代理配置,必然使用代理出口;直连仅为无代理时的兜底。
直连测速的唯一目的:判断 VPS 本身是否具备
super标签能力(IP_TYPE=isp 且速度 > 100 Mbps)。不参与"要不要用代理"的决策。IS_8K_SMOOTH 是跨系统的关键变量,其值由 entrypoint 测速决定,由 show-config.sh 消费,最终影响 OpenClash 节点评分。
完整价值链
run_speed_tests_if_needed()
├── 测速所有 ISP SOCKS5 节点 → 选最快 → ISP_TAG
└── 最快代理速度 → IS_8K_SMOOTH (> 100 Mbps = true)
show-config.sh
├── ISP_TAG != direct + IS_8K_SMOOTH=true → NODE_SUFFIX += " ✈ good" (+10分)
└── IP_TYPE=isp + IS_8K_SMOOTH=true → NODE_SUFFIX += " ✈ super" (+30分)
订阅节点名: "🇺🇸 Reality ✈ host ✈ good ✈ isp"
└── OpenClash Policy-Priority: good命中+10 → 流媒体/家宽策略组优先选中
IS_8K_SMOOTH 阈值
| 速度 | IS_8K_SMOOTH | 产生标签 |
|---|---|---|
| ≥ 100 Mbps | true | good 或 super |
| < 100 Mbps | false | 无 |
变量持久化位置
ISP_TAG/IS_8K_SMOOTH→/.env/status(可删除重新测速,见 Bug #023)- 删除
/.env/status重启 → 重新测速 → 重新生成质量标签 → 订阅更新
编码规范
基本要求
#!/usr/bin/env bash
set -eou pipefail # 严格模式:任何错误立即退出
日志输出规范
log INFO "正常流程信息" # 绿色
log WARN "需要注意的警告" # 黄色
log ERROR "严重错误,可能导致退出" # 红色
log DEBUG "调试信息" # 青色
错误退出规范
set -e 模式下脚本遇错会静默退出,排查困难。任何可能失败的关键操作,必须在退出前打印 log ERROR 日志,明确标识失败的变量名和命令。例如 ensure_var 中命令执行失败时,先 log ERROR "[${key}] 计算失败: ${cmd}" 再 return 1。禁止让 set -e 直接吞掉错误无日志退出。
- 所有日志使用中文
- 日志自动附带时间戳
[YYYY-MM-DD HH:MM:SS] - 日志输出到 stderr (
>&2)
颜色变量
# 仅在 TTY 环境下启用颜色
RED='\\033[1;31m'; GREEN='\\033[1;32m'; YELLOW='\\033[1;33m'; CYAN='\\033[1;36m'; NC='\\033[0m'
BOLD='\\033[1m'; RESET_BOLD='\\033[22m'
函数命名
- 使用
camelCase或snake_case(项目中两种风格混用) - 流媒体检测函数统一命名:
check_<service>_access() - 返回值统一为
"direct"或代理标签名
流媒体/AI 检测函数模式
所有 check_*_access() 函数遵循统一的三层判断:
check_xxx_access() {
# 第一层:地域黑名单 (硬封锁地区直接走代理)
if [[ "${GEOIP_INFO:-}" =~ (香港|中国|...) ]]; then
get_fallback_proxy; return
fi
# 第二层:住宅 IP 优先直连
if [[ "${IP_TYPE:-}" == "isp" ]]; then
echo "direct"; return
fi
# 第三层:HTTP 探测实际可达性
local check_rs=$(http_probe "https://xxx")
if [[ "$check_rs" =~ ^(2|3) ]]; then
echo "direct"
else
get_fallback_proxy
fi
}
调试指南
查看当前环境变量
# 进入容器
docker exec -it sb-xray bash
# 查看持久化变量
cat /.env/sb-xray
# 查看运行状态变量
cat /.env/status
# 查看生成的配置
/scripts/show-config.sh
# 或快捷命令
show
重新触发检测
# 清除状态缓存,重启后将重新检测流媒体/AI解锁和测速
rm -f /.env/status
docker restart sb-xray
# 完全重置所有缓存 (包括 UUID 等,会导致订阅链接变化!)
rm -f /.env/sb-xray /.env/status
docker restart sb-xray
IP 质量体检
# 在容器内运行完整 IP 体检
/scripts/check_ip_type.sh
# 仅检测 IPv4
/scripts/check_ip_type.sh -4
# 仅检测 IPv6
/scripts/check_ip_type.sh -6
常见调试方法
- 模板渲染问题: 检查
docker logs sb-xray中apply_tpl的 DEBUG 日志 - 环境变量未生效: 确认变量已通过
export或写入持久化文件 - 证书问题: 容器内运行
acme.sh --list查看证书状态 - JSON 格式错误:
apply_tpl会自动用jq校验 JSON 文件
JavaScript 脚本代码生成规范(rename.js 类)
强制要求: 编写或修改
sources/hack/*.js前,必须先阅读.agents/skills/_shared/BUGS.md中 rename.js 分区的所有条目。
正则表达式规范
// ✅ 正确:移除"单词"类关键词必须加词边界
const escapedProto = protocol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
name.replace(new RegExp(`\\b${escapedProto}\\b`, 'ig'), '');
// ❌ 错误:无词边界,'ss' 会误删 'Russia' 中的 ss
name.replace(new RegExp(protocol, 'ig'), '');
// ✅ 正确:FlagRules 正则只保留必要捕获组
{ regex: /(美[国國]|United States)/i, emoji: '🇺🇸' }
// ❌ 错误:双层括号引入多余捕获组
{ regex: /((美[国國]|United States))/i, emoji: '🇺🇸' }
// ✅ 正确:锚点无需 g 标志
keyPart.replace(/(\[\s*\d*\s*\]|\d+)$/, '')
// ❌ 错误:$ 锚点加 g 无意义
keyPart.replace(/(\[\s*\d*\s*\]|\d+)$/g, '')
数组操作规范
// ✅ 正确:纯函数,返回新数组,不修改入参
const promoteRegion = (parts) => {
const result = [...parts]; // 先复制
const idx = result.findIndex(...);
if (idx <= 0) return result;
result.unshift(result.splice(idx, 1)[0]);
return result;
};
// ❌ 错误:直接修改入参,产生副作用
const promoteRegion = (parts) => {
const idx = parts.findIndex(...);
parts.unshift(parts.splice(idx, 1)[0]); // 突变原数组!
return parts;
};
常量与缓存规范
// ✅ 正确:派生数据在初始化 IIFE 中一次性计算,缓存到 Constants
(function initFlagRules() {
// ... 构建 flagMap, SORTED_COUNTRY_KEYS ...
Constants.ALL_REGIONS = [
...Constants.PRIORITY_REGIONS,
...Object.keys(RegionMap).filter(k => !Constants.PRIORITY_REGIONS.includes(k))
];
})();
// ✅ 正确:函数内直接引用缓存
const getPriority = (name) => {
return Constants.ALL_REGIONS.findIndex(k => name.includes(k));
};
// ❌ 错误:每次调用都重新构建,性能浪费
const getPriority = (name) => {
const allRegions = [...Constants.PRIORITY_REGIONS, ...Object.keys(RegionMap)]; // 每次重建!
return allRegions.findIndex(k => name.includes(k));
};
功能标志规范
// ✅ 正确:用布尔字段作为功能控制标志
const CleaningRules = [
{ desc: "统一分隔符", regex: /[-_|\s丨✈\/]+/g, value: "-", skipInPreformat: true },
];
// 使用时:
if (rule.skipInPreformat) continue;
// ❌ 错误:用描述文字判断,文字改变则逻辑静默失效
if (rule.desc.includes('分隔符')) continue;
数据去重规范
// ✅ 正确:向数据库类数组添加前先检查唯一性
// CountryDB 添加前:
const existingCodes = CountryDB.map(x => x.code);
if (existingCodes.includes(newEntry.code)) {
throw new Error(`CountryDB 重复 code: ${newEntry.code}`);
}
预格式化路径规范
// ✅ 正确:预格式化快速通道必须保留地名归化
let parts = remainingName.split('✈').flatMap(Utils.cleanPreformatted);
parts = parts
.map(part => Utils.standardizeRegion(part)) // 英文地名 → 中文
.map(part => part.replace(/^[-_||\s]+|[-_||\s]+$/g, '').trim()) // 清理残留分隔符
.filter(part => part !== '');
// ❌ 错误:缺少 standardizeRegion,英文地名未归化
let parts = remainingName.split('✈').flatMap(Utils.cleanPreformatted);
// 直接使用 parts,Hong Kong 不会变成 香港
代码重构规范
Bash 脚本重构(entrypoint.sh 类)
先呈方案,再动手: 列出所有拟修复的问题点,获得用户批准后再开始编写代码
红绿测试驱动:
- 重构前:先写测试套件(
scripts/test_*.sh),确认测试能暴露已知 bug(红灯) - 重构后:运行测试,全部通过才算完成(绿灯),任何失败必须修复
- 重构前:先写测试套件(
可测性设计: 脚本必须支持
source模式执行,用BASH_SOURCE保护入口,且路径变量(ENV_FILE等)需允许外部覆盖(${ENV_FILE:-/default/path})跨平台兼容: 所有
sed -i改用封装函数_sed_i(),避免使用 Linux 专属命令(如shuf,改用$(( RANDOM % N + base )))pipefail + SIGPIPE 防护:
set -o pipefail下,管道上游输出多行而下游只读部分行时(如grep | head -n 1),下游关闭管道会导致上游收到 SIGPIPE(退出码 141),整条管道被判定失败。禁止在 pipefail 模式下使用head -n截取多行输出,改用sed -n '1p'(读完全部输入不触发 SIGPIPE)。仅tr | head -c等单行场景加|| true兜底函数声明顺序(按分层原则,被依赖的先声明):
层次 内容 工具层 纯函数:日志、HTTP、随机生成、模板渲染、持久化 探测层 网络环境探测、选路辅助( _is_restricted_region等)速度层 测速函数 ISP 层 ISP 节点构建、测速、选路决策 业务层 流媒体/AI 可达性检测、证书管理、配置渲染、密钥解密 流程层 主流程各阶段函数(严格按运行时调用顺序声明) 入口层 main_init+ exec 保护结构清晰: 大脚本用
§N 段落名注释分段(建议 14-16 段),main_init内步骤注释须标注对应的daemon.inipriority 值Bug 记录: 重构时消灭的 bug 必须同步写入 BUGS.md
JavaScript 脚本重构(rename.js 类)
- 段落标记统一: 使用
// ── §N 段落名 ──风格分段,全文保持一致 - Utils 方法声明顺序(按依赖复杂度递增):
- 第一层:纯函数(无任何项目数据依赖,如
getNum、extractFlag、shortProtocol) - 第二层:依赖
CleaningRules等静态配置的函数(如cleanName、cleanPreformatted) - 第三层:依赖
RegionMap的函数(如standardizeRegion) - 第四层:依赖 IIFE 初始化产出(如
flagRules)的函数(如splitAndDedup、getPriority、detectFlag) - IIFE 初始化函数(如
initFlagRules())紧随被初始化数据(如RegionMap)之后声明,注释标注「产出」变量
- 第一层:纯函数(无任何项目数据依赖,如
- Pipeline/业务对象内部顺序:
- 私有辅助方法一律加
_前缀,整体前置(在所有阶段入口之前) - 阶段入口方法严格按运行时执行顺序排列(如
filter → format → sort → renumber) - 每个阶段入口添加
// 依赖: _xxx, _yyy注释
- 私有辅助方法一律加
- 算子入口
operator(): 在注释中标注完整执行链,便于快速理解数据流向
代码提交前自检清单
完成 JavaScript 脚本修改后,逐项确认:
- 所有"移除单词"类正则都添加了
\b词边界 - 数组变换辅助函数返回新数组,未修改入参
- 新增
CleaningRules条目检查是否需要skipInPreformat标志 - 功能控制标志使用布尔字段,不依赖描述文字
- 向
CountryDB/RegionMap等查找表添加条目前检查重复 - 新增引用
RegionMap的代码优先使用Constants.ALL_REGIONS - 预格式化分支包含
standardizeRegion()调用 - 修复 Bug 后在
.agents/skills/_shared/BUGS.md追加记录
已知问题速查(rename.js)
快速导航到 .agents/skills/_shared/BUGS.md 的对应条目:
| Bug ID | 一句话描述 | 关键词 |
|---|---|---|
| #001 | 预格式化通道英文地名未转中文 | processPreFormatted, standardizeRegion |
| #002 | 功能标志用字符串描述,修改描述则逻辑失效 | cleanPreformatted, skipInPreformat |
| #003 | CountryDB 末尾重复条目 | CountryDB, UK, KR |
| #004 | 协议名正则无词边界,误删单词内字母 | processRawNode, \b, Russia |
| #005 | promoteRegion 直接修改入参数组 | promoteRegion, 副作用 |
| #006 | FlagRules 正则双层括号 | FlagRules, 捕获组 |
| #007 | allRegions 在多处函数内重复构建 | Constants.ALL_REGIONS, 缓存 |