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

This commit is contained in:
2026-07-27 14:38:13 +08:00
commit 90e53aa754
85 changed files with 15271 additions and 0 deletions
+696
View File
@@ -0,0 +1,696 @@
# Sub-Store Cloudflare 原项目技术实现架构分析
> 参考路径: `ref/sub-store-cloudflare/`
> 版本: 1.1.0 (AGPL-3.0-or-later)
> 仓库: github.com/realchendahuang/sub-store-cloudflare
---
## 1. 项目定位
Sub-Store Cloudflare 是一个 **Cloudflare 原生的机场订阅聚合与路由模板管理工具**,部署形态为 Cloudflare Workers + Static Assets + D1 + Worker Secrets。核心能力:
- **订阅源聚合**:管理多个远程/本地订阅源,合并为集合 (Collection)
- **节点处理**:解析 13 种代理协议 URI、JSON/YAML 数组、客户端配置行,执行过滤/排序/去重/重命名等操作
- **多目标输出**:转换为 mihomo / sing-box / surge / loon / qx / egern / shadowrocket / v2ray / uri 等 13 种客户端格式
- **路由模板**:内置 6 套 ACL4SSR / Loyalsoldier / AI+Streaming mihomo 模板,支持自定义
- **规则转换**:将分流规则在 mihomo/surge/loon/qx 之间互转
- **分享/回收站**:scoped download grants(限定资源+目标+过期的分享令牌)、有界回收站(50 条上限)
**非功能边界**(AGENTS.md 明确约束):
- 不使用 R2/KV/Durable Objects/Queues/Cron
- 不运行时 eval 脚本(脚本为构建时打包)
- 不存储/执行来自 D1/浏览器/远程 URL 的脚本源码
- 远程源缓存使用 Workers Cache API(`caches.default`),非 D1
---
## 2. 整体架构
```
┌─────────────────────────────────────────────────────────────┐
│ Cloudflare Worker │
│ (cloudflare/src/index.ts — Hono app, single entry) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ │
│ │ /api/* │ │ /download/* │ │ Static Assets │ │
│ │ (admin API) │ │ (public dl) │ │ (SPA fallback)│ │
│ │ routes/api │ │ routes/dl │ │ ASSETS binding│ │
│ └──────┬──────┘ └──────┬───────┘ └────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ lib/ 层 │ │
│ │ store.ts subscription.ts rules.ts │ │
│ │ (D1 CRUD) (核心管线) (规则转换) │ │
│ │ defaults.ts targets.ts scripts.ts │ │
│ │ (内置模板) (目标别名) (脚本注册表) │ │
│ │ compatibility http.ts limits.ts read.ts │ │
│ │ -resources.ts (token/CORS/Hdr) (常量) (流读) │ │
│ └──────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ D1 (DB) │ │ Cache API │ │ Worker Secrets│ │
│ │ 5 张表 │ │ caches. │ │ ADMIN_TOKEN │ │
│ │ │ │ default │ │ DOWNLOAD_TOK │ │
│ └────────────┘ └────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 2.1 Monorepo 结构
```
sub-store-cloudflare/
├── package.json # workspace root, pnpm@11.7, node>=22
├── pnpm-workspace.yaml # packages: frontend, cloudflare
├── wrangler.jsonc # 顶层部署配置 (ASSETS + D1 + secrets)
├── cloudflare/ # Worker 后端
│ ├── src/
│ │ ├── index.ts # Hono app 入口 + 导出 ExportedHandler
│ │ ├── types.ts # 所有 TypeScript 类型定义
│ │ ├── worker-configuration.d.ts # Wrangler 生成的 Env 接口
│ │ ├── routes/
│ │ │ ├── api.ts # /api/* 管理接口 (896 行)
│ │ │ └── download.ts # /download/* 公开下载接口
│ │ ├── lib/
│ │ │ ├── store.ts # D1 数据层 CRUD (644 行)
│ │ │ ├── subscription.ts # 核心订阅处理管线 (2430 行)
│ │ │ ├── rules.ts # 分流规则转换
│ │ │ ├── defaults.ts # 6 套内置路由模板
│ │ │ ├── targets.ts # 客户端目标别名归一化
│ │ │ ├── scripts.ts # 构建时脚本注册表 + 运行时
│ │ │ ├── compatibility-resources.ts # download_grants + recycle_bin
│ │ │ ├── http.ts # token 校验 / CORS / 安全头
│ │ │ ├── limits.ts # 6 个字节/数量限制常量
│ │ │ └── read.ts # 流式响应读取 (防 OOM)
│ │ └── generated/ # 构建时生成 (git ignored)
│ │ └── script-registry.ts
│ ├── migrations/
│ │ ├── 0001_initial.sql
│ │ ├── 0002_runtime_schema_cleanup.sql
│ │ └── 0003_compatibility_resources.sql
│ └── package.json # hono, json5, yaml 三个运行时依赖
├── frontend/ # Vue 3 + Vite SPA
│ └── src/ # Pinia stores, views, components
└── scripts/ # 构建时 Node.js 脚本
├── generate-script-registry.mjs # 扫描 config/scripts → registry
├── install-cloudflare.mjs # 一键安装器
├── render-seed-sql.mjs # agent-setup → seed SQL
└── ... (15+ 检查/部署脚本)
```
---
## 3. 运行时环境与绑定
### 3.1 Worker Bindings (wrangler.jsonc)
| 绑定 | 类型 | 用途 |
|------|------|------|
| `DB` | D1Database | 所有结构化配置数据 |
| `ASSETS` | Fetcher | Static Assets (前端 SPA, `run_worker_first: true`) |
| `SUB_STORE_ADMIN_TOKEN` | Secret (string) | `/api/*` 管理 API 鉴权 |
| `SUB_STORE_PUBLIC_DOWNLOAD_TOKEN` | Secret (string) | `/download/*` 公开下载鉴权 |
| `SUB_STORE_APP_NAME` | Var (string) | 应用显示名 |
| `SUB_STORE_PUBLIC_DOWNLOAD_HOSTS` | Var (string) | 逗号分隔的纯下载域名列表 |
### 3.2 关键配置
- **`compatibility_date`**: `2026-07-08`
- **`compatibility_flags`**: `["nodejs_compat"]`
- **`assets.not_found_handling`**: `"single-page-application"` — 前端 SPA 路由兜底
- **`assets.run_worker_first`**: `true` — Worker 先处理请求,未匹配才回退到静态资源
- **`observability`**: enabled, head_sampling_rate=1
### 3.3 下载域名隔离机制
`index.ts` 的 `fetch` handler 在路由之前检查 hostname:
- 如果当前 hostname 在 `SUB_STORE_PUBLIC_DOWNLOAD_HOSTS` 列表中,且 pathname 不以 `/download/` 开头 → 返回 404
- 效果:下载域名只能访问下载端点,管理 UI 不可达
---
## 4. 数据库 Schema (D1 / SQLite)
### 4.1 表结构
**`sources`** — 订阅源
| 列 | 类型 | 说明 |
|----|------|------|
| id | TEXT PK | 1-64 字符 `[a-z0-9_-]` |
| name | TEXT | 显示名 |
| type | TEXT | `remote` / `local` |
| url | TEXT | 远程 URL(可多行,换行分隔,最多 8 个) |
| content | TEXT | local 源的原始内容 |
| enabled | INTEGER | 0/1 |
| filters_json | TEXT | FilterRule[] 的 JSON |
| meta_json | TEXT | 元数据 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,用于调试