Files
proxy-station/CLAUDE.md
YANG JIANKUAN 43a753963c feat: 新增 deploy Skill 与 LA 生产部署文档
线上为 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>
2026-09-03 17:38:38 +08:00

103 lines
8.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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](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 模式
# 部署到 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` 规则永远保留,其策略组失效时降级为 `DIRECT`
- `nodeIds` 为空数组表示「不限制」,非空则按订阅分配的节点范围过滤
### 客户端能力矩阵
`shared/src/protocols.ts``clientSupports()` 决定某节点能否下发给某客户端,生成器据此静默排除并回报 `excluded`。当前唯一规则:**Surge 不支持 Shadowsocks over WebSocket**Surge 的 ss 类型只有 simple-obfs没有 WS 传输)。这不是 bug同机的 VMessTrojan 线路凭据相同、覆盖等价。
### 协议字段差异
同一协议在三端写法不同,映射集中在 `shared/src/protocols.ts` 与各生成器:
- SS cipher分享链接用 Xray 命名 `chacha20-poly1305`SurgeClash 要 `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 静态展开全部节点名ClashShadowRocket 仍用动态引用
**地区组靠节点名称正则匹配,不看 `region` 字段**。节点命名规范与新增地区的完整步骤见 `docs/ADDING-REGIONS.md`——命名不对就进不了地区组。
### 规则集:映射与兜底转换
模板里的 `RULE-SET``DOMAIN-SET` 指向外部 URL。SurgeShadowRocket 直接透传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` 只在 HTTPSlocalhost 可用,局域网 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 会泄露。