# Review 决议记录 > 来源:2026-07-27 对 `backend-dev-plan.md` 的 review,共 45 条问题。 > 本文档对每条问题给出明确决议,开发时以此为准,不得偏离。 > 如需变更决议,先修改本文档再改代码。 --- ## 决议速查 | # | 问题 | 决议 | 落地位置 | |---|------|------|----------| | 1 | ProxyNode 类型 | `map[string]any` | `model/types.go` | | 2 | script 过滤器迁移 | 不保留 script 类型 | `filter/custom.go` | | 3 | process→FilterRule 转换 | 不实现,前端直发 FilterRule[] | — | | 4 | defaultSettings 合并 | 实现 mergeSettings + 特殊浅合并 | `handler/settings.go` | | 5 | envPayload feature flags | 定义并返回,buildTimeScripts=false | `handler/env.go` | | 6 | Flow info URL hash 解析 | 实现完整解析 | `handler/flow.go` | | 7 | normalizeTarget UA 推断 | 实现完整关键词映射 | `model/target.go` | | 8 | 临时源覆盖 | 实现仅覆盖第一个匹配源 | `handler/download.go` | | 9 | parseJsonOrText | 实现 JSON 优先 + 纯文本回退 | `handler/` 工具函数 | | 10 | SW 清理端点 | 不实现(无 CF 迁移) | — | | 11 | getPublicBaseUrl | 实现 PUBLIC_DOWNLOAD_HOSTS 配置 | `handler/link.go` | | 12 | 中文拼音排序 | golang.org/x/text/collate + chinese | `filter/sort.go` | | 13 | splitHostPort | 自写 lastIndexOf(":") 版本 | `util/` | | 14 | JS URL vs Go net/url | net/url 为基 + 逐协议测试 + 边缘处理 | `proxy/uri_parser.go` | | 15 | URL-safe Base64 | 区分 StdEncoding / RawURLEncoding | `util/base64.go` | | 16 | Unicode flag 正则 | RE2 \p{} 支持,实测验证 | `util/flag.go` | | 17 | secureRandomInt | crypto/rand.Int(天然无偏) | `filter/sort.go` | | 18 | formatDuplicateNumber 自定义数字字符 | 实现完整功能 | `filter/dedupe.go` | | 19 | normalizeTaiwanFlag 三模式 | 实现完整 | `filter/flag.go` | | 20 | isUsefulProxy ASCII 校验 | 实现完整 | `filter/quick.go` | | 21 | applyState 字符串状态值 | 实现兼容解析 | `filter/quick.go` | | 22 | snell/ssh/h2-connect | 仅 client_parser 解析 | `proxy/client_parser.go` | | 23 | normalizeClientProxyKind 别名 | 实现别名映射 | `proxy/client_parser.go` | | 24 | isTokenValid 双哈希 | 先 SHA-256 再 ConstantTimeCompare | `util/token.go` | | 25 | CRLF 注入防护 | 校验 + 清洗所有 header 值 | `middleware/security.go` | | 26 | getBearerToken 三种提取 | 实现 Bearer / query / x-sub-store-token | `middleware/auth.go` | | 27 | CSP 安全头 | 收紧为 script-src 'self'(无 unsafe-eval) | `middleware/security.go` | | 28 | SQLite PRAGMA | WAL + busy_timeout + foreign_keys + MaxOpenConns=1 | `database/db.go` | | 29 | archiveAndDeleteResource 事务 | BeginTx 包裹三步 | `handler/` 删除逻辑 | | 30 | importStorage 导入顺序 | settings→sources→templates→collections | `handler/storage.go` | | 31 | exportStorage 排除内置模板 | 过滤 BuiltinTemplateIDs | `handler/storage.go` | | 32 | source_cache TTL 清理 | 后台 goroutine 定时清理 | `database/cache_repo.go` | | 33 | goose 迁移嵌入 | //go:embed + SetBaseFS | `database/migrations.go` | | 34 | goroutine 关闭安全 | sync.WaitGroup + context | `service/subscription.go` | | 35 | 缓存操作错误吞咽 | recover + 日志,不传播 | `database/cache_repo.go` | | 36 | 并发 wait 延迟 | 保留 waitMs 参数 | `service/concurrency.go` | | 37 | MAX_SCRIPT_ACTIONS | custom 规则上限 32 条 | `filter/custom.go` | | 38 | restoreDownloadGrantSnapshot | 不实现,分享已移除 | `handler/recycle.go` | | 39 | FRONTEND_VERSION 常量 | 定义 = "1.0.0" | `config/` | | 40 | TEST_URL 常量 | 定义 generate_204 | `render/` | | 41 | defaultProxyGroups | 无模板时生成 3 默认组 | `template/builtin.go` | | 42 | sing-box 完整结构 | log+inbounds+route+outbounds 全实现 | `render/singbox.go` | | 43 | profile-update-interval 默认值 | 默认 "6" | `handler/download.go` | | 44 | cache-control: no-store | 下载响应设此头 | `handler/download.go` | | 45 | modernc.org/sqlite 性能 | 接受 tradeoff,保留 swap 注释 | `database/db.go` | --- ## 逐条详述 ### 🔴 架构级决策(#1-#3)— 已由 AGENTS.md 约束豁免 #### #1 ProxyNode 类型 → `map[string]any` **决议**:ProxyNode 定义为 `type ProxyNode map[string]any`。 **理由**: - 13 种协议各有不同可选字段(reality-opts, ws-opts, plugin-opts, auth_str, congestion-controller...),struct 需定义全部字段且无法处理未知字段 - 过滤器通过 `getByPath(proxy, "ws-opts.headers.Host")` 做点号路径访问,map 天然支持 - JSON/YAML 反序列化到 map 保留所有字段;struct 会丢失未知字段 - stripUndefined(删除 nil 和 "")在 map 上语义清晰 - 类型安全损失可接受——节点字段本质是动态的 **实现要点**: - `model/types.go`:`type ProxyNode map[string]any` - 深拷贝:`json.Marshal` → `json.Unmarshal`(简单可靠,性能可接受) - 类型安全访问:在 `util/path.go` 提供 `GetByPath(node, "a.b.c")` / `SetByPath` / `GetString` / `GetInt` / `GetBool` 等辅助函数 #### #2 script 过滤器类型 → 不保留 **决议**:Go 版不实现 `script` 类型过滤器。用户自定义逻辑通过 `custom` 声明式规则链实现。 **理由**:全新项目无存量 script 过滤器,无需兼容。 **影响**: - `GET /api/scripts` 返回空数组 `[]`(端点保留,前端兼容) - `applyFilters` 中无 `script` 分支 - `validateScriptActions` 不需要 #### #3 process→FilterRule 转换 → 不实现 **决议**:Go API 直接接受 `FilterRule[]` 格式,不实现前端 `process` 数组到 `FilterRule[]` 的转换。 **理由**:前端尚未实现,将适配 Go API 设计。前端直接发送 FilterRule[] 格式。 --- ### 🟠 关键功能遗漏(#4-#11) #### #4 defaultSettings 合并 → 实现完整合并 **决议**:`GET /api/settings` 返回 `mergeSettings(defaultSettings(), storedSettings)`。 **合并规则**: - 顶层 key:stored 覆盖 default - `theme` 和 `appearanceSetting`:浅合并(stored 的子 key 覆盖 default 的对应子 key,而非整体替换) - 其他嵌套对象:深合并 **落地**:`handler/settings.go` + `config/defaults.go`(定义 `DefaultSettings()` 函数)。 #### #5 envPayload feature flags → 定义并返回 **决议**:`GET /api/env` 返回 `feature` 对象,各 flag 取值如下: ``` buildTimeScripts: false // 无 JS 引擎 proxyConversion: true // POST /api/proxy/parse ruleConversion: true // POST /api/rule/parse recycleBin: true // 回收站 nodeInfo: true // POST /api/utils/node-info surgeMac: true // surge-mac 渲染 ``` **落地**:`handler/env.go`。 #### #6 Flow info URL hash 解析 → 完整实现 **决议**:实现 `parseFlowRequest`,从 URL `#` fragment 中解析参数。 **支持两种格式**: 1. JSON 格式:`#{"flowUrl":"...","noFlow":true,"flowUserAgent":"...","flowHeaders":{}}` 2. Query-string 格式:`#flowUrl=...&noFlow=true&flowUserAgent=...` **额外**:`fetchFlowHeaders` 从响应体中提取 `upload=` / `download=` / `total=` / `expire=` 流量信息(subscription-userinfo header 优先,body 次之)。 **落地**:`handler/flow.go`。 #### #7 normalizeTarget UA 推断 → 完整关键词映射 **决议**:实现完整 UA 关键词→目标格式映射,按优先级匹配。 **映射表**(按检查顺序): | UA 关键词 | 目标格式 | |-----------|---------| | `sing-box` | singbox | | `v2ray` / `v2rayng` | v2ray | | `surge` (含 `mac`) | surge-mac | | `surge` (不含 `mac`) | surge | | `loon` | loon | | `egern` | egern | | `shadowrocket` | shadowrocket | | `quantumult` | qx | | `stash` | stash | **落地**:`model/target.go` 的 `NormalizeTarget(target, ua string) string`。 #### #8 临时源覆盖 → 仅覆盖第一个匹配源 **决议**:实现 `applyTemporarySourceOverride`,通过 `?url=` / `?content=` / `?ua=` query 参数临时覆盖。 **关键行为**:只覆盖集合中**第一个**匹配的源(按 `sourceIds` 顺序),不是全部覆盖。与原项目行为一致。 **落地**:`handler/download.go`。 #### #9 parseJsonOrText → JSON 优先 + 纯文本回退 **决议**:实现 `parseJsonOrText(body []byte) (map[string]any, error)`。 **逻辑**: 1. 尝试 `json.Unmarshal` → 成功则返回 2. 失败则将 body 作为纯文本,返回 `{"content": string(body)}` **应用端点**:template 创建/更新、storage 导入。 **落地**:`handler/` 下的工具函数。 #### #10 Service Worker 清理端点 → 不实现 **决议**:不实现 `/sw.js` 和 `/registerSW.js` 端点。 **理由**:全新项目,无 Cloudflare 版 Service Worker 残留需要清理。 #### #11 getPublicBaseUrl → 支持 PUBLIC_DOWNLOAD_HOSTS **决议**:实现 `getPublicBaseUrl(r *http.Request) string`。 **逻辑**: 1. 读取环境变量 / 配置 `SUB_STORE_PUBLIC_DOWNLOAD_HOSTS`(逗号分隔域名列表) 2. 若非空,返回第一个域名作为 base URL 3. 否则返回 `r.Host`(请求自身的 origin) **落地**:`handler/link.go` 的 `buildDownloadLink`。 --- ### 🟡 实现细节/陷阱(#12-#23) #### #12 中文拼音排序 → golang.org/x/text/collate **决议**:引入 `golang.org/x/text/collate` + `golang.org/x/text/language`,使用 `collate.New(language.SimplifiedChinese)` 做中文拼音排序。 **落地**:`filter/sort.go` 的 `sortProxies`。在 `go.mod` 添加依赖。 #### #13 splitHostPort → 自写版本 **决议**:不使用 `net.SplitHostPort`,自写 `splitHostPort(s string) (host, port string)`。 **行为**:用 `strings.LastIndex(s, ":")` 切分,与原项目一致。能正确处理 IPv6(如 `[::1]:443` → host=`[::1]`, port=`443`)和裸 IPv6(无端口时返回 host=s, port="")。 **落地**:`util/` 或 `proxy/uri_parser.go` 内部函数。 #### #14 JS URL vs Go net/url → net/url 为基 + 逐协议测试 **决议**:以 `net/url.Parse` 为基础解析器,针对已知差异做适配: - **Fragment 处理**:Go 的 `url.Fragment` 不会自动 URL-decode,需要手动 `url.QueryUnescape(fragment)` 获取节点名 - **Userinfo 提取**:用 `u.User.Username()` 和 `u.User.Password()`,不手动切分 - **IPv6 hostname**:`u.Hostname()` 自动去掉方括号 - **Query().Get() 返回 ""**:原项目 `URLSearchParams.get()` 返回 `null`,Go 返回 `""`。所有 `!= null` 判断改为 `!= ""` **落地**:`proxy/uri_parser.go`。为每种协议编写表驱动测试,覆盖正常/异常/边界 case。 #### #15 URL-safe Base64 → 明确区分 **决议**:在 `util/base64.go` 中提供三组函数: | 函数 | 编码 | 用途 | |------|------|------| | `DecodeBase64Std` | `base64.StdEncoding` | vmess JSON | | `DecodeBase64URL` | `base64.URLEncoding` | — | | `DecodeBase64RawURL` | `base64.RawURLEncoding` (无填充) | SSR | | `EncodeBase64URL` | `base64.URLEncoding` | — | | `EncodeBase64RawURL` | `base64.RawURLEncoding` | — | 同时提供 `DecodeBase64Auto(s string)` — 自动尝试 Std 和 RawURL 两种编码(先 Std,失败再 RawURL),因为订阅内容编码不一定规范。 **落地**:`util/base64.go`。 #### #16 Unicode flag 正则 → RE2 支持,实测验证 **决议**:Go RE2 支持 `\p{Regional_Indicator}` 和 `\uFE0F` / `\u200D`。 **验证清单**: - `detectFlag`:用真实国旗 emoji(🇭🇰 🇯🇵 🇺🇸 🇹🇼)测试 - `removeFlag`:测试含 ZWJ (`\u200D`) 和 variation selector (`\uFE0F`) 的复合 emoji - 编写单元测试覆盖所有 Unicode flag 场景 **落地**:`util/flag.go`。 #### #17 secureRandomInt → crypto/rand.Int **决议**:直接使用 `crypto/rand.Int(rand.Reader, big.NewInt(int64(max)))`。 **理由**:Go 的 `crypto/rand.Int` 内部已实现无偏随机(rejection sampling),无需手动实现。比原项目的 JS 版更简洁。 **落地**:`filter/sort.go` 的 shuffle 逻辑。 #### #18 formatDuplicateNumber 自定义数字字符 → 完整实现 **决议**:实现 `formatDuplicateNumber(index int, template string) string`。 **功能**:`template` 可包含自定义数字字符集,用空格分隔。如 `"一 二 三 四 五"` → index=2 输出 "二"。默认为阿拉伯数字 `"1 2 3 4 5..."`。 **落地**:`filter/dedupe.go`。 #### #19 normalizeTaiwanFlag 三模式 → 完整实现 **决议**:flag 过滤器的 `tw` 参数支持三种模式: | 参数值 | 输出 | |--------|------| | `ws` | 🇼🇸 (萨摩亚旗) | | `tw` | 🇹🇼 (台湾旗) | | 默认/其他 | 🇨🇳 (中国旗) | **落地**:`filter/flag.go`。 #### #20 isUsefulProxy ASCII 校验 → 完整实现 **决议**:`isUsefulProxy` 不仅检查节点名关键词,还校验: - `cipher` 值是否为纯 ASCII - `password` 值是否为纯 ASCII - WS Host header(`ws-opts.headers.Host`)是否为纯 ASCII 任一非 ASCII → 标记为 useless。 **落地**:`filter/quick.go`。 #### #21 applyState 字符串状态值 → 兼容解析 **决议**:`applyState` 接受以下值并归一化为 bool: | 输入 | 输出 | |------|------| | `true` / `"ENABLED"` / `"enabled"` | `true` | | `false` / `"DISABLED"` / `"disabled"` | `false` | **落地**:`filter/quick.go` 的 `parseState(v any) bool`。 #### #22 snell/ssh/h2-connect → 仅客户端配置行解析 **决议**:这三种类型仅在 Surge/Loon 客户端配置行解析中出现,不出现在 URI 协议中。 - `snell`:Surge 配置行 `snell = name, server, port, psk, ...` - `ssh`:Surge 配置行 `ssh = name, server, port, ...` - `h2-connect`:Surge 配置行 **落地**:`proxy/client_parser.go`,不放入 `uri_parser.go`。 #### #23 normalizeClientProxyKind 别名 → 实现映射 **决议**:客户端配置行的协议类型别名归一化: ``` shadowsocks → ss socks5-tls → socks5 https → http hysteria 2 → hysteria2 tuic-v5 → tuic ``` **落地**:`proxy/client_parser.go` 的 `normalizeClientProxyKind(kind string) string`。 --- ### 🔒 安全相关(#24-#27) #### #24 isTokenValid 双哈希 → 先 SHA-256 再 ConstantTimeCompare **决议**:所有 token 校验先做 SHA-256 哈希,再用 `subtle.ConstantTimeCompare` 比较哈希值,避免长度泄露。 **两种场景**: 1. **admin_token / download_token**(配置文件中的明文 secret): ``` inputHash = sha256Hex(input) secretHash = sha256Hex(secret) return subtle.ConstantTimeCompare([]byte(inputHash), []byte(secretHash)) == 1 ``` 2. **scoped grant**(数据库存储 `sha256Hex(token)`): ``` inputHash = sha256Hex(input) return subtle.ConstantTimeCompare([]byte(inputHash), []byte(storedHash)) == 1 ``` **落地**:`util/token.go` 的 `IsTokenValid(input, secret string) bool` 和 `IsGrantTokenValid(input string, storedHash string) bool`。 #### #25 CRLF 注入防护 → 校验 + 清洗 **决议**: 1. **setResponseHeader**:设置任何 header 值前校验 `!strings.ContainsAny(value, "\r\n")`,不合法则跳过设置并记录警告日志 2. **safeContentDisposition**:清洗文件名——移除 `\r\n` 和控制字符,对特殊字符做引号转义 **落地**:`middleware/security.go` 或 `handler/download.go` 中的工具函数。 #### #26 getBearerToken 三种提取 → 完整实现 **决议**:token 从三处提取,优先级从高到低: 1. `Authorization: Bearer ` header 2. `?token=` query parameter 3. `x-sub-store-token` header **落地**:`middleware/auth.go` 的 `extractToken(c *fiber.Ctx) string`。 #### #27 CSP 安全头 → 收紧 **决议**:Go 版前端不含 eval,CSP 收紧为: ``` default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'; font-src 'self' data:; ``` **与原项目差异**:移除 `'unsafe-eval'`(原项目前端有 eval,Go 版不需要)。 **落地**:`middleware/security.go`。 --- ### 🗄️ 数据库相关(#28-#33) #### #28 SQLite PRAGMA → 完整配置 **决议**:在 `database/db.go` 打开连接后立即执行: ```sql PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON; PRAGMA synchronous = NORMAL; ``` **连接池**: - `db.SetMaxOpenConns(1)` — modernc.org/sqlite 在高并发写时会出现 `database is locked`,单连接 + WAL 足以应对 sub-store 的个人工具场景 - 读写都在同一连接上,WAL 模式下读不阻塞写 **落地**:`database/db.go` 的 `InitDB(path string) (*sqlx.DB, error)`。 #### #29 archiveAndDeleteResource 事务 → BeginTx 包裹 **决议**:删除资源时在一个事务中执行三步: ``` BEGIN TRANSACTION; INSERT INTO recycle_bin (id, resource_type, resource_id, snapshot_json, deleted_at) VALUES (...); DELETE FROM {sources|collections|templates} WHERE id = ?; DELETE FROM recycle_bin WHERE id NOT IN ( SELECT id FROM recycle_bin ORDER BY deleted_at DESC LIMIT ? ); COMMIT; ``` **落地**:`handler/` 删除逻辑 + `database/recycle_repo.go` 的 `ArchiveAndDelete(tx, resourceType, resourceID, snapshot, maxEntries)`。 #### #30 importStorage 导入顺序 → 强制顺序 **决议**:导入顺序固定为: 1. `settings` — 无依赖 2. `sources` — 无依赖 3. `templates` — 无依赖(但 collections 引用 templates) 4. `collections` — 引用 sources 和 templates 每步独立事务,失败则中止并返回错误。 **落地**:`handler/storage.go` 的 `importStorage`。 #### #31 exportStorage 排除内置模板 → 过滤 **决议**:导出时过滤掉 ID 属于 `BuiltinTemplateIDs` 集合的模板。 ```go var exportedTemplates []Template for _, t := range allTemplates { if !builtinTemplateIDs[t.ID] { exportedTemplates = append(exportedTemplates, t) } } ``` **落地**:`handler/storage.go` 的 `exportStorage`。 #### #32 source_cache TTL 清理 → 后台 goroutine **决议**:启动一个后台 goroutine,每隔 `cache_ttl` 间隔执行一次清理: ```go func StartCacheCleaner(ctx context.Context, db *sqlx.DB, interval time.Duration) { ticker := time.NewTicker(interval) go func() { for { select { case <-ticker.C: db.ExecContext(ctx, "DELETE FROM source_cache WHERE cached_at + ttl < ?", time.Now().Unix()) case <-ctx.Done(): ticker.Stop() return } } }() } ``` 在 graceful shutdown 时通过 context 取消。间隔取 `cache_ttl` 配置值(默认 300s)。 **落地**:`database/cache_repo.go` 的 `StartCacheCleaner`。 #### #33 goose 迁移嵌入 → //go:embed + SetBaseFS **决议**:迁移 SQL 文件嵌入二进制: ```go //go:embed migrations/*.sql var embedMigrations embed.FS func RunMigrations(db *sqlx.DB) error { goose.SetBaseFS(embedMigrations) return goose.Up(db.DB, "migrations") } ``` **落地**:`database/migrations.go` + `database/migrations/*.sql`。启动时自动执行,幂等(goose 跟踪 `goose_db_version`)。 --- ### 🔄 并发/异步(#34-#36) #### #34 goroutine 关闭安全 → WaitGroup + context **决议**:异步缓存写入通过 `sync.WaitGroup` 管理生命周期。 ```go type AsyncWriter struct { wg sync.WaitGroup ctx context.Context cancel context.CancelFunc } func (aw *AsyncWriter) Write(fn func()) { aw.wg.Add(1) go func() { defer aw.wg.Done() select { case <-aw.ctx.Done(): return default: fn() } }() } func (aw *AsyncWriter) Wait() { aw.cancel() aw.wg.Wait() } ``` 在 graceful shutdown 时调用 `Wait()`,带 5 秒超时。 **落地**:`service/subscription.go` 或 `database/cache_repo.go`。 #### #35 缓存操作错误吞咽 → recover + 日志 **决议**:`safeCacheMatch` 和 `safeCachePut` 包装缓存操作: ```go func safeCacheGet(repo *CacheRepo, key string) (string, bool) { defer func() { if r := recover(); r != nil { logrus.Warnf("cache get panicked: %v", r) } }() return repo.Get(key) } ``` 缓存操作失败(SQL 错误、panic)只记录日志,不传播给订阅生成管线。 **落地**:`database/cache_repo.go` 的 safe wrapper。 #### #36 并发 wait 延迟 → 保留参数 **决议**:`RunWithConcurrency` 和 `RunSettledWithConcurrency` 的签名保留 `wait time.Duration` 参数。 **行为**:每个任务开始前(第一个除外)`time.Sleep(wait)`。用于避免同时请求同一 CDN 被限流。对应配置 `fetcher.concurrency_wait`。 **落地**:`service/concurrency.go`。 --- ### 📋 其他遗漏(#37-#45) #### #37 MAX_SCRIPT_ACTIONS → custom 规则上限 32 条 **决议**:`custom` 过滤器的规则链上限 `MAX_CUSTOM_RULES = 32`。不保留原项目的 `MAX_SCRIPT_ACTIONS = 2`(那是 JS 脚本的限制,Go 版无 JS 引擎)。 **落地**:`filter/custom.go` 的验证逻辑。 #### #38 restoreDownloadGrantSnapshot → 不实现 **决议**:分享功能已移除,不再创建或恢复 `share` 类型回收站记录。 **落地**:`handler/recycle.go` 的 restore 逻辑不包含 share 分支。 #### #39 FRONTEND_VERSION 常量 **决议**:定义 `const FrontendVersion = "1.0.0"`,用于 `GET /api/env` 响应。 **落地**:`internal/config/config.go` 或 `handler/env.go`。 #### #40 TEST_URL 常量 **决议**:定义 `const TestURL = "https://www.gstatic.com/generate_204"`,用于 mihomo/sing-box 渲染中 url-test proxy group 的 test URL。 **落地**:`render/mihomo.go` / `render/singbox.go`。 #### #41 defaultProxyGroups → 无模板时生成 3 默认组 **决议**:当 collection 未配置模板或模板无 proxy-groups 时,生成 3 个默认代理组: | 组名 | 类型 | 行为 | |------|------|------| | 🚀 节点选择 | select | 手动选择,含所有节点 + ♻️ 自动选择 | | ♻️ 自动选择 | url-test | 自动测速,含所有节点 | | 🚀 手动切换 | select | 手动选择,仅含所有节点 | **落地**:`template/builtin.go` 的 `DefaultProxyGroups()`。 #### #42 sing-box 完整结构 → 全实现 **决议**:sing-box 输出包含完整顶层配置: ```json { "log": { "level": "info" }, "inbounds": [{ "type": "mixed", "tag": "mixed-in", "listen": "0.0.0.0", "listen_port": 7890, "sniff": true }], "outbounds": [ { "tag": "PROXY", "type": "selector", "outbounds": [...], "interrupt_exist_connections": false }, { "tag": "AUTO", "type": "urltest", "outbounds": [...], "url": "https://www.gstatic.com/generate_204", "interrupt_exist_connections": false }, { "tag": "DIRECT", "type": "direct" }, { "tag": "REJECT", "type": "block" }, ...proxy outbounds... ], "route": { "auto_detect_interface": true, "final": "PROXY" } } ``` **落地**:`render/singbox.go`。 #### #43 profile-update-interval 默认值 **决议**:下载响应设置 `profile-update-interval` header,值为 `metadata.profileUpdateInterval`,若为空则默认 `"6"`。 **落地**:`handler/download.go`。 #### #44 cache-control: no-store **决议**:下载响应设置 `Cache-Control: no-store`,防止 CDN/浏览器缓存订阅内容。 **落地**:`handler/download.go`。 #### #45 modernc.org/sqlite 性能 → 接受 tradeoff **决议**:使用 `modernc.org/sqlite`(纯 Go),接受写性能低于 CGO 版的 tradeoff。 **理由**: - sub-store 是个人工具,写频率低(主要是缓存写入),性能不是瓶颈 - 纯 Go 支持 `CGO_ENABLED=0` 静态编译,跨平台部署简单 - 如未来性能不足,可切换到 `mattn/go-sqlite3`(接口不变,仅换驱动 + 启用 CGO) **落地**:`database/db.go` 添加注释说明 tradeoff 和替代方案。