Files
sub-store/docs/architecture-analysis.md
T

697 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sub-Store Cloudflare 原项目技术实现架构分析
> 参考路径: `ref/sub-store-cloudflare/`
> 版本: 1.1.0 (AGPL-3.0-or-later)
> 仓库: github.com/realchendahuang/sub-store-cloudflare
---
## 1. 项目定位
Sub-Store Cloudflare 是一个 **Cloudflare 原生的机场订阅聚合与路由模板管理工具**,部署形态为 Cloudflare Workers + Static Assets + D1 + Worker Secrets。核心能力:
- **订阅源聚合**:管理多个远程/本地订阅源,合并为集合 (Collection)
- **节点处理**:解析 13 种代理协议 URI、JSON/YAML 数组、客户端配置行,执行过滤/排序/去重/重命名等操作
- **多目标输出**:转换为 mihomo / sing-box / surge / loon / qx / egern / shadowrocket / v2ray / uri 等 13 种客户端格式
- **路由模板**:内置 6 套 ACL4SSR / Loyalsoldier / AI+Streaming mihomo 模板,支持自定义
- **规则转换**:将分流规则在 mihomo/surge/loon/qx 之间互转
- **分享/回收站**scoped download grants(限定资源+目标+过期的分享令牌)、有界回收站(50 条上限)
**非功能边界**AGENTS.md 明确约束):
- 不使用 R2/KV/Durable Objects/Queues/Cron
- 不运行时 eval 脚本(脚本为构建时打包)
- 不存储/执行来自 D1/浏览器/远程 URL 的脚本源码
- 远程源缓存使用 Workers Cache API`caches.default`),非 D1
---
## 2. 整体架构
```
┌─────────────────────────────────────────────────────────────┐
│ Cloudflare Worker │
│ (cloudflare/src/index.ts — Hono app, single entry) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ │
│ │ /api/* │ │ /download/* │ │ Static Assets │ │
│ │ (admin API) │ │ (public dl) │ │ (SPA fallback)│ │
│ │ routes/api │ │ routes/dl │ │ ASSETS binding│ │
│ └──────┬──────┘ └──────┬───────┘ └────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ lib/ 层 │ │
│ │ store.ts subscription.ts rules.ts │ │
│ │ (D1 CRUD) (核心管线) (规则转换) │ │
│ │ defaults.ts targets.ts scripts.ts │ │
│ │ (内置模板) (目标别名) (脚本注册表) │ │
│ │ compatibility http.ts limits.ts read.ts │ │
│ │ -resources.ts (token/CORS/Hdr) (常量) (流读) │ │
│ └──────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ D1 (DB) │ │ Cache API │ │ Worker Secrets│ │
│ │ 5 张表 │ │ caches. │ │ ADMIN_TOKEN │ │
│ │ │ │ default │ │ DOWNLOAD_TOK │ │
│ └────────────┘ └────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 2.1 Monorepo 结构
```
sub-store-cloudflare/
├── package.json # workspace root, pnpm@11.7, node>=22
├── pnpm-workspace.yaml # packages: frontend, cloudflare
├── wrangler.jsonc # 顶层部署配置 (ASSETS + D1 + secrets)
├── cloudflare/ # Worker 后端
│ ├── src/
│ │ ├── index.ts # Hono app 入口 + 导出 ExportedHandler
│ │ ├── types.ts # 所有 TypeScript 类型定义
│ │ ├── worker-configuration.d.ts # Wrangler 生成的 Env 接口
│ │ ├── routes/
│ │ │ ├── api.ts # /api/* 管理接口 (896 行)
│ │ │ └── download.ts # /download/* 公开下载接口
│ │ ├── lib/
│ │ │ ├── store.ts # D1 数据层 CRUD (644 行)
│ │ │ ├── subscription.ts # 核心订阅处理管线 (2430 行)
│ │ │ ├── rules.ts # 分流规则转换
│ │ │ ├── defaults.ts # 6 套内置路由模板
│ │ │ ├── targets.ts # 客户端目标别名归一化
│ │ │ ├── scripts.ts # 构建时脚本注册表 + 运行时
│ │ │ ├── compatibility-resources.ts # download_grants + recycle_bin
│ │ │ ├── http.ts # token 校验 / CORS / 安全头
│ │ │ ├── limits.ts # 6 个字节/数量限制常量
│ │ │ └── read.ts # 流式响应读取 (防 OOM)
│ │ └── generated/ # 构建时生成 (git ignored)
│ │ └── script-registry.ts
│ ├── migrations/
│ │ ├── 0001_initial.sql
│ │ ├── 0002_runtime_schema_cleanup.sql
│ │ └── 0003_compatibility_resources.sql
│ └── package.json # hono, json5, yaml 三个运行时依赖
├── frontend/ # Vue 3 + Vite SPA
│ └── src/ # Pinia stores, views, components
└── scripts/ # 构建时 Node.js 脚本
├── generate-script-registry.mjs # 扫描 config/scripts → registry
├── install-cloudflare.mjs # 一键安装器
├── render-seed-sql.mjs # agent-setup → seed SQL
└── ... (15+ 检查/部署脚本)
```
---
## 3. 运行时环境与绑定
### 3.1 Worker Bindings (wrangler.jsonc)
| 绑定 | 类型 | 用途 |
|------|------|------|
| `DB` | D1Database | 所有结构化配置数据 |
| `ASSETS` | Fetcher | Static Assets (前端 SPA, `run_worker_first: true`) |
| `SUB_STORE_ADMIN_TOKEN` | Secret (string) | `/api/*` 管理 API 鉴权 |
| `SUB_STORE_PUBLIC_DOWNLOAD_TOKEN` | Secret (string) | `/download/*` 公开下载鉴权 |
| `SUB_STORE_APP_NAME` | Var (string) | 应用显示名 |
| `SUB_STORE_PUBLIC_DOWNLOAD_HOSTS` | Var (string) | 逗号分隔的纯下载域名列表 |
### 3.2 关键配置
- **`compatibility_date`**: `2026-07-08`
- **`compatibility_flags`**: `["nodejs_compat"]`
- **`assets.not_found_handling`**: `"single-page-application"` — 前端 SPA 路由兜底
- **`assets.run_worker_first`**: `true` — Worker 先处理请求,未匹配才回退到静态资源
- **`observability`**: enabled, head_sampling_rate=1
### 3.3 下载域名隔离机制
`index.ts``fetch` handler 在路由之前检查 hostname
- 如果当前 hostname 在 `SUB_STORE_PUBLIC_DOWNLOAD_HOSTS` 列表中,且 pathname 不以 `/download/` 开头 → 返回 404
- 效果:下载域名只能访问下载端点,管理 UI 不可达
---
## 4. 数据库 Schema (D1 / SQLite)
### 4.1 表结构
**`sources`** — 订阅源
| 列 | 类型 | 说明 |
|----|------|------|
| id | TEXT PK | 1-64 字符 `[a-z0-9_-]` |
| name | TEXT | 显示名 |
| type | TEXT | `remote` / `local` |
| url | TEXT | 远程 URL(可多行,换行分隔,最多 8 个) |
| content | TEXT | local 源的原始内容 |
| enabled | INTEGER | 0/1 |
| filters_json | TEXT | FilterRule[] 的 JSON |
| meta_json | TEXT | 元数据 JSONua, cacheTtl, subUserinfo 等) |
| created_at | INTEGER | 毫秒时间戳,同时用于排序 |
| updated_at | INTEGER | 毫秒时间戳 |
**`collections`** — 订阅集合
| 列 | 类型 | 说明 |
|----|------|------|
| id | TEXT PK | 同上 |
| name | TEXT | 显示名 |
| source_ids_json | TEXT | string[],空数组表示包含所有 enabled 源 |
| filters_json | TEXT | 集合级 FilterRule[] |
| template_id | TEXT | 关联的路由模板 ID |
| ignore_failed | INTEGER | 0/1,是否忽略失败的源 |
| enabled | INTEGER | 0/1 |
| meta_json | TEXT | 元数据 |
| created_at / updated_at | INTEGER | 同上 |
**`templates`** — 路由模板(仅存自定义的,内置模板在代码中)
| 列 | 类型 | 说明 |
|----|------|------|
| id | TEXT PK | 同上 |
| name | TEXT | 显示名 |
| target | TEXT | 目标客户端(默认 mihomo |
| config_json | TEXT | RoutingTemplateConfig JSON |
| created_at / updated_at | INTEGER | 同上 |
**`app_settings`** — 应用设置(单行,id="default"
| 列 | 类型 | 说明 |
|----|------|------|
| id | TEXT PK | 固定 "default" |
| value_json | TEXT | 设置 JSON(深合并) |
| updated_at | INTEGER | |
**`download_grants`** — 分享令牌(migration 0003
| 列 | 类型 | 说明 |
|----|------|------|
| id | TEXT PK | UUID |
| token_hash | TEXT UNIQUE | SHA-256 hex of token |
| resource_type | TEXT CHECK | `source` / `collection` |
| resource_id | TEXT | |
| target | TEXT | 限定的目标客户端,空=不限 |
| expires_at | INTEGER | 过期时间,NULL=永不过期 |
| enabled | INTEGER | 0/1 |
| created_at / updated_at | INTEGER | |
- 索引: `idx_download_grants_token_hash`, `idx_download_grants_resource`
**`recycle_bin`** — 回收站(migration 0003
| 列 | 类型 | 说明 |
|----|------|------|
| id | TEXT PK | UUID |
| resource_type | TEXT CHECK | `source`/`collection`/`template`/`share` |
| resource_id | TEXT | |
| snapshot_json | TEXT | 删除时的完整快照 |
| deleted_at | INTEGER | |
- 索引: `idx_recycle_bin_deleted_at`
- 上限: 50 条(通过 `DELETE ... LIMIT -1 OFFSET 50` 自动裁剪)
### 4.2 设计要点
- **排序用 created_at**`sortSources` / `sortCollections` 通过 `UPDATE SET created_at = now + index` 重排序,列表查询 `ORDER BY created_at ASC`。同一 batch 中 `now + index` 保证顺序。
- **UPSERT 模式**:所有写入用 `INSERT ... ON CONFLICT(id) DO UPDATE SET ...`,天然幂等。
- **批量操作**`importStorage` 使用 `env.DB.batch(statements)` 单事务导入。
- **内置模板不入库**migration 0002 主动删除旧版内置模板行,改为代码持有 (`BUILTIN_TEMPLATES` 数组)`listTemplates` 合并内置+自定义。
---
## 5. HTTP 路由设计
### 5.1 全局中间件 (index.ts)
1. **全局错误处理**: `app.onError` → JSON `{status:"failed",error:{code:500,...}}` + 结构化日志
2. **CORS preflight**: `OPTIONS *` 返回允许的方法和头
3. **安全头**: `applySecurityHeaders` 对每个响应添加 CSP / Referrer-Policy / X-Content-Type-Options / X-Frame-Options / Permissions-Policy
4. **CORS**: `applyCorsHeaders` 根据 origin 白名单(空则不加 CORS 头)
5. **下载域名隔离**: fetch handler 前置 hostname 检查
### 5.2 管理 API (`/api/*`)
**全局中间件**:
- `requireAdmin`: Bearer token / query `token` / `x-sub-store-token` 头 → SHA-256 + `timingSafeEqual``SUB_STORE_ADMIN_TOKEN` 比对
- `bodyLimit`: 4 MiB (`MAX_API_BODY_BYTES`)
**统一响应格式**:
- 成功: `{ "status": "success", "data": <T> }`
- 失败: `{ "status": "failed", "error": { "code": <number>, "message": <string> } }`
**端点清单**:
| 方法 | 路径 | 功能 |
|------|------|------|
| GET | `/api/env` | 运行时环境信息(backend、version、feature flags |
| GET | `/api/scripts` | 构建时脚本注册表元数据 |
| POST | `/api/proxy/parse` | 一次性代理内容转换 |
| POST | `/api/rule/parse` | 一次性分流规则转换 |
| GET/PATCH | `/api/settings` | 应用设置(GET 合并默认值) |
| GET/POST | `/api/storage` | 全量导出/导入(备份恢复) |
| GET/POST | `/api/sources` | 订阅源列表/创建 |
| PUT | `/api/sources` | 批量排序 |
| POST | `/api/sort/sources` | 排序(兼容旧接口) |
| GET/PATCH/DELETE | `/api/sources/:name` | 单源操作 |
| GET/POST | `/api/collections` | 集合列表/创建 |
| PUT | `/api/collections` | 批量排序 |
| GET/PATCH/DELETE | `/api/collections/:name` | 单集合操作 |
| GET/POST | `/api/templates` | 模板列表/创建 |
| GET/PATCH/DELETE | `/api/templates/:name` | 单模板操作(内置不可删改) |
| GET/POST | `/api/shares` | 分享令牌列表/创建 |
| PATCH/DELETE | `/api/shares/:id` | 令牌操作 |
| GET | `/api/recycle-bin` | 回收站列表 |
| DELETE | `/api/recycle-bin/:id` | 彻底删除 |
| POST | `/api/recycle-bin/:id/restore` | 恢复 |
| POST | `/api/utils/node-info` | IP 信息查询(代理转发 ipwho.is 等) |
| POST | `/api/preview/source` | 源预览(原始+处理后节点) |
| POST | `/api/preview/collection` | 集合预览 |
| GET | `/api/link/source/:name` | 生成下载链接 |
| GET | `/api/link/collection/:name` | 生成下载链接 |
| GET | `/api/source/flow/:name` | 流量信息(解析 subscription-userinfo 头) |
### 5.3 下载 API
**路由**: `GET /collections/:name/:token``GET /sources/:name/:token`,目标格式通过 `?target=` 指定
**鉴权**:
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,用于调试