feat(dsh): spawn_teammate 角色 persona 自动注入 + 测频带外饱和边界登记
CI / 静态门禁 (pre-commit) (push) Failing after 3m46s
CI / 主机侧单测 (MSVC) (push) Successful in 30s

一、子 Agent 角色自动注入(本批主线)

dsh-plugin-subagent-roles fork(仓库 .dsh/plugins/)新增事件路径注入:
spawn_teammate 的 prompt 首行写 @role:<角色id>,插件在子会话首轮模型
请求组装之前把 .dsh/roles/<id>.md 的 persona 注入其系统提示词,并应用
工具白名单。此前只有 subagent_role 路径自动注入,team 路径需 Lead 手工
拼接 persona,本批把两条路径拉齐。

实现要点(细节见 docs/环境与部署/子Agent角色自动注入实现说明.md):
- 在 lead 会话的 tool/call(spawn_teammate) 截获 @role 标记,在
  subagent/catalog 拿 childId 后经 agents.get(childId) 注入——子会话侧
  user/message 必然晚于首轮 system/message,只能在 lead 侧接力。
- tools.restrict() 只校验全局工具名,ops_* 等作用域工具混入会整批抛错;
  降级为解析报错尾部 known global tools 求交集后重试。
- 保留子会话 @role 标记兜底路径(第 2 步起生效)+ 双去重集合。
- 诊断日志 ~/.dsh/role-inject-diag.log,一次健康注入 4 行。

配套文档与口径:
- 新增 docs/环境与部署/子Agent角色自动注入实现说明.md(实现参考 +
  排障速查,9 章),docs/README.md 索引挂载。
- AGENTS.md 委派策略改为自动注入口径(无需手工拼接)。
- .dsh/roles/README.md 删去"调用预算"列:budget.js 的 arm() 只由
  subagent_role 委派路径调用,teammate 路径不挂账,原列是假上限承诺;
  改加脚注指向实现说明 §1.1。

二、测频幅值门放宽至 5 V + 带外饱和边界登记(freq.h)

- FREQ_AMP_MIN 50 V -> 5 V(用户裁定 2026-11-13):台架低端注入(标定
  用 5~25 V 档)与低负荷工况也能测频;浮空二次域噪声几十 mV,仍差两个
  数量级,噪声不测频的本意不变。
- 注释同步登记:IA 回退候选同拿 5.0 比较,二次电流 0.5~2 A 够不到
  "5 A",只注电流实际不回退测频(电流域门限另裁);窗环钳位区间
  [72,96] 只对 41.7~55.6 Hz 满足整倍条件,带外读数饱和钉沿。
- docs/算法/采集与算法层设计说明.md 新增 §7.3 准确测频带与带外饱和
  (60 Hz 实证钉 ~55;用户裁定不改参数,只登记边界)。

验证:/keil-build 0 错误 0 警告(Code=256592 RO=32036 RW=2088
ZI=152584);pre-commit 52 钩子全绿(含 3 根 manual 阶段:cppcheck、
test-cppcheck、test-coverage)。
This commit is contained in:
2026-11-14 10:07:27 +08:00
parent e00531999d
commit 9f68b7bc03
6 changed files with 334 additions and 33 deletions
+14 -12
View File
@@ -18,21 +18,23 @@ spawn_teammate({
### 核心模块专家(双层结构:通用行业标准 + 项目专项)
| 角色 ID | 显示名 | 职责范围 | 工具权限 | 调用预算 |
|---------|--------|---------|---------|---------|
| `algo-specialist` | 采集算法专家 | ADC 语义取数、全周傅氏相量、测频、保护判据 | read/edit/write/grep/glob | 80 |
| `network-specialist` | 网络协议专家 | CH395F SPI 以太网、netTask socket 栈、IEC 60870-5-104、FTP、SNTP | read/edit/write/grep/glob | 80 |
| `storage-specialist` | 存储文件系统专家 | GD5F SPI NAND、dhara FTL、FatFs、日志系统、卷自愈 | read/edit/write/grep/glob | 80 |
| `ui-specialist` | UI/LVGL 专家 | LVGL 160×160 单色菜单、PC 模拟器、菜单树生成器、UI 自动化回归 | read/edit/write/grep/glob | 80 |
| 角色 ID | 显示名 | 职责范围 | 工具权限 |
|---------|--------|---------|---------|
| `algo-specialist` | 采集算法专家 | ADC 语义取数、全周傅氏相量、测频、保护判据 | read/edit/write/grep/glob |
| `network-specialist` | 网络协议专家 | CH395F SPI 以太网、netTask socket 栈、IEC 60870-5-104、FTP、SNTP | read/edit/write/grep/glob |
| `storage-specialist` | 存储文件系统专家 | GD5F SPI NAND、dhara FTL、FatFs、日志系统、卷自愈 | read/edit/write/grep/glob |
| `ui-specialist` | UI/LVGL 专家 | LVGL 160×160 单色菜单、PC 模拟器、菜单树生成器、UI 自动化回归 | read/edit/write/grep/glob |
### 通用工作角色(项目专项)
| 角色 ID | 显示名 | 职责范围 | 工具权限 | 调用预算 |
|---------|--------|---------|---------|---------|
| `code-reviewer` | 代码审查员 | 审查改动的正确性、安全性与规范合规 | read/grep/glob(只读) | 3000 |
| `test-engineer` | 测试工程师 | 主机侧单测、板级测试套件编写维护 | read/edit/write/grep/glob | 3000 |
| `build-gatekeeper` | 构建与门禁验证 | Keil 构建、pre-commit 门禁、CI 本地自查诊断 | read/grep/glob(只读) | 3000 |
| `doc-writer` | 技术文档写手 | 技术文档编写维护、链接有效性、计数准确性 | read/edit/write/grep/glob | 80 |
| 角色 ID | 显示名 | 职责范围 | 工具权限 |
|---------|--------|---------|---------|
| `code-reviewer` | 代码审查员 | 审查改动的正确性、安全性与规范合规 | read/grep/glob(只读) |
| `test-engineer` | 测试工程师 | 主机侧单测、板级测试套件编写维护 | read/edit/write/grep/glob |
| `build-gatekeeper` | 构建与门禁验证 | Keil 构建、pre-commit 门禁、CI 本地自查诊断 | read/grep/glob(只读) |
| `doc-writer` | 技术文档写手 | 技术文档编写维护、链接有效性、计数准确性 | read/edit/write/grep/glob |
> **关于工具调用预算**:角色文件 frontmatter 里都写了 `maxToolCalls`,但该预算**只对 `subagent_role` 路径生效**,对 `spawn_teammate` 起的 teammate 不起约束(插件 `lib/budget.js` 的 `arm()` 只由委派工具调用,team 路径不挂账)。所以本表不再列预算列,免得被当成上限承诺。缺口与后续补齐口径 → `docs/环境与部署/子Agent角色自动注入实现说明.md` §1.1。
## 角色详细说明
+1 -1
View File
@@ -149,7 +149,7 @@
- **Lead**:拆解任务、分配 teammate、协调跨域、汇总结果。不直接做模块级实现。
- **Teammate**:专注自己领域,跨域问题告知 Lead 转交。
- **角色绑定**:prompt 第一行写 `@role:<角色id>`(如 `@role:algo-specialist`),插件自动注入 persona + 工具策略。**不得省略**。角色定义 → `.dsh/roles/README.md`。
- **角色绑定(自动注入)**:`spawn_teammate` 的 `prompt` **首行写 `@role:<角色id>`**(如 `@role:algo-specialist`),插件会在子会话首轮请求组装前自动把 `.dsh/roles/<角色id>.md` 的 persona 注入其系统提示词并应用工具白名单,**无需手动拼接**(缺首行标记 = 无角色,勿省略)。可用角色 → `.dsh/roles/README.md`;`subagent_role` 路径同为自动注入。
- **判断标准**:涉及两个以上模块、需要构建+测试+文档联动 → Agent Teams。单文件小改 → `subagent_role` 可接受。
## 边界(Ask first / Never)
+24 -12
View File
@@ -19,7 +19,8 @@
* 跳变假值,本拍不入窗;
* - 幅值门:源相量有效值 < FREQ_AMP_MIN 判"无有效波形"(浮空输入/PT 断线),
* 停止更新、valid 清零、读数保持 → 台架浮空 rms≈0 不发散;
* - 信号源优先级:UA 基波 → IA 基波(UA 无幅值时用 IA,台架只注电流亦可测频)。
* - 信号源优先级:UA 基波 → IA 基波(UA 无幅值时用 IA;工程量域下 IA 候选
* 需二次电流达到门限数值,见 FREQ_AMP_MIN 注释)。
*
* ⚠ 本层**纯算法**:不碰寄存器、不碰 RTOS、不自己取数(输入 = phasor 使用区)
* → 主机侧可免桩纯逻辑单测。
@@ -28,6 +29,11 @@
* 创建日期:2026-10-12(B8)
* 修改记录:
* v1.0 2026-10-12 DTU 项目组 创建:相位解缠绕测频 + 滑动平均 + 幅值门
* v1.1 2026-11-13 DTU 项目组 幅值门 50 V → 5 V(用户裁定,台架低端注入
* 也能测频);其余判据不变
* v1.2 2026-11-13 DTU 项目组 纯注释:登记窗环钳位区间的带外饱和边界
* (60 Hz 实证钉 ~55;算法与参数不改,
* 设计说明 §7.3)
*/
#ifndef FREQ_H
@@ -50,22 +56,28 @@
#define FREQ_INST_MIN_HZ 35.0f
#define FREQ_INST_MAX_HZ 65.0f
/* 幅值门限(有效值,**二次值 V 口径**,下游常数按工程量重定的结果,依据
/* 幅值门限(有效值,**二次值 V 口径**,依据
* `docs/算法/采集与算法层设计说明.md` §13.7 表第 1 行):
* 低于该值判"无有效波形"。取值 50 V = 0.5×Un(相电压标称 100 V,与
* App/algo/prot_set.c 头部量纲基准同源):正常带载 UA 在 0.9~1.1×Un,
* 50 V 之下只可能是断保险/PT 空载/浮空噪声(浮空实测 ~1.7 V 引脚域、
* 二次域更小),不会吃掉正常纹波(纹波谷值远大于 0.5×Un)。
* ⚠ 过渡期行为(eng=0,两点标定完成前):帧值还是引脚电压(≤5 V < 50 V),
* 本门**恒失效** → freq_valid()=0 → OF/UF 按"必不启动"填充复位——与未标定即闭锁
* 同向(判据链本就停走),不构成额外风险;标定后自动恢复本意。
* 数值沿用原 50.0f 属巧合(原为"远小于千级码值"的拟定值),不是漏改。 */
#define FREQ_AMP_MIN 50.0f
* 低于该值判"无有效波形"。取值 5 V = 0.05×Un(相电压标称 100 V,与
* App/algo/prot_set.c 头部量纲基准同源):2026-10-25 曾按工程量重定为
* 50 V = 0.5×Un,2026-11-13 用户裁定放宽到 5 V,让台架低端注入(标定用
* 5~25 V 档)与低负荷工况也能测频;浮空噪声实测二次域几十 mV,仍差两个
* 数量级 → "噪声不测频"的本意不变,带载 UA(0.9~1.1×Un)远在门上。
* ⚠ 候选共用数值:IA 回退候选同拿 5.0 比较——二次电流域 0.5~2 A 够不到
* "5 A",台架只注电流实际不回退测频;电流域自己的门限另裁,勿在此顺改。
* ⚠ eng=0 期间(未标定,帧值还是引脚电压,满幅 < 3.3 V)仍低于本门 →
* freq_valid()=0 → OF/UF 按"必不启动"填充复位——与未标定即闭锁同向;
* 标定(eng=1)后按本门限正常放行。 */
#define FREQ_AMP_MIN 5.0f
/* 相位增量滑窗环容量(拍):96 拍 = 96 ms 上限。实际平均窗长**自适应**
* (freq.c fq_read_freq:K_eff = round(8·Fs/(2·f_hat)),45~55 Hz 段 73~89 拍),
* 使窗长恰为 DFT 负序泄漏纹波周期 fs/(2f) 的整数倍 -> 纹波相消。
* 保护定值延时 ≥100 ms,~90 ms 窗不影响动作时序判据。 */
* 保护定值延时 ≥100 ms,~90 ms 窗不影响动作时序判据。
* ⚠ 钳位区间 [FREQ_AVG_MIN, FREQ_AVG_TAPS] 只对 41.7~55.6 Hz 满足整倍条件:
* 带外窗非纹波周期整数倍 -> 读数饱和钉沿(高侧 ~55、低侧 ~43.5,且带外
* freq_valid() 仍为 1,数值不可消费)。2026-11-13 台架 60 Hz 实证与
* 「参数不改」裁定见 docs/算法/采集与算法层设计说明.md §7.3。 */
#define FREQ_AVG_TAPS 96u
/* 自适应窗长下限(拍):K_eff 钳位区间下界(启动段窗未满时按已有拍数)。 */
+1
View File
@@ -259,6 +259,7 @@ docs/
|---|---|
| [`项目部署环境要求.md`](环境与部署/项目部署环境要求.md) | 开发机软件环境、目标板资源预算、现场部署与验收步骤 |
| [`并行任务工作树方案.md`](环境与部署/并行任务工作树方案.md) | **多任务并行开发机制**:`git worktree` 隔离、目录与共享/独占分配(junction 规则)、`task-worktree` SKILL 命令面(`@task` 仅为文档别名,显式调用 `/task-worktree`)、单写者文档与合并残留审计、台架锁;**已落地并端到端实测**(见其 §11) |
| [`子Agent角色自动注入实现说明.md`](环境与部署/子Agent角色自动注入实现说明.md) | **`spawn_teammate` 角色 persona 自动注入的实现参考 + 排障参照**(宿主 0.2.2-rc.1 + 2026-11-13 台架实证快照):事件面事实(两参回调/子会话时序/lead 侧接力)、工具白名单降级、兜底与去重、失败模式史、部署生效链(pnpm file: 拷贝陷阱)、诊断日志排障速查;使用侧在 `.dsh/roles/README.md` |
| [`CI/README.md`](CI/README.md) | **远程 CI(Gitea Actions)唯一入口**:服务端开关 / runner 部署与保活 / workflow / 排障。**只涉及本机一台 runner `win-stm32-ci`,其他机器与项目都不涉及**;日常改代码用本地门禁 `scripts/ci_local.py`,不必读这里 |
### 9.3 台账与外部资料
@@ -0,0 +1,256 @@
# 子Agent角色自动注入实现说明
> **定位**:`spawn_teammate` 的 `@role:<角色id>` persona 自动注入功能的**实现参考 + 排障参照**——换机、升级宿主、或再次调试注入问题时照本篇复原。使用侧口径(可用角色清单、`@role:` 怎么写、`tools` 字段怎么填)归 `.dsh/roles/README.md`;宿主侧「委派策略」一句话口径归根 `AGENTS.md`「委派策略(Agent Teams 优先)」节。三处分工:同一事实只在一处详述。
>
> **版本事实**:本篇全部事件面与时序结论 = 宿主 `@deepseek-ai/dsh` **0.2.2-rc.1** + fork 插件 `dsh-plugin-subagent-roles` **0.5.1-team-fork** 的台架实证快照(2026-11-13 探针实测)。事件面是**观测行为,不是宿主 API 契约**——宿主升级后可能变化,出问题先按 §8 查诊断日志,再动插件代码。
---
## 1. 目的与本篇边界
这节说清该功能解决什么问题、本篇只覆盖哪条路径。读者读完应能判断自己的问题是否归本篇管。
**问题**:`spawn_teammate` 的 `prompt` 参数就是一段普通任务文本,宿主**不会**替它读取 `.dsh/roles/<角色id>.md`——不做任何处理时,teammate 子会话会以无角色 persona、无工具策略的裸 agent 起跑。而插件自带的 `subagent_role` 委派工具路径,上游本就自动应用角色(persona + 工具策略 + 调用预算),所以本篇讲的**只是 Agent Teams 路径的增量**:让 `prompt` **首行**的 `@role:<角色id>` 标记获得与 `subagent_role` 同等的自动注入。
**目标**(验收判据):子会话的**首轮模型请求组装之前**,其系统提示词已含角色 persona、工具白名单已生效——Lead 侧无需手工拼接 persona 文本。
**本篇不管的**:
| 内容 | 去处 |
|---|---|
| 有哪些角色、职责范围、怎么选 | `.dsh/roles/README.md`(使用侧) |
| 委派策略总口径(Agent Teams 优先、首行必须写标记) | 根 `AGENTS.md`「委派策略」节 |
| `tools` 字段的保留工具名单与容错口径(使用侧) | `.dsh/roles/README.md`「`tools` 字段说明」节 |
| 工具调用预算(`maxToolCalls`)在 team 路径**不生效** | 见下文 §1.1 |
### 1.1 已知边界:注入路径不带预算
`lib/budget.js` 的预算监视器靠 `arm()` 挂账,而 `arm()` 只在 `subagent_role` 委派工具启动子会话时调用;`spawn_teammate` 起的子会话没有经过它,所以**角色文件 frontmatter 里的 `maxToolCalls` 当前只对 `subagent_role` 路径生效**,对 teammate 子会话不起约束作用(2026-11-13 读码确认,注入段落代码里也没有预算逻辑)。以后要给 team 路径补预算,这是显式的缺口。
### 1.2 代码落点
注入实现整体在 `.dsh/plugins/dsh-plugin-subagent-roles/lib/index.js` **尾部**以标记注释 `// ---- Agent Teams role injection (team fork v7, ...)` 起始的段落里(含段落头的主机事实注释块);该文件前半是上游原有的目录/委派工具/预算/裁剪逻辑,与本篇无关。段落内关键函数(均在下文按名字锚定,本文不写行号):`roleIdFromText()` 解析标记、`findAgentBySession()` 按会话 id 找 agent、`applyToolPolicy()` 工具白名单降级、`installRoleOn()` 真正注入。整段注册包在插件的 `safely()` 里——注册期抛异常只失去注入能力,不会拖垮 profile 启动。
---
## 2. 注入时序:在 lead 会话侧接力
这节回答"persona 为什么能赶上子会话首轮"。关键结论:**不要等子会话把 prompt 吐出来**(那时首轮请求已经组装完),而是在 lead 会话上截获 spawn 调用、提前注册角色,等 `subagent/catalog` 事件把子 agent 身份送到手上再注入。
```mermaid
sequenceDiagram
participant L as lead会话事件流
participant P as 插件 session/event 观察者
participant C as 子会话事件流
L->>P: tool/call name=spawn_teammate (prompt 首行 @role)
P->>P: roleIdFromText 解析角色 id, 存入 pendingByLabel
L->>P: subagent/catalog 带 childId 与 label(=description)
P->>P: agents.get(childId) 取到子 agent 句柄
P->>C: systemPrompt.section 注入 persona 到 deployment:persona-prefix
P->>C: applyToolPolicy 应用工具白名单, 抛错则降级重试
Note over C: 以下才轮到子会话首轮, 注入已完成
Note over C: system/message 首轮请求组装, persona 已在其中
Note over C: user/message 里 @role 标记出现, 兜底路径凭去重集合跳过
```
三段接力(锚点都在 `lib/index.js` 尾部 `Agent Teams role injection` 段内的 `ctx.on('session/event', ...)` 单一观察者里):
1. **截获(lead 的 `tool/call` 事件)**:`data.name == "spawn_teammate"` 时取 `data.arguments`(对象或 JSON 串,两种都解析)里的 `{name, description, prompt}`。`roleIdFromText()` 只看 prompt **第一行**、且只在**前 200 字符**窗口内找——标记匹配模块级正则 `ROLE_MARKER_PATTERN`(`/^@role:([a-z0-9]+(?:-[a-z0-9]+)*)\s*$/m`:角色 id 只允许小写字母数字加连字符,行内 id 之后不允许再带其他词)。命中后以 `d:<description>` 和 `n:<name>` 两个键把 `{roleId, cwd, applied}` 存进 `pendingByLabel`。
2. **识身份(lead 的 `subagent/catalog` 事件)**:payload 带 `data.childId` 与 `data.label`;台架实证 **label 就是 spawn 的 `description`**(同文本),所以先按 `d:` 键查、再按 `n:` 键兜底。拿到 childId 后 `ctx.get('agents').get(childId)` 取子 agent 句柄;失败再试 `findAgentBySession('session-<childId>')`(`agents.list()` 里按 `a.id` / `a.session.header.id` / `a.session.id` 三种字段匹配)。还取不到就记一行 `not live yet`,留给 §5 兜底路径追注。
3. **注入(`installRoleOn()`)**:`agent.ctx.systemPrompt.section({ name: 'deployment:persona-prefix', order, text: persona })`——order 取该 agent 上 `getSectionOrder('DEPLOYMENT_PERSONA_PREFIX')`,拿不到回退 0,即落在宿主部署 persona 前缀区;persona 来自 `loader.loadSync(cwd)` 加载的角色文件(项目 `.dsh/roles/` 覆盖全局 `~/.dsh/roles/`,使用侧细节归 `.dsh/roles/README.md`)。随后 `applyToolPolicy()` 应用工具白名单(§4)。注入成功后把 `agent.id` 记入 `injectedAgents`、其会话 id 记入 `injectedSessions` 两个去重集合。
**时序为什么赶得上**:lead 的 `tool/call` 与 `subagent/catalog` 都发生在**子会话事件流开跑之前**。2026-11-13 探针实测:`spawn captured` → `catalog` 223 ms、`catalog` → `injected` 12 ms;代码注释口径 catalog 事件比子会话首轮组装早约 270 ms,探针取证口径注入早于首轮组装约 224 ms——两个口径同为"约 0.2~0.3 秒量级",结论一致:注入发生在子会话 `system/message`(首轮请求组装完成点)**之前**。
---
## 3. 宿主事件面事实(2026-11-13 台架快照)
这节是实现所依赖的主机行为清单。改插件前逐条对照;宿主升级后注入坏了,第一嫌疑就是这张表里的某条变了。
### 3.1 回调签名:session/event 是两参
`ctx.on('session/event', (sessionObject, event) => {...})`——**第一参是会话对象**(`header.id`、`header.cwd`、`eventsSnapshot` 即该会话既有事件列表的快照),**第二参才是事件本体** `{type, seq, time, data}`。把第一参当事件本体来读 `type` 是早期版本的失败根因(教训见 §6);插件从第一参取会话 id/cwd,从第二参取事件类型与载荷。
### 3.2 子会话的事件顺序
子会话从创建到首轮回复的事件流(台架观测,按 seq 递增):
```
subagent/descriptor → sandbox/mode → approval/policy → permission/preset
→ agent/inbox/spliced → turn/start → step/start
→ system/message(首轮请求已组装完毕)
→ user/message(spawn 的 prompt 在这里出现,含 @role: 首行)
→ request/header → assistant/message
```
关键约束:`system/message` 出现时首轮请求**已经组装完**,`user/message` 必然在它之后——**在子会话侧做任何基于 prompt 内容的注入都注定晚首轮一步**,这是 §2 整套 lead 侧接力的存在理由。
### 3.3 lead 会话上的两个关键事件
| 事件 | 载荷 | 台架实证口径 |
|---|---|---|
| `tool/call` | `data.name == "spawn_teammate"`;`data.arguments` 含 `{name, description, prompt}` | 工具参数在宿主提交调用时已成串,可直接拿到完整 prompt |
| `subagent/catalog` | `data.childId`(子 agent id)+ `data.label` | **label == spawn 的 description**;实测比子会话 `step/start` 早数百毫秒(量级见 §2 末段) |
lead 会话上也会看到 `team/member`、`tool/result` 等后续事件,注入不需要它们。
### 3.4 用到的宿主服务面
| 触点 | 用途 | 备注 |
|---|---|---|
| `ctx.get('agents')` 的 `get(id)` / `list()` | childId → agent 句柄 | `get` 直接命中;`list` 供按会话 id 兜底匹配 |
| `agent.ctx.systemPrompt.section({name, order, text})` | 注册 persona 段 | 名字固定 `deployment:persona-prefix`;order 对齐宿主 `DEPLOYMENT_PERSONA_PREFIX` 区 |
| `agent.ctx.tools.restrict(filter)` | 应用工具白名单 | **只校验全局工具名**,见 §4 |
---
## 4. 工具白名单降级
这节解释一个反直觉现象:把角色白名单整串交给 `restrict()` 会**整批抛错**,而且错得"无名无姓"。实现用"报错尾部反推全集再求交集"绕开。
**背景白话**:工具名册分两层。**全局工具**挂在宿主顶层名册上(本宿主如 `read` `edit` `write` `grep` `glob` `web_search`,完整名单见报错尾部);**作用域工具**由插件束(bundle)注册、只出现在特定 agent 的作用域里(本宿主主要是 OPS 插件的 `ops_bash` `ops_grep` `ops_inspect_local_file` `ops_discover_tools`,以及 `team_task_*` 协作工具)。`tools.restrict()` **只对全局层的名字表做校验**,白名单里混进任何一个它不认识的名字,整个调用直接抛错——不是逐个警告、更不是部分生效。
**降级算法**(`applyToolPolicy()`):
1. 先原样整批 `restrict(filter)`;成功即结束。
2. 失败则再调一次只为**捕获错误文本**,用正则取尾部 `known global tools: ...`(宿主报错自带全部已知全局名,这是唯一能拿到的"合法名字全集")。
3. 白名单与该全集**求交集**后重试。filter 形状认三种:裸数组、`.allow` 数组、`.allowedTools` 数组(角色 frontmatter 的 `tools:` 简写经 loader 归一为 `{allow: [...]}`,所以实测走 `.allow` 分支);`deny` 字段原样透传不进交集。
4. 终态写诊断日志:`tool policy applied via reduced list` / `tool policy still failed after reduce` / `tool policy skipped (unparseable error/unknown filter shape)` / `tool policy reduce threw: ...`。
**实测效果**(台架确认为理想行为):交集后的**全局名白名单正常生效**(只读角色确实拿不到 `edit`/`write`);`ops_*`、`team_task_*` 等作用域工具**不在 restrict 的约束面内、保持可用**——restrict 本来就够不着作用域层,白名单里写没写它们都不改变这一点。因此角色文件的 `tools` 列表不需要按宿主机器裁剪。
真实报错样本(2026-11-13 台架 [示例],旧探针记录格式;`known global tools:` 尾部形态就是降级解析器的依赖面,列表过长处以省略号代):
```text
tools.restrict() names unknown global tools "ops_bash", "ops_inspect_local_file",
"ops_discover_tools", "team_task_get", "team_task_list", "team_task_update";
known global tools: acp_cache, acp_status, compress, decompress, edit, glob, grep,
read, ..., send_message, skill, subagent, ..., todo_write, web_fetch, web_search,
..., working_directory, write
```
---
## 5. 兜底路径与去重
这节讲主路径断链时的保险丝,以及怎么识别"只有兜底在起作用"。
**机制**:同一个事件观察者对**子会话**事件也解析标记——`deepText()` 从事件载荷的嵌套结构(`content`/`parts`/`text`,再深一层 `message`/`data`)里提取文本,命中首行 `@role:` 就记入 `pendingMarker`(会话 id → 角色 id),并用 `findAgentBySession()` 找 agent 注入;agent 暂时找不到就等该会话的下一个事件再试。因为 `user/message` 晚于首轮的 `system/message`,兜底注入的 persona **从第 2 步起才可见**。
**去重**:`injectedAgents`(按 `agent.id`)与 `injectedSessions`(按会话 id)两个集合。主路径注入成功后,子会话后续事件再带标记也会被跳过——防止同一 persona 被重复注册成多个同名 section。已 `injected` 但 agent 迟到的场景由 `pendingMarker` 分支追赶补注。
**台架实证"兜底必然晚一步"**(2026-11-13 早轮旧格式探针日志 [示例],该轮还没有主路径):
```text
[2026-11-13T12:22:10.668Z] evt#9 s=ce3bf8 type=system/message ← 首轮请求已组装
[2026-11-13T12:22:10.669Z] evt#10 s=ce3bf8 type=user/message head=@role:doc-writer ...
[2026-11-13T12:22:10.672Z] injected role "doc-writer" (4739 chars) into agent b4194190-...-db66ffce3bf8
```
注入落在首轮组装之后 4 ms——persona 从第 2 步才可见,这正是当初必须做 lead 侧接力的原因。
**识别法**:一次注入在诊断日志里若只出现 `injected role` 一行、前面没有 `spawn captured` 与 `catalog` 两行 = 主路径断链、兜底在干活——按 §8 修主路径,不要满足于"反正注入了"。
**一处未深究的观察**:兜底路径的事件匹配不分会话归属,lead 自己会话里若出现首行 `@role:` 的文本理论上也进该路径,但台架多轮实测**没有发生向 lead 注入**(诊断日志无此类行;机制未深究,可能与 `agents` 服务不含顶层交互会话有关)。不要把标记写进给 Lead 的消息里,此行为不可依赖。
---
## 6. 失败模式史:四条死路各换一条教训
这节记录定稿"纯事件观察"(段落头自称 v7)之前台架试错过的四条 hook 路径。升级宿主后想加新 hook,先读这节,别重蹈覆辙。
| 尝试 | 主机实际行为 | 教训 |
|---|---|---|
| 包装 `ctx.subagents.startActivation`,指望子会话启动时进 wrapper | wrapper 从未被调用 | **先实证功能真实入口再埋点**:`spawn_teammate` 根本不走 subagent 激活服务,宿主内部走 Agent Teams 自己的 spawn 路径(台架取证指向 `ctx.agentTeams.spawnTeammate`——内部实现名、非公开 API,不要依赖其存在,只作"它不在 subagents 面上"的指认) |
| 直接给 ctx 拿到的服务对象属性赋值包 wrapper | 赋值"成功",但所有调用从不进 wrapper | **服务面改写在宿主侧走不通**:宿主交给插件的是它包的一层代理(白话:插件读写这份"对象"只是在对复印件写字,宿主内部真对象纹丝不动,属性写被静默吞掉)——此后定案:插件只做纯事件观察,不改任何服务 |
| 在 `agent/created` 事件里从 `session.snapshotEvents` 读 prompt | 该 API 不存在;且该事件载荷的 header 里没有 name/teamId | **agent 创建事件自带的信息不足以识身份**:既拿不到 prompt,也无法把 created 关联到具体 teammate——关联只能靠 lead 侧 `subagent/catalog` 的 childId(§3.3) |
| 只观察子会话 `user/message` 的标记 | 注入能成功,但晚首轮一步 | **时序仗只能在 lead 侧打**:子会话事件流开跑时首轮请求已在组装(§3.2),这条降级为 §5 兜底 |
---
## 7. 部署与生效链
这节回答"我改了插件代码,何时生效、怎么确认生效"。换机复原时按本节顺序搭链。
```
仓库 fork(唯一事实源,入库)
.dsh/plugins/dsh-plugin-subagent-roles/
│ pnpm file: 依赖 = 一次性拷贝(不是链接)
▼
web profile 运行时实际读的那份
~/.dsh/profiles/web/node_modules/dsh-plugin-subagent-roles/
▲
│ 重启 dsh web 进程才重新加载
核心树(@deepseek-ai/dsh 安装目录)——没有该插件副本,属正常
```
- **拷贝而非链接,三处证据钉死**:profile 的 `package.json` dependencies 里该条目写 `"dsh-plugin-subagent-roles": "file:<仓库插件目录绝对路径>"`;同目录 `pnpm-lock.yaml` 的 specifier 同为该 `file:` 路径;`node_modules/` 下对应条目经 `ls -la` 确认是**真实目录不是符号链接**。后果:**改仓库里的插件文件不会自动反映到运行时**,每次改完必须手工同步。
- **手工同步 + 重启(台架实走路径)**,改 `lib/index.js` 为例:
```bash
cp "<repo>/.dsh/plugins/dsh-plugin-subagent-roles/lib/index.js" \
"$HOME/.dsh/profiles/web/node_modules/dsh-plugin-subagent-roles/lib/index.js"
# 回读确认(应能 grep 到段落头)
grep -c "Agent Teams role injection" \
"$HOME/.dsh/profiles/web/node_modules/dsh-plugin-subagent-roles/lib/index.js"
```
然后重启该 profile 的宿主进程(`dsh web`)。改了 `lib/` 其他文件同理一并拷贝。
- **生效判据(最硬的一条)**:重启后 `~/.dsh/role-inject-diag.log` 尾部出现新的一行
`event-path injection v7 registered (spawn-capture pre-step + tool-restrict fallback)`——
没有它,后面一切免谈(插件没装载/拷错文件/没重启)。
- **一次性开发工具(存在即可,不要求长期保留)**:注入段早期不是手改 index.js,而是维护在独立段落文件里由拼接脚本幂等写入——`ci-artifacts/role_inject_section.txt`(v7 段全文)与 `ci-artifacts/splice_role_inject.py`(把该段拼进 `lib/index.js`:缺 `node:fs`/`node:os` 两行 import 才插、缺模块级 `ROLE_MARKER_PATTERN` 常量才插、从 `// ---- Agent Teams role injection` 标记起替换到文件末尾)。`ci-artifacts/` 在 gitignore 内,工具丢了不影响复原——照本篇读码重建,或直接编辑 fork 尾部段落。
---
## 8. 排障速查
这节给"症状 → 查哪一行 → 结论"的对照表。总原则:**先查诊断日志,再动插件**;宿主升级后先验证 §3 事件面快照哪条变了,不要凭记忆改代码。
### 8.1 诊断日志本体
插件的 `ctx.logger` 在会话侧不可见,全部取证靠 `diag()` 直接 `appendFileSync` 到 **`~/.dsh/role-inject-diag.log`**(= `os.homedir()/.dsh/`,Windows 本机形如 `C:/Users/<用户名>/.dsh/`)。每行 `[ISO时间戳(UTC)] 消息`;文件无上限增长,太大直接截断/删除(只损失历史取证)。注意 spawn 消息里的时间是 UTC,比对本地时刻要 +8。
一次健康注入 = 4 行(2026-11-13 实测原文):
```text
[2026-11-13T12:27:17.104Z] spawn captured: role=doc-writer name=role-probe6 desc=六次注入探针
[2026-11-13T12:27:17.327Z] catalog: label=六次注入探针 childId=fda63569-c689-4102-afad-d7322dbecc4d matched=true
[2026-11-13T12:27:17.338Z] tool policy applied via reduced list
[2026-11-13T12:27:17.339Z] injected role "doc-writer" (4739 chars) into agent fda63569-c689-4102-afad-d7322dbecc4d (pre=x)
```
行尾 `(pre=x)` 只是诊断信息:注入时观察者**已经**在该子会话上数到过事件(`x`)与否(`?`),不代表成败。
### 8.2 症状对照表
排查顺序按注入生命周期走:装载行 → 截获行 → 识身份行 → 注入行 → 工具策略行,**第一处断在哪一环,问题就在哪一环**。
| 症状(diag 所见) | 结论 | 动作 |
|---|---|---|
| 重启后没有新的 `event-path injection v7 registered` | 插件没装载:profile 没重启 / 同步拷错或没拷 | 走 §7 生效链重做一遍 |
| 有 registered,但 spawn 后无 `spawn captured` | 标记没被认出:不在 prompt 第一行 / 角色 id 含大写或非法字符 / 首行有额外词 / 标记落在前 200 字符窗口外 / 宿主 `tool/call` 事件面变了 | 先核对写法(正则见 §2 步骤 1),再对照 §3 验事件名 |
| `catalog: ... matched=false`(伴随你的 spawn) | catalog 的 label 与 `pendingByLabel` 对不上:多半 `tool/call` 那环没截获(回上一行查) | 若同时有 `spawn captured`,说明宿主 label 不再等于 description——事件面变了 |
| 后台任务频出 `catalog: ... matched=false`(如 label=Mnemon idle checkpoint review) | **无害噪声**:非本插件发起的 catalog 事件,永远不匹配 | 忽略 |
| `catalog: child <id> not live yet` | `agents.get` 与 `list` 兜底都没拿到子 agent | 观察是否随后出现兜底 `injected` 行;频发则查 `agents` 服务面变化 |
| `role "<id>" not found (cwd=...)` | 角色文件缺失/在别的目录树/frontmatter 解析失败 | 查 `<cwd>/.dsh/roles/<id>.md` 与 `~/.dsh/roles/<id>.md`;使用侧格式归 `.dsh/roles/README.md` |
| `install skipped: no systemPrompt.section on agent ...` | 宿主 agent 句柄形状变了(`ctx.systemPrompt` 没了或不是 section 注册面) | 对照 §3.4 重新取证服务面 |
| `tool policy skipped (unparseable error)` | `restrict()` 报错格式变了,降级解析器抓瞎 | persona 不受影响(先注入后策略);按新报错格式修 `applyToolPolicy()` 的尾部正则 |
| `tool policy still failed after reduce` | 交集后的纯全局名名单仍被拒 | 查角色 `tools` 字段形状(认 数组/`.allow`/`.allowedTools` 三种)与宿主 restrict 契约 |
| 有 `injected role` 行、无 `spawn captured`/`catalog` 行 | 主路径断链,兜底在干活(persona 第 2 步才可见) | 按 §5 识别法确认后回查截获/识身份两环 |
| 有 `injected role` 行但子 agent 自报没有 persona | section 名字/order 与宿主装配面冲突,或注入到的不是被请求的那个会话对象 | 核对 `deployment:persona-prefix` 是否仍被宿主采纳;用 childId 与会话 id 对应关系核"注错对象" |
| `session/event handler threw: ...` | 某事件载荷形状意外,被总 try 兜住 | 看尾部异常文本定位是哪条事件;修 `deepText`/字段假设 |
---
## 9. 验收证据(2026-11-13 探针)
这节钉死最近一轮端到端验收的实况,日后升级后拿它当对照基线。
- **主路径 4 行**:即 §8.1 引用的 12:27 轮——`spawn captured`(12:27:17.104) → `catalog matched=true childId=fda63569`(12:27:17.327) → `tool policy applied via reduced list` → `injected role "doc-writer" (4739 chars)`(12:27:17.339);注入早于子会话首轮组装约 224 ms(探针取证口径)。
- **persona 真实可见**:探针子会话首轮回复自报系统提示词中存在角色 persona,并准确引用 `.dsh/roles/doc-writer.md` persona 正文首句原文(「你是 STM32F4-Base 项目的**技术文档写手**」)——证明注入进的是它本轮在用的那份系统提示词,不是旁路。
- **可复现性**:同日 12:31 与 12:49 两轮同链复跑均 4 行齐(后一轮即本篇写作会话 `doc-role-inject` 自己的出生记录,childId 5605001c)。
- **降级路径实证**:`tool policy applied via reduced list` 行本身 + §4 报错样本;doc-writer 白名单中的 `ops_bash` 等被交集剔除后策略照常生效、这些作用域工具实测仍可用。
> 自查本篇链接与围栏的本地命令(门禁时机是提交期,全量留给 pre-commit):
> `& '.venv\Scripts\python.exe' scripts/check_doc_links.py docs/环境与部署/子Agent角色自动注入实现说明.md` 与 `scripts/check_mermaid.py` 同法;两闸各自的详解 → `docs/门禁说明/doc-links.md`、`docs/门禁说明/mermaid-syntax.md`,章节引用口径 → `docs/文档规范.md` §3.5。
+38 -8
View File
@@ -299,16 +299,21 @@ flowchart TD
扫频最坏 0.317 Hz(40 拍算术平均)、0.197 Hz(60 拍复数和的 Kay 法——交叉项的和角不随
`K²` 衰减,**勿再照这条路优化**)。改 `K_eff = round(8·Fs / (2·f̂))`(45~55 Hz 段 73~89 拍)
使窗长恰为纹波周期的整数倍 → 纹波相消,扫频最坏降到 **0.0101 Hz**。
钳位区间 [72, 96] 拍能覆盖的只有 41.7~55.6 Hz,带外读数饱和钉沿——实证与裁定见 §7.3。
- **假值钳制带取 35~65 Hz,而不是系统合理带**:单拍相位差本身带 ±5 Hz 纹波,钳制带贴边会
**非对称丢弃纹波半周** → 均值偏置(实测 45 Hz 点 +0.16 Hz,已修)。35~65 Hz 宽于系统合理带
40.5~60.5 Hz(参考 `CN_HERTZ_SOFTFR_MIN/MAX`,见
`docs/算法/继电保护功能复现规划.md` §10.1),仍能拦住建"窗滑 2 拍"造成的 ≈2f(90~110 Hz)
与相位跳变假值;系统合理带由 OF / UF 的**定值范围**在消费侧把关,不在这里重复把关。
- **幅值门** `FREQ_AMP_MIN = 50.0`(2026-10-25 下游常数按工程量重定后语义为**二次值 V**:50 V = 0.5×Un,
数值沿旧 50.0 属巧合,量纲依据 `App/algo/prot_set.c` 头部量纲基准块):低于门视为
无有效波形(浮空 / PT 断线时 rms ≈ 0,相位无意义)→ 停止更新、`valid` 清零、读数保持,
- **幅值门** `FREQ_AMP_MIN = 5.0`(语义为**二次值 V**,量纲依据 `App/algo/prot_set.c` 头部量纲基准块;
2026-10-25 下游常数按工程量重定为 50 V = 0.5×Un,2026-11-13 用户裁定放宽到 5 V = 0.05×Un,
让台架低端注入与低负荷工况也能测频):低于门视为
无有效波形(浮空 / PT 断线时 rms ≈ 0,相位无意义;浮空实测二次域几十 mV,与门仍差两个数量级)
→ 停止更新、`valid` 清零、读数保持,
台架浮空因此**不发散**;信号刚消失的过渡段(≤20 ms 残窗)读数漂移有界 ≤0.3 Hz。
- **源优先级 UA(C02) → IA(C01)**,按名前缀解析、每拍动态回退(台架只注电流也能测频);
- **源优先级 UA(C02) → IA(C01)**,按名前缀解析、每拍动态回退(工程量口径下 IA 候选同样要
达到幅值门数值,二次电流 0.5~2 A 够不到 5——「只注电流测频」在码值域时代成立、现在不成立,
电流域门限另裁);
**源切换即废相位基准重建**——UA 与 IA 之间没有共同相位基准(双转换器无共同采样触发),跨源相位差无意义。
### 7.2 精度判据(J1~J6,与 `test/host/freq/freq_test.c` 文件头同口径)
@@ -330,6 +335,31 @@ UA 无幅值落 IA → 0.0020 Hz。
**1000 ms** → 测频响应占保护总动作时间 **< 10 %**,暂态假值另有 J6 兜底。**若将来要压响应时间,
必须同时重估精度**:缩短窗就是把精度退回 0.2~0.3 Hz 档,不是免费的。
### 7.3 准确测频带与带外饱和(2026-11-13 台架实证;用户裁定:不改参数,只登记边界)
**什么时候读本节**:有人注入的非 50 Hz 频率读数不跟随(典型:继保仪 60 Hz、屏幕钉 ~55)时,先对照本节确认这是**带外设计边界**而不是链路故障——显示 / 上行链全程无钳位,板端 `freq(hb)` 心跳自己就报钉住值。
四个"带"先分清,互相不能派生:
| 概念 | 数值 | 含义 |
|---|---|---|
| 假值钳制带(`FREQ_INST_MIN_HZ`/`MAX`) | 35~65 Hz | **逐拍**丢弃粗大假值(窗滑 2 拍 ≈2f、相位跳变),不是可测带 |
| 窗环钳位覆盖 | **41.7~55.6 Hz** | `K_eff = round(8·Fs/(2·f̂))` 钳在 `[FREQ_AVG_MIN=72, FREQ_AVG_TAPS=96]` 拍;只有此带内窗长能取到泄漏纹波周期 `Fs/(2f)` 的**整数倍** |
| 精度判据带(J1) | 45~55 Hz | 主机扫频判据 ≤0.02 Hz 的验证范围 |
| 实测准确区(2026-11-13 扫频) | **42~57 Hz** | 带内读数误差 ≤0.07 Hz |
- **带外机理**:真频越出窗环覆盖 → 钳位生效 → 窗不再是纹波周期整数倍(60 Hz 需 4000/60≈66.7 拍,钳到 72 拍 = 8.6 个纹波周期)→ 负序泄漏纹波对消失效、均值偏置;`fq_read_freq()` 的 f̂ 自反馈再把读数锁在带沿钳位生效的不动点上——高侧钉 ~55(4000/72 = 55.56)、低侧钉 ~43.5(4000/96 = 41.7 一带)。
- **实证(同一份生产源 `App/algo/freq.c` 离线扫频,每点 400 拍收敛)**:
| 真频 Hz | 42 | 45 | 50 | 55 | 56 | 57 | 58 | 60 | 62 | 65 | 70 | 80 | 30 | 35 | 40 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 读数 Hz | 41.999 | 44.995 | 50.000 | 55.009 | 55.959 | 56.938 | 56.146 | **54.76** | 55.33 | 54.83 | 54.22 | 54.43 | 43.59 | 43.95 | 43.83 |
台架旁证:继保仪注 60 Hz / ≈9.3 V(`meas: ua_mv=9308.7`),板端 `freq(hb): f=5531 fv=1`——与离线扫频 54.76 一致(差值属台架噪声与相位工作点,机制同一)。
- **⚠ `freq_valid()` 带外仍为 1**:valid 说的是"新鲜 + 过幅值门",**不代表数值可信**。带外只有"偏频方向"可信(钉 ~55 ⇒ 偏高、钉 ~43.5 ⇒ 偏低),数值本身不得当遥测消费。
- **对保护的影响(按 `prot_set.c` 定值域逐条对)**:OF 定值域 50~55(默认 51)——真 60 Hz 时读数 ~55 > 51 照常出口;**唯一定在域顶 55.0 时带外深过频可能不出口**(读数到不了 55 以上)。UF 定值域 45~50——低侧钉 ~43.5 恒低于定值,方向保守不漏判。
- **裁定(用户 2026-11-13)**:国内 50 Hz 系统,60 Hz 只是继保仪带外核验场景,钉在保守侧可接受,**参数不改**。若将来要支持带外扩宽:高侧单把 `FREQ_AVG_MIN` 72→62 即可覆盖到 ~64.5 Hz(零 RAM、窗更短),全带 35~65 Hz 需环容量 96→115(RAM +95 B)且 J4 阶跃判据要复测(最长窗 115 ms);改前先在主机侧把 J1 判据带扩跑。
- **复现方法**(取证程序为一次性、放 gitignored 的 `ci-artifacts/` 不入库,故此处给自含配方):照抄 `test/host/freq/freq_test.c` 的驱动骨架(`#include` 纳入 `sample_ring.c` + `phasor.c` + `freq.c`,`drive_beat` 一拍整帧、纯音 amp=1000 码、`chain_reset` 每点重置),每点 400 拍后读 `freq_get()`,在 30~80 Hz 逐点列表即为上表。若将来扩带,此扫描应升格为带外回归判据(钉位方向 + 带内准确性)再进 `test/host/freq/`。
---
## 8. 保护判据输入:保护定值模型 `prot_set.c`
@@ -681,7 +711,7 @@ V0.1 恒命中 UB/UC/IB/IC/IN,位图 `0xE6`),它表示"这路不能测交
|---|---|---|
| 基波有效值 | ✅ 引脚层过(±2 % 内,三档幅值两点复现) | 台架幅值轮 + 波形轮(基波分量口径)实证;**二次值这一档**:UA / IA 已由继保仪标定实测落盘(2026-11-13 用 5 点最小二乘重标得 `r = 555.14` / `29.61`,`docs/硬件平台/ADC通道映射说明.md` §7.5;2026-11-12 两点算法那一轮是 `554.69` / `29.63`,历史值见 `docs/硬件平台/ADC通道映射说明.md` §7.3),旧结论"出厂变比是单点粗测(5 V 注入只有 33 码、1 LSB = 3 %、真值区间约 909~1111)"仍成立且已在 2026-11-12 被实测定量:实测 554.69 说明那次粗测接线分压把比值放大了一倍(`docs/硬件平台/ADC通道映射说明.md` §7.3);2026-11-13 又用**非标定的 10 V / 100 V** 做了区间外核验(+0.42 % / −0.067 %,`docs/硬件平台/ADC通道映射说明.md` §7.4);**精度等级**仍待标准源送检 |
| 相位 | ✅ 同片内过(录波档 ≤2° 判据) | 同段 UA/UN 两路实测(0° 与 90° 两工况);**跨转换器仍禁比**——两片转换器无共同采样触发,是 V0.1 硬件边界(`docs/硬件平台/ADC通道映射说明.md` §7 通道归属行),不是待修缺陷 |
| 频率 | ✅ 「如实报不可测」成立(这就是现状判据) | 含满幅注入段全不可测、UI 恒 `--`;**45~55 Hz 精度仍未验**——要验须降 `FREQ_AMP_MIN` 单跑一轮 |
| 频率 | ✅ 幅值门已放宽到 5 V(2026-11-13 用户裁定),二次注入 ≥5 V 即可出数显示 | 「如实报不可测」是幅值门 50 V 时代(引脚域 / 撤注入后)的现状判据,浮空仍不发散;**测频带 2026-11-13 已实测定量**:42~57 Hz 读数准确、带外饱和钉 ~55 / ~43.5(60 Hz 注入板端报 55.31 实证),用户裁定参数不改,机理、影响与扩带路径全部登记在 §7.3 |
| 削顶 / 品质 | ✅ DC −1.00 V 档锁存 + sticky 实证;2.0 Hz 低频档与复位复核经用户裁定 2026-11-04 关闭 | `[ERR] rail: edge=0x2 sticky=0x2` 唯一一条、撤注恒持 |
| 谐波 C04 / C05 | 🟡 选择性轨已证(单注 100/250 Hz 分别只进对应桶、纯基波两桶为 0);**仍无消费者、无处上送** | 250 Hz→`Ia5` 行 −4.4 % **已按板端前端真实衰减关闭(2026-11-04)**:IA 前端差分滤波 C35 = 100 nF + R19/R21 各 1 k 构成一阶低通 fc ≈ 796 Hz,板端读数忠实、工频不受影响;谐波产品化需板改(模型与数据 `docs/硬件平台/ADC通道映射说明.md` §8.2);这两根轨归谁用,见 `docs/算法/继电保护功能复现规划.md` 的「遥测谐波轨(可选)」 |
| 保护判据 | 🔒 闭锁中 | §13.4 |
@@ -710,7 +740,7 @@ V0.1 恒命中 UB/UC/IB/IC/IN,位图 `0xE6`),它表示"这路不能测交
| # | 位置 | 现值与语义 | 状态 |
|---|---|---|---|
| 1 | `App/algo/freq.h:62` | `FREQ_AMP_MIN 50.0f`——测频幅值门,已按**二次值 V 口径**重定(50 V = 0.5×Un,与 `prot_set.c` 头部量纲基准同源) | ✅ 已重定;⚠ **副作用**:`eng=0` 期间帧值 ≤5 V < 50 V → 本门**恒不过** → `freq_valid()` 恒 0(`freq.h:58-60` 自陈)。这条直接决定测频轮怎么读、为什么那一轮的"过判据"是"如实报不可测" |
| 1 | `App/algo/freq.h` 的 `FREQ_AMP_MIN` | 测频幅值门,**二次值 V 口径**(与 `prot_set.c` 头部量纲基准同源):2026-10-25 重定为 50 V = 0.5×Un;**2026-11-13 用户裁定放宽到 5 V = 0.05×Un**(台架低端注入也能测频,浮空噪声与门仍差两个数量级) | ✅ 已放宽;`eng=0` 过渡期「本门恒不过 → 如实报不可测」是当时那一轮测频轮的读法(引脚域满幅 <5 V,历史叙述);标定完成后 UA ≥5 V 即出数 |
| 2 | `App/protocol/iec104/iec104_pts.c:69-80` | `k_yc[].deadband` 已按点重定:电压 0.5 V、电流 0.005 A、频率 0.5 Hz(真源在 `run_par`,UI 可改) | ✅ 已重定;④ 变比到手后需再复核一次量纲 |
| 3 | `App/algo/prot_set.c:25-70` | `g_prot_set_meta[]` 定值 = 二次值 A / V(基准 100 V / 1 A):OC1 10 A、OV 120 V、UV 20 V、3U0 15 V… | 🟡 口径对,但**标定前无法在板上验真**;精度目标定了之后连同 `min`/`max` 复核 |
@@ -799,7 +829,7 @@ typedef struct {
| **幅值轮** | 基波幅值计量 | UA(R31 / `AIN1`)注 Vpp 2.828 V(= 峰值 ±1.414 V)、50.000 Hz 正弦 | `meas:` 的 `ua_mv` | 读数 = 万用表 AC 档读数,偏差 ≤2 %(与静态同档)。⚠ 别用 `phasor(hb)` 行比幅值——那是 ×0.1 定点,注 1 V 只剩 10 格 |
| **波形轮** | 读数是否只跟基波走 | 同幅值改方波、三角 | 同上 | 期望值 = **基波分量有效值**(同峰值下的基波 / 峰值比:方波 ≈ **0.90**,闭式 4/π ÷ √2 = 0.900、20 点整周期 DFT 实算 0.904;三角 ≈ **0.573**,闭式 8/π² ÷ √2 = 0.573、实算 0.578),**不是**真有效值(方波 = 1.000、三角 = 0.583 峰值);偏差 ≤2 %。算术与逐波形对照表见 `docs/测试说明/专项验证/ADC前端注入验证流程.md` §8.5 的波形卡。顺带把"这台仪表读的是基波"这条口径钉进文档——用户拿真有效值表去对会以为差了 10 % |
| **相位轮** | 相位 | UA 与 UN 两路**同片**(都走 TPAFE)注同幅、同相 | **录波档**:录波段文件 `test/host/wave_read.py fft`(浮点重算基波相位;段内表序 `UA` = 0、`UN` = 3);**心跳档**:`phasor(hb)` 各行 `Re=` / `Im=` | **录波档**:同段内两路相位差 ≤2°,把发生器相位改 90° 后差值跟到 ≈90°。**心跳档只判"跟随"**(差值落进 80°~100°)——`phasor` 串口行是 ×0.1 定点,Vpp 2.828 V(板上读数 ≈1000 mV)时每格 0.1 V,角度量化最坏 ≈ atan(0.1 / 0.707) ≈ 8°,判不动 2°。⚠ 段与发生器**无绝对相位锁定**(触发瞬间钉在段内点 159),只能比**同段内两路**的相对差,跨段比相位或拿发生器相位当绝对基准都不成立。**只许比同片内**——跨转换器两片没有共同采样触发,比相位无意义(见 `docs/硬件平台/ADC通道映射说明.md` §7 通道归属行),STM32 那五路还有 1 拍乒乓滞后 |
| **测频轮** | 测频 | 同幅值轮的 Vpp 2.828 V / 50 Hz | `freq(hb):` | **预期就是"不出数"**(幅值门 50 V 在引脚域恒不过,§13.7 #1)→ 这条的判据是"如实报不可测"(`freq_valid=0`、UI `Hz` 显示 `--`),**不是**报出一个错误的 50.00。想真验 45~55 Hz 精度(PC 侧判据 0.02 Hz)须临时把 `FREQ_AMP_MIN` 降到引脚域量级(如 0.5 V)单跑一轮(动产品头文件,跑完必须还原并重跑 `/keil-build`) |
| **测频轮** | 测频 | 同幅值轮的 Vpp 2.828 V / 50 Hz | `freq(hb):` | **预期就是"不出数"**(幅值门 50 V 在引脚域恒不过,§13.7 #1)→ 这条的判据是"如实报不可测"(`freq_valid=0`、UI `Hz` 显示 `--`),**不是**报出一个错误的 50.00。想真验 45~55 Hz 精度(PC 侧判据 0.02 Hz)须临时把 `FREQ_AMP_MIN` 降到引脚域量级(如 0.5 V)单跑一轮(动产品头文件,跑完必须还原并重跑 `/keil-build`)。**2026-11-13 更新**:幅值门已由用户裁定**永久**放宽到 5 V(二次域,§13.7 #1),标定完成后继保仪注 ≥5 V / 45~55 Hz 即可直接出数复测,不再需要临时改门。**本轮当日已跑**:≥5 V 出数正常;60 Hz 注入暴露带外钉 ~55 现象,实测定量与「参数不改」裁定登记在 §7.3(60 Hz 属带外,判据仍只覆盖 45~55 Hz) |
| **削顶轮** | 削顶与品质路由(**负向用例**) | 给 UB(R52 / `AIN2`)注 **DC −1.00 V 并保持 ≥5 s**;或 Vpp 2.828 V 而频率 **2.0 Hz** 的正弦——两档都能造出"连续贴同一侧轨带"。⚠ **注 50 Hz 同幅正弦造不出锁存,且这是预期而非故障**:1 kHz 下一个周波只 20 点、正半周必落中带,而 `adc_health_rail_watch()` 任何一拍回中带就把计数清零(`App/algo/adc_health.c`),连续贴底最长 ≈ 209/360 × 20 ≈ 11 拍 < 20。TPAFE 侧在 ±2 V 判据上限内造不出这条(轨带 ±32112 码 ≈ ±4.9 V),只有 STM32 那五路能做 | `cal:` 行的 `rail=` / `edge=`;`rail: edge=…` 的 `[ERR]` 行;录波档另加 `wave_read.py shape` 的三把尺子(`--strict` / 再加 `--clip-superset` / 不带),逐条配法见 `docs/测试说明/专项验证/ADC前端注入验证流程.md` §8.5 的削顶卡 | 负半周钉下轨 → **连续 20 拍同侧后该路贴轨位锁存**(`ADC_RAIL_N_RUN = 20`,`App/algo/adc_health.h:174`)、`edge` 起沿。锁存另有**释放沿前提**:该通道此前须出现过中带样本 → **先上电见基线摆动、再开发生器输出**,开机就注会测出假不通过。撤掉注入后 `rail` 仍非 0 是 sticky 语义(只有 `adc_health_rail_init()` / 复位清)——**复位后仍不清才是 bug**。⚠ 这条会**故意造出 `[ERR]`**,而 `/serial-boot-test` 的 forbidden 标记含 `[ERR]` → 人工判读、排在最后单独一轮,且这一轮的结论不按 `/serial-boot-test` 判 |
| **谐波轮** | 谐波轨 | 基波 + 20 % 二次 + 20 % 五次(IA 上注)。⚠ 峰值预算:Vpp 2.828 V 基波 + 两个 20 % 分量的最坏对齐峰值 ≈ 1.98 V 已顶 ±2 V 红线 → 基波用 **Vpp 2.263 V**、二次与五次各 Vpp 0.453 V | **录波档**:段文件 `test/host/wave_read.py fft --ch 4 --all`(`IA` = 段内表序 4,直接给基波与 2 / 5 次谐波有效值,单位 mV);**旁证**:UI 交流量页 `Ia2` / `Ia5`(1 mV)与 `phasor(hb) C04-IA-F2` / `C05-IA-F5`(×0.1 定点,只证这根轨不是恒 0) | 谐波轨读数 = 注入分量(**±2 %,走录波档**;心跳档判不动:读数 0.160 V 在 ×0.1 串口行上只有 1~2 格)。⚠ 这两根轨**目前没有任何下游消费者**(`prot_elm.c` 无引用、`meas` 只算 `harm=1`),所以谐波轮只证"算得对",**不证"有人用"**——接到哪、给谁用,归 `docs/算法/继电保护功能复现规划.md` 的「遥测谐波轨(可选)」 |
@@ -848,7 +878,7 @@ typedef struct {
|---|---|---|
| **精度目标** | 0.5 级 / 1 级 | 决定标定的判据,否则"误差 ≤ 目标精度"这条无法判定。保护判据本身只需 3~5 %,0.5 级是计量口径 |
| **端子 / 子板变比从哪来** | ① 有原理图(直接算)② 只有实物(万用表同时量端子与引脚)③ 都没有 | 决定变比登记的**可信度**(注意:量纲已不依赖它——出厂默认变比已按台架实测反算写进 `[adc_cal_sec]`,`eng` 能升 1;缺的是精度)。2026-11-11 用继保仪单点注入反算得 1000.0 / 29.4118,推翻了原先按子板参数算定的 606.06(详见 `docs/硬件平台/ADC通道映射说明.md` §7.2);**2026-11-12 那轮两点标定又把 1000.0 推翻**——接线分压使 PT 一次侧只拿到一半电压,实测 UA = 554.69,与参数算定值 606.06 只差 8.5 %(`docs/硬件平台/ADC通道映射说明.md` §7.3)|
| **`eng = 0` 期间测频幅值门怎么处置** | 保持恒失效(现状,UI 恒 `--`)/ 台架临时把 `FREQ_AMP_MIN` 降到引脚域量级跑一轮再还原 / 换成相对量门限 | 建议**常态保持恒失效 + 需要时临时降门跑一轮**;换相对量门限要先重新论证,别在过渡期改。临时降门会改产品头文件,收尾必须还原到 `git diff` 为空并重跑构建 |
| **`eng = 0` 期间测频幅值门怎么处置** | 保持恒失效(现状,UI 恒 `--`)/ 台架临时把 `FREQ_AMP_MIN` 降到引脚域量级跑一轮再还原 / 换成相对量门限 | 当时建议**常态保持恒失效 + 需要时临时降门跑一轮**;换相对量门限要先重新论证,别在过渡期改。临时降门会改产品头文件,收尾必须还原到 `git diff` 为空并重跑构建。**后续落定(2026-11-13)**:标定完成后用户裁定把门**永久**定为 5 V = 0.05×Un(既非保持 50 V 也非临时降门),过渡期问题随之消解 |
| **标定点数与分布怎么定** | 3 点(算法下限)~ 8 点(算法上限)/ **5 点(推荐)** | 点数与位置直接决定拟合结果对不对:少于 3 点或超过 8 点都无法解(算法直接拒绝);点挤在一小段里,斜率被 1/跨度放大成噪声;不覆盖端点就只能外推,区间边缘没有锚点。推荐 **5 点铺在量程端点与中点**(电压 5 / 10 / 25 / 50 / 100 V,电流 0.5 / 1 / 1.25 / 1.5 / 2 A),可继保仪能力调整但必须满足「拉开、覆盖端点」。执行细则 → `docs/测试说明/专项验证/ADC多点标定执行流程.md` §1 |
**送检顺序不可倒过来**:产品构建当前要求 UA/UN/IA 的标定 `VALID` 全部置位才走保护判据。