Files
proxy-station/CLAUDE.md
2026-08-25 11:35:05 +08:00

7.6 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

这是什么

自托管的代理节点与分流规则管理平台Web 界面管理多协议节点VMessTrojanShadowsocksHysteria2与策略组、规则向 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 模式

# 校验生成的 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.yamlallowBuilds

架构

pnpm monoreposhared/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 规则永远保留,其策略组失效时降级为 DIRECT
  • nodeIds 为空数组表示「不限制」,非空则按订阅分配的节点范围过滤

客户端能力矩阵

shared/src/protocols.tsclientSupports() 决定某节点能否下发给某客户端,生成器据此静默排除并回报 excluded。当前唯一规则:Surge 不支持 Shadowsocks over WebSocketSurge 的 ss 类型只有 simple-obfs没有 WS 传输)。这不是 bug同机的 VMessTrojan 线路凭据相同、覆盖等价。

协议字段差异

同一协议在三端写法不同,映射集中在 shared/src/protocols.ts 与各生成器:

  • SS cipher分享链接用 Xray 命名 chacha20-poly1305SurgeClash 要 chacha20-ietf-poly1305normalizeSsCipher()
  • VMess alterId=0 必须输出 Surge 的 vmess-aead=true,否则连不上且症状隐晦
  • Hysteria2 的 salamander 混淆Surge 用 salamander-password=Clash 用 obfsobfs-password
  • SS over WS 在 Clash 侧唯一可行方案是 plugin: v2ray-plugin

这些写法参考了 ../sub-router 项目(用户已在生产使用的同类系统),遇到新的客户端适配问题优先查它。

策略组的两种取节点方式

includeAllNodesfilterRegex 可以共存,生成器里 filterRegex 分支优先

  • filterRegex → 动态筛选Surge include-all-proxies=true, policy-regex-filter=…Clash filter:ShadowRocket use=true, policy-regex-filter=…。新增同地区节点会自动进组
  • includeAllNodes → Surge 静态展开全部节点名ClashShadowRocket 仍用动态引用

地区组靠节点名称正则匹配,不看 region 字段。节点命名规范与新增地区的完整步骤见 docs/ADDING-REGIONS.md——命名不对就进不了地区组。

规则集:映射与兜底转换

模板里的 RULE-SETDOMAIN-SET 指向外部 URL。SurgeShadowRocket 直接透传Clash 需要 rule-providersservices/rulesets/mapping.ts 按序尝试映射到 skk.moe 的 Clash 版与 blackmatrix7 的 Clash 目录,映射不到的标记 needsConvertprovider URL 指向自托管端点 /sub/:token/ruleset/:id.yaml——该端点实时拉取 Surge 版并用 convert.ts 转成 classical provider。规则集拉取带 24h 缓存 + etag失败回落陈旧缓存。

ShadowRocket 分两步导入

ShadowRocket 的原生订阅格式只承载节点,因此拆成两个端点:/shadowrocket(节点 base64 URI 列表)与 /shadowrocket-conf(策略组与规则的 .confconf 里的策略组用 use=true 引用订阅名,所以用户在 App 内必须把节点订阅命名为 SHADOWROCKET_SUB_NAME= Proxy Station。ShadowRocket 不支持 DOMAIN-SETANDURL-REGEX,生成时跳过并回报 skippedRules该 conf 尚未经过真机验证。

鉴权与订阅

/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.tsnodeDisplayName():与出口标签并排显示时剥掉名字末尾的出口词。下发给客户端的名字必须保留出口词,客户端里没有标签,用户只能靠名字区分同协议的两条线路
  • utils/clipboard.tscopyText()navigator.clipboard 只在 HTTPSlocalhost 可用,局域网 HTTP 访问时是 undefined,直接调用会静默失败。必须走这个带 execCommand 降级的封装

数据与安全

SQLite 落在 server/data/,存有节点真实凭据。data/*.db.env 已在 .gitignore——绝不入 git。仓库内所有示例、测试、模板一律用占位凭据。节点通过 Web 界面粘贴分享链接导入,不直接读 edge-nodes 目录(见 docs/SEEDING.md)。

schema 变更走 db/index.ts 里的 BOOTSTRAP_SQLCREATE TABLE IF NOT EXISTS,旧库补列用同文件下方的轻量 ALTER TABLE 循环(吞掉「列已存在」错误)——没有引入 migration 工具。

公网部署必须设 ADMIN_TOKEN 并用反代提供 HTTPS订阅 token 就在 URL 里,明文 HTTP 会泄露。