947 lines
43 KiB
Markdown
947 lines
43 KiB
Markdown
# 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)
|
||
│ └── 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/version 命令 | `cmd/*.go` | cobra |
|
||
| viper 配置加载 | `internal/config/config.go` | viper |
|
||
| logrus 日志初始化 | 日志格式化、级别、输出 | logrus |
|
||
| SQLite 连接初始化 | `internal/database/db.go` | modernc/sqlite, sqlx |
|
||
| goose 迁移执行(启动时自动) | `internal/database/migrations.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 /collections/:name/:token` | `handler/download.go` | `download.ts` |
|
||
| `GET /sources/:name/: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 | `/collections/:name/:token` | 8 | 下载集合 |
|
||
| GET | `/sources/:name/: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 /collections/:id/xxx?target=mihomo
|
||
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
|
||
```
|
||
|
||
### 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 | 集成测试通过 |
|