fix: 问问微信恢复引用编号,日报修复成员头像与 hero 溢出

- 问问微信:Host 分配稳定 citationId 并注入模型上下文,正文 [E#] 与证据按钮同号
- 问问微信:回答返回前校验引用,幻觉编号移除并提示,不再出现不可信的引用按钮
- Agent Hub:收紧「最近会话」快捷路由,内容查询交回 Query Agent
This commit is contained in:
Wxw-Gu
2026-09-17 18:56:32 +08:00
parent 4b2395a8d2
commit b97e1f4bc6
23 changed files with 1635 additions and 228 deletions
@@ -0,0 +1,102 @@
import { readFileSync } from 'fs'
import { resolve } from 'path'
import { describe, expect, it } from 'vitest'
import { REPORT_TEMPLATE_FRAGMENT_CONTRACT_CSS } from '../../src/shared/report-template-fragment-contract'
/**
* hero 头像的几何只有一份权威。
*
* 生产导出会注入 `report-template-fragment-contract.ts`,其中
* `img.tm-avatar.tm-avatar--hero` 用 `!important` 把头像钉在
* `clamp(28px, var(--tm-avatar-hero-size, 40px), 56px)`。
* 如果模板再给容器写死一个尺寸(旧版 `.avatar-grid` 是 58x58、单头像 28x28),
* 两边就会打架:头像实际 40px,2 列 x 40px + 3px gap = 83px 塞不进 58px 的盒子,
* 头像向右向下溢出容器,视觉上越过卡片内边距 —— 这就是"日报头像超出范围"。
*
* 这里断言的是**不变量**,不是措辞:
* 1. 容器尺寸必须来自 contract 的变量,不能写死;
* 2. 容器的最小可用宽度必须能容纳 2 列头像(由变量算出来,而不是靠猜);
* 3. contract 提供的变量在容器里被真正引用(改名/改值不会静默分叉)。
*/
const HERO_AVATAR_GAP_PX = 3
const readResource = (file: string): string => readFileSync(resolve('resources', file), 'utf8')
/** 取出某个选择器的声明块(第一个匹配),返回声明文本。 */
const ruleBody = (css: string, selector: string): string => {
const index = css.indexOf(selector)
if (index < 0) throw new Error(`selector not found: ${selector}`)
const open = css.indexOf('{', index)
const close = css.indexOf('}', open)
if (open < 0 || close < 0) throw new Error(`unterminated rule: ${selector}`)
return css.slice(open + 1, close)
}
/** contract 里 hero 头像的目标尺寸与变量名。 */
const contractHeroAvatar = (): { variable: string; fallbackPx: number } => {
const name = /--tm-avatar-hero-size:\s*([\d.]+)px/.exec(REPORT_TEMPLATE_FRAGMENT_CONTRACT_CSS)
expect(name, 'contract 必须声明 --tm-avatar-hero-size').not.toBeNull()
const forced =
/img\.tm-avatar\.tm-avatar--hero\s*\{[\s\S]*?var\((--tm-avatar-hero-size),\s*([\d.]+)px\)/.exec(
REPORT_TEMPLATE_FRAGMENT_CONTRACT_CSS
)
expect(forced, 'contract 必须用变量 + px 兜底约束 hero 头像').not.toBeNull()
return { variable: forced![1], fallbackPx: Number(forced![2] || name![1]) }
}
const LEGACY_TEMPLATES = ['mobile_daily_report_v1.html', 'mobile_daily_report_v2.html']
describe('日报 hero 头像几何 — 容器与 contract 不得分叉', () => {
it('contract 通过变量约束 hero 头像尺寸(含 px 兜底)', () => {
const { variable, fallbackPx } = contractHeroAvatar()
expect(variable).toBe('--tm-avatar-hero-size')
expect(fallbackPx).toBeGreaterThanOrEqual(28)
expect(fallbackPx).toBeLessThanOrEqual(56)
// 必须带 !important,否则模板的 width/height 会盖掉它,几何又会分裂
const heroRule = /img\.tm-avatar\.tm-avatar--hero\s*\{[\s\S]*?\}/.exec(
REPORT_TEMPLATE_FRAGMENT_CONTRACT_CSS
)![0]
expect(heroRule).toContain('!important')
})
for (const file of LEGACY_TEMPLATES) {
describe(file, () => {
const css = readResource(file)
it('avatar-grid 不再写死容器尺寸(写死就会小于头像本身)', () => {
const body = ruleBody(css, '.avatar-grid {')
expect(body, '容器写死 width 会让 40px 头像溢出').not.toMatch(/(^|\s)width\s*:/)
expect(body, '容器写死 height 会让第二行头像纵向溢出').not.toMatch(/(^|\s)height\s*:/)
})
it('列宽与行高都取 contract 的同一个变量', () => {
const { variable, fallbackPx } = contractHeroAvatar()
const body = ruleBody(css, '.avatar-grid {')
expect(body).toContain('grid-template-columns')
expect(body).toContain('grid-auto-rows')
// 变量名与兜底值都必须和 contract 一致 —— 否则主题化会分叉
expect(body).toContain(`var(${variable}, ${fallbackPx}px)`)
})
it('单头像时列宽等于一个头像,不再用 28px 的窄盒子', () => {
const { variable, fallbackPx } = contractHeroAvatar()
const body = ruleBody(css, '.avatar-grid.avatar-count-1')
expect(body).toContain('grid-template-columns')
expect(body).toContain(`var(${variable}, ${fallbackPx}px)`)
expect(body).not.toMatch(/(^|\s)width\s*:/)
expect(body).not.toMatch(/(^|\s)height\s*:/)
})
it('按 contract 尺寸算出的两列簇宽不会被任何固定宽度切断', () => {
const { fallbackPx } = contractHeroAvatar()
const clusterWidth = fallbackPx * 2 + HERO_AVATAR_GAP_PX
const body = ruleBody(css, '.avatar-grid {')
const fixed = /(^|\s)width\s*:\s*(\d+(?:\.\d+)?)px/.exec(body)
// 既没有固定宽度,也不存在比簇宽更小的固定宽度
if (fixed) expect(Number(fixed[2])).toBeGreaterThanOrEqual(clusterWidth)
expect(clusterWidth).toBeLessThanOrEqual(430 - 2 * 12 - 2 * 20 - 14)
})
})
}
})