697 lines
32 KiB
Markdown
697 lines
32 KiB
Markdown
# 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
|
||
|
||
**路由**: `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,用于调试
|