线上为 1 核 1.9G 机器,放弃 Docker:Node 直跑 + systemd 守护, 前端本地构建后由服务端静态托管,HTTPS 与反代由 aaPanel 的 nginx 管理。 部署脚本支持 web/server/all 三种目标,含本地类型检查与单测、 暂存目录原子换入、健康检查失败自动回滚源码、本地与远端源码树指纹比对 及公网校验。首次部署与一次性配置写在 docs/DEPLOY.md。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8.2 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
这是什么
自托管的代理节点与分流规则管理平台:Web 界面管理多协议节点(VMess/Trojan/Shadowsocks/Hysteria2)与策略组、规则,向 macOS Surge、iOS ShadowRocket、Android ClashMeta 下发托管订阅。规则模板来自 ClashConnectRules/Surge,节点来自 ~/Development/Fusion/Projects/edge-nodes/ 交付的边缘节点舰队。
命令
pnpm dev # server :3000 + web :5173(都监听 0.0.0.0,局域网可访问)
pnpm -r typecheck # 三个包全量类型检查
pnpm test # 服务端单测(vitest)
pnpm --filter @proxy-station/web build
# 单测过滤(在 server/ 目录下)
npx vitest run parse # 按文件名
npx vitest run -t "round-trip" # 按用例名
npx vitest # watch 模式
# 部署到 LA 生产机(web|server|all),详见 .claude/skills/deploy/SKILL.md 与 docs/DEPLOY.md
bash .claude/skills/deploy/scripts/deploy.sh all
# 校验生成的 Clash 配置(最有价值的一条端到端验证)
curl -s "http://localhost:3000/sub/<token>/clash" > /tmp/c.yaml && mihomo -t -f /tmp/c.yaml
网络与安装的两个坑:本机会话注入的 HTTPS_PROXY 会让 git/构建挂死,跑任何命令前先剥掉代理(见用户全局 CLAUDE.md)。pnpm 在无 TTY 时执行 pnpm install 需要 CI=true,原生模块(better-sqlite3、esbuild、vue-demi)的构建许可写在 pnpm-workspace.yaml 的 allowBuilds。
架构
pnpm monorepo:shared/(zod schema 与 DTO,前后端共用源码,非构建产物)+ server/(Hono + better-sqlite3 + Drizzle)+ web/(Vue 3 + Naive UI + Pinia)。生产模式由 server 静态托管 web 产物,单端口。
配置生成流水线(核心)
SQLite → buildProfileIR({nodeIds}) → ProfileIR → 四个序列化器
├─ generateSurge(ir, {selfUrl})
├─ generateShadowrocket(ir) 节点订阅
├─ generateShadowrocketConf(ir, …) 策略组+规则
└─ generateClash(ir, {convertBaseUrl})
services/ir.ts 是唯一的读库出口,把节点、策略组、规则、Surge 原文段落收成与客户端无关的 ProfileIR;四个生成器只读 IR,互不依赖。新增客户端就是加一个序列化器,不要在生成器里再查数据库。
IR 层承担三件防护,改动时不要绕过:
- 停用或已删除的策略组会被剔除,引用它的成员与规则一并跳过
FINAL规则永远保留,其策略组失效时降级为DIRECTnodeIds为空数组表示「不限制」,非空则按订阅分配的节点范围过滤
客户端能力矩阵
shared/src/protocols.ts 的 clientSupports() 决定某节点能否下发给某客户端,生成器据此静默排除并回报 excluded。当前唯一规则:Surge 不支持 Shadowsocks over WebSocket(Surge 的 ss 类型只有 simple-obfs,没有 WS 传输)。这不是 bug,同机的 VMess/Trojan 线路凭据相同、覆盖等价。
协议字段差异
同一协议在三端写法不同,映射集中在 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→ 动态筛选:Surgeinclude-all-proxies=true, policy-regex-filter=…/Clashfilter:/ShadowRocketuse=true, policy-regex-filter=…。新增同地区节点会自动进组 - 仅
includeAllNodes→ Surge 静态展开全部节点名,Clash/ShadowRocket 仍用动态引用
地区组靠节点名称正则匹配,不看 region 字段。节点命名规范与新增地区的完整步骤见 docs/ADDING-REGIONS.md——命名不对就进不了地区组。
规则集:映射与兜底转换
模板里的 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/URL-REGEX,生成时跳过并回报 skippedRules。ShadowRocket 的 conf 语法没有引号机制:策略名(含空格/emoji)一律裸写,加引号会被当作名字的一部分,导致规则指向不存在的策略组而整条失效、流量落到默认行为走代理——Surge 生成器可以加引号,ShadowRocket 生成器绝不可以。
鉴权与订阅
/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 已在 .gitignore——绝不入 git。仓库内所有示例、测试、模板一律用占位凭据。节点通过 Web 界面粘贴分享链接导入,不直接读 edge-nodes 目录(见 docs/SEEDING.md)。
schema 变更走 db/index.ts 里的 BOOTSTRAP_SQL 加 CREATE TABLE IF NOT EXISTS,旧库补列用同文件下方的轻量 ALTER TABLE 循环(吞掉「列已存在」错误)——没有引入 migration 工具。
公网部署必须设 ADMIN_TOKEN 并用反代提供 HTTPS:订阅 token 就在 URL 里,明文 HTTP 会泄露。