100 lines
7.6 KiB
Markdown
100 lines
7.6 KiB
Markdown
# 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](https://github.com/ClashConnectRules/Surge),节点来自 `~/Development/Fusion/Projects/edge-nodes/` 交付的边缘节点舰队。
|
||
|
||
## 命令
|
||
|
||
```bash
|
||
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.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` 规则永远保留,其策略组失效时降级为 `DIRECT`
|
||
- `nodeIds` 为空数组表示「不限制」,非空则按订阅分配的节点范围过滤
|
||
|
||
### 客户端能力矩阵
|
||
|
||
`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` → 动态筛选:Surge `include-all-proxies=true, policy-regex-filter=…`/Clash `filter:`/ShadowRocket `use=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)。**conf 里的策略组用 `use=true` 引用订阅名**,所以用户在 App 内必须把节点订阅命名为 `SHADOWROCKET_SUB_NAME`(= `Proxy Station`)。ShadowRocket 不支持 `DOMAIN-SET`/`AND`/`URL-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.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 会泄露。
|