chore: sub-store Go 重写项目初始化
This commit is contained in:
@@ -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 | 元数据 JSON(ua, 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,用于调试
|
||||
@@ -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. 命令行 flag(cobra)
|
||||
|
||||
---
|
||||
|
||||
## 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 张表的完整 CRUD,Repository 模式
|
||||
|
||||
| 任务 | 产出 | 原项目对照 |
|
||||
|------|------|-----------|
|
||||
| 类型定义 | `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 RegExp,Go 用 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 | 集成测试通过 |
|
||||
@@ -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 / Local(radio 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 子功能 |
|
||||
@@ -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)`。
|
||||
|
||||
**合并规则**:
|
||||
- 顶层 key:stored 覆盖 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 版前端不含 eval,CSP 收紧为:
|
||||
|
||||
```
|
||||
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 和替代方案。
|
||||
Reference in New Issue
Block a user