@routerhub/agent-rules 1.5.223 → 1.5.224

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 CHANGED
@@ -525,6 +525,52 @@ agent-rules 生成的规则文件分两层,行为与归属不同,评审/发
525
525
  - 典型反例:MP-73 vendor 双预算封顶。准入函数 `accountSkip` 在普通路由(`providers/pool.go`)实现了 `case accountSkipVendorBudget`(vendor 超限 → 429),但健康分路由 `pool_health.go` 的 6 处 *2 系列 switch(GenerateText2/Stream2/Responses2/ResponsesStream2/GenerateContentNative2/GenerateContentStreamNative2)全部漏了这个 case,vendor 超限时落入 default 被放行、返回 200 而非 429——同一条约束,普通路由守了、健康分路由没守。
526
526
  - 自查:新增/修改拦截、限额、准入类约束时,先 `grep -rn "switch.*accountSkip\|case accountSkip"` 找出**所有消费同一枚举/函数的路径**,逐个核对是否都补了新 case;Go 的 switch 漏 case 会落入 default(静默放行)而不是编译报错,不能靠编译器兜底。
527
527
 
528
+ ## ⚠️ 展示层按真实数据设计铁律(写页面渲染代码前对照)
529
+
530
+ ⚠️ 本章来自 routerhubai PR #122(Andy 反馈 Models 页两处显示异常)的复盘。两条问题都属于同一形态:**渲染代码写完、功能"能跑",真实数据一进来才露出破绽**——不报错、不崩溃、程序化测试也不会失败,只有真人盯着页面才看得出来。写任何「把后端数据渲染到界面上」的代码前,对照下面三条各问一遍。
531
+
532
+ ### ① 兜底分支的落点,决定了「多显示噪音」还是「少显示信息」
533
+
534
+ - **事故**:Models 页要给「按秒计费的视频模型」在价格下加一条虚线下划线(表示有悬停说明)。判定「这段价格是不是扩展定价」用正则 `/^\s*\$?[0-9]/`——**只看首字符**是不是 `$` 或数字。真实数据里视频模型定价文案是 `from $0.05 per second`,首字符是 `f`,正则不匹配 → 落入"不是扩展定价"的兜底分支 → 反而被判成扩展定价,加了虚线和悬停浮层,浮层里只有一句毫无信息量的 `Other: from $0.05 per second`。
535
+ - ⚠️ **核心:兜底分支(未命中任何规则时走的那条)的语义方向必须显式选过,不能靠"剩下的都归它"顺出来。**
536
+ - **类比:公司前台把访客分成"预约过的"和"没预约的",规则是"能报出正确分机号就算预约过"。来个访客说"我找你们管报销的同事",报不出分机号,前台按规则把他归进"没预约"——可他是真来办事的,只是不知道分机号。规则本身没写错,错在"剩下的都算没预约"这个兜底没人推敲过。**
537
+ - 三条自查:
538
+ 1. **兜底命中时,界面会「多显示」还是「少显示」?** 朝「多显示噪音」偏(本例:多画一条虚线 + 一个空浮层)比朝「少显示」更容易被放过——它不缺内容、不报错,只是让页面变脏。多显示的噪音是静默的,必须专门盯。
539
+ 2. **拿后端真实数据的全量值跑一遍分类,看有几条落进兜底。** 不要只拿脑子里那几种规范写法试。本例只要把真实定价文案喂进正则,`from $0.05 per second` 这种「自然语言前缀 + 金额」的形态立刻现形。
540
+ 3. **落进兜底是因为"规则没覆盖到",还是"确认它不属于"?** 前者应显式告警,而不是安静地当反面处理。
541
+ - ⚠️ **分类判定的输入是「后端真实数据的全集」(含运营手填、上游同步、历史遗留),不是你假设的规范格式。** 只要这段文案由人或上游决定,就必须按"什么形态都可能出现"来写;能用**白名单**(明确列出属于此类的形态)就别用**黑名单**(排除掉不是的)——黑名单每漏一种新形态,就静默误判一类(呼应反模式 ⑥)。
542
+
543
+ ### ② 容器容量必须按「真实数据的最长值」算,不能按字段定义算
544
+
545
+ - **事故**:同一个定价表格是 `table-layout: fixed`(列宽写死、不随内容伸缩),INPUT 列写死 168px,价格文本又加了 `whitespace-nowrap`(禁止换行)。真实文本 `from $0.05 per second` 在该字体下渲染宽度 184px,比列宽多 41px——溢出部分直接画到右边 OUTPUT 列上,与 OUTPUT 列的 `–`(无输出价格)视觉粘连,读起来像 `second—`,看上去像数据错了。
546
+ - ⚠️ **核心:「固定容器 + 禁止换行 + 内容长度由后端决定」三者同时成立 = 溢出是必然事件,只是在等一条更长的数据。**
547
+ - **类比:快递柜格子是按"标准信封"尺寸做的,还规定包裹不许折。某天来个长条形的伞,塞不进去,柜门关不上——不是伞的错,也不是柜子的错,是"格子尺寸按标准件定、却又要求什么都能塞"这两条前提本身就矛盾。**
548
+ - 三条自查:
549
+ 1. 这个容器尺寸是写死的吗?(`table-layout: fixed` + 固定 px/rem 列宽、固定宽卡片、写死高度)
550
+ 2. 里面的文本是后端来的、长度不受前端控制吗?
551
+ 3. 有没有禁止换行(`whitespace-nowrap`)却**没有配套溢出处理**(省略号、允许换行、自适应)?
552
+ - 三条都是「是」→ 拿**真实数据里最长的那条**在浏览器里量一遍渲染宽度(`getBoundingClientRect().width`),确认真实值放得下;放不下就改「允许换行(`break-words`)」「省略号 + 悬停展开」「容器自适应」三者之一,而不是等它溢出。
553
+ - ⚠️ **溢出最坑的地方是"静默且伪装成数据错误"**:不报错、不截断(`nowrap` 让它照画不误),溢出的字与邻列内容粘在一起,看的人第一反应是"这条数据不对",而不是"这个列宽不够"——排查方向从第一秒就是错的。
554
+
555
+ ### ③ 新增展示元素前,先查这份信息是不是已经有承载处
556
+
557
+ - **事故**:还是这个 Models 页,有人给"老模型"加了一个淡灰色的 `YYYY-MM` 日期徽章。但列表里**已经有 NEW 徽章**在表达新旧/时间线,点进详情页也有完整上线日期——这个日期徽章没承载任何别处没有的信息,只是给每一行多贴了一块灰底小字,页面更花。
558
+ - ⚠️ **核心:展示元素的价值 = 它承载了别处没有的独有信息。加之前先问一句,答案是否定就别加。**
559
+ - **类比:已经挂了一块"新品"的牌子,再在旁边贴一张写满上架日期的小纸条——顾客想知道新旧,看牌子就够了;纸条不增加任何信息,只让货架更乱。**
560
+ - 两条自查:
561
+ 1. **这条信息在同一页面/同一视图里是不是已经有地方表达了?**(NEW 徽章已表达时间线、状态色块已表达上下线、另一列已显示该字段)
562
+ 2. **它承载的是"别处看不到的信息",还是"同一个事实的第二种画法"?** 后者一律不加——信息密度不是越高越好,重复表达是纯噪音,还会稀释真正重要的徽章。
563
+ - ⚠️ **特别警惕「顺手补一个更精确的版本」**:把模糊的 NEW 徽章"补充"成精确日期、把状态色块"补充"成文字标签——动机是"更清楚",结果是在说同一件事,读者还要多花一次注意力去确认"这俩是不是一回事"。
564
+
565
+ ### 与「页面功能验证铁律」的分工
566
+
567
+ ⚠️ 本节管**写之前**(写渲染代码时按真实数据形态设计),「⚠️ 页面功能验证铁律」「⚠️ 可视化验证铁律」管**写之后**(用真实数据走真实交互验证)。两条缺一不可:
568
+
569
+ - 只有验证、没有本节 → 每次都是"写完发现不对 → 改 → 又发现不对",靠验证兜底等于靠运气;
570
+ - 只有本节、没有验证 → 设计时想对了,但真实数据里总有想不到的形态,必须真机跑一遍。
571
+
572
+ ⚠️ **判断标准:这段代码渲染的内容,是不是由后端数据(含人填、上游同步、历史数据)决定的?** 是 → 落笔前把上面三条各问一遍。
573
+
528
574
  ## ⚠️ 网关后端编码铁律(来自 PR 评审沉淀)
529
575
 
530
576
  ⚠️ 本章来自 routerhub-gateway 92 个已合入 PR 中多位评审者(hankWaling / baikaifa / sam-pomex / rachelPomex / eason-qing / enzo0824)的 review 评论沉淀,每条都有真实 PR 出处。写 Go 后端(网关/代理/计费/路由类)代码时对照本节自查。本节所有条目均为强制规则,命中即自检。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@routerhub/agent-rules",
3
- "version": "1.5.223",
3
+ "version": "1.5.224",
4
4
  "description": "Shared Copilot agent rules and guidelines for RouterHub projects",
5
5
  "main": "AGENTS.base.md",
6
6
  "bin": {
package/rules/global.md CHANGED
@@ -385,7 +385,7 @@ agent-rules 生成的规则文件分两层,行为与归属不同,评审/发
385
385
  - ⚠️ **PR 必须附效果截图作为可视化证据,且逐条满足以下硬性要求(缺一不可,禁止跳过)**:
386
386
  - **全页图**:用浏览器真实视口(`window.innerWidth` 即截图宽度,4K 屏自然宽 3840)`fullPage` 截完整页面,禁止只截视口一屏。⚠️ **禁止强制把视口硬拉成 3840 宽**——浏览器视口宽由屏幕实际分辨率决定,硬拉宽会让内容按比例缩小(看不清),且与截图像素发生缩放错位,是标注不准的根因之一。若要更高清晰度,用 `set viewport <W> <H> 2`(DPR 倍率)提高渲染精度(CSS 宽不变、像素更密),而不是拉宽 CSS 视口。
387
387
  - **全面多角度**:一张全页图 + 每个关键改动区域的局部放大图,多个改动点要逐个覆盖,确保 reviewer 不看代码就能看全本次全部改动。
388
- - **箭头标注(必须标注准)**:每张截图必须用醒目箭头 + 简短文字标签标注关键改动区域/验证点(修复前红框/红箭头、修复后绿框/绿箭头),标注放在不遮挡原内容的位置,禁止只贴裸图不标注。⚠️ **标注坐标必须来自浏览器 DOM 测量(`getBoundingClientRect()` + `scrollX/scrollY`)精确换算成截图像素,禁止肉眼看图估坐标**——全页拼接/缩放截图里 CSS 坐标 ≠ 截图像素,肉眼估位是标注不准的直接原因。标注统一走 `/screenshot-annotate` skill(坐标换算方法 + `annotate.js` 统一脚本)。
388
+ - **箭头标注(必须标注准)**:每张截图必须用醒目箭头 + 简短文字标签标注关键改动区域/验证点(修复前红框/红箭头、修复后绿框/绿箭头),标注放在不遮挡原内容的位置,禁止只贴裸图不标注。⚠️ **标注坐标必须来自浏览器 DOM 测量(`getBoundingClientRect()` + `scrollX/scrollY`)精确换算成截图像素,禁止肉眼看图估坐标**——全页拼接/缩放截图里 CSS 坐标 ≠ 截图像素,肉眼估位是标注不准的直接原因。标注统一走 `/screenshot-annotate` skill(坐标换算方法 + `annotate.cjs` 统一脚本)。
389
389
  - **URL 可见且能打开**:按「⚠️ 截图规范」把完整 URL 以文字叠进截图本身(`/screenshot-annotate --url`),且该 URL 必须是别人能直接打开的地址(测试环境 / 线上域名),**禁止 `localhost` / `127.0.0.1` / `file:///` 等本机地址**——reviewer 在自己机器上打不开,等于没给。截图旁再给出可点击的链接(PR 正文写成 `[打开页面](URL)`),reviewer 一眼复制、或直接点开亲自复核(呼应「⚠️ 验收环境铁律」)。
390
390
  - **前后对比**:必须同时展示修复前与修复后。
391
391
  - ⚠️ **截图必须直接内嵌在 PR Description 中,让 reviewer 打开 PR 就能看到效果图(`![](CDN_URL)` 方式渲染为可见图片),禁止只在文字里描述"改动了什么"而不放图,也禁止把截图只作为文件附件/提交到分支目录而不在 PR 正文中引用。** 原因:reviewer 看 PR 的第一眼就是看描述,如果看不到图、只能读文字,完全无法直观感知改动效果;截图不内嵌 = 等于没附。
@@ -525,6 +525,52 @@ agent-rules 生成的规则文件分两层,行为与归属不同,评审/发
525
525
  - 典型反例:MP-73 vendor 双预算封顶。准入函数 `accountSkip` 在普通路由(`providers/pool.go`)实现了 `case accountSkipVendorBudget`(vendor 超限 → 429),但健康分路由 `pool_health.go` 的 6 处 *2 系列 switch(GenerateText2/Stream2/Responses2/ResponsesStream2/GenerateContentNative2/GenerateContentStreamNative2)全部漏了这个 case,vendor 超限时落入 default 被放行、返回 200 而非 429——同一条约束,普通路由守了、健康分路由没守。
526
526
  - 自查:新增/修改拦截、限额、准入类约束时,先 `grep -rn "switch.*accountSkip\|case accountSkip"` 找出**所有消费同一枚举/函数的路径**,逐个核对是否都补了新 case;Go 的 switch 漏 case 会落入 default(静默放行)而不是编译报错,不能靠编译器兜底。
527
527
 
528
+ ## ⚠️ 展示层按真实数据设计铁律(写页面渲染代码前对照)
529
+
530
+ ⚠️ 本章来自 routerhubai PR #122(Andy 反馈 Models 页两处显示异常)的复盘。两条问题都属于同一形态:**渲染代码写完、功能"能跑",真实数据一进来才露出破绽**——不报错、不崩溃、程序化测试也不会失败,只有真人盯着页面才看得出来。写任何「把后端数据渲染到界面上」的代码前,对照下面三条各问一遍。
531
+
532
+ ### ① 兜底分支的落点,决定了「多显示噪音」还是「少显示信息」
533
+
534
+ - **事故**:Models 页要给「按秒计费的视频模型」在价格下加一条虚线下划线(表示有悬停说明)。判定「这段价格是不是扩展定价」用正则 `/^\s*\$?[0-9]/`——**只看首字符**是不是 `$` 或数字。真实数据里视频模型定价文案是 `from $0.05 per second`,首字符是 `f`,正则不匹配 → 落入"不是扩展定价"的兜底分支 → 反而被判成扩展定价,加了虚线和悬停浮层,浮层里只有一句毫无信息量的 `Other: from $0.05 per second`。
535
+ - ⚠️ **核心:兜底分支(未命中任何规则时走的那条)的语义方向必须显式选过,不能靠"剩下的都归它"顺出来。**
536
+ - **类比:公司前台把访客分成"预约过的"和"没预约的",规则是"能报出正确分机号就算预约过"。来个访客说"我找你们管报销的同事",报不出分机号,前台按规则把他归进"没预约"——可他是真来办事的,只是不知道分机号。规则本身没写错,错在"剩下的都算没预约"这个兜底没人推敲过。**
537
+ - 三条自查:
538
+ 1. **兜底命中时,界面会「多显示」还是「少显示」?** 朝「多显示噪音」偏(本例:多画一条虚线 + 一个空浮层)比朝「少显示」更容易被放过——它不缺内容、不报错,只是让页面变脏。多显示的噪音是静默的,必须专门盯。
539
+ 2. **拿后端真实数据的全量值跑一遍分类,看有几条落进兜底。** 不要只拿脑子里那几种规范写法试。本例只要把真实定价文案喂进正则,`from $0.05 per second` 这种「自然语言前缀 + 金额」的形态立刻现形。
540
+ 3. **落进兜底是因为"规则没覆盖到",还是"确认它不属于"?** 前者应显式告警,而不是安静地当反面处理。
541
+ - ⚠️ **分类判定的输入是「后端真实数据的全集」(含运营手填、上游同步、历史遗留),不是你假设的规范格式。** 只要这段文案由人或上游决定,就必须按"什么形态都可能出现"来写;能用**白名单**(明确列出属于此类的形态)就别用**黑名单**(排除掉不是的)——黑名单每漏一种新形态,就静默误判一类(呼应反模式 ⑥)。
542
+
543
+ ### ② 容器容量必须按「真实数据的最长值」算,不能按字段定义算
544
+
545
+ - **事故**:同一个定价表格是 `table-layout: fixed`(列宽写死、不随内容伸缩),INPUT 列写死 168px,价格文本又加了 `whitespace-nowrap`(禁止换行)。真实文本 `from $0.05 per second` 在该字体下渲染宽度 184px,比列宽多 41px——溢出部分直接画到右边 OUTPUT 列上,与 OUTPUT 列的 `–`(无输出价格)视觉粘连,读起来像 `second—`,看上去像数据错了。
546
+ - ⚠️ **核心:「固定容器 + 禁止换行 + 内容长度由后端决定」三者同时成立 = 溢出是必然事件,只是在等一条更长的数据。**
547
+ - **类比:快递柜格子是按"标准信封"尺寸做的,还规定包裹不许折。某天来个长条形的伞,塞不进去,柜门关不上——不是伞的错,也不是柜子的错,是"格子尺寸按标准件定、却又要求什么都能塞"这两条前提本身就矛盾。**
548
+ - 三条自查:
549
+ 1. 这个容器尺寸是写死的吗?(`table-layout: fixed` + 固定 px/rem 列宽、固定宽卡片、写死高度)
550
+ 2. 里面的文本是后端来的、长度不受前端控制吗?
551
+ 3. 有没有禁止换行(`whitespace-nowrap`)却**没有配套溢出处理**(省略号、允许换行、自适应)?
552
+ - 三条都是「是」→ 拿**真实数据里最长的那条**在浏览器里量一遍渲染宽度(`getBoundingClientRect().width`),确认真实值放得下;放不下就改「允许换行(`break-words`)」「省略号 + 悬停展开」「容器自适应」三者之一,而不是等它溢出。
553
+ - ⚠️ **溢出最坑的地方是"静默且伪装成数据错误"**:不报错、不截断(`nowrap` 让它照画不误),溢出的字与邻列内容粘在一起,看的人第一反应是"这条数据不对",而不是"这个列宽不够"——排查方向从第一秒就是错的。
554
+
555
+ ### ③ 新增展示元素前,先查这份信息是不是已经有承载处
556
+
557
+ - **事故**:还是这个 Models 页,有人给"老模型"加了一个淡灰色的 `YYYY-MM` 日期徽章。但列表里**已经有 NEW 徽章**在表达新旧/时间线,点进详情页也有完整上线日期——这个日期徽章没承载任何别处没有的信息,只是给每一行多贴了一块灰底小字,页面更花。
558
+ - ⚠️ **核心:展示元素的价值 = 它承载了别处没有的独有信息。加之前先问一句,答案是否定就别加。**
559
+ - **类比:已经挂了一块"新品"的牌子,再在旁边贴一张写满上架日期的小纸条——顾客想知道新旧,看牌子就够了;纸条不增加任何信息,只让货架更乱。**
560
+ - 两条自查:
561
+ 1. **这条信息在同一页面/同一视图里是不是已经有地方表达了?**(NEW 徽章已表达时间线、状态色块已表达上下线、另一列已显示该字段)
562
+ 2. **它承载的是"别处看不到的信息",还是"同一个事实的第二种画法"?** 后者一律不加——信息密度不是越高越好,重复表达是纯噪音,还会稀释真正重要的徽章。
563
+ - ⚠️ **特别警惕「顺手补一个更精确的版本」**:把模糊的 NEW 徽章"补充"成精确日期、把状态色块"补充"成文字标签——动机是"更清楚",结果是在说同一件事,读者还要多花一次注意力去确认"这俩是不是一回事"。
564
+
565
+ ### 与「页面功能验证铁律」的分工
566
+
567
+ ⚠️ 本节管**写之前**(写渲染代码时按真实数据形态设计),「⚠️ 页面功能验证铁律」「⚠️ 可视化验证铁律」管**写之后**(用真实数据走真实交互验证)。两条缺一不可:
568
+
569
+ - 只有验证、没有本节 → 每次都是"写完发现不对 → 改 → 又发现不对",靠验证兜底等于靠运气;
570
+ - 只有本节、没有验证 → 设计时想对了,但真实数据里总有想不到的形态,必须真机跑一遍。
571
+
572
+ ⚠️ **判断标准:这段代码渲染的内容,是不是由后端数据(含人填、上游同步、历史数据)决定的?** 是 → 落笔前把上面三条各问一遍。
573
+
528
574
  ## ⚠️ 网关后端编码铁律(来自 PR 评审沉淀)
529
575
 
530
576
  ⚠️ 本章来自 routerhub-gateway 92 个已合入 PR 中多位评审者(hankWaling / baikaifa / sam-pomex / rachelPomex / eason-qing / enzo0824)的 review 评论沉淀,每条都有真实 PR 出处。写 Go 后端(网关/代理/计费/路由类)代码时对照本节自查。本节所有条目均为强制规则,命中即自检。
@@ -640,7 +686,7 @@ agent-rules 生成的规则文件分两层,行为与归属不同,评审/发
640
686
  - ⚠️ **全页截图**:用 `screenshot --full`(agent-browser)或 `page.screenshot({ fullPage: true })`(Playwright)截完整页面,不要只截视口的一部分;用 agent-browser 截全页前先 `set viewport <视口宽> <合适高度>` 固定视口,确保截图宽度 = 视口宽度。
641
687
  - ⚠️ **每张截图都必须在图上写出「文字版」完整 URL——统一写,不得只靠地址栏**:用 `/screenshot-annotate` 的 `--url` 把 URL 叠进截图顶部标题条,让它成为截图自身的一部分,别人一眼能读到、能照着地址跳转。**禁止只依赖浏览器地址栏**:交付形态是 HTML / PDF 时,地址栏是静态像素——别人既点不了、也复制不走;裁切图 / 局部放大图(从大图里裁一块出来)压根没有地址栏;图缩放到文档版心宽度后,条带里的字往往已糊到认不出。**类比:把门牌号直接印在地图上,而不是让人对着地图回忆自己是从哪个路口拐进来的。**
642
688
  - ⚠️ **交付载体里必须附「可点击的跳转入口」,且该 URL 必须是别人能直接打开访问的地址(测试环境 / 线上域名)**:HTML 文档 / 报告把地址渲染成按钮或链接,写成 `<a target="_blank" rel="noopener" href="...">`(按「HTML 页面/文档中的外部链接默认用新标签页打开」实现)——点击即在新标签页打开,不打断当前文档;Markdown / PR 正文写成 `[打开页面](URL)`。**禁止用本机地址充当 URL**——`localhost`、`127.0.0.1`、内网 IP、`file:///...` 本机路径在别人机器上要么打不开、要么指向他自己的机器,等于没给 URL,做成按钮也只会点开一个打不开的页面(呼应「⚠️ 验收环境铁律」)。
643
- - ⚠️ **标注必须准确,坐标必须有浏览器测量来源**:箭头/框要指哪儿,用 `getBoundingClientRect()` + `scrollX/scrollY` 取元素在整页中的 CSS 坐标,再按截图实际宽度换算成截图像素,统一用 `/screenshot-annotate` skill(含 `annotate.js` 脚本)标注。禁止用肉眼看图估坐标,禁止在强制缩放造成坐标错位的前提下标注。
689
+ - ⚠️ **标注必须准确,坐标必须有浏览器测量来源**:箭头/框要指哪儿,用 `getBoundingClientRect()` + `scrollX/scrollY` 取元素在整页中的 CSS 坐标,再按截图实际宽度换算成截图像素,统一用 `/screenshot-annotate` skill(含 `annotate.cjs` 脚本)标注。禁止用肉眼看图估坐标,禁止在强制缩放造成坐标错位的前提下标注。
644
690
  - **保存到磁盘文件**(`page.screenshot({ path, fullPage: true })`),禁止只用内联展示的截图工具——内联的不落盘,用户无法在 Markdown / HTML 文档里查看。路径统一放 `screenshots/` 临时目录,文件名用「编号 + 英文描述」(如 `01-login-page.png`)。
645
691
 
646
692
  ### 非 UI / 后端 / 基础设施改动的效果截图获取方法