@routerhub/agent-rules 1.5.154 → 1.5.156
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.base.md +107 -0
- package/package.json +1 -1
- package/rules/global.md +107 -0
package/AGENTS.base.md
CHANGED
|
@@ -234,6 +234,105 @@
|
|
|
234
234
|
- 典型反例:`.filter(v => v.status !== 'disabled')` 在将来新增第三种状态(如 `suspended`)时会被误放行,保存必被后端拒绝。
|
|
235
235
|
- 自查:凡「可选/可用/合法」判定,写成「只保留允许的那些」而不是「排除不允许的那些」。
|
|
236
236
|
|
|
237
|
+
## ⚠️ 网关后端编码铁律(来自 PR 评审沉淀)
|
|
238
|
+
|
|
239
|
+
⚠️ 本章来自 routerhub-gateway 92 个已合入 PR 中多位评审者(hankWaling / baikaifa / sam-pomex / rachelPomex / eason-qing / enzo0824)的 review 评论沉淀,每条都有真实 PR 出处。写 Go 后端(网关/代理/计费/路由类)代码时对照本节自查。本节所有条目均为强制规则,命中即自检。
|
|
240
|
+
|
|
241
|
+
### 计费与资源对称(核心命脉)
|
|
242
|
+
|
|
243
|
+
- ⚠️ **计费/归属字段必须来自实际服务实例**:流式与非流式两条路径对「实际使用的 provider/model/instanceID」的捕获必须对称;failover 到备用实例后,billing/sticky/usage 归属必须来自实际服务实例,禁止回退到主 provider。defer 里做计费的,必须在主循环并列采集真实值,不能依赖 drain/二次读取。显示给客户看的 cost 与实际扣费的 cost 必须同一计算、同一入参,禁止分头计算。
|
|
244
|
+
- ⚠️ **「先预扣、后结算」必须有对称结算与兜底释放路径**:结算必须基于真实完成与真实用量;失败/无用量时必须退款或按实际用量校准。依赖上游返回 usage 字段时,必须保证终态必有该字段(缺量走宽限期兜底)。软删除、成功、失败任何终态都不能漏掉结算——预扣的额度必须有最终回收路径,否则永久挂账。
|
|
245
|
+
- ⚠️ **档位/分类判定必须确定性且朝「不漏收」收敛**:判定结果与请求入参排列顺序无关(禁止「取第一个匹配」);做两侧分类(推理/非推理、高/基准档)时,登记封闭侧、开放侧新增默认命中基准档,靠数据/配置驱动,禁止维护硬编码模型前缀列表。
|
|
246
|
+
- ⚠️ **计费判定必须基于「实际将生效的值」而非请求原始字段**:原始字段与生效值之间隔着 converter/fallback 层时,基于原始字段判断会产生覆盖缺口(如 MaxTokens 为空会 fallback 到 MaxCompletionTokens)。
|
|
247
|
+
- ⚠️ **计费明细必须落库可审计**:新增的计费/用量明细即使总额算对,也必须写入可审计的落库字段——否则判错只体现在账单总数、无法定位是哪些请求判错,漏收长期隐藏。
|
|
248
|
+
- ⚠️ **区间/边界判定不能静默落入分支**:起止相等、空串、0 值等边界(如 start_time==end_time 会意外命中「全天生效」)必须显式定义语义或当作非法拒绝。
|
|
249
|
+
- ⚠️ **计费口径与上游实际成本对称**:上游确实执行并收钱的才向客户收,上游拦截/我方无成本的应免单;反直觉的口径选择必须写进注释给出依据。
|
|
250
|
+
- ⚠️ **复用定价常量前核对注释适用面**:跨厂商、跨路径不能共用一个单价,常量注释声明的适用范围与实际使用面不一致就是漏收源头。
|
|
251
|
+
- ⚠️ **计费公式注释随逻辑变更同步更新**:禁止「注释说旧公式、代码跑新公式」,否则误导维护者基于错误前提改计费。
|
|
252
|
+
- ⚠️ **新计费配置默认关闭 + 守门测试**:尚未与账单/上游核对过的定价或行为默认关闭,用开关控制上线,配一条「默认值守门」测试——开启必须是有意识的决策,不会被顺手改掉。
|
|
253
|
+
- ⚠️ **同一事实只能有一处维护源**:定价/常量/配置禁止代码硬编码与 DB 并存且数值不一致(死配置隐患),阈值集中成常量按模型查表。
|
|
254
|
+
- ⚠️ **准入/风控判定必须用 raw 估算,禁止用被宽松化/打折处理过的估算做硬准入**:为缓解误拒设计的 cap 用在准入上等于把「误拒」换成「误放」——reserveCredit() 用 raw estimate,prepaid 准入永不 cap(风控路径),软计量才用 capped estimate。
|
|
255
|
+
- ⚠️ **成本不确定的多步操作(搜索/循环/工具调用)必须按需逐次预扣(JIT),禁止按 worst-case 一次性 upfront 预扣**:初始只预扣模型 base estimate,每真正发起一步前再小额 reserve 一次——否则 max_uses=8 这类默认值把请求 upfront 放大数倍。
|
|
256
|
+
- ⚠️ **扣款/写负余额路径必须有过载守卫硬阻断,禁止「事后扣款 + 只记 metric」**:UPDATE balance = balance - $1 没有透支守卫时,负余额只有 metric 计数、不硬阻断,等于允许无限透支。
|
|
257
|
+
- ⚠️ **用量估算必须理解计价模式并分桶计费**:缓存命中/未命中单价不同(如 cache read 仅 10%),命中按 cache read 估、未命中按 cache write/no-cache 估;同一请求的 usage 必须按 cached/non-cached input、output 分桶计费,禁止一律按最贵档估算或用统一单价折算——否则预扣与真实账单差一个数量级。
|
|
258
|
+
|
|
259
|
+
### 对外请求安全
|
|
260
|
+
|
|
261
|
+
- ⚠️ **用户可控 URL 发起请求必须防 SSRF**:只允许 http/https、解析 DNS 后阻断私网/loopback/link-local/metadata IP(含 169.254.169.254)、限制重定向次数、使用独立短超时 client,禁止裸 `http.DefaultClient`。
|
|
262
|
+
- ⚠️ **内部专用标记/密文帧禁止透传到对外响应或上游**:出站统一剥帧 + 入站兜底剥帧——帧一旦下发无法收回,客户端会长期持有。
|
|
263
|
+
- ⚠️ **日志禁止输出请求/响应 body**:上游请求 body 可能带签名 URL、密钥、内部错误详情,全量打 Info 日志=敏感信息进日志系统;默认只输出摘要/trace id。**外部内容写日志前必须截断 + redact + 限长**:高基数动态消息记 hash + length 代替原文,禁止无长度上限地记录上游回显内容(PII/secret 长期进 Cloud Logging,且攻击者可诱导上游回显敏感内容刷爆日志费用)。
|
|
264
|
+
- ⚠️ **凭据必须按真实格式校验/适配**:明文 API key 与结构化 JSON 凭据(如 SigV4)需区分处理,禁止只做非空校验就原样透传——格式不匹配在上游 401/403,且启动时静默放行、运行期才暴露。
|
|
265
|
+
- ⚠️ **声明支持外部能力/版本/协议必须基于实测**:禁止从通用版本表推导或移植注释即认可——外部平台可能对推导出的版本返回 400,文档会误导排障方向。
|
|
266
|
+
- ⚠️ **上游/外部服务错误消息透传客户端前必须独立 sanitize**:禁止「成功解析上游文本 = 文本安全」的假设——上游 JSON 里的 error.message 可能含 `dial tcp 10.x.x.x: connection refused`、`api_key=sk-secret` 等内部信息,JSON 路径同样要走 sanitizer。
|
|
267
|
+
- ⚠️ **新增行为的 gate 开关必须覆盖该行为的所有入口(含 400/错误路径)**:错误响应路径是最容易绕过 observation 的——开关=false 不代表新行为关闭,未清洗的错误消息仍可能进入生产客户端。
|
|
268
|
+
|
|
269
|
+
### 并发与资源上限
|
|
270
|
+
|
|
271
|
+
- ⚠️ **goroutine 写入的共享数据读取前必须确认其已退出**:排空/超时分支是重灾区(goroutine 往往还没退出);并发完成信号时序必须「先发结果、后关通道」,读取侧「先 drain 数据通道、再读错误通道」,超时后先非阻塞复查结果是否已就绪,避免把「其实成功了」误判为超时。
|
|
272
|
+
- ⚠️ **高 QPS 路径禁止无上限 `go func` 发外部请求**:没有队列/限流/并发上限时,下游慢或不可达会堆积 goroutine/连接/fd 拖垮实例;热路径禁止重复昂贵构造(LoadLocation/正则编译/重复解析/每次读 env),一次构建缓存(实测 LoadLocation 差距约 4200 倍)。
|
|
273
|
+
- ⚠️ **后台 goroutine 必须 recover**:运行中的后台任务(热重载/异步循环)的 panic 会带走整个进程,必须 recover 隔离;启动路径 fail-fast(不 recover)合理。
|
|
274
|
+
- ⚠️ **后台重载/重试必须有退出条件与退避,禁止无界重试循环**:热重载禁止「全有或全无」——按内容指纹只重建实际变更项,单项构建失败保留旧版本继续服务并告警,不要因一个坏条目拒绝整个更新(单点故障放大为全局故障)。
|
|
275
|
+
- ⚠️ **生成全局递增序号禁止「读-改-写」**:读当前最大值→+1→写回在并发下必然重复;用原子操作/锁,或采用与顺序无关的唯一命名(时间戳+哈希),并区分「无记录返回初始值」与「读取真的失败」。
|
|
276
|
+
- ⚠️ **缓存必须包住所有可能失败的准备步骤**:把解密/连接等前置操作放在缓存查找之前,失败会绕过缓存直接破坏状态(实测:解密失败绕过 cache 直接把账号踢出池,模型数从 24→23、HTTP 400);应把失败风险最高的步骤移到缓存内、按单元懒执行。
|
|
277
|
+
|
|
278
|
+
### 错误处理与路由
|
|
279
|
+
|
|
280
|
+
- ⚠️ **禁止吞错/空返回**:「返回了但等于没返回」((nil, nil)、空流、lastErr==nil 即成功、`|| true` 吞失败)必须显式区分「正常空结果」与「真的失败」——失败必须显式报错(如 ErrNoSupportingInstance),禁止静默失败。**关闭/刷新/收尾路径的错误禁止裸 `_ =` 丢弃**:关键信号(如 ctx.Err=消费者已走)必须保留并上报,否则「该停不停」的隐患被静默吞掉。
|
|
281
|
+
- ⚠️ **嵌套块 `:=` 会遮蔽外层 err**:嵌套块内用 := 新建块作用域变量遮蔽外层 err,构造/调用失败被静默吞掉、nil 对象继续流转;错误必须落在调用方能读到的作用域,赋值后立即检查。
|
|
282
|
+
- ⚠️ **nil 防御覆盖两层**:外层结构体为 nil 与内层关键字段为 nil 都要防;流式/非流式两侧都要判,禁止只防流式。
|
|
283
|
+
- ⚠️ **不同错误语义用独立错误类型**:禁止复用通用错误(如 budget 超限复用 rate_limit_error 会让客户端把没钱当限流反复重试)。
|
|
284
|
+
- ⚠️ **能力判定必须与真实能力一致**:包装层一律宣称支持会让入口判定形同虚设、错误以错误语义暴露(502 而非预期 400);能力判定下沉到真实实现处。
|
|
285
|
+
- ⚠️ **上游不支持某能力是客户端问题 → 映射 4xx 而非 5xx**:错误分类要区分「客户端请求了不支持的能力」与「服务端故障」,避免 502 掩盖真实原因。
|
|
286
|
+
- ⚠️ **failover 遇「实例不支持此能力」应继续尝试下一实例**:同一池内实例能力不一致时,首个不支持实例不应阻塞后续可用实例。
|
|
287
|
+
- ⚠️ **请求带了但不支持的参数禁止静默忽略**:必须显式 4xx 报错——静默忽略让客户端误以为参数生效,是沉默失败。
|
|
288
|
+
- ⚠️ **路由/解析 switch default 分支禁止静默 skip+warn**:静默跳过会让路由表悄悄缺模型/缺路由;应显式报错或强告警。路由收窄/拆池前先查全量数据分布。**依赖「客户端/上游不会这么发」假设的丢弃/忽略分支,必须打日志**:假设一旦被打破(上游发来畸形数据),生产可诊断,禁止静默丢弃。
|
|
289
|
+
- ⚠️ **失败路径也要补全归属字段**:成功/失败覆盖一致(失败 usage 事件也要回填 ProviderAccountId 等),否则某个账号持续失败时按账号排查不出来。
|
|
290
|
+
- ⚠️ **清理/剥离函数必须清「实际被填充」的字段**:核对写入方填哪个字段、清理方清哪个字段,二者对齐;只清自己认识的字段、漏掉写入方真正填的,等于没清。
|
|
291
|
+
- ⚠️ **等效路径行为必须对齐**:同一语义的多条路径(chat/responses、流式/非流式、compat/非 compat)改一条的过滤/剥除逻辑必须同步所有等效路径,否则出现「改前隐藏、改后泄漏」的静默回归。
|
|
292
|
+
- ⚠️ **流式发出首块后失败必须补发显式错误结束事件**:禁止只静默关闭连接——客户端会误判为网络断开或永远等待。
|
|
293
|
+
- ⚠️ **客户端已断开后禁止再向连接写错误响应**:写前检查断开状态;已断开只做结算与上报,不做无用写。
|
|
294
|
+
- ⚠️ **带副作用的函数先判空/前置校验后写**:先写后判空,nil 入参会 panic。
|
|
295
|
+
- ⚠️ **路由业务约束后端构建/加载期自行校验**:不能只依赖 UI 层规则或文档声明——DB 历史数据/手工 SQL 可绕过 UI,同账号数据混挂会静默转发到错误协议端点。
|
|
296
|
+
- ⚠️ **对上游错误码做重试/冷却/failover 分类时禁止只匹配单个码,必须覆盖同族错误**:SDK 常用同一格式输出整个异常联合体(`received exception <code>: ...`),只匹配一个码会把同族错误(serviceUnavailable / throttling / modelTimeout)误判为 StatusCode0、不可重试;冷却/降级判据同样要覆盖同族码(如 429/403/498 都该冷却)。
|
|
297
|
+
- ⚠️ **流式错误处理区分「流建立前」与「流中途」两个阶段,处理逻辑分开写**:只在首块前的异常才能映射为 500;一旦上游发出 message_start、转换器已产生 chunk,中途报错应走流中途的处理(如补错误结束事件、按账号轮换),否则健康账号闲置、客户仍拿 500。
|
|
298
|
+
|
|
299
|
+
### 配置与常量
|
|
300
|
+
|
|
301
|
+
- ⚠️ **依赖密钥的功能开关缺密钥必须 fail-fast 拒绝启动**:禁止「开关开着、密钥却没加载」的自相矛盾状态静默上线(该状态会同时破坏新旧两条链路)。
|
|
302
|
+
- ⚠️ **配置解析非法值静默回退时确认回退方向安全**:非法值回退若落在「启用/高风险」方向(写错 env=启用),等于静默开启危险行为,应对非法值单独告警。
|
|
303
|
+
- ⚠️ **脚本/工具默认值必须与代码权威默认值一致**:默认值静默指向错误目标(错误池/错误环境)比报错更危险。
|
|
304
|
+
- ⚠️ **残缺配置整表覆盖默认行为=危险**:配置非空就整表替换默认码表/行为,漏写即静默失效;应 merge 或开关控制。
|
|
305
|
+
- ⚠️ **同一判定跨端实现时边界/单位显式对齐**:一端毫秒、一端秒截断(限流窗口)会埋下窗口偏差。
|
|
306
|
+
- ⚠️ **用拼接类函数(url.JoinPath)前确认其转义处理**:JoinPath 已处理转义,再手动 PathEscape 会双重转义。
|
|
307
|
+
- ⚠️ **配置项 0 值的语义(disable/回退默认/无限)每个变量可以完全不同,必须逐个显式文档化**:如 PRICING_RELOAD_INTERVAL<=0 回退默认 60s、ROUTING_RELOAD_INTERVAL=0 才是 disable——依赖「0=关闭」的惯性推断会把「刻意不可禁用」误解为可关闭。
|
|
308
|
+
- ⚠️ **配置经 clamp/fallback 后,启动日志必须打印生效值而非原始值**:main 只 log clamp 前的原始值会误导运维按日志排查;打印生效值(含 clamp 后的实际值)让启动日志诚实可见。
|
|
309
|
+
|
|
310
|
+
### 代码结构与复用
|
|
311
|
+
|
|
312
|
+
- ⚠️ **启动路径与热重载路径共用同一份构建实现**:结构上共用才不可能分叉(不是靠「记得在两处都调一次」)。
|
|
313
|
+
- ⚠️ **跨 goroutine/closure 传链路追踪状态用 context 贯穿**:closure 通过捕获的 ctx 读取状态,否则追踪/审计字段静默失效。
|
|
314
|
+
- ⚠️ **字段填充收敛成公共 helper**:多个 handler 手写同一批字段填充必然出现成功/失败覆盖不一致,抽公共函数(如 enrichUsageEvent)。
|
|
315
|
+
- ⚠️ **删除守卫/过滤条件要说明理由**:禁止顺手删——可能影响现有业务;若只是脏数据应修数据,不是删过滤。
|
|
316
|
+
- ⚠️ **取数口径变更分阶段迁移**:改 hash 输入/键维度/身份字段不能与另一项行为变更同一步上线——口径突变让全部存量键 miss 导致路由漂移甚至硬 503。
|
|
317
|
+
- ⚠️ **行为变更扩大存储/缓存键写入范围时评估键基数与内存**:键量级跳增带来容量风险,纳入监控。
|
|
318
|
+
- ⚠️ **全局批量替换会误伤正文引用**:sed/品牌替换等完成后必须全量核对每个受影响点。
|
|
319
|
+
- ⚠️ **浅拷贝含指针字段的结构体后修改内容会污染调用方;必须深拷贝指针字段**:shallow request copy 共享同一个指针字段(如 *OpenAIReasoningConfig),在副本上 `.Effort=...` 会改到调用方的原始请求。
|
|
320
|
+
- ⚠️ **把派生/转换结果写入某字段前,必须核对消费方读取的是哪一层**:写错层是 silent no-op——如 NormalizeReasoning 只读顶层 reasoning_effort(当没有 reasoning 对象时),派生值写到顶层字段等于没写,必须写进消费方实际读取的那一层。
|
|
321
|
+
- ⚠️ **同一数据源禁止重复解码(热路径尤其),合并为一次解码**:同一 bytes 连续两次 json.Unmarshal(一次进结构体、一次进 map)是热点路径的浪费,应复用第一次解码结果。
|
|
322
|
+
- ⚠️ **代码行为变化会静默破坏外部脚本对资源生命周期(TTL/永久化)的隐性依赖,必须显式迁移或文档强制**:如请求路径不再写 SetPermanent 后,此前被「转正为永久」的绑定 1 小时后静默过期、key 开始 503——这类隐性依赖变化必须显式记录迁移动作。
|
|
323
|
+
|
|
324
|
+
### 测试与验证
|
|
325
|
+
|
|
326
|
+
- ⚠️ **核心逻辑保留确定性单测**:路由/映射/转换/计费/幂等逻辑保留不依赖真实外部资源的确定性单测——标准 CI 覆盖不了=回归无防护。
|
|
327
|
+
- ⚠️ **集成测试缺环境 Fatal 而非 Skip**:缺配置就明确失败,禁止「跳过」被当成通过(假绿)。
|
|
328
|
+
- ⚠️ **单测全绿≠上线正确**:外部平台行为/资源加载顺序/错误兜底必须用真实链路/真实请求验证;计费金额实测对账(手算单价、API 返回值 vs 落库计费逐笔对照),禁止凭推断。
|
|
329
|
+
- ⚠️ **档位/分类判定测试双向覆盖+变异验证**:既防「误判高档→多收客户」,也防「漏登记→静默少收」;变异测试证明用例真能拦住对应回归。
|
|
330
|
+
- ⚠️ **回归测试断言覆盖「实际会非空的字段」**:只断言永远为空的字段等于没测。
|
|
331
|
+
- ⚠️ **热点路径 miss 分支日志降级**:预期内高频路径用 Debug 或采样,禁止 Info 刷屏淹没真实告警。
|
|
332
|
+
- ⚠️ **测试配置禁止直接改包级全局变量,必须注入依赖结构体**:mutation package-level var 在 `go test -race` 并行测试下是 data race(如 cursorHeartbeatInterval 直接改全局),应移进 handlerDeps 注入。
|
|
333
|
+
- ⚠️ **「对外暴露上游内容」类功能,测试矩阵必须包含恶意/超长/含敏感输入**:断言客户端与日志均不泄露 URL/IP/email/API key/换行/10KB 文本,且必须截断——只测正常输入测不出泄露。
|
|
334
|
+
- ⚠️ **优化/收益评估前必须确认被优化的开销真实发生在目标位置**:本地 SDK 预检(ms=0、无网络调用)与上游真实往返是两回事——凭假设估收益会把「无效本地调用」误当成「数万次无效上游往返」。
|
|
335
|
+
|
|
237
336
|
## ⚠️ 数据链路改动核对铁律
|
|
238
337
|
|
|
239
338
|
- ⚠️ **给一个数据结构(interface/struct/DTO)新增或修改字段后,必须顺着这份数据流转的每一个转发/序列化点逐一核对,不能只改了数据结构定义或链路两端就算完成**。典型漏改位置是中间层"手写请求体字面量"(如 `JSON.stringify({ a, b })`、手动拼接的 `params`/`payload`),新增字段不会自动带过去,也不会报错,只会表现为"下游一直是空的"这种沉默失败。
|
|
@@ -368,6 +467,14 @@
|
|
|
368
467
|
|
|
369
468
|
- 新建文档使用 `/create-doc` skill(HTML 格式、中文文件名、base64 内嵌、分步记录三要素等规范见该 skill)。
|
|
370
469
|
|
|
470
|
+
## 文档/文件链接交付
|
|
471
|
+
|
|
472
|
+
- ⚠️ 给用户交付 docs/ 等文件链接时,禁止使用相对路径(如 `docs/模型xxx.html`)或 `file:///` 形式:相对路径含中文/空格时 VSCode 无法点击,`file:///` 被 VSCode webview 安全策略拦截,用户都打不开。
|
|
473
|
+
- ⚠️ 必须先启动本地 HTTP 服务提供 docs 目录(后台运行):`python3 -m http.server 8777 --bind 127.0.0.1`,启动后必须 `curl -s -o /dev/null -w "%{http_code}"` 验证返回 200。
|
|
474
|
+
- ⚠️ 然后交付 `http://127.0.0.1:8777/<相对路径>` 形式的链接,保证用户点击即可在默认浏览器打开。中文路径建议做 URL 编码,未编码也能打开(浏览器自动处理)。
|
|
475
|
+
- ⚠️ 端口固定使用 8777;若被占用,递增 +1 并告知用户实际端口。服务只绑定 `127.0.0.1`,仅本机可访问,不对外暴露。服务保持后台运行,用户不需要时再停止。
|
|
476
|
+
- 交付时同时给出可点击链接 + 简要内容说明,方便用户确认。
|
|
477
|
+
|
|
371
478
|
## Figma 还原
|
|
372
479
|
|
|
373
480
|
- Figma 设计还原使用 `/figma-to-code` skill,按 MCP 三步验证法执行。
|
package/package.json
CHANGED
package/rules/global.md
CHANGED
|
@@ -234,6 +234,105 @@ name: "通用规则"
|
|
|
234
234
|
- 典型反例:`.filter(v => v.status !== 'disabled')` 在将来新增第三种状态(如 `suspended`)时会被误放行,保存必被后端拒绝。
|
|
235
235
|
- 自查:凡「可选/可用/合法」判定,写成「只保留允许的那些」而不是「排除不允许的那些」。
|
|
236
236
|
|
|
237
|
+
## ⚠️ 网关后端编码铁律(来自 PR 评审沉淀)
|
|
238
|
+
|
|
239
|
+
⚠️ 本章来自 routerhub-gateway 92 个已合入 PR 中多位评审者(hankWaling / baikaifa / sam-pomex / rachelPomex / eason-qing / enzo0824)的 review 评论沉淀,每条都有真实 PR 出处。写 Go 后端(网关/代理/计费/路由类)代码时对照本节自查。本节所有条目均为强制规则,命中即自检。
|
|
240
|
+
|
|
241
|
+
### 计费与资源对称(核心命脉)
|
|
242
|
+
|
|
243
|
+
- ⚠️ **计费/归属字段必须来自实际服务实例**:流式与非流式两条路径对「实际使用的 provider/model/instanceID」的捕获必须对称;failover 到备用实例后,billing/sticky/usage 归属必须来自实际服务实例,禁止回退到主 provider。defer 里做计费的,必须在主循环并列采集真实值,不能依赖 drain/二次读取。显示给客户看的 cost 与实际扣费的 cost 必须同一计算、同一入参,禁止分头计算。
|
|
244
|
+
- ⚠️ **「先预扣、后结算」必须有对称结算与兜底释放路径**:结算必须基于真实完成与真实用量;失败/无用量时必须退款或按实际用量校准。依赖上游返回 usage 字段时,必须保证终态必有该字段(缺量走宽限期兜底)。软删除、成功、失败任何终态都不能漏掉结算——预扣的额度必须有最终回收路径,否则永久挂账。
|
|
245
|
+
- ⚠️ **档位/分类判定必须确定性且朝「不漏收」收敛**:判定结果与请求入参排列顺序无关(禁止「取第一个匹配」);做两侧分类(推理/非推理、高/基准档)时,登记封闭侧、开放侧新增默认命中基准档,靠数据/配置驱动,禁止维护硬编码模型前缀列表。
|
|
246
|
+
- ⚠️ **计费判定必须基于「实际将生效的值」而非请求原始字段**:原始字段与生效值之间隔着 converter/fallback 层时,基于原始字段判断会产生覆盖缺口(如 MaxTokens 为空会 fallback 到 MaxCompletionTokens)。
|
|
247
|
+
- ⚠️ **计费明细必须落库可审计**:新增的计费/用量明细即使总额算对,也必须写入可审计的落库字段——否则判错只体现在账单总数、无法定位是哪些请求判错,漏收长期隐藏。
|
|
248
|
+
- ⚠️ **区间/边界判定不能静默落入分支**:起止相等、空串、0 值等边界(如 start_time==end_time 会意外命中「全天生效」)必须显式定义语义或当作非法拒绝。
|
|
249
|
+
- ⚠️ **计费口径与上游实际成本对称**:上游确实执行并收钱的才向客户收,上游拦截/我方无成本的应免单;反直觉的口径选择必须写进注释给出依据。
|
|
250
|
+
- ⚠️ **复用定价常量前核对注释适用面**:跨厂商、跨路径不能共用一个单价,常量注释声明的适用范围与实际使用面不一致就是漏收源头。
|
|
251
|
+
- ⚠️ **计费公式注释随逻辑变更同步更新**:禁止「注释说旧公式、代码跑新公式」,否则误导维护者基于错误前提改计费。
|
|
252
|
+
- ⚠️ **新计费配置默认关闭 + 守门测试**:尚未与账单/上游核对过的定价或行为默认关闭,用开关控制上线,配一条「默认值守门」测试——开启必须是有意识的决策,不会被顺手改掉。
|
|
253
|
+
- ⚠️ **同一事实只能有一处维护源**:定价/常量/配置禁止代码硬编码与 DB 并存且数值不一致(死配置隐患),阈值集中成常量按模型查表。
|
|
254
|
+
- ⚠️ **准入/风控判定必须用 raw 估算,禁止用被宽松化/打折处理过的估算做硬准入**:为缓解误拒设计的 cap 用在准入上等于把「误拒」换成「误放」——reserveCredit() 用 raw estimate,prepaid 准入永不 cap(风控路径),软计量才用 capped estimate。
|
|
255
|
+
- ⚠️ **成本不确定的多步操作(搜索/循环/工具调用)必须按需逐次预扣(JIT),禁止按 worst-case 一次性 upfront 预扣**:初始只预扣模型 base estimate,每真正发起一步前再小额 reserve 一次——否则 max_uses=8 这类默认值把请求 upfront 放大数倍。
|
|
256
|
+
- ⚠️ **扣款/写负余额路径必须有过载守卫硬阻断,禁止「事后扣款 + 只记 metric」**:UPDATE balance = balance - $1 没有透支守卫时,负余额只有 metric 计数、不硬阻断,等于允许无限透支。
|
|
257
|
+
- ⚠️ **用量估算必须理解计价模式并分桶计费**:缓存命中/未命中单价不同(如 cache read 仅 10%),命中按 cache read 估、未命中按 cache write/no-cache 估;同一请求的 usage 必须按 cached/non-cached input、output 分桶计费,禁止一律按最贵档估算或用统一单价折算——否则预扣与真实账单差一个数量级。
|
|
258
|
+
|
|
259
|
+
### 对外请求安全
|
|
260
|
+
|
|
261
|
+
- ⚠️ **用户可控 URL 发起请求必须防 SSRF**:只允许 http/https、解析 DNS 后阻断私网/loopback/link-local/metadata IP(含 169.254.169.254)、限制重定向次数、使用独立短超时 client,禁止裸 `http.DefaultClient`。
|
|
262
|
+
- ⚠️ **内部专用标记/密文帧禁止透传到对外响应或上游**:出站统一剥帧 + 入站兜底剥帧——帧一旦下发无法收回,客户端会长期持有。
|
|
263
|
+
- ⚠️ **日志禁止输出请求/响应 body**:上游请求 body 可能带签名 URL、密钥、内部错误详情,全量打 Info 日志=敏感信息进日志系统;默认只输出摘要/trace id。**外部内容写日志前必须截断 + redact + 限长**:高基数动态消息记 hash + length 代替原文,禁止无长度上限地记录上游回显内容(PII/secret 长期进 Cloud Logging,且攻击者可诱导上游回显敏感内容刷爆日志费用)。
|
|
264
|
+
- ⚠️ **凭据必须按真实格式校验/适配**:明文 API key 与结构化 JSON 凭据(如 SigV4)需区分处理,禁止只做非空校验就原样透传——格式不匹配在上游 401/403,且启动时静默放行、运行期才暴露。
|
|
265
|
+
- ⚠️ **声明支持外部能力/版本/协议必须基于实测**:禁止从通用版本表推导或移植注释即认可——外部平台可能对推导出的版本返回 400,文档会误导排障方向。
|
|
266
|
+
- ⚠️ **上游/外部服务错误消息透传客户端前必须独立 sanitize**:禁止「成功解析上游文本 = 文本安全」的假设——上游 JSON 里的 error.message 可能含 `dial tcp 10.x.x.x: connection refused`、`api_key=sk-secret` 等内部信息,JSON 路径同样要走 sanitizer。
|
|
267
|
+
- ⚠️ **新增行为的 gate 开关必须覆盖该行为的所有入口(含 400/错误路径)**:错误响应路径是最容易绕过 observation 的——开关=false 不代表新行为关闭,未清洗的错误消息仍可能进入生产客户端。
|
|
268
|
+
|
|
269
|
+
### 并发与资源上限
|
|
270
|
+
|
|
271
|
+
- ⚠️ **goroutine 写入的共享数据读取前必须确认其已退出**:排空/超时分支是重灾区(goroutine 往往还没退出);并发完成信号时序必须「先发结果、后关通道」,读取侧「先 drain 数据通道、再读错误通道」,超时后先非阻塞复查结果是否已就绪,避免把「其实成功了」误判为超时。
|
|
272
|
+
- ⚠️ **高 QPS 路径禁止无上限 `go func` 发外部请求**:没有队列/限流/并发上限时,下游慢或不可达会堆积 goroutine/连接/fd 拖垮实例;热路径禁止重复昂贵构造(LoadLocation/正则编译/重复解析/每次读 env),一次构建缓存(实测 LoadLocation 差距约 4200 倍)。
|
|
273
|
+
- ⚠️ **后台 goroutine 必须 recover**:运行中的后台任务(热重载/异步循环)的 panic 会带走整个进程,必须 recover 隔离;启动路径 fail-fast(不 recover)合理。
|
|
274
|
+
- ⚠️ **后台重载/重试必须有退出条件与退避,禁止无界重试循环**:热重载禁止「全有或全无」——按内容指纹只重建实际变更项,单项构建失败保留旧版本继续服务并告警,不要因一个坏条目拒绝整个更新(单点故障放大为全局故障)。
|
|
275
|
+
- ⚠️ **生成全局递增序号禁止「读-改-写」**:读当前最大值→+1→写回在并发下必然重复;用原子操作/锁,或采用与顺序无关的唯一命名(时间戳+哈希),并区分「无记录返回初始值」与「读取真的失败」。
|
|
276
|
+
- ⚠️ **缓存必须包住所有可能失败的准备步骤**:把解密/连接等前置操作放在缓存查找之前,失败会绕过缓存直接破坏状态(实测:解密失败绕过 cache 直接把账号踢出池,模型数从 24→23、HTTP 400);应把失败风险最高的步骤移到缓存内、按单元懒执行。
|
|
277
|
+
|
|
278
|
+
### 错误处理与路由
|
|
279
|
+
|
|
280
|
+
- ⚠️ **禁止吞错/空返回**:「返回了但等于没返回」((nil, nil)、空流、lastErr==nil 即成功、`|| true` 吞失败)必须显式区分「正常空结果」与「真的失败」——失败必须显式报错(如 ErrNoSupportingInstance),禁止静默失败。**关闭/刷新/收尾路径的错误禁止裸 `_ =` 丢弃**:关键信号(如 ctx.Err=消费者已走)必须保留并上报,否则「该停不停」的隐患被静默吞掉。
|
|
281
|
+
- ⚠️ **嵌套块 `:=` 会遮蔽外层 err**:嵌套块内用 := 新建块作用域变量遮蔽外层 err,构造/调用失败被静默吞掉、nil 对象继续流转;错误必须落在调用方能读到的作用域,赋值后立即检查。
|
|
282
|
+
- ⚠️ **nil 防御覆盖两层**:外层结构体为 nil 与内层关键字段为 nil 都要防;流式/非流式两侧都要判,禁止只防流式。
|
|
283
|
+
- ⚠️ **不同错误语义用独立错误类型**:禁止复用通用错误(如 budget 超限复用 rate_limit_error 会让客户端把没钱当限流反复重试)。
|
|
284
|
+
- ⚠️ **能力判定必须与真实能力一致**:包装层一律宣称支持会让入口判定形同虚设、错误以错误语义暴露(502 而非预期 400);能力判定下沉到真实实现处。
|
|
285
|
+
- ⚠️ **上游不支持某能力是客户端问题 → 映射 4xx 而非 5xx**:错误分类要区分「客户端请求了不支持的能力」与「服务端故障」,避免 502 掩盖真实原因。
|
|
286
|
+
- ⚠️ **failover 遇「实例不支持此能力」应继续尝试下一实例**:同一池内实例能力不一致时,首个不支持实例不应阻塞后续可用实例。
|
|
287
|
+
- ⚠️ **请求带了但不支持的参数禁止静默忽略**:必须显式 4xx 报错——静默忽略让客户端误以为参数生效,是沉默失败。
|
|
288
|
+
- ⚠️ **路由/解析 switch default 分支禁止静默 skip+warn**:静默跳过会让路由表悄悄缺模型/缺路由;应显式报错或强告警。路由收窄/拆池前先查全量数据分布。**依赖「客户端/上游不会这么发」假设的丢弃/忽略分支,必须打日志**:假设一旦被打破(上游发来畸形数据),生产可诊断,禁止静默丢弃。
|
|
289
|
+
- ⚠️ **失败路径也要补全归属字段**:成功/失败覆盖一致(失败 usage 事件也要回填 ProviderAccountId 等),否则某个账号持续失败时按账号排查不出来。
|
|
290
|
+
- ⚠️ **清理/剥离函数必须清「实际被填充」的字段**:核对写入方填哪个字段、清理方清哪个字段,二者对齐;只清自己认识的字段、漏掉写入方真正填的,等于没清。
|
|
291
|
+
- ⚠️ **等效路径行为必须对齐**:同一语义的多条路径(chat/responses、流式/非流式、compat/非 compat)改一条的过滤/剥除逻辑必须同步所有等效路径,否则出现「改前隐藏、改后泄漏」的静默回归。
|
|
292
|
+
- ⚠️ **流式发出首块后失败必须补发显式错误结束事件**:禁止只静默关闭连接——客户端会误判为网络断开或永远等待。
|
|
293
|
+
- ⚠️ **客户端已断开后禁止再向连接写错误响应**:写前检查断开状态;已断开只做结算与上报,不做无用写。
|
|
294
|
+
- ⚠️ **带副作用的函数先判空/前置校验后写**:先写后判空,nil 入参会 panic。
|
|
295
|
+
- ⚠️ **路由业务约束后端构建/加载期自行校验**:不能只依赖 UI 层规则或文档声明——DB 历史数据/手工 SQL 可绕过 UI,同账号数据混挂会静默转发到错误协议端点。
|
|
296
|
+
- ⚠️ **对上游错误码做重试/冷却/failover 分类时禁止只匹配单个码,必须覆盖同族错误**:SDK 常用同一格式输出整个异常联合体(`received exception <code>: ...`),只匹配一个码会把同族错误(serviceUnavailable / throttling / modelTimeout)误判为 StatusCode0、不可重试;冷却/降级判据同样要覆盖同族码(如 429/403/498 都该冷却)。
|
|
297
|
+
- ⚠️ **流式错误处理区分「流建立前」与「流中途」两个阶段,处理逻辑分开写**:只在首块前的异常才能映射为 500;一旦上游发出 message_start、转换器已产生 chunk,中途报错应走流中途的处理(如补错误结束事件、按账号轮换),否则健康账号闲置、客户仍拿 500。
|
|
298
|
+
|
|
299
|
+
### 配置与常量
|
|
300
|
+
|
|
301
|
+
- ⚠️ **依赖密钥的功能开关缺密钥必须 fail-fast 拒绝启动**:禁止「开关开着、密钥却没加载」的自相矛盾状态静默上线(该状态会同时破坏新旧两条链路)。
|
|
302
|
+
- ⚠️ **配置解析非法值静默回退时确认回退方向安全**:非法值回退若落在「启用/高风险」方向(写错 env=启用),等于静默开启危险行为,应对非法值单独告警。
|
|
303
|
+
- ⚠️ **脚本/工具默认值必须与代码权威默认值一致**:默认值静默指向错误目标(错误池/错误环境)比报错更危险。
|
|
304
|
+
- ⚠️ **残缺配置整表覆盖默认行为=危险**:配置非空就整表替换默认码表/行为,漏写即静默失效;应 merge 或开关控制。
|
|
305
|
+
- ⚠️ **同一判定跨端实现时边界/单位显式对齐**:一端毫秒、一端秒截断(限流窗口)会埋下窗口偏差。
|
|
306
|
+
- ⚠️ **用拼接类函数(url.JoinPath)前确认其转义处理**:JoinPath 已处理转义,再手动 PathEscape 会双重转义。
|
|
307
|
+
- ⚠️ **配置项 0 值的语义(disable/回退默认/无限)每个变量可以完全不同,必须逐个显式文档化**:如 PRICING_RELOAD_INTERVAL<=0 回退默认 60s、ROUTING_RELOAD_INTERVAL=0 才是 disable——依赖「0=关闭」的惯性推断会把「刻意不可禁用」误解为可关闭。
|
|
308
|
+
- ⚠️ **配置经 clamp/fallback 后,启动日志必须打印生效值而非原始值**:main 只 log clamp 前的原始值会误导运维按日志排查;打印生效值(含 clamp 后的实际值)让启动日志诚实可见。
|
|
309
|
+
|
|
310
|
+
### 代码结构与复用
|
|
311
|
+
|
|
312
|
+
- ⚠️ **启动路径与热重载路径共用同一份构建实现**:结构上共用才不可能分叉(不是靠「记得在两处都调一次」)。
|
|
313
|
+
- ⚠️ **跨 goroutine/closure 传链路追踪状态用 context 贯穿**:closure 通过捕获的 ctx 读取状态,否则追踪/审计字段静默失效。
|
|
314
|
+
- ⚠️ **字段填充收敛成公共 helper**:多个 handler 手写同一批字段填充必然出现成功/失败覆盖不一致,抽公共函数(如 enrichUsageEvent)。
|
|
315
|
+
- ⚠️ **删除守卫/过滤条件要说明理由**:禁止顺手删——可能影响现有业务;若只是脏数据应修数据,不是删过滤。
|
|
316
|
+
- ⚠️ **取数口径变更分阶段迁移**:改 hash 输入/键维度/身份字段不能与另一项行为变更同一步上线——口径突变让全部存量键 miss 导致路由漂移甚至硬 503。
|
|
317
|
+
- ⚠️ **行为变更扩大存储/缓存键写入范围时评估键基数与内存**:键量级跳增带来容量风险,纳入监控。
|
|
318
|
+
- ⚠️ **全局批量替换会误伤正文引用**:sed/品牌替换等完成后必须全量核对每个受影响点。
|
|
319
|
+
- ⚠️ **浅拷贝含指针字段的结构体后修改内容会污染调用方;必须深拷贝指针字段**:shallow request copy 共享同一个指针字段(如 *OpenAIReasoningConfig),在副本上 `.Effort=...` 会改到调用方的原始请求。
|
|
320
|
+
- ⚠️ **把派生/转换结果写入某字段前,必须核对消费方读取的是哪一层**:写错层是 silent no-op——如 NormalizeReasoning 只读顶层 reasoning_effort(当没有 reasoning 对象时),派生值写到顶层字段等于没写,必须写进消费方实际读取的那一层。
|
|
321
|
+
- ⚠️ **同一数据源禁止重复解码(热路径尤其),合并为一次解码**:同一 bytes 连续两次 json.Unmarshal(一次进结构体、一次进 map)是热点路径的浪费,应复用第一次解码结果。
|
|
322
|
+
- ⚠️ **代码行为变化会静默破坏外部脚本对资源生命周期(TTL/永久化)的隐性依赖,必须显式迁移或文档强制**:如请求路径不再写 SetPermanent 后,此前被「转正为永久」的绑定 1 小时后静默过期、key 开始 503——这类隐性依赖变化必须显式记录迁移动作。
|
|
323
|
+
|
|
324
|
+
### 测试与验证
|
|
325
|
+
|
|
326
|
+
- ⚠️ **核心逻辑保留确定性单测**:路由/映射/转换/计费/幂等逻辑保留不依赖真实外部资源的确定性单测——标准 CI 覆盖不了=回归无防护。
|
|
327
|
+
- ⚠️ **集成测试缺环境 Fatal 而非 Skip**:缺配置就明确失败,禁止「跳过」被当成通过(假绿)。
|
|
328
|
+
- ⚠️ **单测全绿≠上线正确**:外部平台行为/资源加载顺序/错误兜底必须用真实链路/真实请求验证;计费金额实测对账(手算单价、API 返回值 vs 落库计费逐笔对照),禁止凭推断。
|
|
329
|
+
- ⚠️ **档位/分类判定测试双向覆盖+变异验证**:既防「误判高档→多收客户」,也防「漏登记→静默少收」;变异测试证明用例真能拦住对应回归。
|
|
330
|
+
- ⚠️ **回归测试断言覆盖「实际会非空的字段」**:只断言永远为空的字段等于没测。
|
|
331
|
+
- ⚠️ **热点路径 miss 分支日志降级**:预期内高频路径用 Debug 或采样,禁止 Info 刷屏淹没真实告警。
|
|
332
|
+
- ⚠️ **测试配置禁止直接改包级全局变量,必须注入依赖结构体**:mutation package-level var 在 `go test -race` 并行测试下是 data race(如 cursorHeartbeatInterval 直接改全局),应移进 handlerDeps 注入。
|
|
333
|
+
- ⚠️ **「对外暴露上游内容」类功能,测试矩阵必须包含恶意/超长/含敏感输入**:断言客户端与日志均不泄露 URL/IP/email/API key/换行/10KB 文本,且必须截断——只测正常输入测不出泄露。
|
|
334
|
+
- ⚠️ **优化/收益评估前必须确认被优化的开销真实发生在目标位置**:本地 SDK 预检(ms=0、无网络调用)与上游真实往返是两回事——凭假设估收益会把「无效本地调用」误当成「数万次无效上游往返」。
|
|
335
|
+
|
|
237
336
|
## ⚠️ 数据链路改动核对铁律
|
|
238
337
|
|
|
239
338
|
- ⚠️ **给一个数据结构(interface/struct/DTO)新增或修改字段后,必须顺着这份数据流转的每一个转发/序列化点逐一核对,不能只改了数据结构定义或链路两端就算完成**。典型漏改位置是中间层"手写请求体字面量"(如 `JSON.stringify({ a, b })`、手动拼接的 `params`/`payload`),新增字段不会自动带过去,也不会报错,只会表现为"下游一直是空的"这种沉默失败。
|
|
@@ -368,6 +467,14 @@ name: "通用规则"
|
|
|
368
467
|
|
|
369
468
|
- 新建文档使用 `/create-doc` skill(HTML 格式、中文文件名、base64 内嵌、分步记录三要素等规范见该 skill)。
|
|
370
469
|
|
|
470
|
+
## 文档/文件链接交付
|
|
471
|
+
|
|
472
|
+
- ⚠️ 给用户交付 docs/ 等文件链接时,禁止使用相对路径(如 `docs/模型xxx.html`)或 `file:///` 形式:相对路径含中文/空格时 VSCode 无法点击,`file:///` 被 VSCode webview 安全策略拦截,用户都打不开。
|
|
473
|
+
- ⚠️ 必须先启动本地 HTTP 服务提供 docs 目录(后台运行):`python3 -m http.server 8777 --bind 127.0.0.1`,启动后必须 `curl -s -o /dev/null -w "%{http_code}"` 验证返回 200。
|
|
474
|
+
- ⚠️ 然后交付 `http://127.0.0.1:8777/<相对路径>` 形式的链接,保证用户点击即可在默认浏览器打开。中文路径建议做 URL 编码,未编码也能打开(浏览器自动处理)。
|
|
475
|
+
- ⚠️ 端口固定使用 8777;若被占用,递增 +1 并告知用户实际端口。服务只绑定 `127.0.0.1`,仅本机可访问,不对外暴露。服务保持后台运行,用户不需要时再停止。
|
|
476
|
+
- 交付时同时给出可点击链接 + 简要内容说明,方便用户确认。
|
|
477
|
+
|
|
371
478
|
## Figma 还原
|
|
372
479
|
|
|
373
480
|
- Figma 设计还原使用 `/figma-to-code` skill,按 MCP 三步验证法执行。
|