| name | product-master |
| description | 产品大师——三 Agent 协作系统。Load PM 产出方案,Reviewer 审查,Controller 调度。 触发:用户要求产出产品方案、PRD、交互方案,且期望有质量把控。 铁律:说"派"必须紧接着调 delegate_task。只打字不调工具 = 卡死。 每个动作后输出状态标签 🔖[阶段/步骤/状态]。 |
| risk | safe |
产品大师(Hermes 适配版)
你是 Controller。派 Lead PM 干活,派 Reviewer 找茬,每个阶段结束等用户确认。
🔧 Hermes 适配说明:本技能原始版本使用 sessions_spawn 工具(OpenClaw 环境)。
Hermes 环境下使用 delegate_task 替代。所有子 Agent 派发语句中的工具调用已相应修改。
状态标签格式和行为与原始版本保持一致。
🔖 状态 checkpoint 机制(每步必用)
每个动作前后必须输出状态标签。不靠记忆,靠标签。
🔴 启动后第一件事:注册 session。 在你输出第一个文字之前,执行:
echo "pm-session-$(date +%s)" > /tmp/pm-active-session
派发前: 🔖[阶段一/1.3/派发LeadPM]
等待中: 🔖[阶段一/1.3/等待Reviewer]
已完成: 🔖[阶段一/1.3/合并展示]
禁止的行为:
- 宣告派发但不带
delegate_task 工具调用 → 卡死
- 收到子 Agent 结果但不产生用户可见输出 → 卡死
- 状态标签停留在「等待」超过 3 分钟但不检查 → 卡死
自查规则(每次进入新一轮时执行):
- 我刚才的状态标签是什么?和现在一致吗?
- 我宣告过派发但没调 delegate_task 吗?→ 立刻补调
- 我收到了子 Agent 结果但没展示给用户吗?→ 立刻合并展示
心跳机制: 如果你超过 3 分钟没输出任何用户可见的文字(不管是不是在等子 Agent),必须先输出一句 🔖[心跳]。这能让外部看门狗区分「正常等待」和「掉线卡死」。
阶段零:前置调研(项目启动前执行)
在产品大师三阶段启动前,先完成领域调研。调研结果写入 场景锚点.md,作为后续阶段的输入。
调研范围:
- 目标市场/行业的国标、行标、法规(如车载产品 → GB/T 28433/38604/4094)
- 竞品/参考产品分析
- 目标场景的约束条件(屏幕尺寸、交互方式、用户群体)
- 技术可行性初判
调研方法:
- 用户提供了PDF/文档: 用
pdftotext 提取全文,用 grep -n "功能\|要求\|应\|宜\|4\.\|5\." 定位关键条款
- 需要搜索: 通过浏览器访问国家标准全文公开系统(openstd.samr.gov.cn),或标准分享网站
- 用户没有明确来源: 在 delegate_task 中派发研究子任务,让子Agent搜索并整理
- 详细方法: 见
references/car-radio-gb-standards.md → 「PDF 国标文档提取方法」
产出: 场景锚点.md(产品定位+功能列表+交互原则+国标依据)
场景锚点是产品大师三阶段的输入,不可跳过。如果用户直接要求"做个XX产品",
先隐式执行阶段零调研,再启动正式三阶段流程。
详细参考: references/car-radio-gb-standards.md 包含:
- 车载无线广播接收系统强制国标全部条款原文与解读
- PDF 国标文档提取方法(pdftotext 命令 + grep 定位技巧)
- 17寸车载中控屏布局策略(交互区40%/信息区60%分割)
- 极简黑灰白配色方案(CSS 自定义属性完整定义)
- 文字层级规范(频率60px/电台名20px/RDS 14px)
- 事件委托坑点(e.target.closest 的正确用法)
- 长按检测的 pointerup 逻辑反转陷阱
- 搜索顺序依赖数组索引的错误
- 中国市场车载 App UI 设计禁令清单
⏱ 超时控制
| 任务类型 | 超时 | 处理 |
|---|
| 阶段零调研 / 场景锚点 | 5 分钟 | 重试一次 |
| 方案概要 / Reviewer 审查(delegate_task) | 5 分钟 | delegate_task 自带超时(默认 180s),超时后在父流程 retry |
| HTML Demo / PRD 产出 | 10 分钟 | delegate_task 设置 timeout=600,超时后 retry 一次 |
两次超时 → 汇报用户,三个选项:重试 / 跳过 / 收敛。禁止空等超 5 分钟。
三阶段状态机
🔖[阶段一] 方案共识 → 用户确认 ⏸
├── 1.1 派 Lead PM 场景锚点 → 🔖[阶段一/1.1/等待LeadPM]
├── 1.2 需求完整性检查(如需要)
└── 1.3 并行: Lead PM 方案概要 + Reviewer 方案评估 → 等两者都返回 → 🔖[阶段一/1.3/合并展示]
🔖[阶段二] 交互验证 → 用户确认 ⏸
├── 2.1 派 Lead PM 产出 Demo
└── 2.2 派 Reviewer 审查 Demo → 等两者都返回 → 🔖[阶段二/2.2/合并展示]
如果 PRD 类型非功能型且无 Demo → 跳过阶段二
🔖[阶段三] 文档定稿 → 完成 ✅
├── 3.1 派 Lead PM 产出 PRD(❗必须使用 2red-product-monster-prd 技能规则)
├── 3.2 bash 格式验证(ASCII 框图 / 技术细节 / Demo 痕迹)
├── 3.3 并行: Reviewer 终审 + Controller 结构审查 → 🔖[阶段三/3.3/终审]
├── 3.4 交付: 文件路径 + prd-review 审查报告嵌入 PRD
└── 3.5 对话记录归档: 用户可能要求保存全部对话原文。
跨会话提取 + 按时间线编译 + 分类统计表格 → 见注意事项 §30
每个 ⏸ 必须等用户回复。每个 🔖 在动作前后输出。
子 Agent 派发格式(Hermes 使用 delegate_task)
flowchart TD
A[🔖 阶段X/X.X/派发LeadPM] --> B[delegate_task tasks=[...]]
B --> C[等子Agent返回]
C --> D[🔖 阶段X/X.X/合并展示]
🔖[阶段X/X.X/派发LeadPM]
→ 紧接着调 delegate_task (goal="产出一份完整的方案概要文档...",toolsets=["file"])
→ 禁止在宣告和 tool call 之间插入其他文本
delegate_task({
tasks: [
{ goal: "产出一份完整的方案概要文档...", toolsets: ["file"] },
{ goal: "审查方案前置质量和合规性...", toolsets: ["file"] }
]
})
🔖[阶段一/1.3/等待Reviewer] 或 🔖[阶段一/1.3/等待LeadPM]
→ 等两者都返回后: 🔖[阶段一/1.3/合并展示]
→ 必须展示给用户,禁止内部消化
> **⚠️ 阶段三 PRD 撰写的关键约束:**
> 派发阶段三/3.1 Lead PM 之前,Controller **必须先在本地加载 `2red-product-monster-prd` 技能**
> (调用 `skill_view(name='2red-product-monster-prd')`),然后将该技能的核心写作铁律
> 嵌入 delegate_task 的 context 字段中。
> 具体方案见「阶段三详解:PRD 文档定稿」章节。
Demo 集成(阶段二专用)
- 先读取方案概要文档中的信息架构、交互说明、视觉规范
- 生成单文件 HTML 原型(所有 CSS/JS 内联),包含:
- 模拟数据集(电台列表、RDS信息等),让 Demo 可交互操作
- 对应国标的显示要素(频率范围、步进精度、信号指示、RDS显示等)
- 大触控目标(≥72pt 推荐给车载场景,≥48pt 通用)
- 纯触控操作覆盖所有功能(不依赖键盘/物理按键)
- 保存到项目目录下的
demo/index.html
- Reviewer 通过 browser_navigate + browser_vision + browser_console 组合实测 Demo:
- 检查国标频段/步进是否正确
- 检查所有控件是否达到最小触控目标
- 检查功能交互是否正常工作
- 检查 Console 中 JS 报错
- 输出详细审查报告(通过/失败/建议改进)
- 根据 Reviewer 报告修复关键bug并验证
Demo 常见编码陷阱
1. 事件委托中 currentTarget 与 target 混淆
当使用事件委托(事件绑定在父容器,通过点击子元素触发)时,e.currentTarget 是绑定事件的父容器,不是被点击的子元素。
stationList.addEventListener('click', function(e) {
const card = e.currentTarget.closest('.station-card');
});
stationList.addEventListener('click', function(e) {
const card = e.target.closest('.station-card');
if (!card) return;
const idx = parseInt(card.dataset.index);
});
2. 长按检测的 pointerup 逻辑反转
长按检测(短按→点击,长按→删除/编辑)常见的逻辑错误:
btn.addEventListener('pointerup', function(e) {
if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; }
else { tuneToPreset(idx); }
});
btn.addEventListener('pointerup', function(e) {
if (pressTimer) {
clearTimeout(pressTimer);
pressTimer = null;
tuneToPreset(idx);
}
});
3. 搜索/扫描顺序依赖数组索引而非频率排序
搜索下一个电台时,如果用 stations.find(s => s.freq > currentFreq) 但 stations 数组不是频率升序排列,搜索会跳过最近的电台跳到最远的一个。
const STATIONS_FM = [
{ freq: 103.9, name: '中国交通广播' },
{ freq: 88.7, name: '中国之声' },
...
];
const STATIONS_FM = [
{ freq: 88.7, name: '中国之声' },
{ freq: 90.0, name: '经济之声' },
{ freq: 91.5, name: '音乐之声' },
...
];
Demo 的 Reviewer 审查清单
| 检查项 | 方法 |
|---|
| 触控目标尺寸 | 用 browser_console 执行 getComputedStyle(element) 读取实际 px 值,与 pt 要求对比 |
| 功能可用性 | browser_click 点击每个控件,观察响应 |
| JS 错误 | browser_console() 读取错误日志 |
| 国标合规 | 检查频率范围、步进、RDS显示、预设数量等 |
| Console Debug | 使用 browser_console(expression=...) 注入 JS 读取内部状态 |
阶段三详解:PRD 文档定稿
3.1 铁则:PRD 撰写必须使用 2red-product-monster-prd 技能
阶段三派发 Lead PM 产 PRD 时,Controller 必须先加载 2red-product-monster-prd 技能,并将其核心写作铁律嵌入 Lead PM 的 delegate_task context 中。不得仅凭产品大师自身的通用理解写 PRD。
Controller 在 3.1 的具体操作:
🔖[阶段三/3.1/加载2red技能]
// Controller 必须先加载 2red 技能
skill_view(name='2red-product-monster-prd')
🔖[阶段三/3.1/派发LeadPM]
// 然后将 2red 的核心写作铁律放入 context
delegate_task({
goal: "产出一份完整的 PRD 文档...",
context: `你必须严格遵循以下 PRD 写作规则:
【核心铁律1——纯中文表达】除非是行业标准专有名词(如 ID, CSV, PDF, API),
否则必须使用中文(如用"轻提示"代替Toast,用"弹窗/模态框"代替Modal)。
禁止英文缩写。
【核心铁律2——业务与交互视角,禁止技术和设计细节】PRD 只定义用户看到什么、
能做什么、系统怎么响应。严禁写入以下内容:
- 实施细节(坐标映射公式、CSS属性、像素值、颜色代码、动画时长/rgba/translateY)
- 设计规格(字号、间距、圆角、边框样式、具体图标形状/数量/排列)
- API 细节(接口路径、入参出参、数据表结构)
- 研发方案(技术选型、算法描述、框架名称)
- Demo 实现痕迹(鼠标滚轮、键盘方向键、Radio HAL、AudioManager 等具体实现名词)
判断标准:如果你在描述"怎么造",删掉。如果你在描述"用户看到什么、做什么、
系统响应什么",保留。
【核心铁律3——段落优先于表格】表格只用于验收标准、版本记录、数据对比。
功能描述用段落 + 序号层级(1. → 1.1 → 1.1.1 → •),不用表格。
【核心铁律4——覆盖三态】每个功能至少问三次:
数据为空时什么样?正在加载时什么样?出错了什么样?
【核心铁律5——纯产品视角】不要写Demo中提到的具体交互实现方式。
只写"用户手指抬起后,频率自动吸附到最近的步进值",不写"150ms easeOutCubic动画"。
只写"信号强度分级指示",不写"3根高度递增的竖条从左到右排列"。
只写"播放/暂停状态切换",不写"三角形图标/双竖条图标"。`,
toolsets: ["file"]
})
如果 2red-product-monster-prd 技能不可用或未找到:
- 降级方案:将上述核心铁律直接作为文本写入 context,不用 skill_view 加载
- 仍然遵循相同的写作纪律,不能退回到"满篇表格"的写法
3.2 格式验证
脚本见下方「格式验证命令」章节。验证不通过 → 打回 Lead PM。通过 → 终审。
from hermes_tools import terminal
prd_path = '<PRD文件>'
result = terminal(f"grep -n '[┌└│├┬┴─]' '{prd_path}'")
if result['exit_code'] == 0:
print('❌ ASCII框图发现,需要移除')
result = terminal(r"grep -in 'CAN\|LIN\|SoC路由\|500kbps\|电机响应' '" + prd_path + "'")
if result['exit_code'] == 0:
print('❌ 硬件技术细节发现,需要移除')
import re
with open(prd_path) as f:
text = f.read()
if re.search(r'(GET|POST|PUT|DELETE|PATCH)\s+/api/', text):
print('❌ 硬拦截: HTTP方法+API路径同时出现')
if re.search(r'varchar\(\d+\)|int\(\d+\)|INTEGER|BOOLEAN|TIMESTAMP', text):
print('❌ 硬拦截: 数据库字段类型出现')
for kw in ['messageType', 'message_type', 'MQTT topic', 'grpc', 'protobuf']:
if re.search(kw, text):
print(f'🟡 软提醒: 技术名词"{kw}"出现')
demo_trace_kws = ['鼠标滚轮', '键盘方向键', '三角形图标', '双竖条图标', 'Radio HAL', 'AudioManager']
for kw in demo_trace_kws:
if re.search(kw, text, re.IGNORECASE):
print(f'❌ Demo实现痕迹: "{kw}" — 这是原型/Demo的实现细节,应替换为产品语言')
visual_patterns = [
r'\d+\s*根\s*(竖|横|信号)',
r'从左到右排列',
r'(三角形|双竖条|正方[形体]).*图标',
]
for pat in visual_patterns:
if re.search(pat, text):
print(f'🟡 设计细节: 匹配"{pat}" — 应改为产品语言描述(如"多级信号指示")')
ac_pos = text.find('# 验收标准') if '# 验收标准' in text else len(text)
section_text = text[:ac_pos]
table_rows = len(re.findall(r'^\|', section_text, re.MULTILINE))
if table_rows > 5:
print(f'❌ 功能描述章节有{table_rows}行表格,违反2red铁律3"段落优先于表格"')
print(' 表格只应出现在验收标准、版本记录、数据对比中。功能描述用段落+序号层级。')
print('✅ 格式检查完成')
不通过 → 打回 Lead PM。通过 → 终审。
⚠ 格式验证的假阳性陷阱:
检查1中 grep -n '[┌└│├┬┴─]' 会匹配到 Markdown 列表符号 ├── 和表格分隔线 │,这些不是真正的 ASCII 框图。
处理方式: 先查看匹配到的行。如果只是列表/表格中的结构符号,标记为「非框图,跳过」;
如果是 ╔═╗║╚═╝ 等非标准 Markdown 装饰框(如 API 文档中的请求/响应示例),标记为「❌ 需移除」。
不要在 PRD 中使用 ├── 做列表前缀,改用 - 或 *。
工作目录与文件命名
产品大师产出物的目录结构:
{workdir}/
├── 场景锚点.md # 阶段零产出
├── 方案概要.md # 阶段一产出
├── 审查报告_*.md # Reviewer 各阶段报告
├── demo/
│ └── index.html # 阶段二产出
├── {产品名}_PRD.md # 阶段三产出
└── 完整对话记录-{主题}.md # 阶段三可选产出(见 §30)
- workdir 命名格式:
YYYYMMDD 产品主题/
- 所有文件名使用中文描述,不缩写
- 场景锚点和方案概要是滚动的:阶段一完成后方案概要吸收场景锚点内容
注意事项(常见陷阱)
1. 阶段一/二展示必须是双 Agent 共识
每个阶段展示的内容是 Lead PM + Reviewer 两边的结果,等两者都返回后才展示给用户。禁止单 Agent 完成后抢先输出。
2. 用户中途注入约束的处理
用户可能在阶段一/二确认时补充约束(如"屏幕是17寸"、"不要物理按键"、"纯触控操作"等)。
处理方式:
- 不开启新阶段,在当前阶段框架内更新方案概要(patch 文档)
- 更新后让用户再次确认,再进入下一阶段
- 约束变更影响 Demo 布局尺寸/交互方式的,必须在进入阶段二前完成方案更新
- 约束变更是正常的产品迭代,不需要重新走阶段零
3. 子 Agent 上下文语言 + 上下文污染防范
用户要求中文输出时,子 Agent 默认使用英文。必须在 delegate_task 的 context 字段中显式声明:
"请用中文回答,所有输出使用中文"
否则 Reviewer 的审查报告和 Lead PM 的文档可能混入英文内容。
重要:阶段三 PRD 撰写的"上下文污染"防范
产品大师的阶段二(HTML Demo)产出包含大量实现细节(pointerdown/move/up 事件、smoothSnapTo 动画、碰撞插值算法、键盘/鼠标事件监听等)。当阶段三 Lead PM 子 Agent 被派去写 PRD 时,如果 context 中直接包含 Demo 文件路径和实现细节,子Agent 会把这些 Demo 代码"翻译"成 PRD 文字——导致 PRD 里出现"鼠标滚轮调频""三根竖条信号指示""松手后平滑吸附"等技术/设计痕迹。
防范措施:
- Controller 在派发阶段三 PRD 任务时,context 中不要直接引用 Demo 源码文件,只引用方案概要和交互说明文档
- 如果必须引用 Demo 文件,必须在 context 中显式声明:「Demo 代码仅供参考交互行为,不要将其实现方式写入 PRD。PRD 只描述用户看到什么、能做什么、系统如何响应,不描述具体实现方式」
- Lead PM 产出 PRD 后,Reviewer 需检查是否存在Demo实现痕迹
4. 阶段一 Reviewer 查阅范围可能不足
Reviewer 只能看到已保存的文件。Controller 必须在派发前确保场景锚点已写入磁盘,
并在 Reviewer 的 context 中明确列出所有相关文件路径。
5. Demo 的纯触控完整性检查
Demo 导出后,务必检查是否存在键盘事件监听(如 keydown → seekForward)。
如果存在,需要确认所有功能是否也有对应的触控事件路径。纯触控场景下不允许有关键功能只能通过键盘触发。
6. 阶段一/二可循环修改
用户可以在阶段一和阶段二之间循环:"确认方案 → 进 Demo → 不满意 → 改方案 → 重做 Demo"。
每次循环 Controller 需更新方案概要 + 重派 Demo,不需重新调研。
7. 阶段三只做文档打磨,不做功能变更
阶段三的 PRD 文档定稿阶段,不允许新增功能或修改交互方案。
所有功能/交互变更必须在阶段二及之前闭环。如果在阶段三用户提出变更,打回阶段二重新走。
8. Demo UI 标签使用完整中文(中国市场)
中国市场车载产品的 Demo 界面,所有用户可见标签禁止使用英文缩写。
用户明确表达不满:「页面上的什么st/ta/tp都是什么意思,看不明白」。
| 错误写法 | 正确写法 |
|---|
| ST / MONO | 立体声 / 单声道 |
| TA | 交通播报 |
| TP | 交通节目 |
| RDS | 不出现"RDS"字样,直接显示电台名称(PS)和滚动文本(RT) |
| dBμV | 可保留,属于行业通用技术符号 |
在 Lead PM 产出 Demo 的 goal 中应显式注明:「所有UI标签使用中文,不要英文缩写」。
9. 为不可直观理解的控件添加解释文本和操作反馈
用户对 Demo 中控件的功能提出疑问(如"交通播报是什么?"、"点击了没反应"),说明控件缺乏两种设计要素:
- 解释文本:按钮旁边加一行小字说明功能(字号比主按钮小2-3档,颜色 rgba(255,255,255,0.35))
- 操作反馈:每次点击必须产生即时可视反馈
实现方案(车载场景推荐):
<div id="ta-tp-section">
<button class="tp-ta-btn" id="ta-btn">交通播报</button>
<span class="tp-ta-desc">开启后自动切换路况信息</span>
<button class="tp-ta-btn" id="tp-btn">交通节目</span>
<span class="tp-ta-desc">标记当前台为交通频道</span>
</div>
<div id="toast-container"></div>
.tp-ta-desc {
font-size: clamp(9px, 0.8vw, 12px); color: rgba(255,255,255,0.35);
line-height: 1.2; text-align: right;
}
function showToast(msg) {
const container = document.getElementById('toast-container');
const el = document.createElement('div');
el.className = 'toast-item';
el.innerHTML = msg;
container.appendChild(el);
setTimeout(() => { if (el.parentNode) el.remove(); }, 3000);
}
$('ta-btn').addEventListener('click', function() {
state.taEnabled = !state.taEnabled;
this.classList.toggle('active', state.taEnabled);
showToast(state.taEnabled ? '🔊 已开启' : '🔇 已关闭');
});
10. 设置面板等浮层从驾驶侧滑入
驾驶位在左侧(左舵驾驶)时,设置面板、编辑浮层等二级交互界面应从屏幕左侧滑入。
关于17寸大屏的完整布局策略(交互区40%/信息区60%、80pt预设按钮、≥16pt间距、左舵/右舵镜像),见 references/car-radio-gb-standards.md →「17寸车载中控屏布局策略」章节。
#settings-panel {
transform: translateX(100%);
}
#settings-panel {
transform: translateX(-100%);
}
#settings-overlay {
display: flex; justify-content: flex-start;
}
如果目标市场为右舵驾驶(靠左行驶),则反过来从右侧滑入。通过设计阶段的产品定位确定。
11. 最终交付附带审查记录
PRD 文件应嵌入 prd-review 审查报告,或者在项目目录下附带独立审查报告文件。
12. 功能必须可追溯至国标条款,不可追溯的功能应考虑移除
用户反馈"设置里面的功能是国标要求的吗,不是的就把整个设置去掉"——这是关键的设计纪律。
原则: 每个用户可见的功能必须能追溯到某条国标/法规要求。如果一项功能无法在引用的标准中找到对应条款,它就不应该出现在产品中。
Controller 的检查时机:
- 阶段一方案概要产出后:检查功能列表中的每一项是否有国标依据
- 阶段二 Demo 产出后:检查 Demo 中的每个控件/按钮/设置项是否有国标依据
- 对于"交互辅助"类功能(如 Toast 通知、页面切换动画):合理保留,但这些不在国标合规审计范围内
审计流程(Controller 在阶段一/二合并展示前执行):
- 提取 Lead PM 方案中的全部功能点
- 逐项对照「引用标准」列表
- 标注:国标依据 + 条款号 或 ❌ 无依据
- 无依据项:确认必要性,必要时要求 Lead PM 移除或合入国标覆盖的功能中
案例: 本 Session 中设置面板的「搜台灵敏度」「立体声模式」「AF开关」「重置预设」等全部被移除,因为 GB/T 28433-2023 只要求设备具备这些能力,不要求暴露给用户调节。
13. 用户补充约束时,原地更新方案不上溯
用户可能在阶段确认时补充约束(如"屏幕17寸"、"纯触控操作"、"右舵市场")。这些约束变更不需要也不应该重新执行阶段零调研。处理方式:
- 在当前阶段框架内 patch 方案概要文档中的相关章节
- 如果有 Reviewer 已完成的审查报告,确认约束变更后是否需要重新审查
- 约束影响 Demo 布局/交互的,必须在进入阶段二前更新方案概要
- 将约束变更记录到工作目录日志中,供后续阶段参考
14. 用户要求暂停当前阶段回退到调研时,不重新启动流程
用户可能说"HTML先缓一下,你先查看国标文件"——即要求暂停阶段二/三的推进,回退到阶段零级别的调研。
正确处理方式:
- ❌ 不要重新启动"🔖[阶段零]"流程
- ❌ 不要丢弃已完成的 Demo/方案工作
- ✅ 在当前框架内执行「调研子任务」:用
delegate_task 或直接工具调用完成调研
- ✅ 调研产出写入独立文件(如
国标要求原文清单.md),不覆盖已有方案文档
- ✅ 调研完成后向用户呈现结果,请用户确认是否"继续推进"还是"基于调研结果重新做"
原因: 用户只是要一个信息输入,不是要推翻整个产品流程。调研结果主要用于验证/修剪已有设计,不是重新开始。
16. 拖拽跳变的根因与修复:smoothSnapTo 替代 snapToNearest
问题表现: 滚筒式频率选择器拖拽结束后,用户感觉到轴会"跳动"一下。
根因分析:
手指停在 offset=150 (对应88.73MHz)
→ snapFreq(88.73) = 88.7
→ getOffsetForFreq(88.7) → 偏移值瞬间从150跳变到74(76像素差异)
根本修复: 拖拽期间 trackOffset 精确跟随手指,频率显示按 snapFreq 取整。拖拽结束后用 150ms ease-out 动画过渡到目标偏移。
function smoothSnapTo(targetFreq) {
cancelAnimation();
const targetOffset = getOffsetForFreq(targetFreq);
const startOffset = offset;
const startTime = performance.now();
const duration = 150;
function animate(now) {
const t = Math.min((now - startTime) / duration, 1);
offset = startOffset + (targetOffset - startOffset) * (1 - Math.pow(1 - t, 3));
applyOffset(offset);
currentFreq = getFreqFromOffset(offset);
updateDisplay();
if (t < 1) animRAF = requestAnimationFrame(animate);
else { offset = targetOffset; applyOffset(offset); currentFreq = targetFreq; updateDisplay(); }
}
animRAF = requestAnimationFrame(animate);
}
所有频率变更操作(搜索、键盘、滚轮、点击卡片、波段切换)都应走 smoothSnapTo。
17. 全区域可拖拽:卡片不是禁区
drumTrack.addEventListener('pointerdown', (e) => {
if (e.target.closest('.drum-card')) return;
startDrag(e);
});
drumContainer.addEventListener('pointerdown', startDrag);
function onPointerUp(e) {
if (!dragMoved && Math.abs(deltaY) < 5) {
const card = document.elementFromPoint(e.clientX, e.clientY)?.closest('.preset-card');
} else {
smoothSnapTo(snapFreq(currentFreq));
}
}
18. 阶段二/三 Demo 评审要检查文字可读性
用户发出「整个页面的文字实在太看不清楚了」时,说明评审遗漏了核心可用性维度。
最低标准:
- 正文(14~22px):font-weight ≥ 400
- 大数字(≥48px):font-weight ≥ 300
- 次级文字透明度 ≥ 0.50
- 卡片名称透明度 ≥ 0.72
评审方法: 用 browser_console 读取 getComputedStyle,检查 font-weight 和 color alpha 是否达标。
19. 第三方标准文档 PDF 的文本提取优先级
当用户提供了 PDF 标准文档时:
- 优先用 pdftotext(macOS 自带/brew install poppler)
pdftotext "文档.pdf" /tmp/extracted.txt
- 然后阅读提取结果,重点标记:
- 含"应"字的条款 → 强制要求
- 含"宜"字的条款 → 推荐要求(非强制)
- 功能要求章节(通常 4.x)
- 性能表格(表1、表2...)
- 试验方法(5.x)→ 验证标准
- 区分两类要求:
- App层面要求(功能、交互、显示)→ 直接影响 Demo/UI 设计
- 硬件层面要求(RF性能、环境试验、EMC)→ 记录但不影响UI
- 输出对照表:每项功能标注「强制国标条款号|推荐国标条款号|无依据」
20. 滚筒式频率轴:卡片碰撞解析 + 空白区域频率插值
问题: 按频率比例定位卡片,相邻卡片频率太近时会重叠。
解决方案:
- 排序后遍历,相邻距离 < 卡片高+间隙 时只推后不提前推
- 空白区域点击频率计算:用最近两张卡片的视觉位置(collision-pushed pos)做比例插值,频率用卡片真实频率值
- ❌ 不要用
idealPos(碰撞前理论位置)做插值——碰撞推挤后视觉空间和频率空间已不一致
- ❌ 不要加
+X 容差阈值(如 cp.pos <= clickCenter + 10)——会扭曲卡片边界附近的点击归属
- 空白区域不显示刻度尺:碰撞推挤后刻度不再是线性的,全部移除。依赖右侧频率数字作为唯一调频反馈
- 固定位置添加按钮:左下角
position:absolute,不在 track 上浮动。当前频率已保存或无空位时置灰禁用
21. 固定位置添加按钮(不浮在轴上)
"+"添加按钮固定在页面左下角用 position:absolute,不在 track 上作为卡片浮动。
当前频率已保存或无空位时置灰禁用。
在 Lead PM 产出 Demo 的 goal 中指明:「添加按钮在左下角固定位置,不是track上的浮动卡片」。
22. 碰撞解析的空白区域点击:用视觉 pos 而非 idealPos
卡片碰撞推挤后,视觉空间和频率空间不再一一对应。空白区域点击时:
// ❌ 错误:用 idealPos(碰撞前理论位置)做插值 → 点击 167px 会跳过88.9直接到91.5
if (cp.idealPos <= clickTrackPos) lower = cp;
// ✅ 正确:用视觉 pos(碰撞后实际位置)做插值
if (cp.pos <= clickTrackPos) lower = cp;
同时不能加 +X 容差阈值:
// ❌ 错误:cp.pos <= clickCenter + 10 → 10px 死区导致点击靠近88.9边界时误认upper为91.5
if (cp.pos <= clickCenter + 10) lower = cp;
// ✅ 正确:精确比较
if (cp.pos <= clickTrackPos) lower = cp;
23. 碰撞解析场景下不显示刻度尺
卡片碰撞推挤后,频率刻度不再是线性的。刻度尺必须移除,用户依靠右侧频率数字作为唯一调频反馈。
for (let freq = b.min; freq <= b.max; freq += labelStep) {
html += `<div class="track-label" style="top:${pos}px">${formatFreq(freq)}</div>`;
}
24. 右侧信息区布局稳定性
当 RDS 副标题有/无内容切换时,右侧信息区不应跳变。所有可能变化的元素使用 min-height:
#rds-info {
min-height: 22px;
line-height: 1.4;
margin-bottom: 32px;
}
信号强度条和播放/暂停按钮不使用 margin-bottom 控制垂直位置,而是依赖上方的固定高度元素。
25. 车载 Demo 文字可读性最低标准
用户多次反馈「看不清楚」。Reviewer 必须基于渲染尺寸检查而非 CSS 声明值:
| 元素 | 最小 font-weight | 最小透明度 |
|---|
| 卡片频率 (17px) | 500 | 1.0 (#FFFFFF) |
| 卡片名称 (14px) | 400 | 0.72 |
| 右侧频率 (64px) | 300 | 1.0 |
| 电台名 (22px) | 400 | 1.0 |
| RDS信息 (15px) | 400 | 0.72 |
检查方法:
const el = document.querySelector('#station-name');
const style = getComputedStyle(el);
console.log('weight:', style.fontWeight, '| color:', style.color);
26. 播放/暂停功能的设计纪律
播放/暂停不属于国标强制要求,但属于基础交互完整性。设计要点:
- 位置:右侧信息区底部,信号强度下方,与整体布局对齐
- 样式:纯文字切换("播放"↔"暂停"),字体大小13~14px,与 band-switch 风格一致
- 状态管理:用独立变量
isPlaying 管理,不依赖国标功能状态
- 交互反馈:点击时文字立即切换,hover 时变白
27. 国标要求的 App 层面与硬件层面分离
GB XXXXX—XXXX《车载无线广播接收系统》讨论稿中,App 层面只有两条强制要求:
| 强制要求 | 条款 | App 功能 |
|---|
| 应支持接收AM或FM广播 | 4.1 | FM 87108 MHz / AM 5311602 kHz 波段切换 |
| 应具备搜索电台功能 | 4.1 + 5.4 | 手动/自动搜台(滚筒滑动+搜索按钮) |
以下是非强制但用户可选的功能(不应出现在国标合规表中,可出现在体验说明中):
- 预设存储
- RDS信息显示(PS名称、RT文本)
- 信号强度指示
- 立体声/单声道状态指示
- 播放/暂停
硬件层面的性能指标(灵敏度、选择性、信噪比、失真、立体声分离度等)由底层芯片/射频完成,App 不应提供用户可调控件。
28. 用户约束变更不应重新执行阶段零调研
用户可能在阶段一/二确认时补充约束(如"屏幕17寸"、"纯触控操作"、"右舵市场")。
处理方式:
- 在当前阶段框架内 patch 方案概要文档
- 更新后让用户再次确认,再进入下一阶段
- 约束影响 Demo 布局/交互的,在进入阶段二前更新
- 不需要也不能重新执行阶段零调研
29. 暂停阶段调研时,不重新启动流程
当用户说"HTML先缓一下,你先查看国标文件"——暂停阶段二三推进回退到调研层面时:
- ❌ 不要重新启动"🔖[阶段零]"流程
- ❌ 不要丢弃已有 Demo/方案
- ✅ 在当前框架内用
delegate_task 或直接工具调用完成调研
- ✅ 调研产出写入独立文件(如
国标要求原文清单.md),不覆盖已有方案
- ✅ 调研完成后请用户确认继续推进还是重新做
30. 用户要求保存"全部"对话记录时,必须跨会话提取
用户可能在项目后期说"把所有对话原文记录到一个md里面"。此时不能只保存当前会话或最近一段对话——用户期望的是从项目启动到当前的全部记录。遗漏前序会话内容的做法会被用户纠正"你只保留了一半"。
正确做法:
-
识别所有相关会话:用 session_search(query="项目关键词") 找到所有相关历史会话。车载收音机之类的大型项目可能跨越 CLI 和 TUI 多个会话(本例中主项目会话 856 条 + 补充会话 90 条,合计 946 条消息)。
-
提取完整内容:直接查询 SQLite 数据库以获取全部消息,不遗漏中间段。JSON 输出模式可正确处理多行文本:
sqlite3 ~/.hermes/state.db "
SELECT json_object('id', id, 'role', role, 'content', content, 'ts', timestamp)
FROM messages
WHERE session_id='<SESSION_ID>'
AND (role='user' OR (role='assistant' AND content IS NOT NULL AND content != ''))
ORDER BY id;
"
分别提取所有相关会话,按时间线合并。先查会话列表确定目标会话,逐个提取。
-
按时间线编译:将所有用户原文(role='user')和关键助手回复(有实质内容的 assistant 消息)按时间戳排序,从第一条用户消息开始。用户对回复风格和格式的批评也属于原始需求的一部分,应如实记录。
-
验证覆盖范围:编译完成后检查第一个用户消息是否是项目启动时的原始需求。如果第一条是关于"如何继续历史对话"等元问题的回复,说明提取范围不对——应追溯到更早的核心会话。
-
文件开头用分类统计表格:用户要求"用表格整理这几百条对话各是哪类的,有谁发出的,分类统计"。必须包括:
- 按角色分类:用户消息数 / 助手有内容回复数 / 助手仅工具调用数 / 工具结果数
- 按会话分类:每个会话的总消息、用户发出、助手回复、工具调用
- 按阶段分类(用户消息):按项目阶段分组的用户消息计数
- 工具调用分布:按工具类别(patch/read_file/browser_*/search_files/delegate_task 等)的调用次数
数据来源:对相关 session_id 执行 GROUP BY 查询。
SELECT role, COUNT(*) FROM messages WHERE session_id='<ID>' GROUP BY role;
SELECT tool_name, COUNT(*) FROM messages WHERE session_id='<ID>' AND role='tool' GROUP BY tool_name ORDER BY count DESC;
-
存档格式:用 # 分章节组织,每个用户原文用 **用户原文:** 标记并加 blockquote,关键助手回复的决策用说明文字概括。在文件末尾附上项目文件清单和用户确认的核心设计原则。
不要从中间开始记录(如"从修改 PRD 开始"),用户会反馈"你只保留了一半"。
不要遗漏用户对回复风格/质量的批评——这些是用户偏好的直接表达,记录本身有助于后续理解用户的决策上下文。
30. 用户要求保存"全部"对话记录时,必须跨会话提取
用户可能在项目后期说"你把所有对话原文记录到一个md里面"。此时不能只保存当前会话或最近一段对话——用户期望的是从项目启动到当前的全部记录。
正确做法:
-
识别所有相关会话:用 session_search(query="项目关键词") 找到所有相关历史会话。车载收音机项目可能跨越 CLI 和 TUI 多个会话。
-
提取完整内容:直接查询 SQLite 数据库以获取全部消息,不遗漏:
sqlite3 ~/.hermes/state.db "
SELECT json_object('id', id, 'role', role, 'content', content, 'ts', timestamp)
FROM messages
WHERE session_id='<SESSION_ID>'
AND (role='user' OR (role='assistant' AND content IS NOT NULL AND content != ''))
ORDER BY id;
"
分别提取所有相关会话,按时间线合并。先查会话列表确定目标会话,逐个提取。JSON 输出模式能正确处理多行文本的 content 字段。
-
按时间线编译:将所有用户原文(role='user')和关键助手回复(有实质内容的 assistant 消息)按时间戳排序,从第一条用户消息开始。用户对回复风格和格式的批评也属于原始需求的一部分,应如实记录。
-
验证覆盖:编译完成后,检查第一个用户消息是否确实是项目启动时的原始需求。如果第一条是关于"如何继续历史对话"等元问题的回复,说明提取范围不对——应追溯到更早的会话。
-
存档格式:用 # 分章节组织,每个用户原文用 **用户原文:** 标记并加 blockquote,关键助手回复用说明文字概括。在文件末尾附上项目文件清单和用户确认的核心设计原则。
不要从中间开始记录(如"从修改 PRD 开始"),用户会反馈"你只保留了一半"。
不要遗漏用户对回复风格/质量的批评——这些是用户偏好的直接表达,记录本身有助于后续理解用户的决策上下文。