迁入通用节点部署工具、离线交付页生成器和共用技能,隔离真实配置与交付产物。统一 Codex 与 Claude Code 开发约定,补充节点迁移与运维文档。 完善 FINAL、策略组引用、输入校验、Surge 字段编码与 AI DNS 例外保护,增加隔离测试和订阅验证工具。 验证:全量类型检查、44 项单测、前端构建、脚本语法、技能入口与凭据隔离检查均通过。
18 KiB
AGENTS.md
本文件是 Codex 与 Claude Code 共用的项目开发约定。CLAUDE.md 用 @AGENTS.md 导入本文件;项目规则只在这里维护。双工具入口、技能发现和使用方法见 docs/AI-COLLABORATION.md。
协作方式
- 默认用中文沟通,代码标识符沿用项目现有英文命名。
- 按任务读取相关代码与文档;以代码、测试及用户最新要求为准,发现说明过时就同步修正。
- 本地阅读、编辑、类型检查、单测和构建按任务自主完成。部署或生产数据变更须属于用户授权的任务;已有明确授权不重复确认。普通代码修改不隐含上线或修改生产数据。
- 保留用户已有改动。提交、推送与部署分别按用户要求执行。
- 项目技能唯一源文件在
.claude/skills/;.agents/skills/通过相对符号链接供 Codex 发现。修改技能时编辑源文件,避免维护两份副本。
这是什么
本仓库统一维护边缘 VPS 节点的部署管理与 Proxy Station 代理节点、分流规则管理平台。Web 界面管理多协议节点(VMess/Trojan/Shadowsocks/Hysteria2)与策略组、规则,向 macOS Surge、iOS ShadowRocket、Android ClashMeta 下发托管订阅。规则模板来自 ClashConnectRules/Surge,边缘舰队的工具与私有交付记录已从原 ../edge-nodes/ 迁入本项目。
节点部署管理
edge-nodes/README.md是节点工作区入口;edge-nodes/build-delivery-doc.py从 Git 忽略的edge-nodes/private/fleet.json读取清单并离线生成汇总交付页,不在源码里保存真实站点信息。- 通用部署脚本、参考资料与占位交付模板唯一源码在
.claude/skills/edge-node-multi-protocol/,Codex 从.agents/skills/edge-node-multi-protocol相对链接加载。部署/重建/维护 VPS 节点使用此技能;发布 Proxy Station 应用用deploy,生产订阅数据查询与修改用update-db。 - 真实配置、面板入口、分享链接、二维码、交付文档和各站点脚本工作副本在
edge-nodes/private/<id>-edge-node-docs/,全部禁止入 Git 或公开静态托管,也不发送到在线二维码服务。新增配置/产物用umask 077,私有目录用受控备份转交,新克隆不会携带这些文件。 - 当前舰队为 sg(AWS 新加坡)、la(DMIT 洛杉矶,同时承载 Proxy Station)、rn(RackNerd 洛杉矶,当前工作副本为
rn2-edge-node-docs/)、jp(VMISS 东京,裸 nginx)。rn-edge-node-docs/与private/archive/是历史资料,不作为当前配置来源;原兄弟目录仅保留为迁移前备份,后续在本仓库维护。 - TCP 基础线路由 nginx+3x-ui/Xray 承载 VMess/Trojan/SS × direct/WARP,QUIC(HY2/TUIC)由 sing-box 独立承载。3x-ui 客户端不能只写投影表,必须保留
settings.clients,否则升级迁移会丢失认证。改远端前读取节点交付记录与技能参考,备份受影响配置和数据库,并保留已有证书续期动作。 - 迁移工程、修改脚本或复制文件不会授权远端部署;节点与应用的部署各自按任务执行。应用运行时仍通过管理 API/Web 导入节点,不自动读取私有目录。TUIC/SOCKS5 交付线路不属于本系统当前支持的导入类型。
命令
CI=true pnpm install --frozen-lockfile # 按锁文件安装依赖
pnpm dev # server :3000 + web :5173(都监听 0.0.0.0,局域网可访问)
pnpm -r typecheck # 三个包全量类型检查
pnpm test # 服务端单测(vitest)
pnpm --filter @proxy-station/web build
# macOS Surge:先取实际 HTTPS 订阅到私有临时文件,再用官方 CLI 与规则集快照验证
python3 scripts/check-surge.py /private/tmp/<profile>.conf --fetch-rulesets
# 单测过滤(在仓库根目录下)
pnpm --filter @proxy-station/server exec vitest run parse
pnpm --filter @proxy-station/server exec vitest run -t "round-trip"
pnpm --filter @proxy-station/server exec vitest # watch 模式
# 部署到 LA 生产机(web|server|all),详见 .claude/skills/deploy/SKILL.md 与 docs/DEPLOY.md
bash .claude/skills/deploy/scripts/deploy.sh all
# 本机离线生成边缘舰队交付页(清单与产物均在 Git 忽略的私有目录)
python3 edge-nodes/build-delivery-doc.py
# 校验生成的 Clash 配置(最有价值的一条端到端验证)
curl -s "http://localhost:3000/sub/<token>/clash" > /tmp/c.yaml && mihomo -t -f /tmp/c.yaml
本机开发及当前生产部署使用 Node 22。依赖使用 pnpm 与已提交的 pnpm-lock.yaml;不要混用 npm/yarn 或无故更新锁文件。pnpm 在无 TTY 时安装需要 CI=true,原生模块(better-sqlite3、esbuild、vue-demi)的构建许可写在 pnpm-workspace.yaml 的 allowBuilds。
本机会话注入的代理曾造成 git/安装/网络检查挂起;遇到此问题,在单条命令作用域内用 env -u HTTPS_PROXY -u https_proxy -u HTTP_PROXY -u http_proxy -u ALL_PROXY -u all_proxy <命令> 直连。现有运维脚本已自行处理代理。不依赖其他工具的全局指令文件,也不修改用户全局代理配置。
架构
应用部分为 pnpm monorepo:shared/(zod schema 与 DTO,前后端共用源码,非构建产物)+ server/(Hono + better-sqlite3 + Drizzle)+ web/(Vue 3 + Naive UI + Pinia)。生产模式由 server 静态托管 web 产物,单端口。edge-nodes/ 与节点部署技能是 Bash/Python 运维工具,不加入 pnpm 包,也不随应用部署上传;Docker 构建上下文排除它们。
配置生成流水线(核心)
SQLite → buildProfileIR({nodeIds}) → ProfileIR → 四个序列化器
├─ generateSurge(ir, {selfUrl})
├─ generateShadowrocket(ir) 节点订阅
├─ generateShadowrocketConf(ir) 策略组+规则
└─ generateClash(ir, {convertBaseUrl})
server/src/services/ir.ts 是配置生成流水线的统一读库入口,把节点、策略组、规则、Surge 原文段落收成与客户端无关的 ProfileIR;四个生成器只读 IR,互不依赖。新增客户端就是加一个序列化器,不要在生成器里再查数据库。
IR 层承担三件防护,改动时不要绕过:
- 停用或已删除的策略组会被剔除,引用它的成员与规则一并跳过
FINAL规则永远保留,其策略组失效时降级为DIRECTnodeIds为空数组表示「不限制」,非空则按订阅分配的节点范围过滤
客户端能力矩阵
shared/src/protocols.ts 的 clientSupports() 决定某节点能否下发给某客户端,生成器据此排除并回报 excluded。Surge 不支持 Shadowsocks over WebSocket;本项目对 Surge 的 VMess 仅下发 alterId=0,加密方式只支持 auto/aes-128-gcm/chacha20-poly1305(转换为 IETF 名称)。其他客户端的能力独立判断。不要把无法表达的输入静默改成另一个协议组合。
协议字段差异
同一协议在三端写法不同,映射集中在 shared/src/protocols.ts 与各生成器:
- SS cipher:分享链接用 Xray 命名
chacha20-poly1305,Surge/Clash 要chacha20-ietf-poly1305(normalizeSsCipher()) - VMess
alterId=0必须输出 Surge 的vmess-aead=true,否则连不上且症状隐晦 - Hysteria2 的 salamander 混淆:Surge 用
salamander-password=,Clash 用obfs/obfs-password - SS over WS 在 Clash 侧唯一可行方案是
plugin: v2ray-plugin
这些写法参考了 ../sub-router 项目(用户已在生产使用的同类系统),遇到新的客户端适配问题优先查它。
策略组的两种取节点方式
includeAllNodes 与 filterRegex 可以共存,生成器里 filterRegex 分支优先:
- 有
filterRegex→ 动态筛选:Surge 保留显式节点/内置成员,全部成员组作为include-other-group来源,并可同时启用include-all-proxies=true;正则只筛动态来源,不筛显式成员。Clash 用include-all-proxies: true+filter;ShadowRocket 用policy-regex-filter。新增同地区节点会自动进组 - 仅
includeAllNodes→ Surge 静态展开全部节点名,Clash/ShadowRocket 仍用动态引用
地区组靠节点名称正则匹配,不看 region 字段。节点命名规范与新增地区的完整步骤见 docs/ADDING-REGIONS.md——命名不对就进不了地区组。
节点与组共用客户端的策略名称空间:禁止名称中的配置分隔符、引号、反斜线、控制字符和首尾空格;不能与内置策略或另一类实体重名。导入预览与实际写入都走同一 schema。组成员必须存在且类型正确,创建/更新(包括改名)校验完整引用图无环,并在事务内写组、替换成员与级联引用。
Surge 不支持组级 url=;组的 testUrl 仅用于 Clash/ShadowRocket。Surge 使用 General 的 proxy-test-url 或节点 test-url,fallback 不输出 tolerance。协议、规则和参数规范以 Surge 官方手册 与本机 surge-cli --check 为依据,不能凭其他客户端语法推断。
规则集:映射与兜底转换
模板里的 RULE-SET/DOMAIN-SET 指向外部 URL。Surge/ShadowRocket 直接透传;Clash 需要 rule-providers,services/rulesets/mapping.ts 按序尝试映射到 skk.moe 的 Clash 版与 blackmatrix7 的 Clash 目录,映射不到的标记 needsConvert,provider URL 指向自托管端点 /sub/:token/ruleset/:id.yaml——该端点实时拉取 Surge 版并用 convert.ts 转成 classical provider。规则集拉取带 24h 缓存 + etag,失败回落陈旧缓存。
ShadowRocket 分两步导入
ShadowRocket 的原生订阅格式只承载节点,因此拆成两个端点:/shadowrocket(节点 base64 URI 列表)与 /shadowrocket-conf(策略组与规则的 .conf)。ShadowRocket 没有 Surge 的 use= 订阅引用语法(真机验证过:use=true 会被当成解析不了的成员残留),策略组收节点只能靠 policy-regex-filter 从 App 内全部节点里筛(「收纳全部节点」输出 policy-regex-filter=.*),因此订阅名随意,但用户在 App 里挂的其他订阅的节点也会被筛进来。ShadowRocket 不支持 DOMAIN-SET/AND/OR/NOT/URL-REGEX/PROTOCOL,生成时跳过并回报 skippedRules。ShadowRocket 的 conf 语法没有引号机制:策略名(含空格/emoji)一律裸写,加引号会被当作名字的一部分,导致规则指向不存在的策略组而整条失效、流量落到默认行为走代理——Surge 生成器可以加引号,ShadowRocket 生成器绝不可以。
鉴权与订阅
除公开的 /api/health 外,/api/* 走 adminAuth:设置了 ADMIN_TOKEN 则要求 Bearer,未设置时仅放行 localhost 与 RFC1918 私有网段(公网 IP 一律拒绝)。/sub/:token/* 公开,token 用常量时间比对。订阅 URL 完全由请求来源决定——后端只返回路径,前端用 window.location.origin 拼接,#!MANAGED-CONFIG 与 Clash provider URL 取本次请求的 origin,所以没有也不需要「订阅基址」配置项。
未匹配的 /api/* 显式返回 404,不落到 SPA 回退(否则会拿到一页 HTML,调接口时极难排查)。
前端约定
UI 走「站牌导视」设计方向(轨道交通导视系统语言):白底 #f7f7f4、深藏青重点色 #2b3f5c、琥珀 #b26a00 = 直连出口、青 #007a72 = WARP 净出口,协议各有线路色圆标。颜色只在承载信息时出现——避免发光、亮边条、装饰性渐变。四个方向的设计对比留在 docs/design-demos.html。
两个共用工具:
utils/nodeName.ts的nodeDisplayName():与出口标签并排显示时剥掉名字末尾的出口词。下发给客户端的名字必须保留出口词,客户端里没有标签,用户只能靠名字区分同协议的两条线路utils/clipboard.ts的copyText():navigator.clipboard只在 HTTPS/localhost 可用,局域网 HTTP 访问时是undefined,直接调用会静默失败。必须走这个带execCommand降级的封装
数据与安全
SQLite 落在 server/data/,存有节点真实凭据。data/、*.db、.env、edge-nodes/private/、config.env 及其备份、node-links/ 已在 .gitignore——绝不入 git。可入库的示例、测试、模板一律用占位凭据;填写后的交付副本只放私有目录。节点通过 Web 界面粘贴分享链接导入,应用不直接读 edge-nodes/private/(见 docs/SEEDING.md)。
schema 变更同时更新 shared/src/schemas.ts/DTO(涉及对外字段时)、server/src/db/schema.ts 的 Drizzle 定义和 server/src/db/index.ts 的启动 SQL。新表在 BOOTSTRAP_SQL 中加 CREATE TABLE IF NOT EXISTS,旧库补列用同文件下方的轻量 ALTER TABLE 循环(吞掉「列已存在」错误)——没有引入 migration 工具。
公网部署必须设 ADMIN_TOKEN 并用反代提供 HTTPS:订阅 token 就在 URL 里,明文 HTTP 会泄露。
初始化与已有数据
server/src/db/seed.ts 只在策略组为空时从 server/assets/surge-template.conf 初始化策略组、规则和 Surge 段落。修改模板不会更新已有数据库;既有订阅仍以数据库为准。生产数据变更使用 update-db 技能,经管理 API 修改并先做 SQLite 在线备份,避免绕过校验和级联更新。组成员与排序 API 有全量替换语义,修改前先读取完整当前值。
server/src/env.ts 直接读 process.env,没有 dotenv 加载;仅复制 .env.example 成 .env 并不会让 pnpm dev 自动读取它。变量须由启动环境提供(Docker Compose 与 systemd 有各自注入方式)。相对 DATA_DIR 始终基于 server/ 解析。
Surge AI DNS 与协议规则
改 DNS/隐私行为先读 docs/SURGE-PRIVACY.md、server/src/services/generators/surge-ai-dns.ts 及对应测试。AI 域名 DNS 映射来自 IR 内启用的 AI 路由规则;不要把全局 DNS 或所有端口 53 请求改送 AI。Surge 的 PROTOCOL 应用协议规则不能直接复制到其他客户端:Clash 只把 TCP/UDP 转成 NETWORK,其他应用协议跳过;ShadowRocket 跳过 PROTOCOL。生成配置校验不能替代设备 DNS/WebRTC 真机验证。
手写 Host 项优先;生成的域名 DNS 映射按连接规则顺序保留 AI 之前的非 AI 域名/规则集例外,不把 Host 当成能表达进程、协议或 IP 条件的路由系统。宽泛 AI 域名规则仍须审阅覆盖范围。
规则变更先读 docs/SURGE-CHANGES.md。API 必须保住一条启用且末尾的 FINAL;禁止停用、删除、改类型或创建重复 FINAL,排序必须提交完整且无重复的 ids。IR 对遗留坏数据防御性补齐 FINAL,其失效策略降级 DIRECT。重置规则在事务内保留自定义规则,不重复插入原主键,也不能用 reset 代替定向调整生产规则。
规则按首条匹配执行(另留意 pre-matching)。业务例外须在覆盖它的广告/通用规则集之前,特定 UDP 拦截须在对应服务域名规则之前。靠前的 IP 规则集是否加 no-resolve 必须按意图决定:它能避免主动解析,但会改变未解析域名的 IP 拦截覆盖。普通规则值与策略引用用 surge-format.ts 编码,逻辑表达式保留结构;不要手拼含逗号的正则或含换行的字段。
共享订阅不附公开默认控制口令;默认关闭 Remote Controller/HTTP API,确需使用时按设备配置独立凭据与访问限制。MITM 不关闭上游服务器证书验证;需要 MITM 时另做设备 CA、开关与 HTTPS 行为验证。
验证与完成标准
| 改动 | 建议验证 |
|---|---|
| 服务端逻辑/解析器/生成器 | pnpm --filter @proxy-station/server typecheck + 相关 Vitest;新增行为用占位凭据构造 IR/分享链接用例 |
| shared schema/DTO/协议能力 | pnpm -r typecheck + pnpm test,兼顾前后端消费者与各客户端产物 |
| Vue 页面/样式/状态 | pnpm --filter @proxy-station/web typecheck + pnpm --filter @proxy-station/web build;页面交互与局域网 HTTP 复制按需验证 |
| 数据库结构/启动行为 | 临时 DATA_DIR 或 createDb(':memory:') 验证新库与旧库升级;注意下述导入副作用 |
| 文档/协作入口/技能 | 检查路径、导入、符号链接、技能 frontmatter 与脚本语法;不为纯文档修改触发部署 |
| 节点部署工具/交付页生成器 | Bash 语法与 Python 编译检查、占位配置离线验证;迁移核对原文件、私有路径 Git 忽略及技能链接,不为迁移执行远端脚本 |
涉及节点、组、规则、DNS 或 Surge 段落时,在上述检查之外对实际订阅运行 scripts/check-surge.py;涉及规则集或顺序用 --fetch-rulesets,审阅域名遮蔽提示。脚本调用官方 Surge CLI,不更换活动配置;输出 OK 不表示测速、业务连接或设备隐私已验收。敏感配置用权限 600 的临时文件,不进入仓库或对话。生产写入先在线备份,再 API 更新、重读、HTTPS 拉订阅与 CLI 验证;必要时同时用 mihomo 校验受影响的 Clash 产物。
db/index.ts 在模块加载时就执行 export const db = createDb(),包括只导入 createDb 的情形;临时数据库测试/启动验证应在加载模块前设置绝对路径的临时 DATA_DIR,不要拿本地真实 server/data/ 做试验。现有生成器单测使用内存中的 IR;运行服务还会执行初始化。
改生成器时检查节点过滤、组成员引用、规则顺序和 FINAL 兜底;Clash 可用 mihomo 校验,Surge/ShadowRocket 的客户端语法与实际连接仍需真机验收。excluded/skippedRules 是生成器返回字段,订阅 HTTP 响应只返回配置正文。
完成后说明改动、已通过的验证与尚未完成的实际验证。新增业务约束或修正旧认识时同步更新这里或相应 docs/,让后续 Codex/Claude Code 会话能接续。