chore: sub-store Go 重写项目初始化

This commit is contained in:
2026-07-27 14:38:13 +08:00
commit 90e53aa754
85 changed files with 15271 additions and 0 deletions
+696
View File
@@ -0,0 +1,696 @@
# Sub-Store Cloudflare 原项目技术实现架构分析
> 参考路径: `ref/sub-store-cloudflare/`
> 版本: 1.1.0 (AGPL-3.0-or-later)
> 仓库: github.com/realchendahuang/sub-store-cloudflare
---
## 1. 项目定位
Sub-Store Cloudflare 是一个 **Cloudflare 原生的机场订阅聚合与路由模板管理工具**,部署形态为 Cloudflare Workers + Static Assets + D1 + Worker Secrets。核心能力:
- **订阅源聚合**:管理多个远程/本地订阅源,合并为集合 (Collection)
- **节点处理**:解析 13 种代理协议 URI、JSON/YAML 数组、客户端配置行,执行过滤/排序/去重/重命名等操作
- **多目标输出**:转换为 mihomo / sing-box / surge / loon / qx / egern / shadowrocket / v2ray / uri 等 13 种客户端格式
- **路由模板**:内置 6 套 ACL4SSR / Loyalsoldier / AI+Streaming mihomo 模板,支持自定义
- **规则转换**:将分流规则在 mihomo/surge/loon/qx 之间互转
- **分享/回收站**scoped download grants(限定资源+目标+过期的分享令牌)、有界回收站(50 条上限)
**非功能边界**AGENTS.md 明确约束):
- 不使用 R2/KV/Durable Objects/Queues/Cron
- 不运行时 eval 脚本(脚本为构建时打包)
- 不存储/执行来自 D1/浏览器/远程 URL 的脚本源码
- 远程源缓存使用 Workers Cache API`caches.default`),非 D1
---
## 2. 整体架构
```
┌─────────────────────────────────────────────────────────────┐
│ Cloudflare Worker │
│ (cloudflare/src/index.ts — Hono app, single entry) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ │
│ │ /api/* │ │ /download/* │ │ Static Assets │ │
│ │ (admin API) │ │ (public dl) │ │ (SPA fallback)│ │
│ │ routes/api │ │ routes/dl │ │ ASSETS binding│ │
│ └──────┬──────┘ └──────┬───────┘ └────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ lib/ 层 │ │
│ │ store.ts subscription.ts rules.ts │ │
│ │ (D1 CRUD) (核心管线) (规则转换) │ │
│ │ defaults.ts targets.ts scripts.ts │ │
│ │ (内置模板) (目标别名) (脚本注册表) │ │
│ │ compatibility http.ts limits.ts read.ts │ │
│ │ -resources.ts (token/CORS/Hdr) (常量) (流读) │ │
│ └──────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ D1 (DB) │ │ Cache API │ │ Worker Secrets│ │
│ │ 5 张表 │ │ caches. │ │ ADMIN_TOKEN │ │
│ │ │ │ default │ │ DOWNLOAD_TOK │ │
│ └────────────┘ └────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 2.1 Monorepo 结构
```
sub-store-cloudflare/
├── package.json # workspace root, pnpm@11.7, node>=22
├── pnpm-workspace.yaml # packages: frontend, cloudflare
├── wrangler.jsonc # 顶层部署配置 (ASSETS + D1 + secrets)
├── cloudflare/ # Worker 后端
│ ├── src/
│ │ ├── index.ts # Hono app 入口 + 导出 ExportedHandler
│ │ ├── types.ts # 所有 TypeScript 类型定义
│ │ ├── worker-configuration.d.ts # Wrangler 生成的 Env 接口
│ │ ├── routes/
│ │ │ ├── api.ts # /api/* 管理接口 (896 行)
│ │ │ └── download.ts # /download/* 公开下载接口
│ │ ├── lib/
│ │ │ ├── store.ts # D1 数据层 CRUD (644 行)
│ │ │ ├── subscription.ts # 核心订阅处理管线 (2430 行)
│ │ │ ├── rules.ts # 分流规则转换
│ │ │ ├── defaults.ts # 6 套内置路由模板
│ │ │ ├── targets.ts # 客户端目标别名归一化
│ │ │ ├── scripts.ts # 构建时脚本注册表 + 运行时
│ │ │ ├── compatibility-resources.ts # download_grants + recycle_bin
│ │ │ ├── http.ts # token 校验 / CORS / 安全头
│ │ │ ├── limits.ts # 6 个字节/数量限制常量
│ │ │ └── read.ts # 流式响应读取 (防 OOM)
│ │ └── generated/ # 构建时生成 (git ignored)
│ │ └── script-registry.ts
│ ├── migrations/
│ │ ├── 0001_initial.sql
│ │ ├── 0002_runtime_schema_cleanup.sql
│ │ └── 0003_compatibility_resources.sql
│ └── package.json # hono, json5, yaml 三个运行时依赖
├── frontend/ # Vue 3 + Vite SPA
│ └── src/ # Pinia stores, views, components
└── scripts/ # 构建时 Node.js 脚本
├── generate-script-registry.mjs # 扫描 config/scripts → registry
├── install-cloudflare.mjs # 一键安装器
├── render-seed-sql.mjs # agent-setup → seed SQL
└── ... (15+ 检查/部署脚本)
```
---
## 3. 运行时环境与绑定
### 3.1 Worker Bindings (wrangler.jsonc)
| 绑定 | 类型 | 用途 |
|------|------|------|
| `DB` | D1Database | 所有结构化配置数据 |
| `ASSETS` | Fetcher | Static Assets (前端 SPA, `run_worker_first: true`) |
| `SUB_STORE_ADMIN_TOKEN` | Secret (string) | `/api/*` 管理 API 鉴权 |
| `SUB_STORE_PUBLIC_DOWNLOAD_TOKEN` | Secret (string) | `/download/*` 公开下载鉴权 |
| `SUB_STORE_APP_NAME` | Var (string) | 应用显示名 |
| `SUB_STORE_PUBLIC_DOWNLOAD_HOSTS` | Var (string) | 逗号分隔的纯下载域名列表 |
### 3.2 关键配置
- **`compatibility_date`**: `2026-07-08`
- **`compatibility_flags`**: `["nodejs_compat"]`
- **`assets.not_found_handling`**: `"single-page-application"` — 前端 SPA 路由兜底
- **`assets.run_worker_first`**: `true` — Worker 先处理请求,未匹配才回退到静态资源
- **`observability`**: enabled, head_sampling_rate=1
### 3.3 下载域名隔离机制
`index.ts``fetch` handler 在路由之前检查 hostname
- 如果当前 hostname 在 `SUB_STORE_PUBLIC_DOWNLOAD_HOSTS` 列表中,且 pathname 不以 `/download/` 开头 → 返回 404
- 效果:下载域名只能访问下载端点,管理 UI 不可达
---
## 4. 数据库 Schema (D1 / SQLite)
### 4.1 表结构
**`sources`** — 订阅源
| 列 | 类型 | 说明 |
|----|------|------|
| id | TEXT PK | 1-64 字符 `[a-z0-9_-]` |
| name | TEXT | 显示名 |
| type | TEXT | `remote` / `local` |
| url | TEXT | 远程 URL(可多行,换行分隔,最多 8 个) |
| content | TEXT | local 源的原始内容 |
| enabled | INTEGER | 0/1 |
| filters_json | TEXT | FilterRule[] 的 JSON |
| meta_json | TEXT | 元数据 JSONua, cacheTtl, subUserinfo 等) |
| created_at | INTEGER | 毫秒时间戳,同时用于排序 |
| updated_at | INTEGER | 毫秒时间戳 |
**`collections`** — 订阅集合
| 列 | 类型 | 说明 |
|----|------|------|
| id | TEXT PK | 同上 |
| name | TEXT | 显示名 |
| source_ids_json | TEXT | string[],空数组表示包含所有 enabled 源 |
| filters_json | TEXT | 集合级 FilterRule[] |
| template_id | TEXT | 关联的路由模板 ID |
| ignore_failed | INTEGER | 0/1,是否忽略失败的源 |
| enabled | INTEGER | 0/1 |
| meta_json | TEXT | 元数据 |
| created_at / updated_at | INTEGER | 同上 |
**`templates`** — 路由模板(仅存自定义的,内置模板在代码中)
| 列 | 类型 | 说明 |
|----|------|------|
| id | TEXT PK | 同上 |
| name | TEXT | 显示名 |
| target | TEXT | 目标客户端(默认 mihomo |
| config_json | TEXT | RoutingTemplateConfig JSON |
| created_at / updated_at | INTEGER | 同上 |
**`app_settings`** — 应用设置(单行,id="default"
| 列 | 类型 | 说明 |
|----|------|------|
| id | TEXT PK | 固定 "default" |
| value_json | TEXT | 设置 JSON(深合并) |
| updated_at | INTEGER | |
**`download_grants`** — 分享令牌(migration 0003
| 列 | 类型 | 说明 |
|----|------|------|
| id | TEXT PK | UUID |
| token_hash | TEXT UNIQUE | SHA-256 hex of token |
| resource_type | TEXT CHECK | `source` / `collection` |
| resource_id | TEXT | |
| target | TEXT | 限定的目标客户端,空=不限 |
| expires_at | INTEGER | 过期时间,NULL=永不过期 |
| enabled | INTEGER | 0/1 |
| created_at / updated_at | INTEGER | |
- 索引: `idx_download_grants_token_hash`, `idx_download_grants_resource`
**`recycle_bin`** — 回收站(migration 0003
| 列 | 类型 | 说明 |
|----|------|------|
| id | TEXT PK | UUID |
| resource_type | TEXT CHECK | `source`/`collection`/`template`/`share` |
| resource_id | TEXT | |
| snapshot_json | TEXT | 删除时的完整快照 |
| deleted_at | INTEGER | |
- 索引: `idx_recycle_bin_deleted_at`
- 上限: 50 条(通过 `DELETE ... LIMIT -1 OFFSET 50` 自动裁剪)
### 4.2 设计要点
- **排序用 created_at**`sortSources` / `sortCollections` 通过 `UPDATE SET created_at = now + index` 重排序,列表查询 `ORDER BY created_at ASC`。同一 batch 中 `now + index` 保证顺序。
- **UPSERT 模式**:所有写入用 `INSERT ... ON CONFLICT(id) DO UPDATE SET ...`,天然幂等。
- **批量操作**`importStorage` 使用 `env.DB.batch(statements)` 单事务导入。
- **内置模板不入库**migration 0002 主动删除旧版内置模板行,改为代码持有 (`BUILTIN_TEMPLATES` 数组)`listTemplates` 合并内置+自定义。
---
## 5. HTTP 路由设计
### 5.1 全局中间件 (index.ts)
1. **全局错误处理**: `app.onError` → JSON `{status:"failed",error:{code:500,...}}` + 结构化日志
2. **CORS preflight**: `OPTIONS *` 返回允许的方法和头
3. **安全头**: `applySecurityHeaders` 对每个响应添加 CSP / Referrer-Policy / X-Content-Type-Options / X-Frame-Options / Permissions-Policy
4. **CORS**: `applyCorsHeaders` 根据 origin 白名单(空则不加 CORS 头)
5. **下载域名隔离**: fetch handler 前置 hostname 检查
### 5.2 管理 API (`/api/*`)
**全局中间件**:
- `requireAdmin`: Bearer token / query `token` / `x-sub-store-token` 头 → SHA-256 + `timingSafeEqual``SUB_STORE_ADMIN_TOKEN` 比对
- `bodyLimit`: 4 MiB (`MAX_API_BODY_BYTES`)
**统一响应格式**:
- 成功: `{ "status": "success", "data": <T> }`
- 失败: `{ "status": "failed", "error": { "code": <number>, "message": <string> } }`
**端点清单**:
| 方法 | 路径 | 功能 |
|------|------|------|
| GET | `/api/env` | 运行时环境信息(backend、version、feature flags |
| GET | `/api/scripts` | 构建时脚本注册表元数据 |
| POST | `/api/proxy/parse` | 一次性代理内容转换 |
| POST | `/api/rule/parse` | 一次性分流规则转换 |
| GET/PATCH | `/api/settings` | 应用设置(GET 合并默认值) |
| GET/POST | `/api/storage` | 全量导出/导入(备份恢复) |
| GET/POST | `/api/sources` | 订阅源列表/创建 |
| PUT | `/api/sources` | 批量排序 |
| POST | `/api/sort/sources` | 排序(兼容旧接口) |
| GET/PATCH/DELETE | `/api/sources/:name` | 单源操作 |
| GET/POST | `/api/collections` | 集合列表/创建 |
| PUT | `/api/collections` | 批量排序 |
| GET/PATCH/DELETE | `/api/collections/:name` | 单集合操作 |
| GET/POST | `/api/templates` | 模板列表/创建 |
| GET/PATCH/DELETE | `/api/templates/:name` | 单模板操作(内置不可删改) |
| GET/POST | `/api/shares` | 分享令牌列表/创建 |
| PATCH/DELETE | `/api/shares/:id` | 令牌操作 |
| GET | `/api/recycle-bin` | 回收站列表 |
| DELETE | `/api/recycle-bin/:id` | 彻底删除 |
| POST | `/api/recycle-bin/:id/restore` | 恢复 |
| POST | `/api/utils/node-info` | IP 信息查询(代理转发 ipwho.is 等) |
| POST | `/api/preview/source` | 源预览(原始+处理后节点) |
| POST | `/api/preview/collection` | 集合预览 |
| GET | `/api/link/source/:name` | 生成下载链接 |
| GET | `/api/link/collection/:name` | 生成下载链接 |
| GET | `/api/source/flow/:name` | 流量信息(解析 subscription-userinfo 头) |
### 5.3 下载 API (`/download/*`)
**路由**: `GET /download/collection/:name/:target?/:token?``GET /download/source/:name/:target?/:token?`
**鉴权**:
1. 先校验全局 `SUB_STORE_PUBLIC_DOWNLOAD_TOKEN`timingSafeEqual
2. 不通过则查 `download_grants` 表(token_hash 匹配 + enabled + 未过期 + resource/target 匹配)
3. 都不通过 → 403
**target 推断**:
- 显式: URL path param 或 query `target`
- 隐式: User-Agent 推断(sing-box/v2ray/surge/loon/egern/shadowrocket/quantumult/stash → 对应 target,默认 mihomo
**临时覆盖**: query 参数 `url` / `content` / `ua` 可临时覆盖源配置(不持久化),用于预览/调试。
**响应头**: content-type 按 target 分配(YAML / JSON / text/plain),`subscription-userinfo` / `profile-web-page-url` / `content-disposition` / `x-sub-store-cache` 透传。
---
## 6. 核心订阅处理管线 (subscription.ts)
这是整个项目最核心的模块(2430 行),负责从订阅源到客户端格式的完整转换。
### 6.1 管线流程
```
buildSubscriptionResult(options)
├─ getSources(options)
│ ├─ 单源模式: [options.source]
│ └─ 集合模式: 按 collection.sourceIds 过滤 options.sources
│ (sourceIds 为空 = 所有 enabled 源)
├─ loadProxyNodes(internal) ──────────────────────────────┐
│ │ │
│ ├─ 过滤 enabled 源 │
│ │ │
│ ├─ 并发任务 (runWithConcurrency / runSettledWithConcurrency)
│ │ 每个 source: │
│ │ ├─ loadSubscriptionRaw(sub, settings, ua, runtime)
│ │ │ ├─ local: 直接返回 content
│ │ │ └─ remote: splitSourceUrls (最多8个URL)
│ │ │ 并发 fetchSubscriptionUrl (每个URL)
│ │ │ ├─ Cache API 查缓存 (cacheTtl, 默认300s)
│ │ │ ├─ fetch with UA + If-None-Match/Modified
│ │ │ ├─ 304 → 用缓存 (cacheStatus=refresh)
│ │ │ ├─ !ok → throw
│ │ │ ├─ 读 body (限 2MiB/URL, 总 12MiB)
│ │ │ ├─ 写缓存 (waitUntil, 非阻塞)
│ │ │ └─ error → stale cache if allowed
│ │ │ 合并多 URL 内容, decodeMaybeBase64
│ │ │ │
│ │ ├─ parseProxies(raw) ← 解析为 ProxyNode[] │
│ │ └─ applyFilters(nodes, source.filters, ...) │
│ │ (源级过滤) │
│ │ │
│ ├─ ignoreFailed=true: runSettled (失败源跳过) │
│ │ ignoreFailed=false: runWithConcurrency (失败则整体报错)
│ │ │
│ ├─ flat() 合并所有源节点 │
│ ├─ applyFilters(allNodes, collection.filters, ...) │
│ │ (集合级过滤) │
│ └─ ensureUniqueProxyNames (重名加 -2, -3 后缀) │
│ │
├─ renderBuildTarget(proxies, options) ─────────────────────┘
│ 按 target 分发到对应渲染器
└─ selectResponseMetadata(internal)
从第一个有效源提取 subscription-userinfo 等元数据
```
### 6.2 代理协议解析 (parseProxies)
输入内容自动检测格式:
1. **JSON/JSON5 数组** (`[``{` 开头): `parseJsonProxies` — 支持 `{proxies: [...]}` 或顶层数组
2. **YAML** (`proxies:` 开头): `parseYamlProxies` — 提取 `proxies` 字段
3. **URI 行** (默认): `parseProxyLines` — 逐行解析
`decodeMaybeBase64` 先检测是否为结构化内容,否则尝试 base64 解码。
**支持的 URI 协议** (parseProxyUri):
- `vless://` — VLESS + Reality
- `vmess://` — VMess (base64 JSON)
- `trojan://` — Trojan
- `ss://` — Shadowsocks (base64 或明文 userinfo)
- `ssr://` — ShadowsocksR (base64)
- `hysteria://` / `hy://` — Hysteria v1
- `hysteria2://` / `hy2://` — Hysteria v2
- `tuic://` — TUIC v5
- `anytls://` — AnyTLS
- `socks://` / `socks5://` / `socks5+tls://` — SOCKS5
- `http://` / `https://` — HTTP proxy
- `wireguard://` / `wg://` — WireGuard
**支持的客户端配置行** (parseClientProxyLine):
- **Quantumult X 格式**: `shadowsocks=host:port,...` / `vmess=host:port,...`
- **Surge/Loon 格式**: `name = ss,host,port,...` / `name = vmess,host,port,...`
- 支持协议: ss, ssr, vmess, vless, trojan, http, socks5, hysteria2, tuic, anytls, snell, ssh, h2-connect
### 6.3 过滤器系统 (applyFilters)
`FilterRule.type` 支持 11 种操作,按顺序串行执行:
| type | 功能 | 关键字段 |
|------|------|----------|
| `include` | 保留匹配项 | `pattern` (正则), `field` (默认 name) |
| `exclude` | 排除匹配项 | 同上 |
| `rename` | 正则替换 | `pattern`, `replacement`, `field` |
| `delete-field` | 删除字段匹配内容 | `patterns[]``pattern`, `field` |
| `dedupe` | 去重 | `fields[]` (默认 name), `action`: delete/rename |
| `sort` | 排序 | `direction`: asc/desc/random |
| `regex-sort` | 正则优先级排序 | `expressions[]`, `direction` |
| `flag` | 国旗添加/移除 | `mode`: add/remove, `tw`: cn/tw/ws |
| `quick` | 快速设置 | `udp`, `tfo`, `scert`, `useless` 等 |
| `resolve` | DNS 解析域名→IP | `provider`, `recordType`, `filter` |
| `script` | 构建时脚本 | `scriptId`, `scriptKind`, `arguments` |
**路径访问**: `getByPath` / `setByPath` 支持点号路径 (如 `ws-opts.headers.Host`)。
**正则编译**: `compileRegex` 支持 `(?i)` 前缀作为大小写不敏感标志。
**国旗检测** (detectFlag): 8 个地区正则 → emoji 旗帜,台湾旗可配置为 cn/tw/ws。
**DNS 解析** (resolveProxyDomains): 支持 Google/Cloudflare/Ali/Tencent/Custom DoH,并发解析,可过滤失败/IP类型,保留原域名为 servername/sni。
### 6.4 目标渲染 (renderBuildTarget)
| 目标 | 渲染器 | 输出格式 | Content-Type |
|------|--------|----------|--------------|
| mihomo / stash | `renderMihomoYaml` | YAML (proxies + proxy-groups + rules) | text/yaml |
| surge | `renderSurgeProxies` | 文本行 | text/plain |
| surge-mac | `renderSurgeMacProxies` | 文本行 (含 ssh/h2-connect/snell) | text/plain |
| surfboard | `renderSurfboardProxies` | 文本行 (ss/vmess/trojan/http/socks5) | text/plain |
| loon | `renderLoonProxies` | 文本行 | text/plain |
| egern | `renderEgernYaml` | YAML ({proxies: [...]}) | text/yaml |
| qx | `renderQxProxies` | 文本行 | text/plain |
| sing-box | `renderSingBoxJson` | JSON (outbounds + route) | application/json |
| v2ray | `renderProxyUris` + base64 | base64 编码的 URI 行 | text/plain |
| uri / shadowrocket | `renderProxyUris` | URI 行 | text/plain |
| json | 直接 JSON.stringify | {proxies: [...]} | application/json |
**mihomo YAML 渲染** 特殊处理:
- proxy-groups 中的 `$all` 展开为所有节点名
- `filter` 正则匹配节点名
- 引用不存在的组/节点会被过滤
- `DIRECT` / `REJECT` 为允许的字面量
**sing-box JSON 渲染**:自动生成 PROXY (selector) + AUTO (urltest) + DIRECT + REJECT outbound,附带 mixed-in inbound 和 route 配置。
**目标兼容性** (isTargetCompatible): 每种目标只输出它支持的协议类型,不支持的节点被跳过。
---
## 7. 脚本系统 (scripts.ts + generate-script-registry.mjs)
### 7.1 构建时注册表
脚本不存储在 D1 中,而是在构建时从 `config/script-plugins.json`(公开)和 `config/script-plugins.local.json`(个人)扫描,生成 `cloudflare/src/generated/script-registry.ts`
**约束**:
- 最多 32 个脚本,每个最大 32 KiB
- 不能使用 import/export/eval/require/new Function
- 公开脚本不能使用 fetch/$httpClient/setTimeout
- 必须声明 `function filter(...)``function operator(...)`
- 脚本代码通过 `node --check` 语法检查
### 7.2 运行时执行
```typescript
type ScriptRuntime = {
arguments: Record<string, unknown>; // 参数(含默认值)
options: Record<string, unknown>; // 原始 options
targetPlatform: SubscriptionTarget; // 目标客户端
context: Record<string, unknown>; // sourceId, collectionId, scriptId
proxyUtils: typeof PROXY_UTILS; // 工具函数
substore: { env: "Cloudflare" };
};
```
**两种脚本类型**:
- `filter`: 返回 `boolean[]`(每个节点一个布尔值),保留 true 的节点
- `operator`: 返回 `ProxyNode[]`(可修改节点,但不能增加数量)
**PROXY_UTILS** 提供: `isIPv4`, `isIPv6`, `isIP`, `removeFlag`, `Base64.encode/decode`
**限制**: 每个 source/collection 最多 2 个 script action (`MAX_SCRIPT_ACTIONS = 2`)。
---
## 8. 路由模板系统 (defaults.ts)
### 8.1 内置模板(6 套)
| ID | 名称 | 特点 |
|----|------|------|
| `mihomo-basic` | Mihomo Basic | 最简,内联规则 |
| `acl4ssr-mihomo` | ACL4SSR Mihomo | 默认推荐,17 个 rule-provider |
| `acl4ssr-mihomo-no-emoji` | ACL4SSR 无 Emoji | 同上去 emoji |
| `loyalsoldier-whitelist` | Loyalsoldier 白名单 | 代理优先 |
| `loyalsoldier-blacklist` | Loyalsoldier 黑名单 | 直连优先 |
| `ai-streaming-mihomo` | AI + Streaming | AI/流媒体/TG/GitHub 路由 |
### 8.2 模板结构
```typescript
type RoutingTemplateConfig = {
mixedPort?: number; // 7890
allowLan?: boolean; // false
mode?: string; // "rule"
logLevel?: string; // "info"
dns?: Record<string, unknown>; // fake-ip + DoH
sniffer?: Record<string, unknown>;
proxyGroups?: TemplateProxyGroup[]; // 10 个默认组
ruleProviders?: Record<string, unknown>; // http provider
rules?: string[]; // RULE-SET 规则链
};
```
**proxy-group 特殊值**:
- `$all` → 展开为所有节点名
- `filter` → 正则筛选节点
- 引用其他组名 → 自动包含
### 8.3 模板别名归一化
`normalizeMihomoTemplateConfig` 将 kebab-case 别名拷贝到 camelCase(如 `mixed-port``mixedPort`),然后删除 kebab-case 键。
---
## 9. 规则转换 (rules.ts)
将分流规则在 mihomo / surge / loon / qx 之间互转。
**支持的规则类型**: DOMAIN, DOMAIN-SUFFIX, DOMAIN-KEYWORD, IP-CIDR, IP-CIDR6, GEOIP, GEOSITE, PROCESS-NAME, DST-PORT, MATCH
**别名归一化**: HOST→DOMAIN, HOST-SUFFIX→DOMAIN-SUFFIX, IPCIDR→IP-CIDR, FINAL→MATCH, DEST-PORT→DST-PORT 等
**QX 特殊处理**: DOMAIN→HOST, IP-CIDR6→IP6-CIDR, MATCH→FINAL,只保留 `no-resolve` 选项
---
## 10. 安全机制
### 10.1 Token 鉴权
- **Admin Token**: SHA-256 双向 hash + `crypto.subtle.timingSafeEqual`(防时序攻击)
- **Download Token**: 同上
- **Scoped Grant Token**: 24 字节随机 → base64url,数据库存 SHA-256 hex,明文只返回一次
### 10.2 安全头
```
Content-Security-Policy: default-src 'self'; ... script-src 'self' 'unsafe-eval'; ...
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Permissions-Policy: camera=(), microphone=(), ...
```
### 10.3 输入限制
| 限制 | 值 | 位置 |
|------|-----|------|
| API body | 4 MiB | `MAX_API_BODY_BYTES` |
| 远程源 URL 数 | 8 | `MAX_REMOTE_SOURCE_URLS` |
| 单 URL 响应 | 2 MiB | `MAX_REMOTE_SOURCE_RESPONSE_BYTES` |
| 源总响应 | 12 MiB | `MAX_REMOTE_SOURCE_TOTAL_BYTES` |
| 流量信息响应 | 64 KiB | `MAX_FLOW_RESPONSE_BYTES` |
| DoH 响应 | 64 KiB | `MAX_DOH_RESPONSE_BYTES` |
| 回收站条目 | 50 | `MAX_RECYCLE_ENTRIES` |
| 脚本数 | 32 | `MAX_SCRIPTS` |
| 脚本大小 | 32 KiB | `MAX_SCRIPT_BYTES` |
| 脚本 action | 2 | `MAX_SCRIPT_ACTIONS` |
| ID 长度 | 1-64 `[a-z0-9_-]` | `validateRecordId` |
### 10.4 流式读取 (read.ts)
`readResponseText` 先检查 Content-Length,再流式读取并累计字节,超限则 cancel reader 并抛错——防止 OOM。
---
## 11. 缓存策略
远程订阅源使用 Workers Cache API (`caches.default`) 缓存:
- **缓存键**: SHA-256(`url\n userAgent`) → `https://sub-store-cache.invalid/source/<hash>`
- **TTL**: `cacheTtl` 设置(源 meta 或全局 settings),默认 300s,范围 0-3600s
- **缓存内容**: 响应体 + 自定义头(subscription-userinfo, etag, last-modified 等)
- **缓存命中**: `cacheStatus=hit`
- **304 Not Modified**: 用缓存内容,`cacheStatus=refresh`
- **新鲜获取**: 写缓存 (waitUntil)`cacheStatus=miss`
- **错误降级**: `remoteCacheStaleOnError !== false` 时用 stale cache`cacheStatus=stale`
- **强制刷新**: query `refresh=1``noCache=1` 跳过缓存读取
---
## 12. 并发控制
```typescript
async function runWithConcurrency<T>(tasks, concurrency, waitMs)
```
- 固定数量 worker 池,从共享 cursor 取任务
- `waitMs`: 每个任务前延迟(仅 index > 0 时),防止突发请求
- `concurrency`: 默认 3,范围 1-12
- `ignoreFailed=true``runSettledWithConcurrency`Promise.allSettled 语义),失败源跳过
- `ignoreFailed=false``runWithConcurrency`,任一失败则整体 reject
---
## 13. 前端架构概览
### 13.1 技术栈
- Vue 3 + TypeScript + Vite
- Pinia (状态管理)
- NutUI (组件库)
- CodeMirror (代码编辑器)
- 8 套主题 (light/dark/pureblack/darkblue/lightblue/sereneblues/monokai/mocha)
### 13.2 核心交互
- **Admin Token**: 存 localStorage,从 URL `?token=` 同步并清除
- **API 基址**: `VITE_API_URL=/`(同源),构建时注入
- **环境检测**: `/api/env` 返回 backend 类型(Cloudflare/Node/Docker/各客户端),前端据此显示图标和功能开关
- **设置合并**: 前端合并后端默认设置 + 用户设置(`mergeSettings`
### 13.3 页面结构
- Sub / SubEditor — 订阅源列表与编辑器
- Collection (隐含在 Sub 页) — 集合管理
- Preview — 节点预览
- CompareTable — 节点对比
- Tools — 工具页
- My — 个人/设置
- editCode/cmView — 代码编辑器
---
## 14. 构建与部署
### 14.1 构建管线
```bash
pnpm run build
├─ scripts:generate → generate-script-registry.mjs
│ 扫描 config/script-plugins.json + .local.json
│ → cloudflare/src/generated/script-registry.ts
└─ build:frontend → Vite build → frontend/dist/
```
### 14.2 部署方式
1. **Deploy to Cloudflare 按钮**: 用根 `wrangler.jsonc`CF 自动创建 D1
2. **Agent/CLI 安装器**: `pnpm run install:cloudflare`,支持导入源/集合
3. **快速安装**: `pnpm run install:quick`,先部署后配置
### 14.3 数据库迁移
```bash
pnpm run db:migrations:apply
→ wrangler d1 migrations apply DB --remote --config ../wrangler.jsonc
```
3 个 migration
- 0001: 创建 sources/collections/templates/app_settings + 默认 daily 集合
- 0002: 清理旧内置模板行 + 重建 app_settings
- 0003: 添加 download_grants + recycle_bin
---
## 15. Go + SQLite 重构要点
基于以上分析,Go 重构需关注:
### 15.1 对应关系
| 原项目 | Go 重构 |
|--------|---------|
| Cloudflare Workers | Go HTTP server (net/http 或框架) |
| D1 (SQLite) | SQLite (mattn/go-sqlite3 或 modernc.org/sqlite) |
| Hono | 路由库 (chi/gin/echo 等) |
| Workers Cache API | 自建缓存层 (内存 LRU 或 Redis) |
| Worker Secrets | 环境变量/配置文件 |
| Static Assets | embed.FS 或独立静态服务 |
| `crypto.subtle.timingSafeEqual` | `crypto/subtle.ConstantTimeCompare` |
| `caches.default` | 需自建 HTTP 缓存层 |
### 15.2 核心移植清单
1. **数据库层**: 5 张表 schema 直接复用(SQLite 方言基本兼容),UPSERT 改为 Go 风格
2. **订阅管线**: `subscription.ts` 的解析/过滤/渲染逻辑需完整移植(最大工作量)
3. **协议解析**: 13 种 URI 协议 + 客户端配置行解析器
4. **过滤器**: 11 种 FilterRule 类型的执行逻辑
5. **渲染器**: 13 种目标格式的渲染器
6. **路由模板**: 6 套内置模板 + 模板渲染(proxy-group 展开)
7. **规则转换**: rules.ts 逻辑
8. **脚本系统**: 构建时注册表模式可保留(Go plugin 或代码生成),运行时 sandbox 需重新设计
9. **缓存**: 远程源 HTTP 缓存需自建(带 etag/304/stale 语义)
10. **并发控制**: worker 池模式可用 goroutine + semaphore 实现
11. **安全**: timingSafeEqual / 安全头 / 输入限制 / 流式读取
### 15.3 架构差异点
- **无 Cache API**: Go 版需自建缓存层(内存或 Redis),原项目依赖 Workers Cache API 的特性需要替代
- **无 waitUntil**: Go 版用 goroutine + context,缓存写入可异步但需管理生命周期
- **脚本沙箱**: 原项目用 JS 闭包在 Worker 中执行,Go 版需要 Go plugin / WASM / 嵌入式 JS 引擎 (goja) 等方案
- **静态资源**: 原 `run_worker_first` + SPA fallback 在 Go 中需显式实现路由优先级
- **D1 batch**: Go SQLite 用事务替代
---
## 16. 关键设计决策总结
1. **内置模板代码持有**: 避免迁移同步问题,所有部署即时获得模板修复
2. **源级 + 集合级双重过滤**: 先对每个源独立过滤,再对合并后的节点做集合级过滤
3. **ignoreFailed 语义**: 集合级配置,决定多源获取是 allSettled 还是 all
4. **created_at 排序**: 用时间戳整数排序,sort 操作批量更新 created_at
5. **scoped grants**: 分享令牌可限定资源+目标+过期,hash 存储
6. **有界回收站**: 50 条上限自动裁剪,防止无限增长
7. **构建时脚本**: 安全(无运行时 eval)但灵活性受限
8. **stale cache 降级**: 远程源失败时用过期缓存保证可用性
9. **目标兼容性过滤**: 不支持的协议类型自动跳过,而非报错
10. **临时覆盖**: 下载时可通过 query 临时替换源 URL/content/UA,用于调试
+950
View File
@@ -0,0 +1,950 @@
# Sub-Store Go 后端功能开发计划
> 技术栈:Go 1.23+ / Fiber v3 / Cobra / Viper / Logrus / SQLite
> 参考原项目:`ref/sub-store-cloudflare/cloudflare/` (TypeScript, 5404 行)
> 核心原则:优先使用成熟开源库,不自造轮子
---
## 1. 技术选型
### 1.1 核心框架
| 用途 | 库 | 版本 | 理由 |
|------|-----|------|------|
| HTTP 框架 | `github.com/gofiber/fiber/v3` | v3.x | 用户指定 |
| CLI 启动 | `github.com/spf13/cobra` | v1.x | 用户指定 |
| 配置管理 | `github.com/spf13/viper` | v1.x | 用户指定 |
| 日志 | `github.com/sirupsen/logrus` | v1.x | 用户指定 |
| SQLite 驱动 | `modernc.org/sqlite` | latest | 纯 Go,无 CGO,跨平台编译 |
| ORM/查询 | `github.com/jmoiron/sqlx` | latest | 轻量,sql 行映射,不引入 GORM 重量级 |
| 迁移 | `github.com/pressly/goose/v3` | latest | 成熟 SQL 迁移工具,//go:embed 嵌入 |
| 中文排序 | `golang.org/x/text` | latest | collate + language.SimplifiedChinese 拼音排序 |
### 1.2 功能库
| 用途 | 库 | 理由 |
|------|-----|------|
| YAML 序列化 | `gopkg.in/yaml.v3` | mihomo 配置和模板操作 |
| HTTP 客户端 | `github.com/go-resty/resty/v2` | 重试/超时/流式读取,比 net/http 更简洁 |
| UUID | `github.com/google/uuid` | grant/recycle ID 生成 |
| 验证 | `github.com/go-playground/validator/v10` | 请求体校验 |
| 正则 | `regexp` (标准库 RE2) | 注意:RE2 不支持反向引用/lookahead,原项目部分正则需适配 |
| Base64 | `encoding/base64` (标准库) | |
| Crypto | `crypto/subtle`, `crypto/sha256`, `crypto/rand` (标准库) | token 校验、hash、随机 |
| URL 解析 | `net/url` (标准库) | URI 协议解析 |
| JSON | `encoding/json` (标准库) | |
| 信号处理 | `os/signal` (标准库) | 优雅关闭 |
| 嵌入静态资源 | `embed` (标准库) | 前端 SPA |
### 1.3 原项目依赖对照
| 原项目 (TS) | Go 替代 | 说明 |
|-------------|---------|------|
| Hono | Fiber v3 | 路由 + 中间件 |
| D1 (SQLite) | modernc.org/sqlite + sqlx | 直接 SQLite |
| `caches.default` (Cache API) | SQLite `source_cache` 表 | 零额外依赖,重启不丢,WAL 读写不互斥 |
| `crypto.subtle.timingSafeEqual` | `crypto/subtle.ConstantTimeCompare` | |
| `c.req.json()` 等 | Fiber `c.Bind()` / `c.Body()` | |
| `ExecutionCtx.waitUntil` | `go func()` + context | goroutine 异步 |
| `env.DB.batch()` | sqlx `BeginTx()` + 多语句 | 事务 |
| `atob` / `btoa` | `encoding/base64` | |
| `yaml` npm 包 | `gopkg.in/yaml.v3` | |
| `json5` npm 包 | `encoding/json` + 宽容解析 | JSON5 场景仅用于代理节点数组解析,可用标准 JSON + 回退 |
| `structuredClone` | 深拷贝工具函数 | `reflect` 或手写 |
---
## 2. 项目结构
```
sub-store/
├── main.go # 入口,调用 cmd
├── cmd/
│ ├── root.go # cobra root command
│ ├── serve.go # sub-store serve (启动 HTTP 服务)
│ ├── migrate.go # sub-store migrate (执行数据库迁移)
│ └── version.go # sub-store version
├── internal/
│ ├── config/
│ │ └── config.go # viper 配置加载,结构体定义
│ ├── model/
│ │ ├── types.go # 核心类型:ProxyNode, FilterRule, SourceRecord, ...
│ │ ├── source.go # SubscriptionSource
│ │ ├── collection.go # SubscriptionCollection
│ │ ├── template.go # RoutingTemplate, RoutingTemplateConfig
│ │ ├── target.go # SubscriptionTarget 常量与别名
│ │ └── response.go # API 响应封装
│ ├── database/
│ │ ├── db.go # SQLite 连接初始化
│ │ ├── migrations/
│ │ │ ├── 0001_initial.sql
│ │ │ ├── 0002_runtime_cleanup.sql
│ │ │ └── 0003_compat_resources.sql
│ │ ├── source_repo.go # sources 表 CRUD
│ │ ├── collection_repo.go # collections 表 CRUD
│ │ ├── template_repo.go # templates 表 CRUD
│ │ ├── settings_repo.go # app_settings 表 CRUD
│ │ ├── grant_repo.go # download_grants 表 CRUD
│ │ ├── recycle_repo.go # recycle_bin 表 CRUD
│ │ └── cache_repo.go # source_cache 表 CRUD
│ ├── handler/
│ │ ├── env.go # GET /api/env
│ │ ├── source.go # /api/sources CRUD
│ │ ├── collection.go # /api/collections CRUD
│ │ ├── template.go # /api/templates CRUD
│ │ ├── settings.go # /api/settings
│ │ ├── storage.go # /api/storage 导出导入
│ │ ├── share.go # /api/shares CRUD
│ │ ├── recycle.go # /api/recycle-bin
│ │ ├── preview.go # /api/preview/*
│ │ ├── link.go # /api/link/*
│ │ ├── flow.go # /api/source/flow/*
│ │ ├── tools.go # /api/proxy/parse, /api/rule/parse, /api/utils/node-info
│ │ ├── script.go # /api/scripts (注册表元数据)
│ │ └── download.go # /download/* 公开下载
│ ├── middleware/
│ │ ├── auth.go # admin token 鉴权
│ │ ├── cors.go # CORS
│ │ ├── security.go # 安全响应头
│ │ ├── bodylimit.go # 请求体大小限制
│ │ └── downloadhost.go # 下载域名隔离
│ ├── service/
│ │ ├── subscription.go # 核心订阅处理管线
│ │ ├── fetcher.go # 远程源抓取 + 缓存查询
│ │ └── concurrency.go # 并发控制 worker pool
│ ├── proxy/
│ │ ├── parser.go # 统一入口:parseProxies
│ │ ├── format.go # 格式检测 + Base64 解码
│ │ ├── uri_parser.go # 13 种 URI 协议解析
│ │ ├── client_parser.go # QX/Surge/Loon 配置行解析
│ │ └── normalize.go # 节点归一化 + 预览 ID
│ ├── filter/
│ │ ├── pipeline.go # applyFilters 管线执行器
│ │ ├── include_exclude.go # include/exclude
│ │ ├── rename.go # rename
│ │ ├── delete_field.go # delete-field
│ │ ├── dedupe.go # dedupe (delete + rename)
│ │ ├── sort.go # sort + regex-sort
│ │ ├── flag.go # flag operator
│ │ ├── quick.go # quick settings
│ │ ├── resolve.go # DNS resolve
│ │ ├── custom.go # 声明式自定义规则链 (替代 JS 脚本)
│ │ └── util.go # getByPath/setByPath/compileRegex
│ ├── render/
│ │ ├── mihomo.go # mihomo/stash YAML
│ │ ├── surge.go # surge + surge-mac
│ │ ├── surfboard.go # surfboard
│ │ ├── loon.go # loon
│ │ ├── qx.go # quantumult x
│ │ ├── egern.go # egern YAML
│ │ ├── singbox.go # sing-box JSON
│ │ ├── uri.go # v2ray/uri/shadowrocket
│ │ └── json.go # JSON 原始输出
│ ├── rules/
│ │ └── converter.go # 分流规则转换
│ ├── template/
│ │ ├── builtin.go # 6 套内置模板定义
│ │ └── render.go # proxy-group 展开 + 模板渲染
│ └── util/
│ ├── token.go # token 生成 + hash + 验证
│ ├── path.go # 点号路径 get/set
│ ├── base64.go # base64 编解码
│ ├── ip.go # IPv4/IPv6 检测
│ └── flag.go # 国旗检测 + 移除
├── config/
│ └── config.example.yaml # 配置文件示例
├── web/
│ └── dist/ # 前端构建产物 (embed.FS)
└── go.mod
```
---
## 3. 配置设计
### 3.1 配置文件 (`config.yaml`)
```yaml
# 服务配置
server:
host: "0.0.0.0"
port: 3000
read_timeout: 30s
write_timeout: 60s
body_limit: 4194304 # 4 MiB
# 数据库
database:
path: "./data/sub-store.db"
# 鉴权
auth:
admin_token: "" # 必填,启动时检查
download_token: "" # 必填,启动时检查
download_hosts: [] # 纯下载域名列表
# 远程源抓取
fetcher:
default_timeout: 30s
default_user_agent: "clash.meta/v1.19.24"
default_flow_user_agent: "clash.meta/v1.19.24"
concurrency: 3
concurrency_wait: 0s
cache_ttl: 300s
cache_stale_on_error: true
max_source_urls: 8
max_response_bytes: 2097152 # 2 MiB
max_total_bytes: 12582912 # 12 MiB
# 回收站
recycle:
max_entries: 50
# 应用
app:
name: "Sub-Store"
version: "1.0.0"
```
### 3.2 Viper 加载策略
1. 默认值 (`config.SetDefault`)
2. 配置文件 (`config.yaml`,路径由 `--config` flag 指定,默认 `./config.yaml`)
3. 环境变量(前缀 `SUB_STORE_``.``_`,如 `SUB_STORE_AUTH_ADMIN_TOKEN`
4. 命令行 flagcobra
---
## 4. 数据库 Schema
直接复用原项目的 3 个 migration SQL,适配 modernc.org/sqlite 语法(基本兼容)。
### 4.1 表结构
```sql
-- 0001_initial.sql
CREATE TABLE IF NOT EXISTS sources (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'remote',
url TEXT NOT NULL DEFAULT '',
content TEXT NOT NULL DEFAULT '',
enabled INTEGER NOT NULL DEFAULT 1,
filters_json TEXT NOT NULL DEFAULT '[]',
meta_json TEXT NOT NULL DEFAULT '{}',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS collections (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
source_ids_json TEXT NOT NULL DEFAULT '[]',
filters_json TEXT NOT NULL DEFAULT '[]',
template_id TEXT NOT NULL DEFAULT 'acl4ssr-mihomo',
ignore_failed INTEGER NOT NULL DEFAULT 1,
enabled INTEGER NOT NULL DEFAULT 1,
meta_json TEXT NOT NULL DEFAULT '{}',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS templates (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
target TEXT NOT NULL DEFAULT 'mihomo',
config_json TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS app_settings (
id TEXT PRIMARY KEY,
value_json TEXT NOT NULL DEFAULT '{}',
updated_at INTEGER NOT NULL
);
-- 0003_compatibility_resources.sql
CREATE TABLE IF NOT EXISTS download_grants (
id TEXT PRIMARY KEY,
token_hash TEXT NOT NULL UNIQUE,
resource_type TEXT NOT NULL CHECK (resource_type IN ('source', 'collection')),
resource_id TEXT NOT NULL,
target TEXT NOT NULL DEFAULT '',
expires_at INTEGER,
enabled INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_download_grants_token_hash
ON download_grants(token_hash);
CREATE INDEX IF NOT EXISTS idx_download_grants_resource
ON download_grants(resource_type, resource_id);
CREATE TABLE IF NOT EXISTS recycle_bin (
id TEXT PRIMARY KEY,
resource_type TEXT NOT NULL CHECK (resource_type IN ('source', 'collection', 'template', 'share')),
resource_id TEXT NOT NULL,
snapshot_json TEXT NOT NULL,
deleted_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_recycle_bin_deleted_at
ON recycle_bin(deleted_at DESC);
-- 0004_source_cache.sql
CREATE TABLE IF NOT EXISTS source_cache (
cache_key TEXT PRIMARY KEY, -- sha256(url + "\n" + userAgent)
content TEXT NOT NULL, -- 远程源响应体文本
metadata TEXT NOT NULL DEFAULT '{}', -- JSON: subscription-userinfo / etag / last-modified / ...
cached_at INTEGER NOT NULL, -- 写入时间戳 (unix seconds)
ttl INTEGER NOT NULL DEFAULT 300 -- TTL 秒数
);
CREATE INDEX IF NOT EXISTS idx_source_cache_expiry
ON source_cache(cached_at + ttl);
```
> 注:原项目 0002 migration 是清理旧内置模板行,Go 版无需复制此历史包袱,初始 schema 直接不含内置模板行。
> source_cache 表替代原项目的 Workers Cache API,重启后缓存仍有效。
---
## 5. 功能模块开发计划
### Phase 0:项目骨架 (P0)
**目标**:可编译、可启动、可连接数据库
| 任务 | 产出 | 依赖库 |
|------|------|--------|
| go mod init + 依赖引入 | `go.mod` | 全部 |
| cobra root/serve/migrate/version 命令 | `cmd/*.go` | cobra |
| viper 配置加载 | `internal/config/config.go` | viper |
| logrus 日志初始化 | 日志格式化、级别、输出 | logrus |
| SQLite 连接初始化 | `internal/database/db.go` | modernc/sqlite, sqlx |
| goose 迁移执行 | `cmd/migrate.go` | goose |
| 3 个 migration SQL 文件 | `internal/database/migrations/` | |
| Fiber app 骨架 | 路由注册、健康检查 | fiber/v3 |
| 优雅关闭 | signal handling | |
**验证**`sub-store serve` 启动,`GET /health` 返回 200,数据库表创建成功。
---
### Phase 1:数据层 (P0)
**目标**6 张表的完整 CRUDRepository 模式
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| 类型定义 | `internal/model/*.go` | `types.ts` 全部类型 |
| Target 别名归一化 | `model/target.go` | `targets.ts` |
| SourceRepository | `source_repo.go` | `store.ts` listSources/getSource/upsertSource/deleteSource/sortSources |
| CollectionRepository | `collection_repo.go` | `store.ts` listCollections/... |
| TemplateRepository | `template_repo.go` | `store.ts` listTemplates/... + 内置模板合并 |
| SettingsRepository | `settings_repo.go` | `store.ts` getSettings/updateSettings (深合并) |
| GrantRepository | `grant_repo.go` | `compatibility-resources.ts` |
| RecycleRepository | `recycle_repo.go` | `compatibility-resources.ts` |
| 内置模板定义 | `internal/template/builtin.go` | `defaults.ts` 6 套模板 |
| ID 归一化 `toId()` | `util/` | `store.ts` toId() |
| 深合并 `mergeDeep()` | `util/` | `store.ts` mergeDeep() |
| 排序逻辑 (created_at = now+index) | Repository 层 | `store.ts` sortSources/sortCollections |
**关键设计**
- 内置模板在 Go 代码中定义为 `var BuiltinTemplates``ListTemplates` 合并内置 + DB 查询
- `upsert` 使用 `INSERT ... ON CONFLICT(id) DO UPDATE SET ...`
- `sortSources` 使用事务批量 `UPDATE SET created_at = ? WHERE id = ?`
- settings 深合并:递归 merge map[string]interface{}
**验证**:单元测试覆盖所有 Repository 的 CRUD + 排序 + 深合并。
---
### Phase 2:中间件 + 基础 API (P0)
**目标**:鉴权、CORS、安全头、基础 API 端点
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| Admin 鉴权中间件 | `middleware/auth.go` | `http.ts` requireAdmin + isTokenValid |
| Token 验证 (SHA-256 + ConstantTimeCompare) | `util/token.go` | `http.ts` isTokenValid |
| Bearer token 提取 | `middleware/auth.go` | `http.ts` getBearerToken |
| CORS 中间件 | `middleware/cors.go` | `http.ts` applyCorsHeaders |
| 安全头中间件 | `middleware/security.go` | `http.ts` applySecurityHeaders |
| Body limit 中间件 | `middleware/bodylimit.go` | `bodyLimit()` |
| 下载域名隔离 | `middleware/downloadhost.go` | `index.ts` hostname 检查 |
| 统一响应格式 | `model/response.go` | `http.ts` success/failed |
| `GET /api/env` | `handler/env.go` | `api.ts` envPayload |
| `GET /api/settings` + `PATCH /api/settings` | `handler/settings.go` | `api.ts` settings |
| `GET/POST /api/storage` | `handler/storage.go` | `api.ts` exportStorage/importStorage |
| 静态文件服务 | `embed.FS` + Fiber static | `index.ts` ASSETS fallback |
| 全局错误处理 | Fiber error handler | `index.ts` app.onError |
| SPA fallback | 未匹配路由返回 index.html | `notFound` handler |
**验证**`curl -H "Authorization: Bearer <token>" /api/env` 返回环境信息。
---
### Phase 3:源/集合/模板 API (P0)
**目标**:完整的管理 API,不含订阅处理
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| Source CRUD handler | `handler/source.go` | `api.ts` /api/sources 全部端点 |
| Collection CRUD handler | `handler/collection.go` | `api.ts` /api/collections |
| Template CRUD handler | `handler/template.go` | `api.ts` /api/templates |
| 排序 API | `handler/source.go` | `api.ts` PUT /api/sources, POST /api/sort/sources |
| ID 验证 `^[a-z0-9_-]{1,64}$` | handler 层 | `api.ts` validateRecordId |
| Source 验证 (remote URL / local content) | handler 层 | `api.ts` validateSource |
| Collection 验证 (sourceIds 引用检查) | handler 层 | `api.ts` validateCollection |
| 内置模板保护 (不可删改) | handler 层 | `api.ts` BUILTIN_TEMPLATE_IDS |
| 删除时归档到回收站 | handler 层 | `api.ts` archiveAndDeleteResource |
| 引用检查 (source 被 collection 引用时不可删) | handler 层 | `api.ts` deleteSource |
| Template config 解析 (JSON/YAML → 归一化) | handler 层 | `api.ts` parseTemplateConfig |
| Template alias 归一化 (mixed-port→mixedPort) | `template/` | `store.ts` normalizeMihomoTemplateConfig |
**验证**:完整 API 测试 — 创建源/集合/模板,查询、更新、排序、删除(含回收站归档)。
---
### Phase 4:代理协议解析器 (P0) ⭐ 核心模块
**目标**:解析 13 种 URI 协议 + 2 种客户端配置行格式
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| ProxyNode 类型 | `model/types.go` | `types.ts` ProxyNode |
| 格式检测 (JSON/YAML/URI 行) | `proxy/format.go` | `subscription.ts` looksLikeStructuredSubscription |
| Base64 解码 (maybe) | `proxy/format.go` | `subscription.ts` decodeMaybeBase64 |
| 统一入口 parseProxies | `proxy/parser.go` | `subscription.ts` parseProxies |
| JSON 代理数组解析 | `proxy/parser.go` | `subscription.ts` parseJsonProxies |
| YAML 代理数组解析 | `proxy/parser.go` | `subscription.ts` parseYamlProxies |
| URI 行解析入口 | `proxy/uri_parser.go` | `subscription.ts` parseProxyLines + parseProxyUri |
| `vless://` 解析 | `proxy/uri_parser.go` | `subscription.ts` parseVless |
| `vmess://` 解析 (base64 JSON) | `proxy/uri_parser.go` | `subscription.ts` parseVmess |
| `trojan://` 解析 | `proxy/uri_parser.go` | `subscription.ts` parseTrojan |
| `ss://` 解析 | `proxy/uri_parser.go` | `subscription.ts` parseShadowsocks |
| `ssr://` 解析 (base64) | `proxy/uri_parser.go` | `subscription.ts` parseShadowsocksR |
| `hysteria://` / `hy://` 解析 | `proxy/uri_parser.go` | `subscription.ts` parseHysteria |
| `hysteria2://` / `hy2://` 解析 | `proxy/uri_parser.go` | `subscription.ts` parseHysteria2 |
| `tuic://` 解析 | `proxy/uri_parser.go` | `subscription.ts` parseTuic |
| `anytls://` 解析 | `proxy/uri_parser.go` | `subscription.ts` parseAnytls |
| `socks://` / `socks5://` / `socks5+tls://` | `proxy/uri_parser.go` | `subscription.ts` parseSocks |
| `http://` / `https://` (HTTP proxy) | `proxy/uri_parser.go` | `subscription.ts` parseHttpProxy |
| `wireguard://` / `wg://` | `proxy/uri_parser.go` | `subscription.ts` parseWireGuard |
| QX 配置行解析 | `proxy/client_parser.go` | `subscription.ts` parseQxProxyLine |
| Surge/Loon 配置行解析 | `proxy/client_parser.go` | `subscription.ts` parseNamedClientProxyLine |
| 客户端选项解析 (CSV + key=value) | `proxy/client_parser.go` | `subscription.ts` splitClientCsv/parseClientOptions |
| 节点归一化 + stripUndefined | `proxy/normalize.go` | `subscription.ts` normalizeProxy/stripUndefined |
| 预览 ID 生成 | `proxy/normalize.go` | `subscription.ts` addPreviewIds/stableProxyId |
**正则兼容性注意**:原项目用 JS RegExpGo 用 RE2。RE2 不支持反向引用和 lookahead/lookbehind。需要检查以下正则:
- `looksLikeStructuredSubscription` 中的正则 — 应兼容
- `detectFlag` 中的地区匹配正则 — 应兼容
- `compileRegex``(?i)` 前缀 — Go 用 `(?i)` 内联标志,兼容
- filter 中的用户自定义正则 — 大部分兼容,极少数 lookahead 场景需文档说明限制
**验证**:为每种协议准备测试用例(URI → ProxyNode 结构比对),覆盖正常/异常/边界。
---
### Phase 5:过滤器管线 (P0) ⭐ 核心模块
**目标**11 种 FilterRule 类型的完整执行
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| 管线执行器 applyFilters | `filter/pipeline.go` | `subscription.ts` applyFilters |
| include/exclude | `filter/include_exclude.go` | `subscription.ts` matchFilter |
| rename | `filter/rename.go` | `subscription.ts` renameProxies |
| delete-field | `filter/delete_field.go` | `subscription.ts` deleteFieldMatches |
| dedupe (delete + rename 两种模式) | `filter/dedupe.go` | `subscription.ts` handleDuplicateProxies |
| sort (asc/desc/random) | `filter/sort.go` | `subscription.ts` sortProxies |
| regex-sort | `filter/sort.go` | `subscription.ts` regexSortProxies |
| flag (add/remove + 台湾旗) | `filter/flag.go` | `subscription.ts` flagProxies |
| quick (udp/tfo/scert/useless/vmess-aead) | `filter/quick.go` | `subscription.ts` applyQuickSettings |
| resolve (DNS DoH 解析) | `filter/resolve.go` | `subscription.ts` resolveProxyDomains |
| custom (声明式规则链,替代 JS 脚本) | `filter/custom.go` | `subscription.ts` applyScriptAction |
| 点号路径 get/set | `util/path.go` | `subscription.ts` getByPath/setByPath |
| 正则编译 (支持 `(?i)` 前缀) | `util/` | `subscription.ts` compileRegex |
| 国旗检测/移除 | `util/flag.go` | `subscription.ts` detectFlag/removeFlag |
| ensureUniqueProxyNames | `filter/pipeline.go` | `subscription.ts` ensureUniqueProxyNames |
| IP 地址检测 (IPv4/IPv6) | `util/ip.go` | `subscription.ts` isIpv4/isIpv6 |
| isUsefulProxy | `filter/quick.go` | `subscription.ts` isUsefulProxy |
| crypto/rand 安全随机 (shuffle) | `filter/sort.go` | `subscription.ts` secureRandomInt |
**resolve 过滤器细节**
- 5 个 DoH provider: Cloudflare / Google / Ali / Tencent / Custom
- DoH JSON API: `?name=<host>&type=A|AAAA`
- 并发解析 + 过滤模式 (disabled/removeFailed/IPOnly/IPv4Only/IPv6Only)
- 保留原域名为 servername/sni
**验证**:每种 filter 类型独立单元测试 + 管线顺序执行测试。
---
### Phase 6:目标渲染器 (P0) ⭐ 核心模块
**目标**13 种客户端格式的完整渲染
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| 渲染入口 renderTarget | `render/` 分发 | `subscription.ts` renderTarget/renderBuildTarget |
| mihomo YAML (proxy-groups 展开 + 模板) | `render/mihomo.go` | `subscription.ts` renderMihomoYaml |
| proxy-group $all 展开 + filter 正则 | `render/mihomo.go` | `subscription.ts` expandGroupProxies |
| surge 文本行 | `render/surge.go` | `subscription.ts` toSurgeProxyLine |
| surge-mac (ssh/h2-connect/snell) | `render/surge.go` | `subscription.ts` toSurgeMacProxyLine |
| surfboard (ss/vmess/trojan/http/socks5) | `render/surfboard.go` | `subscription.ts` toSurfboardProxyLine |
| loon 文本行 | `render/loon.go` | `subscription.ts` toLoonProxyLine |
| quantumult x 文本行 | `render/qx.go` | `subscription.ts` toQxProxyLine |
| egern YAML | `render/egern.go` | `subscription.ts` toEgernProxy |
| sing-box JSON (含 PROXY/AUTO/DIRECT/REJECT) | `render/singbox.go` | `subscription.ts` renderSingBoxJson |
| URI 行 (13 种协议) | `render/uri.go` | `subscription.ts` toProxyUri |
| v2ray (base64 编码 URI 行) | `render/uri.go` | `subscription.ts` base64Utf8 |
| JSON 原始 | `render/json.go` | `subscription.ts` JSON.stringify |
| 目标兼容性过滤 isTargetCompatible | `render/` | `subscription.ts` isTargetCompatible |
| Content-Type 分配 | `render/` | `subscription.ts` getTargetContentType |
| 文本代理行工具 (joinTextProxy/quoteTextValue/sanitizeName) | `render/` | `subscription.ts` joinTextProxy 等 |
**验证**:每种目标格式用固定 ProxyNode 集合渲染,比对输出。sing-box 输出需通过 JSON schema 验证。
---
### Phase 7:订阅处理管线 (P0) ⭐ 核心模块
**目标**:从源/集合到最终输出的完整管线
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| buildSubscriptionResult 入口 | `service/subscription.go` | `subscription.ts` buildSubscriptionResult |
| getSources (单源/集合模式) | `service/subscription.go` | `subscription.ts` getSources |
| loadProxyNodes (并发 + ignoreFailed) | `service/subscription.go` | `subscription.ts` loadProxyNodes |
| 本地源加载 | `service/subscription.go` | `subscription.ts` loadSubscriptionRaw (local 分支) |
| 远程源 URL 分割 (多行多 URL) | `service/fetcher.go` | `subscription.ts` splitSourceUrls |
| 远程源抓取 (HTTP + 缓存 + 304 + stale) | `service/fetcher.go` | `subscription.ts` fetchSubscriptionUrl |
| UA 选择逻辑 | `service/fetcher.go` | `subscription.ts` getSourceUserAgent |
| 并发控制 worker pool | `service/concurrency.go` | `subscription.ts` runWithConcurrency/runSettledWithConcurrency |
| 缓存层 (SQLite source_cache 表 + SHA-256 key) | `database/cache_repo.go` | `subscription.ts` remoteCacheKey/safeCacheMatch/safeCachePut |
| 缓存元数据存储 (subscription-userinfo 等) | `database/cache_repo.go` | `subscription.ts` setInternalMetadataHeader |
| 响应元数据选择 | `service/subscription.go` | `subscription.ts` selectResponseMetadata |
| 元数据从 source/Response 提取 | `service/subscription.go` | `subscription.ts` metadataFromSource/metadataFromResponse |
| 流式响应读取 (限字节) | `service/fetcher.go` | `read.ts` readResponseText |
| 预览 (previewSource/previewCollection) | `service/subscription.go` | `subscription.ts` previewSubscription/previewSourceContent |
| 一次性转换 (convertSubscriptionContent) | `service/subscription.go` | `subscription.ts` convertSubscriptionContent |
| 下载链接构建 | `handler/link.go` | `api.ts` buildDownloadLink |
| 流量信息查询 (fetchFlowHeaders + parse) | `handler/flow.go` | `api.ts` fetchFlowHeaders/parseFlowHeaders |
**缓存设计**(替代 Workers Cache API):
- 使用 SQLite `source_cache` 表持久化缓存(无需额外依赖,重启不丢)
- key: `sha256(url + "\n" + userAgent)` → 存储内容 + 元数据 JSON
- TTL: 配置项 `cache_ttl`,默认 300s
- 304 Not Modified: HTTP 请求携带 `If-None-Match` / `If-Modified-Since`
- stale on error: 配置项 `cache_stale_on_error`
- 异步写缓存: `go func()` (替代 `waitUntil`)
- 定期清理: `DELETE FROM source_cache WHERE cached_at + ttl < strftime('%s','now')`
**并发设计**
```go
// runWithConcurrency: 固定 worker 池
func RunWithConcurrency[T any](tasks []func() (T, error), concurrency int, wait time.Duration) ([]T, error)
// runSettledWithConcurrency: allSettled 语义 (失败跳过)
func RunSettledWithConcurrency[T any](tasks []func() (T, error), concurrency int, wait time.Duration) ([]Result[T])
```
**验证**:端到端测试 — 创建 remote 源 + local 源 + 集合,下载 mihomo/sing-box/uri 格式,验证节点数量和内容正确。
---
### Phase 8:下载路由 + 分享/回收站 API (P1)
**目标**:公开下载端点 + 完整的分享/回收站功能
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| `GET /download/collection/:name/:target?/:token?` | `handler/download.go` | `download.ts` |
| `GET /download/source/:name/:target?/:token?` | `handler/download.go` | `download.ts` |
| Download token 验证 (全局 + scoped grant) | `handler/download.go` | `download.ts` rejectInvalidDownloadToken |
| Target 推断 (显式 + UA 推断) | `handler/download.go` | `download.ts` getDownloadTarget |
| 临时源覆盖 (url/content/ua query) | `handler/download.go` | `download.ts` getTemporarySourceOverride |
| 响应头设置 (content-type, userinfo, etc.) | `handler/download.go` | `download.ts` renderDownload |
| Share CRUD handler | `handler/share.go` | `api.ts` /api/shares |
| Grant token 生成 (24 byte random → base64url) | `util/token.go` | `compatibility-resources.ts` randomToken |
| Grant token hash (SHA-256 hex) | `util/token.go` | `compatibility-resources.ts` sha256Hex |
| Scoped download 授权检查 | `handler/download.go` | `compatibility-resources.ts` authorizeScopedDownload |
| Recycle bin CRUD handler | `handler/recycle.go` | `api.ts` /api/recycle-bin |
| 回收站恢复逻辑 (source/collection/template/share) | `handler/recycle.go` | `api.ts` restore |
| 回收站自动裁剪 (50 条上限) | `recycle_repo.go` | `compatibility-resources.ts` archiveAndDeleteResource |
**验证**:创建分享令牌 → 用令牌下载 → 验证 scoped 限制(资源/目标/过期)。删除源 → 回收站恢复。
---
### Phase 9:工具 API (P1)
**目标**:格式转换器、规则转换、节点信息
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| `POST /api/proxy/parse` | `handler/tools.go` | `api.ts` proxy/parse |
| `POST /api/rule/parse` | `handler/tools.go` | `api.ts` rule/parse |
| 规则转换器 | `rules/converter.go` | `rules.ts` convertRules |
| 规则类型别名归一化 | `rules/converter.go` | `rules.ts` KIND_ALIASES |
| QX 规则特殊处理 | `rules/converter.go` | `rules.ts` qxKind |
| `POST /api/utils/node-info` | `handler/tools.go` | `api.ts` utils/node-info |
| `GET /api/scripts` | `handler/script.go` | `api.ts` scripts |
**规则转换细节**
- 支持规则类型: DOMAIN, DOMAIN-SUFFIX, DOMAIN-KEYWORD, IP-CIDR, IP-CIDR6, GEOIP, GEOSITE, PROCESS-NAME, DST-PORT, MATCH
- 别名映射: HOST→DOMAIN, FINAL→MATCH, DEST-PORT→DST-PORT 等
- QX 特殊: DOMAIN→HOST, IP-CIDR6→IP6-CIDR, MATCH→FINAL
- 输入格式: YAML (`rules:` 列表) 或纯文本行
**验证**:各目标格式规则转换测试 + 节点信息查询测试。
---
### Phase 10:声明式自定义规则 (P2) — 合并入 Phase 5
> 原计划的 goja JS 脚本引擎已移除。原项目的脚本能力(filter/operator 自定义 JS)改为声明式规则链实现,归入 Phase 5 过滤器管线的 `custom` 类型。
**目标**:用声明式规则组合替代 JS 脚本,覆盖原脚本的所有实际使用场景
**设计**`custom` filter 类型,内部为有序规则链,每条规则是一个原子操作:
```json
{
"type": "custom",
"args": {
"rules": [
{ "action": "set", "field": "name", "mode": "regex-replace", "pattern": "【.*?】", "replacement": "" },
{ "action": "include", "field": "name", "pattern": "香港|日本" },
{ "action": "set", "field": "udp", "value": true },
{ "action": "delete", "field": "tls.skipCertVerify" },
{ "action": "rename", "template": "{country}-{server}" }
]
}
}
```
| 任务 | 产出 | 原项目对照 |
|------|------|-----------|
| custom filter 类型实现 | `filter/custom.go` | `scripts.ts` applyScriptAction |
| 原子操作:set/delete/include/exclude | `filter/custom.go` | 脚本中对节点字段的操作 |
| 原子操作:regex-replace (字段值正则替换) | `filter/custom.go` | 脚本中 `p.name.replace(/.../, ...)` |
| 原子操作:rename (模板化重命名) | `filter/custom.go` | 脚本中节点名拼接逻辑 |
| 原子操作:条件分支 (when 字段匹配) | `filter/custom.go` | 脚本中 if/else 逻辑 |
| 参数验证 | `filter/custom.go` | `scripts.ts` validateScriptActions |
| `$arguments` 支持 | custom args 直接映射 | `scripts.ts` $arguments |
| ProxyUtils 等价能力 | 复用 util/flag.go, util/ip.go, util/base64.go | `scripts.ts` PROXY_UTILS |
**与原项目差异**
- 原项目:用户编写 JS 函数,运行时闭包/eval 执行
- Go 版:用户配置声明式规则链,Go 原生执行
- 优势:无任意代码执行风险、无需 JS 引擎、前端可构建可视化规则编辑器、性能更高
- 限制:不支持 JS 的完整编程能力(循环、递归、复杂条件组合),但实际场景中 99% 的脚本都是字段操作 + 正则 + 条件过滤,声明式规则完全覆盖
**验证**:将原项目常见脚本用例转换为声明式规则,验证等效输出。
---
### Phase 11:集成与打磨 (P1)
| 任务 | 说明 |
|------|------|
| 前端 embed.FS 集成 | `//go:embed web/dist`Fiber static + SPA fallback |
| 请求日志中间件 | logrus 结构化日志 |
| Panic recovery 中间件 | Fiber Recover |
| 速率限制 (可选) | Fiber limiter 中间件 |
| 配置热更新 (可选) | viper WatchConfig |
| 健康检查端点 | `GET /health` |
| pprof 端点 (可选) | debug 模式 |
| 集成测试 | 端到端 API 测试 |
| Dockerfile | 多阶段构建 |
| systemd service 文件 | 可选,用户提供时生成 |
---
## 6. API 端点完整清单
### 管理 API (需 Admin Token)
| 方法 | 路径 | Phase | 说明 |
|------|------|-------|------|
| GET | `/api/env` | 2 | 环境信息 |
| GET | `/api/scripts` | 9 | 脚本注册表 (仅元数据,供前端展示可选脚本) |
| GET | `/api/settings` | 2 | 应用设置 |
| PATCH | `/api/settings` | 2 | 更新设置 |
| GET | `/api/storage` | 2 | 导出备份 |
| POST | `/api/storage` | 2 | 导入备份 |
| GET | `/api/sources` | 3 | 源列表 |
| POST | `/api/sources` | 3 | 创建源 |
| PUT | `/api/sources` | 3 | 排序源 |
| POST | `/api/sort/sources` | 3 | 排序源 (兼容) |
| GET | `/api/sources/:name` | 3 | 查询单源 |
| PATCH | `/api/sources/:name` | 3 | 更新源 |
| DELETE | `/api/sources/:name` | 3 | 删除源 |
| GET | `/api/collections` | 3 | 集合列表 |
| POST | `/api/collections` | 3 | 创建集合 |
| PUT | `/api/collections` | 3 | 排序集合 |
| POST | `/api/sort/collections` | 3 | 排序集合 (兼容) |
| GET | `/api/collections/:name` | 3 | 查询单集合 |
| PATCH | `/api/collections/:name` | 3 | 更新集合 |
| DELETE | `/api/collections/:name` | 3 | 删除集合 |
| GET | `/api/templates` | 3 | 模板列表 |
| POST | `/api/templates` | 3 | 创建模板 |
| GET | `/api/templates/:name` | 3 | 查询模板 |
| PATCH | `/api/templates/:name` | 3 | 更新模板 |
| DELETE | `/api/templates/:name` | 3 | 删除模板 |
| GET | `/api/shares` | 8 | 分享列表 |
| POST | `/api/shares` | 8 | 创建分享 |
| PATCH | `/api/shares/:id` | 8 | 更新分享 |
| DELETE | `/api/shares/:id` | 8 | 删除分享 |
| GET | `/api/recycle-bin` | 8 | 回收站列表 |
| DELETE | `/api/recycle-bin/:id` | 8 | 彻底删除 |
| POST | `/api/recycle-bin/:id/restore` | 8 | 恢复 |
| POST | `/api/preview/source` | 7 | 源预览 |
| POST | `/api/preview/collection` | 7 | 集合预览 |
| GET | `/api/link/source/:name` | 7 | 源下载链接 |
| GET | `/api/link/collection/:name` | 7 | 集合下载链接 |
| GET | `/api/source/flow/:name` | 7 | 流量信息 |
| POST | `/api/proxy/parse` | 9 | 代理转换 |
| POST | `/api/rule/parse` | 9 | 规则转换 |
| POST | `/api/utils/node-info` | 9 | 节点信息 |
### 公开下载 API (需 Download Token 或 Scoped Grant)
| 方法 | 路径 | Phase | 说明 |
|------|------|-------|------|
| GET | `/download/collection/:name/:target?/:token?` | 8 | 下载集合 |
| GET | `/download/source/:name/:target?/:token?` | 8 | 下载源 |
### 其他
| 方法 | 路径 | Phase | 说明 |
|------|------|-------|------|
| GET | `/health` | 0 | 健康检查 |
| GET | `/*` | 11 | 静态文件 (SPA fallback) |
---
## 7. 开发顺序与依赖关系
```
Phase 0: 项目骨架
├──→ Phase 1: 数据层 (model + repository)
│ │
│ └──→ Phase 2: 中间件 + 基础 API
│ │
│ └──→ Phase 3: 源/集合/模板 API
│ │
│ ├──→ Phase 4: 代理协议解析器 ─────┐
│ │ │
│ ├──→ Phase 5: 过滤器管线 ────────┤
│ │ │
│ └──→ Phase 6: 目标渲染器 ────────┤
│ │
│ ↓
│ Phase 7: 订阅处理管线 (依赖 4+5+6)
│ │
│ ├──→ Phase 8: 下载 + 分享/回收站
│ │
│ └──→ Phase 9: 工具 API
│ │
└────────────────────────────────────┴──→ Phase 10: 声明式规则 (Phase 5 扩展)
└──→ Phase 11: 集成打磨
```
**关键路径**: Phase 0 → 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 11
**可并行**:
- Phase 4/5/6 可并行开发(三者独立,7 依赖三者完成)
- Phase 9 可与 Phase 8 并行
---
## 8. 测试策略
### 8.1 测试分层
| 层级 | 范围 | 工具 |
|------|------|------|
| 单元测试 | 每个函数/方法 | `testing` + `testify/assert` |
| Repository 测试 | 临时 SQLite 文件 | `testing` + sqlx |
| API 集成测试 | HTTP 端到端 | `httptest` + Fiber test utils |
| 协议解析测试 | 每种 URI/客户端格式 | 表驱动测试 |
| 渲染测试 | 每种目标格式 | 固定输入 + golden file 比对 |
| 端到端测试 | 完整订阅流程 | 测试 HTTP server + 真实 HTTP 请求 |
### 8.2 关键测试用例
**协议解析** (Phase 4):
```
vless://uuid@host:443?security=reality&pbk=xxx&fp=chrome&type=ws#name
→ ProxyNode{type:"vless", server:"host", port:443, uuid:"uuid", ...}
```
**过滤器** (Phase 5):
```
input: [节点A(香港), 节点B(日本), 节点C(美国)]
filter: {type:"include", field:"name", pattern:"香港|日本"}
output: [节点A, 节点B]
```
**渲染** (Phase 6):
```
input: [ProxyNode{type:"ss", name:"test", server:"1.2.3.4", port:8388, cipher:"aes-256-gcm", password:"pass"}]
target: mihomo
output: YAML with proxies: [{name:"test", type:"ss", ...}]
```
**端到端** (Phase 7):
```
1. 创建 remote source (url: mock server)
2. 创建 collection (sourceIds: [source.id])
3. GET /download/collection/:id/mihomo?token=xxx
4. 验证 YAML 输出包含正确的 proxy 节点
```
### 8.3 测试数据
- 13 种协议各准备 2-3 个 URI 测试样本
- 每种 filter 类型准备输入/输出 golden case
- 每种渲染目标准备 golden output file
- Mock 远程订阅服务器返回固定内容
---
## 9. 部署方案
### 9.1 单二进制
```bash
# 构建
go build -o sub-store ./main.go
# 运行
./sub-store serve --config config.yaml
# 迁移
./sub-store migrate --config config.yaml
```
### 9.2 配置
- `--config` flag 指定配置文件路径
- 环境变量 `SUB_STORE_*` 覆盖配置
- admin_token / download_token 必填,启动时检查
### 9.3 Docker (可选,用户需求时)
```dockerfile
# 多阶段构建
FROM golang:1.23 AS builder
COPY . /src
WORKDIR /src
RUN CGO_ENABLED=0 go build -o /sub-store ./main.go
FROM alpine:latest
COPY --from=builder /sub-store /sub-store
COPY config.yaml /config.yaml
EXPOSE 3000
ENTRYPOINT ["/sub-store", "serve", "--config", "/config.yaml"]
```
> 注:使用 modernc.org/sqlite (纯 Go)`CGO_ENABLED=0` 可静态编译。
---
## 10. 风险与注意事项
### 10.1 正则兼容性
Go 的 `regexp` 包使用 RE2 引擎,不支持:
- 反向引用 (`\1`)
- Lookahead (`(?=...)`, `(?!...)`)
- Lookbehind (`(?<=...)`, `(?<!...)`)
原项目中用户自定义的 filter 正则可能使用这些特性。解决方案:
- 文档说明限制
- 对于 `(?i)` 前缀,Go 支持 `(?i)` 内联标志
- 极端情况考虑引入 `github.com/dlclark/regexp2` (完整 PCRE 支持) 作为回退
### 10.2 JSON5 解析
原项目用 `json5` npm 包解析代理节点 JSON。Go 标准库 `encoding/json` 不支持 JSON5(单引号、尾逗号、注释等)。
解决方案:
- 大多数订阅返回标准 JSON,标准库足够
- 对于需要 JSON5 兼容的场景,使用 `github.com/tidwall/gjson` 进行宽容解析,或 `github.com/yourbasic/json5`
### 10.3 自定义规则安全性
声明式规则链不存在脚本注入风险(无任意代码执行)。需注意:
- 正则 ReDoS:对用户输入的正则设置编译超时或回溯限制
- 规则链深度:限制最大规则数量(默认 32 条)
### 10.4 并发安全
- SQLite 写锁:modernc.org/sqlite 支持 WAL 模式,读写可并发
- `SetMaxOpenConns(1)`:避免多连接 `database is locked`WAL 下读不阻塞写
- `PRAGMA busy_timeout=5000`:写锁等待 5 秒
- source_cache 表:WAL 模式下读写不互斥,缓存读写不影响其他表操作
- 异步缓存写入:`sync.WaitGroup` + `context` 管理生命周期,graceful shutdown 时等待完成(5s 超时)
- 缓存操作错误吞咽:`recover()` + 日志,不影响订阅生成
- Repository 层:无状态,安全
- 全局状态:仅配置和缓存,均为并发安全
### 10.5 ProxyNode 类型与深拷贝
ProxyNode 定义为 `type ProxyNode map[string]any`(详见 `docs/review-resolutions.md` #1)。
- 深拷贝:`json.Marshal``json.Unmarshal`(简单可靠,性能可接受)
- 类型安全访问:`util/path.go` 提供 `GetByPath` / `SetByPath` / `GetString` / `GetInt` / `GetBool` 等辅助函数
- `stripUndefined`:遍历 map,删除值为 `nil``""` 的 key
---
## 11. 原项目代码量评估
| 模块 | 原项目行数 | 预估 Go 行数 | 复杂度 |
|------|-----------|-------------|--------|
| types.ts | 188 | ~250 | 低 |
| store.ts | 644 | ~600 | 中 |
| subscription.ts | 2430 | ~3000 | ⭐ 极高 |
| rules.ts | 159 | ~180 | 低 |
| defaults.ts | 246 | ~300 | 低 |
| targets.ts | 59 | ~80 | 低 |
| scripts.ts | 157 | ~100 | 低 (custom 规则替代,仅保留注册表元数据) |
| compatibility-resources.ts | 261 | ~300 | 中 |
| http.ts | 77 | ~100 | 低 |
| limits.ts | 6 | ~10 | 低 |
| read.ts | 44 | ~50 | 低 |
| api.ts (routes) | 896 | ~1000 | 中 |
| download.ts | 165 | ~200 | 中 |
| index.ts | 72 | ~150 | 低 |
| **合计** | **5404** | **~6320** | |
| + config/cmd/middleware/util | — | ~1500 | |
| **总计** | | **~7800** | |
---
## 12. 里程碑
| 里程碑 | Phase | 交付物 | 验证标准 |
|--------|-------|--------|----------|
| M1: 可启动 | 0-1 | 二进制可运行,DB 初始化 | `serve` 启动,表创建 |
| M2: 基础 API | 2-3 | 源/集合/模板 CRUD | API 测试全通过 |
| M3: 协议解析 | 4 | 13 种 URI 解析 | 解析测试全通过 |
| M4: 过滤+渲染 | 5-6 | 11 种 filter + 13 种渲染 | 单元测试全通过 |
| M5: 订阅管线 | 7 | 完整下载流程 | 端到端测试通过 |
| M6: 完整后端 | 8-9 | 下载/分享/回收站/工具 | 全部 API 测试通过 |
| M7: 声明式规则 | 10 | custom filter 规则链 | 规则转换测试通过 |
| M8: 生产就绪 | 11 | 前端集成/部署/Docker | 集成测试通过 |
+635
View File
@@ -0,0 +1,635 @@
# Sub-Store 前端功能规划文档
> 基于原项目 `ref/sub-store-cloudflare/frontend` 的交互分析,规划 Go 重构版前端。
> 核心原则:**上下布局,PC 优先**,弃用移动端 swipe/popup/floating-button 交互模式。
---
## 1. 原前端交互痛点分析
### 1.1 移动端优先的架构问题
| 痛点 | 原项目实现 | PC 上的问题 |
|------|-----------|-------------|
| **Swipe 手势操作** | `SubListItem``nut-swipe` 左/右滑动暴露 复制/下载/删除 按钮 | 桌面端无触屏,swipe 不触发,操作隐藏在不可见区域 |
| **底部 TabBar** | `TabBar.vue` 3 个 tab 固定底部,768px+ 才切换为侧边栏 | PC 上侧边栏是浮动定位 (`position: fixed`),非真正双栏布局 |
| **底部 Popup 弹窗** | 添加订阅、模板编辑、目标选择均用 `nut-popup position="bottom"` | 占满下半屏,PC 上极不协调,无法利用横向空间 |
| **浮动按钮** | `nut-drag` 可拖拽的刷新/添加按钮,固定在屏幕边缘 | PC 上浮动按钮遮挡内容,拖拽行为多余 |
| **单列滚动列表** | 源和集合在同一个页面上下排列,tag 筛选条横排 | PC 宽屏下大量留白,列表项信息密度低 |
| **编辑器 Tab 横滚** | SubEditor 的 display/content/actions 三个 tab 横向排列 | PC 上应该用左右分栏或垂直 Tab |
| **SubEditor 巨型组件** | 单文件 2174 行,混合 source/collection 两种编辑逻辑 | 维护困难,状态管理混乱 |
| **全屏预览覆盖** | Preview.vue 用 `position: fixed; inset: 0` 全屏覆盖 | PC 上应该是侧面板或分栏,不离开当前上下文 |
| **无键盘快捷键** | 所有操作只能点击 | PC 用户期望 Ctrl+S 保存、Esc 关闭等 |
| **无右键菜单** | 操作按钮散落在列表项内部 | PC 用户期望右键弹出操作菜单 |
| **Tools 页面粗糙** | 转换器/分享/回收站堆在一个页面,原生 HTML 控件 | 功能孤立,与主流程割裂 |
### 1.2 信息架构问题
- 源 (Source) 和集合 (Collection) 混在同一列表,靠标题区分,概念不清
- 模板管理藏在 "My" 设置页,与集合编辑中的模板选择脱节
- 分享 (Share) 和回收站 (Recycle Bin) 被丢进 Tools 工具页,而非关联到对应资源
- 预览/对比需要跳路由或弹全屏,丢失列表上下文
- 下载链接生成操作分散:列表项有复制链接,编辑页有链接生成,Tools 有分享创建
---
## 2. 整体布局设计:上下布局
```
┌──────────────────────────────────────────────────────────────────┐
│ TopBar (h: 48px) │
│ [Logo] Sub-Store [搜索框] [刷新] [主题] [语言] [设置] │
├──────────────────────────────────────────────────────────────────┤
│ NavBar (h: 40px) │
│ [订阅源] [集合] [模板] [工具] [回收站] [+ 新建 ▾] │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Main Content Area (flex: 1, overflow: auto) │
│ │
│ ┌──────────────────────┐ ┌──────────────────────────────────┐ │
│ │ Left Panel │ │ Right Panel / Detail │ │
│ │ (列表区, 固定宽度) │ │ (编辑/预览/详情, flex: 1) │ │
│ │ w: 360px │ │ │ │
│ │ │ │ │ │
│ │ [搜索] [筛选 tag] │ │ │ │
│ │ ┌─────────────────┐ │ │ │ │
│ │ │ Source/Col Card │ │ │ │ │
│ │ ├─────────────────┤ │ │ │ │
│ │ │ Source/Col Card │ │ │ │ │
│ │ ├─────────────────┤ │ │ │ │
│ │ │ ... │ │ │ │ │
│ │ └─────────────────┘ │ │ │ │
│ └──────────────────────┘ └──────────────────────────────────┘ │
│ │
├──────────────────────────────────────────────────────────────────┤
│ StatusBar (h: 28px, 可选) │
│ ● 已连接 · 12 源 · 3 集合 · v1.0.0 [最后同步: 2分钟前] │
└──────────────────────────────────────────────────────────────────┘
```
### 2.1 布局规则
- **TopBar**: 始终固定顶部,包含全局搜索、全局操作(刷新/主题/语言/设置)
- **NavBar**: 水平标签栏,切换主功能区域,当前页高亮
- **Main Content**: 根据当前 Tab 显示不同内容,内部可左右分栏
- **StatusBar**: 底部状态栏,显示连接状态/数据统计/同步时间(可选关闭)
- **最小宽度**: 1024px(PC 优先),窄屏降级为单列+抽屉
### 2.2 响应式策略
| 宽度 | 布局 | 说明 |
|------|------|------|
| ≥1280px | 双栏 (列表+详情) | 详情区常驻右侧 |
| 1024-1279px | 双栏 (列表+详情) | 列表区缩窄至 320px |
| 768-1023px | 单栏 + 右抽屉 | 详情从右侧滑入抽屉 |
| <768px | 单栏 + 全屏推入 | 详情全屏推入(移动端兼容) |
---
## 3. 功能模块规划
### 3.1 订阅源管理 (Sources)
#### 3.1.1 源列表 (Left Panel)
**功能点**:
- 卡片式列表,每张卡片显示:名称、类型徽章 (remote/local)、启用开关、流量摘要、标签
- 顶部工具栏:搜索框(实时过滤名称/URL)、标签筛选条、排序按钮
- 列表项右键菜单:编辑、复制链接、预览、克隆、删除、创建分享
- 拖拽排序(HTML5 drag API,非 vuedraggable touch 模式)
- 选中状态:单击选中(高亮),双击打开编辑
- 多选:Ctrl+Click 多选,批量启用/禁用/删除/导出
- 列表底部:显示总数和已加载数
**卡片信息层次**:
```
┌──────────────────────────────────────┐
│ [●] 机场A [remote] │
│ https://example.com/sub... [⚡] │
│ ↑ 12.3GB ↓ 89.7GB / 100GB │
│ [HK] [JP] [⋮] │
└──────────────────────────────────────┘
```
#### 3.1.2 源编辑 (Right Panel / Drawer)
**布局**: 垂直分区,非 Tab 切换
**区域 1 - 基本信息**:
- ID(创建后只读)、显示名、备注、标签(逗号分隔或 chip 输入)
- 图标 URL、图标彩色开关
**区域 2 - 数据源**:
- 类型切换:Remote / Localradio button 横排)
- Remote: URL 文本域(支持多行多 URL)、UA 输入、passThrough UA 开关、subUserinfo 输入
- Local: 全屏代码编辑器按钮 + 文件导入按钮 + 内容验证按钮 + CodeMirror 编辑器(内嵌)
**区域 3 - 节点处理** (Filter Pipeline):
- 可视化流水线编辑器,每个 filter 是一个可折叠卡片
- 拖拽排序 filter 执行顺序
- 每个 filter 类型有专属配置表单:
- **Region Filter**: 复选框组 (HK/JP/SG/US/UK/DE/KR/TW)keep/exclude 开关
- **Type Filter**: 复选框组 (ss/vmess/vless/trojan/...)keep/exclude 开关
- **Regex Filter**: 正则输入框(支持多个),keep/exclude 开关,字段选择
- **Regex Rename**: 表格式输入 (表达式 → 替换为)
- **Regex Delete**: 正则输入框(支持多个),字段选择
- **Regex Sort**: 优先级正则列表 + 方向选择
- **Handle Duplicate**: 去重字段选择、动作 (delete/rename)、后缀模板、连接符、位置
- **Sort**: 方向选择 (asc/desc/random)
- **Flag Operator**: 模式 (add/remove)、台湾旗配置
- **Quick Setting**: UDP/TFO/scert/useless/vmess-aead 下拉选择
- **Resolve Domain**: DNS provider 选择、记录类型、过滤模式、自定义 URL、EDNS、并发数
- **Script**: 脚本选择下拉(从注册表加载)、参数表单(根据 metadata.parameters 动态生成)
- 新增 filter 按钮 → 下拉菜单选择类型
- 每个 filter 可启用/禁用(开关)、删除(按钮)
**区域 4 - 操作栏** (底部固定):
- [预览] [对比] [保存] [另存为]
- Ctrl+S 快捷键保存
#### 3.1.3 源预览
**功能**: 调用 `/api/preview/source` 显示原始节点 vs 处理后节点
**布局**: 右侧面板内的分栏视图
- 左栏:原始节点列表(可搜索、可排序)
- 右栏:处理后节点列表
- 顶部:节点数量统计 (原始 N → 处理后 M, 跳过 K)
- 每个节点显示:名称、类型、服务器:端口、协议详情
- 支持展开节点查看完整字段
#### 3.1.4 源下载链接
**功能**: 生成各目标的下载链接
**交互**: 点击列表项的"复制链接"按钮 → 弹出链接面板(右侧抽屉或 popover)
- 目标选择:13 种目标客户端(下拉或 grid 按钮)
- 链接显示:只读文本框 + 复制按钮
- 二维码生成(可选)
- 临时覆盖选项:URL/content/UA 输入框(展开式高级选项)
---
### 3.2 集合管理 (Collections)
#### 3.2.1 集合列表 (Left Panel)
**与源列表同结构**,但卡片额外显示:
- 包含的源数量/名称摘要
- 关联的模板名
- ignoreFailed 状态徽章
#### 3.2.2 集合编辑 (Right Panel / Drawer)
**区域 1 - 基本信息**: 同源编辑
**区域 2 - 集合配置**:
- 模板选择:下拉选择(内置 6 套 + 自定义),旁边"管理模板"链接
- 源选择器:双栏穿梭框 (Transfer) 布局
- 左栏:所有可用源(可搜索、可按 tag 筛选)
- 右栏:已选源(可拖拽排序)
- 全选/反选/清空按钮
- 空选择 = 包含所有 enabled 源(需明确提示)
- ignoreFailed 切换:下拉选择 (跳过失败源 / 全部要求成功)
**区域 3 - 节点处理**: 同源编辑的 Filter Pipeline(集合级 filter
**区域 4 - 操作栏**: 同源编辑
#### 3.2.3 集合预览
**功能**: 调用 `/api/preview/collection`,显示合并后的节点
**布局**: 同源预览,但显示多源合并信息
- 额外显示:各源的节点数量、失败源列表
- 可按来源分组显示
---
### 3.3 模板管理 (Templates)
**独立 Tab 页**,非隐藏在设置中。
#### 3.3.1 模板列表
- 表格布局:名称、目标客户端、类型 (内置/自定义)、操作
- 内置模板:只读,可查看不可编辑/删除
- 自定义模板:编辑、删除、克隆
- 新建按钮、从文件导入按钮 (JSON/YAML)
#### 3.3.2 模板编辑
**布局**: 右侧面板或模态对话框
**区域 1 - 元信息**:
- ID(创建后只读)、名称、目标客户端选择
**区域 2 - 配置编辑器**:
- CodeMirror JSON/YAML 编辑器(全宽)
- 语法高亮、格式化按钮、校验按钮
- 可视化预览(可选):解析 proxy-groups 和 rules,以树形图展示
**区域 3 - 内置模板预览**:
- 点击内置模板 → 只读模式显示配置 + 结构说明
---
### 3.4 工具 (Tools)
**独立 Tab 页**,包含三大工具,内部用子 Tab 或卡片分区。
#### 3.4.1 格式转换器
**功能**: 一次性代理/规则转换 (`/api/proxy/parse`, `/api/rule/parse`)
**布局**: 左右分栏
- 左栏:输入
- 类型切换:代理转换 / 规则转换
- 目标选择:下拉
- 输入文本域(支持粘贴 URI/YAML/JSON
- 从文件导入按钮
- [转换] 按钮
- 右栏:输出
- 输出文本域(只读)
- 统计信息:解析 N / 输出 M / 跳过 K
- 复制按钮、下载按钮
#### 3.4.2 分享管理 (Shares)
**功能**: 创建/管理 download grants
**布局**: 列表 + 表单
**列表区**:
- 表格:资源类型、资源 ID、目标、过期时间、状态、操作
- 操作:启用/禁用切换、删除、复制链接
- 按资源类型筛选
**创建表单** (右侧面板或顶部展开):
- 资源类型:source / collection
- 资源 ID:下拉选择(从现有数据加载)
- 目标:下拉(auto 或指定)
- 过期时间:数字 + 单位(小时/天),或永不过期
- [创建] → 生成 token + URL,显示并可复制
#### 3.4.3 节点信息查询
**功能**: 调用 `/api/utils/node-info` 查询 IP 信息
**布局**: 简单表单 + 结果卡片
- 输入:服务器地址
- 结果:IP、国家、地区、城市、连接信息
---
### 3.5 回收站 (Recycle Bin)
**独立 Tab 页**
**布局**: 表格 + 详情抽屉
**列表**:
- 表格:资源类型、资源 ID、删除时间、操作
- 操作:恢复、彻底删除
- 按资源类型筛选
- 显示总数 / 上限 50 提示
**恢复逻辑**:
- 恢复时检查 ID 冲突(后端已处理,前端需展示友好错误)
- 恢复成功后刷新对应列表
---
### 3.6 设置 (Settings)
**模态对话框或独立全屏页**,非当前 My.vue 的卡片堆叠。
**布局**: 左侧分类菜单 + 右侧设置表单
**分类**:
#### 3.6.1 请求设置
- 默认 User-Agent
- 默认流量 User-Agent
- 默认超时 (ms)
- 后端请求并发数
- 并发等待时间 (ms)
- 远程缓存 TTL (s)
- 缓存错误降级开关
- 节点信息 API URL
#### 3.6.2 外观设置
- 主题选择(8 套主题)
- 简单模式开关
- 图标显示开关
- 图标彩色开关
- 列表视图模式(单列/双列)
- 浮动按钮开关(移动端兼容用)
- 编辑器分组模式
#### 3.6.3 数据管理
- 导出备份(下载 JSON
- 导入备份(上传 JSON
- Admin Token 设置
- 下载 Token 显示
#### 3.6.4 关于
- 后端类型、版本、存储引擎
- GitHub 链接
- 文档链接
---
## 4. 全局交互设计
### 4.1 键盘快捷键
| 快捷键 | 功能 |
|--------|------|
| `Ctrl+K` | 聚焦全局搜索 |
| `Ctrl+N` | 新建(当前 Tab 对应类型) |
| `Ctrl+S` | 保存当前编辑 |
| `Ctrl+Shift+S` | 另存为 |
| `Ctrl+P` | 预览当前选中项 |
| `Ctrl+D` | 复制下载链接 |
| `Delete` | 删除选中项(需确认) |
| `Esc` | 关闭面板/对话框/取消选中 |
| `↑↓` | 列表中上下移动选中 |
| `Enter` | 打开选中项编辑 |
| `Space` | 切换选中项启用/禁用 |
### 4.2 右键菜单
**列表项右键**:
- 编辑
- 预览节点
- 复制下载链接 ▸ (子菜单:mihomo / sing-box / surge / ...)
- 创建分享
- 克隆
- 导出
- 删除
**空白区域右键**:
- 新建源
- 新建集合
- 粘贴导入
- 刷新列表
### 4.3 拖放操作
- **列表内拖拽**: 排序(显示插入位置指示线)
- **跨列表拖拽**: 源拖入集合(自动添加到集合的 sourceIds)
- **文件拖入**: 拖入 JSON/YAML 文件 → 导入备份或模板
### 4.4 通知系统
- **Toast**: 操作成功/失败(右上角,自动消失)
- **通知中心**: 右上角铃铛图标,记录历史通知
- **内联状态**: 列表项上的流量加载状态、同步状态
### 4.5 确认对话框
- 删除操作:模态确认框,显示资源名称
- 导入覆盖:模态确认框,显示将影响的数据统计
- 离开未保存编辑:路由守卫拦截,提示保存
---
## 5. 数据流与状态管理
### 5.1 Store 结构(Pinia
```
stores/
├── global.ts # 全局状态:环境信息、连接状态、主题
├── sources.ts # 源列表、CRUD、排序
├── collections.ts # 集合列表、CRUD、排序
├── templates.ts # 模板列表、CRUD
├── shares.ts # 分享列表、CRUD
├── recycleBin.ts # 回收站列表、恢复/删除
├── settings.ts # 应用设置
├── scripts.ts # 脚本注册表元数据
└── notify.ts # 通知队列
```
### 5.2 API 层
保持原项目的 axios 拦截器模式,但简化适配层:
```typescript
// api/client.ts — axios 实例 + 拦截器
// api/sources.ts — 源 CRUD
// api/collections.ts — 集合 CRUD
// api/templates.ts — 模板 CRUD
// api/shares.ts — 分享 CRUD
// api/recycle.ts — 回收站
// api/settings.ts — 设置 + 导出/导入
// api/tools.ts — 转换器、节点信息
// api/env.ts — 环境
```
**关键简化**: 原项目的 `api/app/index.ts` (705 行) 包含大量 UI ↔ API 的 filter 格式转换逻辑 (`toApiFilters` / `fromApiFilters` / `toActionMeta`)。重构版应将此逻辑移入编辑器组件的 model 层,API 层只做纯 HTTP 调用。
### 5.3 Filter Pipeline 数据模型
```typescript
// 编辑器内部的 UI 模型(与原项目 UiProcess 一致)
type FilterAction = {
id: string; // 前端 UUID
type: ActionType; // 'Region Filter' | 'Type Filter' | ...
args: Record<string, any>;
customName?: string;
disabled: boolean;
};
// 提交到后端时转换为 FilterRule[]
// 从后端加载时转换为 FilterAction[]
// 转换逻辑封装在 composables/useFilterTransform.ts
```
---
## 6. 技术选型建议
### 6.1 框架与 UI 库
| 组件 | 原项目 | 重构建议 | 理由 |
|------|--------|----------|------|
| 框架 | Vue 3 | Vue 3 | 保持一致 |
| UI 库 | NutUI (移动端) | Naive UI / PrimeVue | PC 优先,需数据表格/穿梭框/树形组件 |
| 状态管理 | Pinia | Pinia | 保持一致 |
| 路由 | Vue Router | Vue Router | 保持一致 |
| HTTP | Axios | Axios 或 ofetch | 保持 Axios 即可 |
| 代码编辑器 | CodeMirror 5 | CodeMirror 6 / Monaco | CM6 更现代,Monaco 更强大 |
| 拖拽 | vuedraggable (SortableJS) | 原生 HTML5 Drag API + vue-draggable-plus | PC 用原生拖拽更可靠 |
| i18n | vue-i18n | vue-i18n | 保持一致 |
| 图标 | Font Awesome + NutUI Icon | Lucide / Tabler Icons | 更现代的图标集 |
| 样式 | SCSS + CSS 变量 | UnoCSS / Tailwind CSS | 原子化 CSS,开发更快 |
| 主题 | 8 套主题 (SCSS 变量) | CSS 变量 + 暗色模式 | 简化主题系统 |
### 6.2 组件库选择考量
**Naive UI** (推荐):
- Vue 3 原生,TypeScript 友好
- 内置数据表格、穿梭框、树形、抽屉、右键菜单
- 暗色模式原生支持
- 中文社区活跃
**PrimeVue**:
- 组件最全(含 CodeMirror 集成)
- 主题系统强大
- 但体积较大
---
## 7. 页面路由规划
```
/ → 重定向到 /sources
/sources → 订阅源管理(列表 + 右侧详情/编辑)
/sources/:id → 订阅源管理(选中指定源,右侧显示编辑)
/collections → 集合管理
/collections/:id → 集合管理(选中指定集合)
/templates → 模板管理
/templates/:id → 模板管理(选中指定模板)
/tools → 工具(默认显示转换器)
/tools/converter → 格式转换器
/tools/shares → 分享管理
/tools/node-info → 节点信息查询
/recycle-bin → 回收站
/settings → 设置(模态对话框,非独立路由)
/preview/:type/:id → 预览(可选独立路由,用于书签/分享)
```
**路由模式**:
- 列表+详情使用嵌套路由,详情区为子路由 `<router-view>`
- 预览使用 query 参数 `?preview=true` 或子路由,避免全屏覆盖
---
## 8. 功能优先级
### P0 — MVP 必须实现
1. 上下布局框架(TopBar + NavBar + Main + StatusBar
2. 订阅源列表 + 编辑(基本表单 + URL/Content + Filter Pipeline
3. 集合列表 + 编辑(源选择器 + 模板选择 + Filter Pipeline
4. 源/集合预览(原始 vs 处理后节点对比)
5. 下载链接生成 + 复制
6. 模板列表 + 查看(内置只读)
7. 设置(请求设置 + Admin Token
8. 导出/导入备份
9. 键盘快捷键(Ctrl+S, Ctrl+K, Esc
10. 暗色模式
### P1 — 增强体验
1. 模板创建/编辑(CodeMirror 编辑器)
2. 分享管理(创建/列表/启停/删除)
3. 回收站(列表/恢复/彻底删除)
4. 格式转换器工具
5. 节点信息查询工具
6. 右键菜单
7. 拖拽排序 + 跨列表拖拽
8. 多选批量操作
9. 全局搜索(跨类型搜索源/集合/模板)
### P2 — 高级功能
1. 脚本参数动态表单
2. 节点信息内联展示(预览中展开节点详情)
3. 拖入文件导入
4. 二维码生成
5. 通知中心
6. 自定义主题
7. 移动端响应式降级
---
## 9. 与后端 API 的对接
### 9.1 API 端点映射
| 前端功能 | 后端 API | 方法 |
|----------|----------|------|
| 源列表 | `/api/sources` | GET |
| 创建源 | `/api/sources` | POST |
| 更新源 | `/api/sources/:name` | PATCH |
| 删除源 | `/api/sources/:name` | DELETE |
| 排序源 | `/api/sources` (PUT) 或 `/api/sort/sources` (POST) | PUT/POST |
| 集合列表 | `/api/collections` | GET |
| 创建集合 | `/api/collections` | POST |
| 更新集合 | `/api/collections/:name` | PATCH |
| 删除集合 | `/api/collections/:name` | DELETE |
| 排序集合 | `/api/collections` (PUT) 或 `/api/sort/collections` (POST) | PUT/POST |
| 模板列表 | `/api/templates` | GET |
| 创建模板 | `/api/templates` | POST |
| 更新模板 | `/api/templates/:name` | PATCH |
| 删除模板 | `/api/templates/:name` | DELETE |
| 预览源 | `/api/preview/source` | POST |
| 预览集合 | `/api/preview/collection` | POST |
| 下载链接 | `/api/link/source/:name`, `/api/link/collection/:name` | GET |
| 流量信息 | `/api/source/flow/:name` | GET |
| 转换器 | `/api/proxy/parse`, `/api/rule/parse` | POST |
| 分享列表 | `/api/shares` | GET |
| 创建分享 | `/api/shares` | POST |
| 更新分享 | `/api/shares/:id` | PATCH |
| 删除分享 | `/api/shares/:id` | DELETE |
| 回收站列表 | `/api/recycle-bin` | GET |
| 恢复 | `/api/recycle-bin/:id/restore` | POST |
| 彻底删除 | `/api/recycle-bin/:id` | DELETE |
| 设置 | `/api/settings` | GET/PATCH |
| 导出 | `/api/storage` | GET |
| 导入 | `/api/storage` | POST |
| 环境 | `/api/env` | GET |
| 脚本列表 | `/api/scripts` | GET |
| 节点信息 | `/api/utils/node-info` | POST |
### 9.2 鉴权
- 所有 `/api/*` 请求携带 `Authorization: Bearer <token>`
- Token 存储在 localStorage,可通过 URL `?token=` 参数初始化
- 401 响应 → 清除 Token,显示重新输入界面
### 9.3 响应格式适配
统一响应包络:
```json
{ "status": "success", "data": <T> }
{ "status": "failed", "error": { "code": <number>, "message": <string> } }
```
前端 axios 拦截器:
- 成功:解包 `response.data.data`
- 失败:提取 `response.data.error.message`Toast 通知
---
## 10. 原项目功能对照清单
| 原项目功能 | 重构状态 | 说明 |
|-----------|----------|------|
| 源列表 + swipe 操作 | ✅ 保留,改右键菜单 | 弃用 swipe |
| 集合列表 + swipe 操作 | ✅ 保留,改右键菜单 | 弃用 swipe |
| 源编辑 (SubEditor) | ✅ 保留,拆分组件 | 2174 行 → 拆为 3-4 个子组件 |
| 集合编辑 | ✅ 保留,穿梭框选源 | 弃用 checkbox+draggable |
| Filter Pipeline 编辑 | ✅ 保留,改进交互 | 可视化流水线 + 专属配置表单 |
| 预览/对比 | ✅ 保留,右侧面板 | 弃用全屏覆盖 |
| 下载链接 | ✅ 保留,popover | 弃用 PreviewPanel 弹窗 |
| 模板管理 | ✅ 提升为独立 Tab | 从 My 页移出 |
| 设置 (My.vue) | ✅ 保留,改模态/独立页 | 分类菜单 + 表单 |
| 备份导出/导入 | ✅ 保留 | 移入设置-数据管理 |
| 工具页 | ✅ 保留,增强 | 转换器+分享+节点信息 |
| 回收站 | ✅ 提升为独立 Tab | 从 Tools 移出 |
| 分享管理 | ✅ 提升为 Tools 子页 | 从 Tools 移出 |
| 浮动按钮 | ❌ 移除 | PC 不需要 |
| 底部 TabBar | ❌ 移除 | 改为顶部 NavBar |
| 底部 Popup | ❌ 移除 | 改为右侧抽屉/模态 |
| 主题系统 | ✅ 保留,简化 | 8 套 → 暗色/亮色 + 配色变量 |
| i18n | ✅ 保留 | 中/英双语 |
| 标签筛选 | ✅ 保留 | 列表顶部 chip 筛选 |
| 拖拽排序 | ✅ 保留,原生 HTML5 | 弃用 vuedraggable |
| CodeMirror 编辑 | ✅ 保留,升级 CM6 | 本地内容/模板编辑 |
| 脚本参数 | ✅ 保留,动态表单 | 根据 metadata.parameters 生成 |
| 流量信息 | ✅ 保留 | 卡片内联显示 |
| 节点信息查询 | ✅ 保留 | Tools 子功能 |
+652
View File
@@ -0,0 +1,652 @@
# Review 决议记录
> 来源:2026-07-27 对 `backend-dev-plan.md` 的 review,共 45 条问题。
> 本文档对每条问题给出明确决议,开发时以此为准,不得偏离。
> 如需变更决议,先修改本文档再改代码。
---
## 决议速查
| # | 问题 | 决议 | 落地位置 |
|---|------|------|----------|
| 1 | ProxyNode 类型 | `map[string]any` | `model/types.go` |
| 2 | script 过滤器迁移 | 不保留 script 类型 | `filter/custom.go` |
| 3 | process→FilterRule 转换 | 不实现,前端直发 FilterRule[] | — |
| 4 | defaultSettings 合并 | 实现 mergeSettings + 特殊浅合并 | `handler/settings.go` |
| 5 | envPayload feature flags | 定义并返回,buildTimeScripts=false | `handler/env.go` |
| 6 | Flow info URL hash 解析 | 实现完整解析 | `handler/flow.go` |
| 7 | normalizeTarget UA 推断 | 实现完整关键词映射 | `model/target.go` |
| 8 | 临时源覆盖 | 实现仅覆盖第一个匹配源 | `handler/download.go` |
| 9 | parseJsonOrText | 实现 JSON 优先 + 纯文本回退 | `handler/` 工具函数 |
| 10 | SW 清理端点 | 不实现(无 CF 迁移) | — |
| 11 | getPublicBaseUrl | 实现 PUBLIC_DOWNLOAD_HOSTS 配置 | `handler/link.go` |
| 12 | 中文拼音排序 | golang.org/x/text/collate + chinese | `filter/sort.go` |
| 13 | splitHostPort | 自写 lastIndexOf(":") 版本 | `util/` |
| 14 | JS URL vs Go net/url | net/url 为基 + 逐协议测试 + 边缘处理 | `proxy/uri_parser.go` |
| 15 | URL-safe Base64 | 区分 StdEncoding / RawURLEncoding | `util/base64.go` |
| 16 | Unicode flag 正则 | RE2 \p{} 支持,实测验证 | `util/flag.go` |
| 17 | secureRandomInt | crypto/rand.Int(天然无偏) | `filter/sort.go` |
| 18 | formatDuplicateNumber 自定义数字字符 | 实现完整功能 | `filter/dedupe.go` |
| 19 | normalizeTaiwanFlag 三模式 | 实现完整 | `filter/flag.go` |
| 20 | isUsefulProxy ASCII 校验 | 实现完整 | `filter/quick.go` |
| 21 | applyState 字符串状态值 | 实现兼容解析 | `filter/quick.go` |
| 22 | snell/ssh/h2-connect | 仅 client_parser 解析 | `proxy/client_parser.go` |
| 23 | normalizeClientProxyKind 别名 | 实现别名映射 | `proxy/client_parser.go` |
| 24 | isTokenValid 双哈希 | 先 SHA-256 再 ConstantTimeCompare | `util/token.go` |
| 25 | CRLF 注入防护 | 校验 + 清洗所有 header 值 | `middleware/security.go` |
| 26 | getBearerToken 三种提取 | 实现 Bearer / query / x-sub-store-token | `middleware/auth.go` |
| 27 | CSP 安全头 | 收紧为 script-src 'self'(无 unsafe-eval | `middleware/security.go` |
| 28 | SQLite PRAGMA | WAL + busy_timeout + foreign_keys + MaxOpenConns=1 | `database/db.go` |
| 29 | archiveAndDeleteResource 事务 | BeginTx 包裹三步 | `handler/` 删除逻辑 |
| 30 | importStorage 导入顺序 | settings→sources→templates→collections | `handler/storage.go` |
| 31 | exportStorage 排除内置模板 | 过滤 BuiltinTemplateIDs | `handler/storage.go` |
| 32 | source_cache TTL 清理 | 后台 goroutine 定时清理 | `database/cache_repo.go` |
| 33 | goose 迁移嵌入 | //go:embed + SetBaseFS | `cmd/migrate.go` |
| 34 | goroutine 关闭安全 | sync.WaitGroup + context | `service/subscription.go` |
| 35 | 缓存操作错误吞咽 | recover + 日志,不传播 | `database/cache_repo.go` |
| 36 | 并发 wait 延迟 | 保留 waitMs 参数 | `service/concurrency.go` |
| 37 | MAX_SCRIPT_ACTIONS | custom 规则上限 32 条 | `filter/custom.go` |
| 38 | restoreDownloadGrantSnapshot | 恢复时写回 tokenHash | `handler/recycle.go` |
| 39 | FRONTEND_VERSION 常量 | 定义 = "1.0.0" | `config/` |
| 40 | TEST_URL 常量 | 定义 generate_204 | `render/` |
| 41 | defaultProxyGroups | 无模板时生成 3 默认组 | `template/builtin.go` |
| 42 | sing-box 完整结构 | log+inbounds+route+outbounds 全实现 | `render/singbox.go` |
| 43 | profile-update-interval 默认值 | 默认 "6" | `handler/download.go` |
| 44 | cache-control: no-store | 下载响应设此头 | `handler/download.go` |
| 45 | modernc.org/sqlite 性能 | 接受 tradeoff,保留 swap 注释 | `database/db.go` |
---
## 逐条详述
### 🔴 架构级决策(#1-#3)— 已由 AGENTS.md 约束豁免
#### #1 ProxyNode 类型 → `map[string]any`
**决议**ProxyNode 定义为 `type ProxyNode map[string]any`
**理由**
- 13 种协议各有不同可选字段(reality-opts, ws-opts, plugin-opts, auth_str, congestion-controller...),struct 需定义全部字段且无法处理未知字段
- 过滤器通过 `getByPath(proxy, "ws-opts.headers.Host")` 做点号路径访问,map 天然支持
- JSON/YAML 反序列化到 map 保留所有字段;struct 会丢失未知字段
- stripUndefined(删除 nil 和 "")在 map 上语义清晰
- 类型安全损失可接受——节点字段本质是动态的
**实现要点**
- `model/types.go``type ProxyNode map[string]any`
- 深拷贝:`json.Marshal``json.Unmarshal`(简单可靠,性能可接受)
- 类型安全访问:在 `util/path.go` 提供 `GetByPath(node, "a.b.c")` / `SetByPath` / `GetString` / `GetInt` / `GetBool` 等辅助函数
#### #2 script 过滤器类型 → 不保留
**决议**Go 版不实现 `script` 类型过滤器。用户自定义逻辑通过 `custom` 声明式规则链实现。
**理由**:全新项目无存量 script 过滤器,无需兼容。
**影响**
- `GET /api/scripts` 返回空数组 `[]`(端点保留,前端兼容)
- `applyFilters` 中无 `script` 分支
- `validateScriptActions` 不需要
#### #3 process→FilterRule 转换 → 不实现
**决议**Go API 直接接受 `FilterRule[]` 格式,不实现前端 `process` 数组到 `FilterRule[]` 的转换。
**理由**:前端尚未实现,将适配 Go API 设计。前端直接发送 FilterRule[] 格式。
---
### 🟠 关键功能遗漏(#4-#11
#### #4 defaultSettings 合并 → 实现完整合并
**决议**`GET /api/settings` 返回 `mergeSettings(defaultSettings(), storedSettings)`
**合并规则**
- 顶层 keystored 覆盖 default
- `theme``appearanceSetting`:浅合并(stored 的子 key 覆盖 default 的对应子 key,而非整体替换)
- 其他嵌套对象:深合并
**落地**`handler/settings.go` + `config/defaults.go`(定义 `DefaultSettings()` 函数)。
#### #5 envPayload feature flags → 定义并返回
**决议**`GET /api/env` 返回 `feature` 对象,各 flag 取值如下:
```
buildTimeScripts: false // 无 JS 引擎
proxyConversion: true // POST /api/proxy/parse
ruleConversion: true // POST /api/rule/parse
scopedShares: true // 分享令牌
recycleBin: true // 回收站
nodeInfo: true // POST /api/utils/node-info
surgeMac: true // surge-mac 渲染
```
**落地**`handler/env.go`
#### #6 Flow info URL hash 解析 → 完整实现
**决议**:实现 `parseFlowRequest`,从 URL `#` fragment 中解析参数。
**支持两种格式**
1. JSON 格式:`#{"flowUrl":"...","noFlow":true,"flowUserAgent":"...","flowHeaders":{}}`
2. Query-string 格式:`#flowUrl=...&noFlow=true&flowUserAgent=...`
**额外**`fetchFlowHeaders` 从响应体中提取 `upload=` / `download=` / `total=` / `expire=` 流量信息(subscription-userinfo header 优先,body 次之)。
**落地**`handler/flow.go`
#### #7 normalizeTarget UA 推断 → 完整关键词映射
**决议**:实现完整 UA 关键词→目标格式映射,按优先级匹配。
**映射表**(按检查顺序):
| UA 关键词 | 目标格式 |
|-----------|---------|
| `sing-box` | singbox |
| `v2ray` / `v2rayng` | v2ray |
| `surge` (含 `mac`) | surge-mac |
| `surge` (不含 `mac`) | surge |
| `loon` | loon |
| `egern` | egern |
| `shadowrocket` | shadowrocket |
| `quantumult` | qx |
| `stash` | stash |
**落地**`model/target.go``NormalizeTarget(target, ua string) string`
#### #8 临时源覆盖 → 仅覆盖第一个匹配源
**决议**:实现 `applyTemporarySourceOverride`,通过 `?url=` / `?content=` / `?ua=` query 参数临时覆盖。
**关键行为**:只覆盖集合中**第一个**匹配的源(按 `sourceIds` 顺序),不是全部覆盖。与原项目行为一致。
**落地**`handler/download.go`
#### #9 parseJsonOrText → JSON 优先 + 纯文本回退
**决议**:实现 `parseJsonOrText(body []byte) (map[string]any, error)`
**逻辑**
1. 尝试 `json.Unmarshal` → 成功则返回
2. 失败则将 body 作为纯文本,返回 `{"content": string(body)}`
**应用端点**template 创建/更新、storage 导入。
**落地**`handler/` 下的工具函数。
#### #10 Service Worker 清理端点 → 不实现
**决议**:不实现 `/sw.js``/registerSW.js` 端点。
**理由**:全新项目,无 Cloudflare 版 Service Worker 残留需要清理。
#### #11 getPublicBaseUrl → 支持 PUBLIC_DOWNLOAD_HOSTS
**决议**:实现 `getPublicBaseUrl(r *http.Request) string`
**逻辑**
1. 读取环境变量 / 配置 `SUB_STORE_PUBLIC_DOWNLOAD_HOSTS`(逗号分隔域名列表)
2. 若非空,返回第一个域名作为 base URL
3. 否则返回 `r.Host`(请求自身的 origin
**落地**`handler/link.go``buildDownloadLink`
---
### 🟡 实现细节/陷阱(#12-#23
#### #12 中文拼音排序 → golang.org/x/text/collate
**决议**:引入 `golang.org/x/text/collate` + `golang.org/x/text/language`,使用 `collate.New(language.SimplifiedChinese)` 做中文拼音排序。
**落地**`filter/sort.go``sortProxies`。在 `go.mod` 添加依赖。
#### #13 splitHostPort → 自写版本
**决议**:不使用 `net.SplitHostPort`,自写 `splitHostPort(s string) (host, port string)`
**行为**:用 `strings.LastIndex(s, ":")` 切分,与原项目一致。能正确处理 IPv6(如 `[::1]:443` → host=`[::1]`, port=`443`)和裸 IPv6(无端口时返回 host=s, port="")。
**落地**`util/``proxy/uri_parser.go` 内部函数。
#### #14 JS URL vs Go net/url → net/url 为基 + 逐协议测试
**决议**:以 `net/url.Parse` 为基础解析器,针对已知差异做适配:
- **Fragment 处理**Go 的 `url.Fragment` 不会自动 URL-decode,需要手动 `url.QueryUnescape(fragment)` 获取节点名
- **Userinfo 提取**:用 `u.User.Username()``u.User.Password()`,不手动切分
- **IPv6 hostname**`u.Hostname()` 自动去掉方括号
- **Query().Get() 返回 ""**:原项目 `URLSearchParams.get()` 返回 `null`Go 返回 `""`。所有 `!= null` 判断改为 `!= ""`
**落地**`proxy/uri_parser.go`。为每种协议编写表驱动测试,覆盖正常/异常/边界 case。
#### #15 URL-safe Base64 → 明确区分
**决议**:在 `util/base64.go` 中提供三组函数:
| 函数 | 编码 | 用途 |
|------|------|------|
| `DecodeBase64Std` | `base64.StdEncoding` | vmess JSON |
| `DecodeBase64URL` | `base64.URLEncoding` | — |
| `DecodeBase64RawURL` | `base64.RawURLEncoding` (无填充) | SSR |
| `EncodeBase64URL` | `base64.URLEncoding` | — |
| `EncodeBase64RawURL` | `base64.RawURLEncoding` | — |
同时提供 `DecodeBase64Auto(s string)` — 自动尝试 Std 和 RawURL 两种编码(先 Std,失败再 RawURL),因为订阅内容编码不一定规范。
**落地**`util/base64.go`
#### #16 Unicode flag 正则 → RE2 支持,实测验证
**决议**Go RE2 支持 `\p{Regional_Indicator}``\uFE0F` / `\u200D`
**验证清单**
- `detectFlag`:用真实国旗 emoji(🇭🇰 🇯🇵 🇺🇸 🇹🇼)测试
- `removeFlag`:测试含 ZWJ (`\u200D`) 和 variation selector (`\uFE0F`) 的复合 emoji
- 编写单元测试覆盖所有 Unicode flag 场景
**落地**`util/flag.go`
#### #17 secureRandomInt → crypto/rand.Int
**决议**:直接使用 `crypto/rand.Int(rand.Reader, big.NewInt(int64(max)))`
**理由**Go 的 `crypto/rand.Int` 内部已实现无偏随机(rejection sampling),无需手动实现。比原项目的 JS 版更简洁。
**落地**`filter/sort.go` 的 shuffle 逻辑。
#### #18 formatDuplicateNumber 自定义数字字符 → 完整实现
**决议**:实现 `formatDuplicateNumber(index int, template string) string`
**功能**`template` 可包含自定义数字字符集,用空格分隔。如 `"一 二 三 四 五"` → index=2 输出 "二"。默认为阿拉伯数字 `"1 2 3 4 5..."`
**落地**`filter/dedupe.go`
#### #19 normalizeTaiwanFlag 三模式 → 完整实现
**决议**flag 过滤器的 `tw` 参数支持三种模式:
| 参数值 | 输出 |
|--------|------|
| `ws` | 🇼🇸 (萨摩亚旗) |
| `tw` | 🇹🇼 (台湾旗) |
| 默认/其他 | 🇨🇳 (中国旗) |
**落地**`filter/flag.go`
#### #20 isUsefulProxy ASCII 校验 → 完整实现
**决议**`isUsefulProxy` 不仅检查节点名关键词,还校验:
- `cipher` 值是否为纯 ASCII
- `password` 值是否为纯 ASCII
- WS Host header`ws-opts.headers.Host`)是否为纯 ASCII
任一非 ASCII → 标记为 useless。
**落地**`filter/quick.go`
#### #21 applyState 字符串状态值 → 兼容解析
**决议**`applyState` 接受以下值并归一化为 bool
| 输入 | 输出 |
|------|------|
| `true` / `"ENABLED"` / `"enabled"` | `true` |
| `false` / `"DISABLED"` / `"disabled"` | `false` |
**落地**`filter/quick.go``parseState(v any) bool`
#### #22 snell/ssh/h2-connect → 仅客户端配置行解析
**决议**:这三种类型仅在 Surge/Loon 客户端配置行解析中出现,不出现在 URI 协议中。
- `snell`Surge 配置行 `snell = name, server, port, psk, ...`
- `ssh`Surge 配置行 `ssh = name, server, port, ...`
- `h2-connect`Surge 配置行
**落地**`proxy/client_parser.go`,不放入 `uri_parser.go`
#### #23 normalizeClientProxyKind 别名 → 实现映射
**决议**:客户端配置行的协议类型别名归一化:
```
shadowsocks → ss
socks5-tls → socks5
https → http
hysteria 2 → hysteria2
tuic-v5 → tuic
```
**落地**`proxy/client_parser.go``normalizeClientProxyKind(kind string) string`
---
### 🔒 安全相关(#24-#27
#### #24 isTokenValid 双哈希 → 先 SHA-256 再 ConstantTimeCompare
**决议**:所有 token 校验先做 SHA-256 哈希,再用 `subtle.ConstantTimeCompare` 比较哈希值,避免长度泄露。
**两种场景**
1. **admin_token / download_token**(配置文件中的明文 secret):
```
inputHash = sha256Hex(input)
secretHash = sha256Hex(secret)
return subtle.ConstantTimeCompare([]byte(inputHash), []byte(secretHash)) == 1
```
2. **scoped grant**(数据库存储 `sha256Hex(token)`):
```
inputHash = sha256Hex(input)
return subtle.ConstantTimeCompare([]byte(inputHash), []byte(storedHash)) == 1
```
**落地**`util/token.go` 的 `IsTokenValid(input, secret string) bool` 和 `IsGrantTokenValid(input string, storedHash string) bool`。
#### #25 CRLF 注入防护 → 校验 + 清洗
**决议**
1. **setResponseHeader**:设置任何 header 值前校验 `!strings.ContainsAny(value, "\r\n")`,不合法则跳过设置并记录警告日志
2. **safeContentDisposition**:清洗文件名——移除 `\r\n` 和控制字符,对特殊字符做引号转义
**落地**`middleware/security.go` 或 `handler/download.go` 中的工具函数。
#### #26 getBearerToken 三种提取 → 完整实现
**决议**:token 从三处提取,优先级从高到低:
1. `Authorization: Bearer <token>` header
2. `?token=<token>` query parameter
3. `x-sub-store-token` header
**落地**`middleware/auth.go` 的 `extractToken(c *fiber.Ctx) string`。
#### #27 CSP 安全头 → 收紧
**决议**Go 版前端不含 evalCSP 收紧为:
```
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
connect-src 'self';
font-src 'self' data:;
```
**与原项目差异**:移除 `'unsafe-eval'`(原项目前端有 eval,Go 版不需要)。
**落地**`middleware/security.go`。
---
### 🗄️ 数据库相关(#28-#33
#### #28 SQLite PRAGMA → 完整配置
**决议**:在 `database/db.go` 打开连接后立即执行:
```sql
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA foreign_keys = ON;
PRAGMA synchronous = NORMAL;
```
**连接池**
- `db.SetMaxOpenConns(1)` — modernc.org/sqlite 在高并发写时会出现 `database is locked`,单连接 + WAL 足以应对 sub-store 的个人工具场景
- 读写都在同一连接上,WAL 模式下读不阻塞写
**落地**`database/db.go` 的 `InitDB(path string) (*sqlx.DB, error)`。
#### #29 archiveAndDeleteResource 事务 → BeginTx 包裹
**决议**:删除资源时在一个事务中执行三步:
```
BEGIN TRANSACTION;
INSERT INTO recycle_bin (id, resource_type, resource_id, snapshot_json, deleted_at) VALUES (...);
DELETE FROM {sources|collections|templates} WHERE id = ?;
DELETE FROM recycle_bin WHERE id NOT IN (
SELECT id FROM recycle_bin ORDER BY deleted_at DESC LIMIT ?
);
COMMIT;
```
**落地**`handler/` 删除逻辑 + `database/recycle_repo.go` 的 `ArchiveAndDelete(tx, resourceType, resourceID, snapshot, maxEntries)`。
#### #30 importStorage 导入顺序 → 强制顺序
**决议**:导入顺序固定为:
1. `settings` — 无依赖
2. `sources` — 无依赖
3. `templates` — 无依赖(但 collections 引用 templates
4. `collections` — 引用 sources 和 templates
每步独立事务,失败则中止并返回错误。
**落地**`handler/storage.go` 的 `importStorage`。
#### #31 exportStorage 排除内置模板 → 过滤
**决议**:导出时过滤掉 ID 属于 `BuiltinTemplateIDs` 集合的模板。
```go
var exportedTemplates []Template
for _, t := range allTemplates {
if !builtinTemplateIDs[t.ID] {
exportedTemplates = append(exportedTemplates, t)
}
}
```
**落地**`handler/storage.go` 的 `exportStorage`。
#### #32 source_cache TTL 清理 → 后台 goroutine
**决议**:启动一个后台 goroutine,每隔 `cache_ttl` 间隔执行一次清理:
```go
func StartCacheCleaner(ctx context.Context, db *sqlx.DB, interval time.Duration) {
ticker := time.NewTicker(interval)
go func() {
for {
select {
case <-ticker.C:
db.ExecContext(ctx, "DELETE FROM source_cache WHERE cached_at + ttl < ?", time.Now().Unix())
case <-ctx.Done():
ticker.Stop()
return
}
}
}()
}
```
在 graceful shutdown 时通过 context 取消。间隔取 `cache_ttl` 配置值(默认 300s)。
**落地**`database/cache_repo.go` 的 `StartCacheCleaner`。
#### #33 goose 迁移嵌入 → //go:embed + SetBaseFS
**决议**:迁移 SQL 文件嵌入二进制:
```go
//go:embed migrations/*.sql
var embedMigrations embed.FS
func RunMigrations(db *sqlx.DB) error {
goose.SetBaseFS(embedMigrations)
return goose.Up(db.DB, "migrations")
}
```
**落地**`cmd/migrate.go` + `database/migrations/*.sql`。
---
### 🔄 并发/异步(#34-#36
#### #34 goroutine 关闭安全 → WaitGroup + context
**决议**:异步缓存写入通过 `sync.WaitGroup` 管理生命周期。
```go
type AsyncWriter struct {
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
}
func (aw *AsyncWriter) Write(fn func()) {
aw.wg.Add(1)
go func() {
defer aw.wg.Done()
select {
case <-aw.ctx.Done():
return
default:
fn()
}
}()
}
func (aw *AsyncWriter) Wait() {
aw.cancel()
aw.wg.Wait()
}
```
在 graceful shutdown 时调用 `Wait()`,带 5 秒超时。
**落地**`service/subscription.go` 或 `database/cache_repo.go`。
#### #35 缓存操作错误吞咽 → recover + 日志
**决议**`safeCacheMatch` 和 `safeCachePut` 包装缓存操作:
```go
func safeCacheGet(repo *CacheRepo, key string) (string, bool) {
defer func() {
if r := recover(); r != nil {
logrus.Warnf("cache get panicked: %v", r)
}
}()
return repo.Get(key)
}
```
缓存操作失败(SQL 错误、panic)只记录日志,不传播给订阅生成管线。
**落地**`database/cache_repo.go` 的 safe wrapper。
#### #36 并发 wait 延迟 → 保留参数
**决议**`RunWithConcurrency` 和 `RunSettledWithConcurrency` 的签名保留 `wait time.Duration` 参数。
**行为**:每个任务开始前(第一个除外)`time.Sleep(wait)`。用于避免同时请求同一 CDN 被限流。对应配置 `fetcher.concurrency_wait`。
**落地**`service/concurrency.go`。
---
### 📋 其他遗漏(#37-#45
#### #37 MAX_SCRIPT_ACTIONS → custom 规则上限 32 条
**决议**`custom` 过滤器的规则链上限 `MAX_CUSTOM_RULES = 32`。不保留原项目的 `MAX_SCRIPT_ACTIONS = 2`(那是 JS 脚本的限制,Go 版无 JS 引擎)。
**落地**`filter/custom.go` 的验证逻辑。
#### #38 restoreDownloadGrantSnapshot → 恢复时写回 tokenHash
**决议**:从回收站恢复 `share` 类型时,快照中的 `tokenHash` 需写回 `download_grants` 表。这是 share 恢复的特殊逻辑——其他资源类型(source/collection/template)恢复时不需要处理 tokenHash。
**落地**`handler/recycle.go` 的 restore 逻辑。
#### #39 FRONTEND_VERSION 常量
**决议**:定义 `const FrontendVersion = "1.0.0"`,用于 `GET /api/env` 响应。
**落地**`internal/config/config.go` 或 `handler/env.go`。
#### #40 TEST_URL 常量
**决议**:定义 `const TestURL = "https://www.gstatic.com/generate_204"`,用于 mihomo/sing-box 渲染中 url-test proxy group 的 test URL。
**落地**`render/mihomo.go` / `render/singbox.go`。
#### #41 defaultProxyGroups → 无模板时生成 3 默认组
**决议**:当 collection 未配置模板或模板无 proxy-groups 时,生成 3 个默认代理组:
| 组名 | 类型 | 行为 |
|------|------|------|
| 🚀 节点选择 | select | 手动选择,含所有节点 + ♻️ 自动选择 |
| ♻️ 自动选择 | url-test | 自动测速,含所有节点 |
| 🚀 手动切换 | select | 手动选择,仅含所有节点 |
**落地**`template/builtin.go` 的 `DefaultProxyGroups()`。
#### #42 sing-box 完整结构 → 全实现
**决议**:sing-box 输出包含完整顶层配置:
```json
{
"log": { "level": "info" },
"inbounds": [{
"type": "mixed",
"tag": "mixed-in",
"listen": "0.0.0.0",
"listen_port": 7890,
"sniff": true
}],
"outbounds": [
{ "tag": "PROXY", "type": "selector", "outbounds": [...], "interrupt_exist_connections": false },
{ "tag": "AUTO", "type": "urltest", "outbounds": [...], "url": "https://www.gstatic.com/generate_204", "interrupt_exist_connections": false },
{ "tag": "DIRECT", "type": "direct" },
{ "tag": "REJECT", "type": "block" },
...proxy outbounds...
],
"route": {
"auto_detect_interface": true,
"final": "PROXY"
}
}
```
**落地**`render/singbox.go`。
#### #43 profile-update-interval 默认值
**决议**:下载响应设置 `profile-update-interval` header,值为 `metadata.profileUpdateInterval`,若为空则默认 `"6"`。
**落地**`handler/download.go`。
#### #44 cache-control: no-store
**决议**:下载响应设置 `Cache-Control: no-store`,防止 CDN/浏览器缓存订阅内容。
**落地**`handler/download.go`。
#### #45 modernc.org/sqlite 性能 → 接受 tradeoff
**决议**:使用 `modernc.org/sqlite`(纯 Go),接受写性能低于 CGO 版的 tradeoff。
**理由**
- sub-store 是个人工具,写频率低(主要是缓存写入),性能不是瓶颈
- 纯 Go 支持 `CGO_ENABLED=0` 静态编译,跨平台部署简单
- 如未来性能不足,可切换到 `mattn/go-sqlite3`(接口不变,仅换驱动 + 启用 CGO)
**落地**`database/db.go` 添加注释说明 tradeoff 和替代方案。