Files
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

540 lines
64 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: workbuddy-extension-surface
description: WorkBuddy 宿主的**扩展面清单**与「外部功能/插件能否迁到 WorkBuddy」的评估法,含**「直接给另一个会话派活」的原生通道**。当用户问「这个功能能不能做到 WorkBuddy 里」「AA / 某开源插件 能不能装进 WorkBuddy」「WorkBuddy 支持哪些扩展形态」「能不能给 WorkBuddy 加个面板/侧栏入口/手机接入」「**能不能直接让另一个会话执行任务 / 派活给别的会话**」时使用。核心:WorkBuddy 内核 = @genie/agent-cli(CodeBuddy 系),与 DSH/cordis 零共享 ⇒ 外部插件不能移植、只能按 WorkBuddy 扩展面重做;🔴 **跨会话派活有原生 API(gateway 的 `/api/v1/jobs`:`resume?cwd=` + `{id}/reply`),⛔ 别只答「排定时器」**(见 §1.4.4)。
version: 1.8.9
updated_at: 2026-09-30
agent_created: true
last_change: 2026-09-30(第九次)新增 §1.3d(钩子热生效/闸门体检会瞎/注册表逐字复原法)+ **§1.3c:Hook 注册表会被整段清空 ⇒ 机制静默失效**(用户指令「那个上下文到达一定量 就开接续会话的机制 没运行,强化一下」)。事故实物:`settings.json` 在 **09-26 21:17** 被一次无关改动把**整个 `hooks` 段清空**(同目录备份 `bak-decision-20260927-233116` 的 `hooks = {}` 即证)⇒ `stop-dialog-guard.py`(=「上下文到量 ⇒ 收口 ⇒ 接续」的**唯一触发源**)连同另外三个闸门一起掉线,**静默 4 天**(该 guard 日志最后一行 09-26 19:19;desktop 工作区 09-26 17:01;`lock-hook.log` 09-28 10:47)。⇒ **三条判据**:① 看备份 mtime + 内容(定位"哪一刻清空的")② 看钩子自己日志的**最后一行时间**(=停止工作的时刻)③ 看日志里的**事件分布**(本次 `UserPromptSubmit`×115 / `Stop`×0 ⇒ 那个 Stop 钩子**从未生效**)。🔴 **加固=把"注册表体检"做进开工第 0 步的状态脚本**(独立于钩子,否则自检一起死):比对"关键闸门必须挂在哪条事件上" + 对每个钩子指向的 `.py` 做 `exists`;且读配置**必须认 `CODEBUDDY_CONFIG_DIR`**(写成 `~/.workbuddy`/`WORKBUDDY_CONFIG_DIR` ⇒ 读到不存在的文件 ⇒ 异常被吞 ⇒ **自检恒静默**,本机 guard 的 `path_health()` 正是此病)。⛔ 装钩子命令**别加 `-E`**(屏蔽 `PYTHONUTF8` ⇒ 含中文载荷解码炸 ⇒ 表现为"钩子从没被调用")。全文 ⇒ 交付物/接续机制未运行的根因与强化-20260930.md。
last_change_prev: 2026-09-30(第七次)新增 **§1.3b:「要不要伪造钩子」的答案 = 不用伪造 —— 官方本来就有 idle 钩子**(用户指令「能否伪造 hook 执行 上报给主会话,破解一下 workbuddy 机制试试」)。🔑 源码原文:`resetIdleTimer` 里 `setTimeout(… executeNotificationHooks(L, "CodeBuddy is waiting for your input", IDLE_PROMPT), 6e4)` ⇒ **会话空闲满 60 秒,宿主自己 spawn 一个 `Notification` 钩子(`notification_type="idle_prompt"`)**;同族还有 `permission_prompt` / `auth_success`;matcher 规则 `getMatchValue(NOTIFICATION)=notification_type`;天然闸门 `!isIdle || hasLiveBackgroundTask()` ⇒ **忙/有后台任务就不发**。🔴 为什么这条=**无人值守**:触发=宿主自己的 60 s 定时器|唤醒对象=**会话自己**(⛔ 不用切窗口)|钩子是子进程 ⇒ **零 token、不产生新会话**。✅「能上报」实测:探针日志 `gateway_password_present: true` + `CODEBUDDY_SESSION_ID` 在环境里 ⇒ 钩子可走 `wake-session.py` 那条链 `reply` 回自己;且触发那刻会话**正 idle**(`hasWaiter=true`/`queueLen=0`)⇒ **投递即时生效**,不会重蹈 `parkInQueue`。⛔ **同时登记三条判负路**:`/sessions/{id}/reply`(只认 live)|`/jobs/{id}/reply`(worker 从不 drain,9 min 整窗零动作)|`stop→reply→respawn`(pending 真写出、respawn 真消费、argv 真拼 `--resume`,**但 worker 卡 `resuming…`**;A/B 未删除会话对照同样卡)。🔴 **新增原语 + 17:55 反转**:`POST /api/v1/runs` + `{id,type:"message",text}` ⇒ 202 `{runId}`,**实测「会话忙时排队、空闲即交付」—— 外部程序真能叫醒会话**(详见 §1.3b)。⚠️ 代价:钩子闭环=约每 60 s 一轮 ⇒ 最坏 **1440 轮/天**,开之前须三道闸门。已落地只记日志的探针 `.workbuddy/tools/idle-hook-probe.py` + **项目级** `.codebuddy/settings.json`(⛔ 不装全局);⚠️ **负向读数**:本会话 2.5 分钟真空闲窗**未触发**探针 ⇒ 项目级配置对已运行会话不生效(全局的是热生效)。全文 ⇒ 交付物/唤醒-idle钩子破解-20260930.md。
last_change_prev: 2026-09-30(第六次)§1.4.4 补 **`/jobs` 实测 + 解除「只能操作 live 会话」的限制**(用户否决「这个方案限制了使用 不行」)。实测:`GET /api/v1/sessions` **返回全部 20 个会话**(会话列表本不受 live 限制)|`GET /api/v1/jobs/resumable` **20 条候选含非 live 会话** + 分页|`POST /jobs/resume?cwd=…` 返回 `{kind:"background", pid, tempo:"idle"}`|`stop` → `{"stopped":true}`|`GET /api/v1/status` = `{busy, activeSessionId, runStatus}`(= T2 归还核对判据)。🔑 源码原文:`resume` =「启动常驻 worker,**不重放 prompt**;恢复后以 **idle 状态**出现」⇒ **不接管桌面、不自动烧 token** ⇒ **任意会话(含归档)可被拉起派活**。⚠️ 三坑:**`resume` 不校验 sessionId(假 id 也起真 worker,⛔ 不是只读探针)**|`/jobs` 按 id 前缀聚合(stop 后须复核)|`cwd` 不强制正斜杠。
last_change_prev: 2026-09-30(第五次)§1.4.6 补 **「持续运行 ≠ 持续唤醒」+ 链式续棒实测成立**(用户追问「后台任务不能持续运行吗」「一次性任务完成后继续创建,这样就能持续?」)。三条硬读数:**① 能长跑**(后台任务进程**不随本轮结束被杀** —— 上一轮起的常驻静默心跳循环,跨过本轮结束与下次唤醒仍在跳);**② 但长跑≠唤醒**(同一实验里它吐过心跳,`sessions.updated_at` 没动);**③ 链式续棒成立**(第 1 棒 `fired 13:09:12` 唤醒我 → **我在那一轮里起第 2 棒** → `fired 13:10:31` 照样唤醒)。⛔ 自举续命两条路都堵(detached spawn 死/循环不完成);⛔ 持久化兜底被封死(沙箱回收 + 程序黑名单)⇒ 常驻唯一可行起法=**会话后台任务 + stdout 全重定向**。新工具 `.workbuddy/collab/wake-pulse.sh <秒数>`。
last_change_prev: 2026-09-30(第四次)新增 **§1.4.6 第二条唤醒原语:会话自己起的「一次性后台任务」** —— 四组对照实测定死三条:**① 唤醒的触发=「任务完成」这一个事件,与 stdout 无关**(有输出的 `sleep 18` 与 **0 字节**的 `sleep 36` 都被唤醒;中途 2 次输出**都未唤醒**)**② 静默任务完全安全**(各只 1 条通知)**③ 长时长不被回收**(`sleep 300` 整 300 秒跑满照常唤醒)。🔴 头号推论:**「常驻循环 + 周期 echo」当不了脉冲 —— 永不"完成" ⇒ 永不唤醒**;想靠后台任务当脉冲**只能一次性**(单发/批量错峰/被唤醒后续下一棒)。✅ 同时**纠正旧因果**:09-29 的"看着像卡"**不是**"输出唤醒导致" ⇒ 真因是**输出把转录推过 10 MiB ⇒ diagnostic-log dropped**(**毒药是"输出的量",不是"被唤醒"**)。另:闭合 §1.4.1 的旧"未验证点"(`/sessions/live` **包含**桌面正在聊的会话;`writerOccupied=true` 下 `reply` 仍 200)。全文 ⇒ 交付物/唤醒-会话自建后台任务方案评估-20260930.md。
last_change_prev: 2026-09-30(第三次)§1.4.1 补**「唤醒」本机直连实测**:`$WS/.workbuddy/collab/wake-session.py`(枚举回环口 → 三判据指纹 → 取 live → 精确匹配目标 → `reply`),**⛔ 无代理 / 无垫片 / 无 443**,实测 `200 {"delivered":true}`。两条硬教训:**① 判指纹 ⛔ 不能带凭据**(带 `x-access-token` 时 `/health` 由 401 变 200 ⇒ 判据全灭;而这条恰是「真网关 vs 垫片 20090」的唯一分水岭)**② `delivered:true` ≠ 已唤醒**(busy ⇒ 入队等空闲边界,转录里还没有 user 消息)。⚠️ **本机并存多个网关口、每个只服务它自己那一个 live 会话** ⇒ 必须按 `live.sessionId==目标` 匹配,⛔ 升序取第一个必错。⇒ **修正旧结论**:5 分钟级唤醒**可达且零新会话**(载体=能继承口令的周期进程;会话后台任务在 WorkBuddy 进程树内 ⇒ 拿得到口令,推翻了"常驻进程拿不到口令"的适用范围),但与 T1 有**字面冲突**(T1 本意=别让输出回流拖死宿主 ⇒ 完全静默即满足本意),采纳前须用户覆盖。全文 ⇒ 交付物/唤醒-本机网关直连测试-20260930.md。
last_change_prev: 2026-09-30(第二次)§1.4 补**源码级依据**:自动化的 rrule 解析器是**硬白名单**(`FREQ` 只认 HOURLY/DAILY/WEEKLY/MONTHLY/YEARLY;全包 `MINUTELY`/`SECONDLY` 命中 0;`INTERVAL≥1`)⇒ **「最小 1 小时」不是 UI 限制而是解析器级**,且源码注明**三处实现必须同步改**;`HOURLY` 的 `formatRRule` **不产出 BYMINUTE** ⇒ 一律落整点;🔴 **手改 DB 塞 `FREQ=MINUTELY` ⇒ 解析器抛错 ⇒ 任务永不触发(幽灵任务),⛔ 别做**。方案全文 ⇒ 交付物/唤醒的解决方式-官方文档核对与方案分析-20260930.md §11。
last_change_prev: 2026-09-30 §1.4 补**硬阻断**:网关 `scheduled-tasks` 在**桌面端是死能力**(宿主硬编码 `CODEBUDDY_DISABLE_CRON=1` ⇒ 调度器不启动;且 `start()` 是唯一 `setStorageDir()` 处 ⇒ `durable:true` 的写入是**静默空操作**,仍回 `200+id`)+ 任务状态是**进程内**的(多网关口互不可见)+ 「叫醒闲置会话」的正解是 §1.4.1 的 `POST /sessions/{id}/reply` 由自动化驱动。事故全文 ⇒ 交付物/查唤醒为什么断-结论-20260930.md。
last_change_prev: 2026-09-28(第三次)§1.3a 的硬纪律由六条扩为 **七条** —— 新增 **⑦「作用域必须把监管者自己排除在外」**(实测事故:监控钩子作用域写成整个大项目根,而**监管者自己的工作区就在里面** ⇒ 监控到自己头上;叠加"会话内常驻轮询"⇒ **自己把自己顶住、会话永不回 idle**。⇒ 显式白名单 + 自己标 `scope=home` + 被丢弃必写 `skipped.jsonl`/stderr;**自检要逐类跑** home/target/other/outside。完整事故 ⇒ `session-mechanism §7.7 第 5 条`)。此前同日第二次:新增 §1.3a「Hook 的运行细节与坑」(事件全集;⛔「没有'自动化跑完了'这种事件,`TaskCompleted` 是 Task 待办项不是自动化」;四档作用域;`transcript_path` 结构;六条硬纪律含"路径别靠层数""命令别用非 ASCII 路径""验收要取配置原文真跑")+ §1.4 增 `scheduled-tasks` 路由及其 R5 风险三条。此前同日第一次:新增 §2.5「判『迁不进』之后的退路:落进源平台自己的插件包 + 三条硬判据」+ §4 自检一条。
---
# workbuddy-extension-surface — WorkBuddy 扩展面与迁移评估法
## 0. 先钉死一件事:WorkBuddy 不是 DSH 的壳
- WorkBuddy 内核 = **`@genie/agent-cli`(CodeBuddy / Genie 系)**,安装在 `%LOCALAPPDATA%\Programs\WorkBuddy\`。
- 本地实测:`resources/` 下**找不到** `dsh` / `cordis` / `deepseek` 任一痕迹。
- ⇒ **DSH 插件(`cordis.patch.yml` + `@deepseek-ai/dsh-client-ui-primitives` + `dsh.profile.bundles`)一律装不进 WorkBuddy**。遇到"能不能移植"的问题,第一步就把这条写清楚,⛔ 别让用户以为可以直接搬。
## 1. 扩展面清单(7 个,全部本地可验)
| 扩展面 | 落点 / 判据 | 关键约束 |
|---|---|---|
| **Skill** | `<plugin>/.codebuddy-plugin/plugin.json` 的 `category: skill` + `SKILL.md` + `references/` | 提示词与脚本层,**⛔ 无自定义 UI 面板** |
| **MCP app** | 同目录 `.mcp.json`;`category: mcp-app` | **`type: stdio`**;env 有 `${CODEBUDDY_PLUGIN_ROOT}` / `${CODEBUDDY_PLUGIN_DATA}` |
| **Connector** | `%WORKBUDDY_CONFIG_DIR%/connectors/` + `enterprise-registry.json` | 外部系统集成;先搜连接器再谈自建 |
| **Hook** | `settings.json` → `hooks` | **只有 4 个事件**:`SessionStart` / `PreToolUse` / `UserPromptSubmit` / `SessionEnd`;**会话启动时快照**(改完须完全重启)。**⚠️ 但契约能力很强 —— 见 §1.3,别被"只有 4 个事件"骗了** |
| **Automation** | 排期(一次性 / 周期) | 由宿主拉起新会话 |
| **发布为应用** | sites 技能:本地项目 → 在线链接 | 发布的是**项目产物**,⛔ 不是 agent 会话、不是本机服务 |
| **guest SDK** | `resources/wb-guest-sdk/wb.js`(`@genie/workbuddy-desktop-sdk`) | 能力最强,但**无公开文档** ⇒ 按「未公开」处理 |
| **🔴 远程控制网关(gateway)** | WorkBuddy.exe 起的 Express 监听(`127.0.0.1:<动态口>`) | **官方自带的"被外部访问"通道** —— 见 §1.4,**问「手机/外部客户端能否访问 WorkBuddy」时必查** |
### 1.1 插件形态的 `category` 实测取值(判"外部能做什么"最硬的尺子)
```bash
grep -rho '"category"[[:space:]]*:[[:space:]]*"[^"]*"' <WorkBuddy>/resources/app.asar.unpacked/resources/plugins/workbuddy-builtin | sort | uniq -c | sort -rn
```
2026-09-28 读数:`skill` 52 · `interaction` 10 · `welcomeMode` 6 · `builtin-plugin` 5 · **`mcp-app` 4** · `template` 2。
⇒ **第三方可用的实质只有 `skill` 与 `mcp-app` 两种**;想加"侧栏入口 + 自定义弹窗"这类交互,**没有公开对位**。
### 1.2 guest SDK 的能力面(反解 `wb.js`)
- 通用命名空间:`storage` `config` `account` `openAuth` `skills` `experts` `automations` `workspaces` `models` `notifications` `ops` `connectors` `conversations` `artifacts` `search` `changes` `intentRecognition` `metrics` `netdrive` `inspirationCode` `cloud`
- 桌面专属:`paths` `shell` `mcp` `buddyApps`
- `conversations` 方法(节选):`sendPrompt` `runPrompt` `requests` `artifacts` `files` `searchFiles` `changes` `cancel` `resend` `getPendingInfo` **`resolvePending` / `rejectPending`** `configSetModel` `configSetPermissionMode` `configSetExpert` …
- 事件(节选):**`wb:conversation:permission`**(权限请求)`stateChange` `timelineEvent` `planUpdate` `artifactUpdate` `creditChanged` `contextUsageChanged`
⚠️ `buddyApps` 在命名空间里但**未见对外形态与清单规范** ⇒ 任何依赖它的方案都必须标「未公开、需实测」。
### 1.3 🔴 Hook 的实质能力 —— 最被低估的扩展面
**契约 = Claude-Code 式**,支持三件事:
| 能力 | 字段 | 用途 |
|---|---|---|
| **阻断/放行工具调用** | `hookSpecificOutput.permissionDecision` = `allow` / `deny` | ⛔ 可以真的**拦住**一次工具调用 |
| **给模型一段理由** | `hookSpecificOutput.permissionDecisionReason` | 模型据此重写或继续 ⇒ **这是"把外部信息送进模型"的通道** |
| **注入上下文** | `hookSpecificOutput.additionalContext` | 挂在 `SessionStart` / `UserPromptSubmit` 等事件上 |
**双证据(2026-09-28 实测)**:
1. WorkBuddy 自身 CLI 内 `hookSpecificOutput` / `permissionDecision` / `permissionDecisionReason` / `additionalContext` 分别命中 **96 / 70 / 42 / 78** 处(`app.asar.unpacked/cli/dist/**`)。
2. 本机**已装且在跑**的钩子 `ai1net-decision-laya/bridge/decision_bridge.py` 正在用 `{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":…}}` 打回残缺提问,用 `additionalContext` 注入常驻规矩。
🔴 **推论(重要)**:**"没有侧栏面板/自定义弹窗"这个限制,对"审批、作答、通知"一类交互是可以绕过的** —— 用钩子把外部系统的答复通过 `deny` + reason 交给模型,或用 `additionalContext` 注入。
⇒ 做可行性评估时,**别因为"没有 UI 扩展点"就判"做不到"**;先问一句"这件事能不能只用钩子 + 拒绝理由表达"。
⚠️ 三条代价(必须一并写进方案):
1. **钩子有超时上限**【待实测具体值】⇒ 想"阻塞等人"只能等一小段,超时须有降级路径(⛔ 不能把桌面会话挂死)。
2. `deny` 是**拒绝语义**,模型可能误读成"工具被拒"而不是"用户答复" ⇒ reason 里必须写明"这是答复,不是拒绝",并用真机验收。
3. 阻断会**吞掉原生弹窗**(用户回桌面看不到)⇒ 这类接管必须挂在**用户显式开关**后面,⛔ 不默认接管。
### 1.3a 🔴 Hook 的**运行细节与坑**(2026-09-28 实测定型 · 写钩子前必读)
**事件全集(从 CLI 枚举取出)**:`SessionStart` · `SessionEnd` · `UserPromptSubmit` · `PreToolUse` · `PostToolUse` · `PostToolUseFailure` · `Stop` · `StopFailure` · `SubagentStart` · `SubagentStop` · `PreCompact` · `PostCompact` · `Notification` · `PermissionRequest` · `PermissionDenied` · `Elicitation` · `ElicitationResult` · `TaskCreated` · **`TaskCompleted`** · `TeammateIdle` · `Setup` · `FileChanged` · `CwdChanged` · `FinalStop` · `WorktreeCreate/Remove` · `InstructionsLoaded` · `ConfigChange`。
🔴 **两个必知**:
- ⛔ **没有"自动化跑完了"这种事件**。`TaskCompleted` 是 **Task 工具的待办项**完成(`task_id`/`task_subject`),**不是定时自动化** —— 别把它当"自动化完成钩子"用(本会话差点踩)。
- 自动化的结果**不需要钩子**:宿主已经把收官结论写进 `automation_runs.thread_title`(SQLite 直读即可)。
**作用域(四档,可项目级 = 只影响一个工作区)**:`USER`(`~/.workbuddy/settings.json`)· **`PROJECT`(`<工作区>/.codebuddy/settings.json`)** · **`PROJECT_LOCAL`(`.codebuddy/settings.local.json`)** · `CLI`。`hooks` 在**项目级可设键白名单**里。
⚠️ 但**多源如何合并(项目级会不会覆盖全局同事件的数组)在压缩产物里无法确证** ⇒ 结论:**要么走项目级并实测验证,要么直接追加到同一份文件的同一数组里**(数组天然共存、零歧义)。⛔ 别在没验证前假设"项目级只是叠加"——猜错的代价是**静默打断别人的钩子**。
**载荷(stdin JSON,实测字段)**:`hook_event_name` · `session_id` · `transcript_path` · `cwd`(+事件特有字段,如 `tool_name` / `prompt`)。
**`transcript_path`** = `<配置目录>/projects/<cwd 编码>/<sessionId>.jsonl`;行类型 = `session-meta` · `message`(`role` ∈ user/assistant,正文在 `content`)· `reasoning` · `function_call` · `function_call_result` · `file-history-snapshot`。
⇒ **取"某会话的最后答复" = 扫尾部、找最后一条 `type=message & role=assistant` 的 `content`**(`content` 可能是字符串或 `[{type,text}]` 块列表,两种都要兜)。
**写钩子的七条硬纪律(都是踩过的)**:
1. ⛔ **绝不写 stdout**(stdout 是钩子协议通道);**永远 exit 0**;异常全部吞掉并**只写自己的日志**(fail-open)。
2. ⛔ **不读令牌、不联网、不起子进程** —— 钩子**继承 WorkBuddy 的 env(含 `CODEBUDDY_GATEWAY_PASSWORD`)**,但那意味着"能排 AI 执行"的强能力 ⇒ 只做本地文件读写,**别构成权限扩大**(R5)。
3. 🔴 **路径不能靠"脚本在第几层"**(实测 bug:脚本从 A 目录移到 B 目录后,`dirname(dirname(__file__))` 指错 ⇒ **台账静默写到别处**,而"测试全绿、rc=0")。⇒ 用**内容标志上溯**锚定工作区(本会话用 `state.py`),并留一个 `--where` 自证入口。
4. 🔴 **钩子命令里别用非 ASCII 路径**(中文目录)—— 宿主起命令时可能编码损坏 ⇒ 脚本放 ASCII 路径。
5. ⚠️ **钩子 = 会话启动时快照**:对**在跑的会话无效**,装完要**新会话**(很可能还要**完全重启 WorkBuddy**;⚠️ 关窗 ≠ 退出)⇒ 装完必须**验证真的触发了**,⛔ 别默认它生效。
6. 🔴 **验收手段**:让脚本往固定台账写一行,然后**断言"跑一次、台账恰好 +1、stdout 恰好 0 字节、rc=0"**;并**从 settings.json 里取出那条命令原文去真的执行一遍**(而不是手打一遍),才算验到"装的那条能用"。
7. 🔴🔴 **作用域必须把「监管者自己」排除在外**(2026-09-28 实测事故:监控钩子的作用域写成**整个大项目根**,而监管者自己的工作区也在里面 ⇒ **监控到自己头上**;叠加"会话内常驻轮询"就变成**自己把自己顶住、会话永不回 idle**)。
⇒ 三件都要:**显式白名单**列被监管对象 | 自己标 **`scope=home`**(记账但⛔不当被监管对象)| **被丢弃必写 `skipped.jsonl` + stderr**(⛔ 不静默排除)。
⇒ **自检必须逐类跑**(home / target / other / outside 各喂一个 payload),⛔ 别只跑"应该通过"的那一类。
📂 完整事故与处置 ⇒ 技能 `session-mechanism`(内 `references/作业规矩/`) **§7.7 第 5 条**。
### 1.3b 🔴 「要不要伪造钩子」的答案:**不用伪造 —— 官方有 idle 钩子**(2026-09-30 源码级定案)
**问题**:能不能"伪造一次钩子执行",让钩子(它自带网关口令)把消息回报给主会话,从而做到**不靠人、不靠自动化、不靠切窗口**的唤醒?
**答案**:触发源**本来就是现成的**,不需要伪造。
```js
resetIdleTimer(L){ // app.asar → /cli/dist/codebuddy-headless.js
… let ea = setTimeout(async () => {
if (!this.stateMachine.isIdle(L) || this.hasLiveBackgroundTask()) return void this.resetIdleTimer(L);
await this.sessionHookManager.executeNotificationHooks(
L, "CodeBuddy is waiting for your input", IDLE_PROMPT);
}, 6e4); // ★ 60_000 ms
}
```
⇒ **会话空闲满 60 秒 ⇒ 宿主自己去 spawn 一个 `Notification` 钩子**(`notification_type = "idle_prompt"`)。
同族触发点:`permission_prompt`(要授权时)· `auth_success`(登录成功时)。
| 要点 | 内容 |
|---|---|
| **matcher 怎么写** | `getMatchValue(NOTIFICATION) = notification_type` ⇒ 用 `"^idle_prompt$"` |
| **天然闸门** | `!isIdle \|\| hasLiveBackgroundTask()` ⇒ **忙 / 有后台任务在跑就不发** |
| **载荷** | 标准钩子 JSON + `message` + `notification_type` |
| ⚠️ **别混** | 同源另有一处 **PTY 服务**的空闲定时器(读 `CODEBUDDY_PTY_IDLE_TIMEOUT_MS`),**与钩子无关** |
**为什么这一条=无人值守(三条性质全中)**:① 触发=**宿主自己的 60 s 定时器**,与人在不在无关;② 唤醒对象=**会话自己**(⛔ 不必切窗口/切会话);③ 钩子是子进程 ⇒ **零 token、不产生新会话**。
**「能上报」已实测(探针日志)**:`gateway_password_present: true` + `CODEBUDDY_SESSION_ID` 在环境里
⇒ 钩子脚本里照 `wake-session.py` 那条链走即可(枚举回环口 → 三判据指纹(**⛔ 不带凭据**)→ 按 `live.sessionId == 自己` 匹配 → `POST /sessions/{id}/reply`)。
🔴 **时序优势**:触发那一刻会话**正处在 idle**(源码就是这么判的)⇒ `hasWaiter=true`、`queueLen=0` ⇒ **投递即时生效**,不会重蹈 `parkInQueue`。
🔴 **同时登记三条已判负的"外部程序→任意会话"路(⛔ 后人别再重做)**:
| 通道 | 死因 |
|---|---|
| `POST /sessions/{id}/reply` | **只认 live**;非 live ⇒ `parkInQueue` + `hasWaiter=false`(实测投递后 **4.5 小时**未成为用户消息) |
| `POST /jobs/{id}/reply` | 写 `jobs/<id>/inbox/*.json`,但 **resume 起的 worker 从不 drain**(400 ms 轮询 + 目录监听,**9 分钟整窗零动作**) |
| `stop → reply(落 pending) → respawn` | `pending-reply.json` 真写出 ✅、respawn 真消费 ✅、文本真 `unshift` 进 `["--resume", sid]` ✅ —— **但 worker 卡在 `detail:"resuming…"`**,`firstTerminalAt:null`、日志 0 B;**未删除会话 A/B 对照同样卡** |
✅🔴 **另一条已端到端实测成立的唤醒腿:`POST /api/v1/runs`**(17:55 反转,**推翻**同日早先"外部程序叫不醒会话"的判断)。
体:`{"id":…,"type":"message","text":…}` ⇒ `202 {"runId":…,"status":"accepted"}`;宿主日志 `[GatewayAcpBridge] handleMessageAsync … got session <sid>, isShared=true` ⇒ **绑到「该网关所服务的那个会话」**。
🔴 **正确语义=「会话忙时排队,一旦空闲立刻把 text 作为用户消息交付」**(实测:17:49 投出、投出后 `GET /runs/{id}` 一直 `active:true`,**17:55 该文本真的作为用户消息出现在目标会话里并跑起一轮**)⇒ 外部程序**可以**唤醒会话、**不需要人切窗口**。
⚠️ `active:true` ≠ 挂死,是**在等交付时机**。🔴 与 `sessions/{id}/reply` 的分水岭=**有没有消费者**:`runs` 走 `GatewayAcpBridge`(waiter 会被真正消费),`reply` 非 live 时只是 `parkInQueue`(无消费者)。
另:`POST /api/v1/process/start`(宿主起进程,对齐 E2B)、`POST /api/v1/team/messages/send`(Agent 团队信箱,成员 **2 s** 轮询)也已探明。
⚠️ **代价与闸门(开之前必须写进方案)**:钩子闭环 = **约每 60 秒一轮 AI ⇒ 最坏 1440 轮/天**。
最少三道闸门:**总开关文件**(读不到就静默退出)+ **同会话退避**(连续 K 次无产出自动停)+ **唤醒后只做一件可判的事**(无活立即结束本轮)。
⛔ **不做无限自唤醒**、⛔ 不挂全局。
**落地物(只记日志、零注入、可逆)**:`.workbuddy/tools/idle-hook-probe.py`(⛔ 不联网/不发消息/不写 stdout/永 exit 0)+ **项目级** `.codebuddy/settings.json`。
✅ **已坐实(2026-09-30 19:0x):成立。** 全天唯一一次真触发 `18:55:00.210`(宿主日志 `[HookExecutor] spawn …idle-hook-probe.py`,全天 999 条 spawn 仅此 1 条);
与末条 `[addHistory] COMPLETED 18:54:00.193` **精确差 60.0 s**;该行 env **无** `TOOL_CALL_ID`(人工喂的会有)⇒ 不是手工。
🔴 **两条已作废的旧猜测(⛔ 别再引用)**:
- ~~"项目级 settings 对已运行会话不生效 / 可能只有新会话才加载"~~ —— **判错**。配置生效正常,**是被闸门压的**。
- ~~"某段时间有空闲窗却没触发 ⇒ 机制不成立"~~ —— 那两段会话其实**一直在跑工具**,没有 ≥60 s 空档。
🔑 **两根隐形闸门(真相)**:`resetIdleTimer` 装在**每次 `addHistory` 末尾** ⇒
① 状态机必须 `idle`(回合进行中永不计时)② 该会话**没有 pending/running 的后台任务**(`hasLiveBackgroundTask()`)。
**闸门不过 ⇒ 只重装表,宿主一个字节都不记** ⇒ 现场就是"什么都没发生"(本次被一个僵尸后台任务压了 **6h20m**)。
⚠️ ⇒ **自己在会话里起的后台任务会静默掐死 idle 钩子**;想用它做唤醒,会话必须"活着 + 空闲 + 无后台任务"。
🔑 **判"钩子/定时器类机制为何不触发"必须同时取四类日志(缺一件都不许下"机制不成立")**:
宿主日志 `E:/ProgramData/.workbuddy/logs/<日期>/<工作区名>__<hash>.log` 里
① `[addHistory] COMPLETED`(计时器装点)② `[SessionRunStateMachine] transition`(闸门①)③ `[BashTool] background task created|completed|failed|killed`(闸门②)④ `[HookExecutor] spawn`(机制到底有没有被执行)
—— 再加 **会话进程存活**(`[CliPrewarmPool] activated prewarm entry exited unexpectedly (code=1)` ⇒ 计时器随之消失)。
📂 全文 `交付物/唤醒-idle钩子破解-20260930.md`|**坐实 + 四类日志取证法** `交付物/唤醒-idle钩子坐实-20260930.md`
### 1.4 🔴 远程控制网关 —— 官方自带的"外部访问"面(极易漏)
**判据**:WorkBuddy 运行时会起 Express 服务,页面标题是 `CodeBuddy Remote Control` / `CodeBuddy Gateway`。
**怎么找**(一条命令):
```bash
netstat -ano | grep LISTENING | grep 127.0.0.1 # 取 WorkBuddy.exe 的 PID 对应的口
curl -s -m 4 http://127.0.0.1:<port>/ | grep -oE "<title>[^<]*|api/v1/[a-z{}:]+"
```
| 项 | 实测(2026-09-28) |
|---|---|
| 端点 | `POST /api/v1/runs`(起一次 agent run)· `GET /api/v1/runs/:runId/stream`(**SSE**)· **`GET /api/v1/sessions/live`** · **`POST /api/v1/sessions/{id}/reply`** · `GET /api/v1/sessions[/{id}/history|replay|rename]` · `GET/POST /api/v1/acp` · `POST /api/v1/webhooks/:platform` · `/api/v1/health` · `/api/v1/status`(全表 129 条) |
| 鉴权 | **有** —— 无凭据 `401 {"error":{"code":"AUTH_REQUIRED"}}` |
| 配置键 | `CODEBUDDY_GATEWAY_{AUTH,PASSWORD,BASE_PATH,FORCE_TUNNEL,DISABLE_API_DOCS,ACK_*}` + `WECHAT_KF_*` / `WECOM_*` |
| 启动 | CLI `--serve` + `--host`(默认 **127.0.0.1**)+ `--base-path` + **`--auth <mode>`**;运行期 `/gateway start` / `/gateway stop` |
| 鉴权模式 | `--auth none` ⇒ 关闭(日志 "Auth disabled (mode: none)",teammate 模式即用它)|`--auth password` ⇒ 取 `CODEBUDDY_GATEWAY_PASSWORD`,否则读 settings 的 **`gateway.password`**,再没有就**随机生成** |
| 手机端界面 | ⚠️ `dist/web-ui` **默认未构建**(首页自报 "Web UI is not built"),源码在 `packages/agent-cli/src/node/remote-gateway/web-ui`(**分发版里没有**) |
| 🔴 **定时任务路由(2026-09-28 新发现 · 强能力)** | `GET/POST /api/v1/scheduled-tasks`(`GET` 必带 `?sessionId=`,否则 `400 sessionId is required`)· `DELETE /api/v1/scheduled-tasks/{id}`。**POST 体**:`cron`(**5 字段**,`分 时 日 月 周`)+ `prompt`(必填)+ `recurring`(默认 true,**false = 一次性**)+ `durable`(默认 false,跨 session 持久化)+ `sessionId`(可选)。⇒ **它能在本机按任意分钟粒度排一次 AI 执行** |
🔴🔴 **但在 WorkBuddy 桌面端,这条路是死的 —— 建了不会响(2026-09-30 源码级定案,⛔ 先读这段再动手)**
| 事实 | 证据 |
|---|---|
| 宿主给每个 CLI agent 子进程拼环境时**硬编码** `CODEBUDDY_DISABLE_CRON: "1"` | `app.asar` → `/main/code-cache.js`,**两处**:`buildAgentCliRuntimeEnv()` 与 `buildCliEnvFromResolved()` |
| CLI 侧调度器 `CronSchedulerService.start(session)` **第一行**就 `if("1"===process.env["CODEBUDDY_DISABLE_CRON"]) return;` ⇒ **调度器从不启动** | `cli/dist/codebuddy-headless.js` |
| `start()` 同时是**唯一**调 `setStorageDir()` 的地方 ⇒ `storageDir` 恒 `undefined` ⇒ `createTask` 里 `durable:true` 分支是 `if(storageDir){写文件}` **没有 else** ⇒ **静默空操作**,却照样返回 `200 + id` | 同上(`CronStateManagerService.createTask` / `CronStorage`) |
| ⇒ 实测表现:`POST` 回 200 带 id;`GET /scheduled-tasks?sessionId=…` **永远 `[]`**;`{project}/.codebuddy/scheduled_tasks.json` **永不生成**;`_wake.stamp` 一类"到点自证"文件永不出现 | 2026-09-30 12:0x 复现(non-durable 立刻可见、durable 立刻不可见) |
⇒ **判据**:① **⛔ 不要用网关 `scheduled-tasks` 当定时器**(桌面端)② 时间驱动只认**自动化**(rrule 最小 **1 小时**)
③ 要"叫醒某个正在闲置的会话",走 §1.4.1 的 `POST /api/v1/sessions/{id}/reply`,**由自动化来发**
④ 🔴 **5 分钟级唤醒可以做 —— 但载体不是"定时器",是"本机 gateway 直连"**(2026-09-30 12:55 实测,见 §1.4.1 末段):
cron 死 / 自动化最小 1 小时 / 钩子闲时无事件 ⇒ 唯一可行=**一个能继承口令的进程**周期调 `reply`。
⚠️ 「常驻进程拿不到口令」**只对独立进程成立**;**会话后台任务跑在 WorkBuddy 进程树内 ⇒ 它拿得到**。
🔴 **但常驻循环的唤醒来源要说清(2026-09-30 13:0x 实测)**:唤醒靠的是**它主动调的那一次 `reply`**(注入用户消息),⛔ **不是**它的输出、**更不是**"它在运行"—— 后台任务**只有"完成"才唤醒**,常驻循环永不完成 ⇒ 见 **§1.4.6**。且常驻**必须完全静默**(stdout 全重定向)。
🔴 **「自动化最小 1 小时」的源码级依据(2026-09-30 补 · ⛔ 别再去试 5 分钟)**
自动化自己的 rrule 解析器(`app.asar` → `/main/log-acl-guard.js`,偏移 ≈403.6 KB)是**硬白名单**:
```js
const rawFreq = map.get("FREQ");
if (!rawFreq) invalidRule("missing FREQ");
if (rawFreq !== "HOURLY" && rawFreq !== "DAILY" && rawFreq !== "WEEKLY"
&& rawFreq !== "MONTHLY" && rawFreq !== "YEARLY")
invalidRule(`unsupported FREQ=${rawFreq}`);
...
interval: parsePositiveInt(map.get("INTERVAL"), 1) // ≥1,单位由 FREQ 决定
```
| 事实 | 读数 |
|---|---|
| 全包检索 `MINUTELY`/`SECONDLY` | **命中 0** |
| 理论最小粒度 | `FREQ=HOURLY;INTERVAL=1` = **每小时** |
| 是不是"只是 UI 限制" | ❌ **不是**。源码注释明写:同一实现**在 workbuddy-server `automation/schedule-utils.ts` 与 VSCode 扩展 `automation-storage.ts` 各有一份等价副本,多处必须同步改** ⇒ 三层一致 |
| HOURLY 能否错开分钟 | ⚠️ `formatRRule` 对 HOURLY 只产出 `FREQ=HOURLY;INTERVAL=n;BYDAY=…`,**不含 BYMINUTE** ⇒ HOURLY 一律落在**整点那一分钟** |
| 手改 DB 塞 `FREQ=MINUTELY` 会怎样 | 解析器抛 `unsupported FREQ=MINUTELY` ⇒ **该任务永不触发** ⇒ **造出一条幽灵任务**(有登记、有状态、从不执行)⇒ ⛔ 别做 |
⚠️ **另注**:任务状态是**进程内**的 —— 本机并存多个网关口(各 WorkBuddy 进程一个,都回同一套 `AUTH_REQUIRED`),
投到 A 口建的任务,B 口列不出来、也不会执行;⛔ 别用"扫到的第一个口"当"目标会话所在的口"。
🔴 **`scheduled-tasks` 是一条"新能力",用前必做权限影响评估(R5)** —— 三条实测理由:
1. 它建出来的东西**不在 `automations` 表里** ⇒ 任何"看 automations 的监管"都**看不见它**(=凭空多一个隐形执行者)。
2. 它比自动化工具细得多(自动化 recurring**最小 1 小时**,它是**任意 5 字段 cron**)⇒ 一旦谁用它绕开既有编排,**编排纪律就形同虚设**。
3. 它落在**网关权限面**上:能拿到 `CODEBUDDY_GATEWAY_PASSWORD` 的进程(=WorkBuddy 进程树内)就能用它**在你机器上排 AI 执行**。
⇒ 结论:**⛔ 不要因为"能做到"就顺手用**;要么不做,要么先出评估+用户确认,并把它的产物纳入监管视野(能列出/能删)。
#### 🔴 1.4.1 「操作**现有**会话」是官方原生能力 —— 别自研桥
**问「能不能用手机/外部客户端接管或回复**现在正在跑的那个会话**(而非另起一个)」时,答案是这对端点:**
| 端点 | 实测 schema / 官方摘要(原文) |
|---|---|
| `GET /api/v1/sessions/live` | 摘要「**当前活会话与 writer 占用(不建立 ACP)**」⇒ `200 {sessionId: string\|null, writerOccupied: boolean}` |
| `POST /api/v1/sessions/{id}/reply` | 摘要「**向当前活会话投递回复(不占用 ACP writer)**」⇒ body **`{text: string}`**(required)|`200 {delivered: boolean}`|**`409`「不是当前活会话」** |
| `GET /api/v1/sessions/{id}/history` | 读历史 |
| `GET /api/v1/sessions` | query `cwd`(绝对路径;**`*` = 跨项目**)· `projectId`;每项返回 `cwd`/`projectId` |
🔑 **为什么这一对正好满足"操作现在的会话"**:`reply` 明确写着**「不占用 ACP writer」** ⇒ **投递回复不会夺走桌面端的写权**,两端可共存;而 `409` 把"只能回当前活会话"钉死(防串会话)。**对比**:`POST /api/v1/runs` 是"另起一个 run" —— ⛔ **那个不是**"操作现有会话",别用错。
🔴🔴 **「唤醒」本机直连 · 已实测可用(2026-09-30 12:55 · ⛔ 无代理 / 无垫片 / 无 443)**
成品工具:`$WS/.workbuddy/collab/wake-session.py`(零依赖);它做四步:**枚举回环口 → 三判据指纹 → 取 `live` → 精确匹配目标 → `reply`**。
| 步骤 | 落地的判据 |
|---|---|
| 找网关 | ① `GET /` 含 `CodeBuddy Gateway` / `CodeBuddy Remote Control` ② `GET /api/v1/health` ⇒ **401** ③ 能答 `/api/v1/sessions/live` |
| 选对网关 | 🔴 **本机并存多个网关口,每个只服务它自己那一个 live 会话** ⇒ 必须按 **`live.sessionId == 目标`** 精确匹配(实测 12:49 三个口分别 live=`55283→11a263a2` / `56975→fe146dd9` / `59486→6ecf6d98`);⛔ **升序取第一个必错** |
| 凭据 | 只从环境变量 `CODEBUDDY_GATEWAY_PASSWORD` 读;header **`x-access-token`**(`?password=` 一律失效) |
🔴 **两个必须记住的坑**:
1. **判指纹时⛔不能带凭据**:带 `x-access-token` 时 `/api/v1/health` 会从 **401 变 200** ⇒ 判据全线落空(首版就因此全灭)。
⚠️ 而这条**恰好**是「**真网关 vs 垫片 20090**」的**唯一分水岭** —— 垫片把根页原样转出去(标记同样命中)但它自己不鉴权 ⇒ `health=200` ⇒ 被正确排除。
2. **`delivered:true` ≠ 已唤醒**:目标当时 `writerOccupied=True`(正在跑)⇒ **消息入队、等空闲边界才被消费**(实测:HTTP 200 + `sessions.updated_at` 被推进,但转录里**还没有** user 消息)。⇒ 判"是否真闭环"必须看**那条消息是否作为用户消息现身**,⛔ 别拿 ack 当闭环。
⇒ 它使「5 分钟唤醒」**变为可达且零新会话**(前提=有一个能继承口令的周期进程);⛔ 但与 T1(`交付物/手机App经覆盖网络操作WorkBuddy-架构定稿v3-20260929.md` §0.5)存在**字面冲突**(T1 本意是"别让输出回流拖死宿主"⇒ 走「完全静默」即满足本意),采纳前须用户明确覆盖。
📄 全文 ⇒ `交付物/唤醒-本机网关直连测试-20260930.md`
**怎么验它存不存在**(一条判据,免凭据):`401` = 路由存在需鉴权;`404` = 不存在。
```bash
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:<port>/api/v1/sessions/live # 期望 401
```
✅ **该未验证点已闭合(2026-09-30 13:0x 实测)**:`/sessions/live` **包含桌面 App 里正在聊的那一个** —— 对 `59486` 取 live 得到的就是**我自己这个会话**(`6ecf6d98`),且当时 `writerOccupied=true` 下 `POST /reply` 仍回 **`200 {"delivered":true}`**(**不是 409**)⇒ 「reply 只认 live」成立,且 **busy ≠ 拒绝**(入队、等空闲边界才被消费)。
#### 🔴 1.4.2 「手机操作会话」还有第二条官方通道:**远程控制客户端(channels)** —— 手机侧免开发
比 1.4.1 更省:**不用做手机端界面**,用现成的手机 IM 当客户端。
| 端点 | 官方摘要(原文) |
|---|---|
| `GET /api/v1/channels` | **「获取远程控制客户端列表」** ⇒ `clients[]`:`clientType` · `instanceId` · `displayName` · `status`(`connected|disconnected|connecting|error`)· `hidden` · **`streaming`** |
| `POST /api/v1/channels/wechat` | 「创建微信实例」 |
| `POST /api/v1/channels/wecom` | 「创建企微实例」 |
| `POST /api/v1/channels/{type}/{instanceId}/start|stop` | 「启动 / 停止客户端」 |
| `GET /api/v1/channels/{type}/{instanceId}/qr` | **「获取扫码状态」**(手机扫码配对) |
🔑 **判读**:`status` 含 `connected/streaming`(**常驻连接**,非一次性 run)⇒ 这是"把手机 IM 当远程控制端"的官方形态,**手机侧零开发**。
🔴🔴 **它还有现成的图形界面 —— 这是「手机操作 WorkBuddy」最短的路,⛔ 别一上来就设计桥/APK**(2026-09-28 实测推翻我此前的动手方案):
| 位置 | 中文文案(App 原文) |
|---|---|
| **设置 → `settings.nav.claw`** | 「**{name}设置**」 |
| 该页的**分组标题** `settings.claw.wecomConnection` | 「**远程通道**」(英文 Remote Channels) |
| 分组说明 `settings.claw.wecomConnectionDesc` | 「**连接第三方消息通道,即可远程指挥电脑端 WorkBuddy 执行任务。**」 |
| **`settings.claw.weixinBotIntegration`** | 「**微信助理**」—— 说明:「**通过微信直接下发指令,结果实时回传至微信聊天窗口**」;按钮 `weixinBot.scanQr` = 「**微信扫一扫**」 |
| `settings.claw.wechatkfIntegration` | 「**微信客服号**」—— 「与微信助理功能一致,微信扫码即可绑定,**远程遥控 WorkBuddy 干活**」 |
| 其它通道 | 企业微信 · 飞书 · QQ · 钉钉 · Slack · Telegram · Discord · 元宝 · 微信小程序 |
| 另有看板项 `colleagues.dashboard.remoteMobile` | 「**手机远程**」 |
⚠️ **判据**:「手机操作 WorkBuddy」这类需求,**先答"设置 → 远程通道 → 微信助理 → 扫码"**(用户零开发、官方支持)。
⚠️ **一处要说清的边界**:远程通道建立的是**它自己的「助理」会话**(`claw.workspace` = Assistants,Local Assistant 页签;`settings.claw.sessionManagementDesc` 原文「本地助理采用**单窗口对话模式**」)⇒ **不等于**"附着到你桌面上任意一个正在聊的会话"。要"操作**现有**会话"仍得用 §1.4.1 的 `/sessions/{id}/reply`。
⚠️ 另一套相邻配置在 `settings.json` 的 **`claw`** 段(`channels` / `users.<uid>.channels.<type>.enabled`,如 `wechatmp`),与 gateway 的 `wechat`/`wecom` **可能是两套**,⛔ 别混为一谈,用前先实测。
⇒ **提问"能不能从手机/外部访问 WorkBuddy、能不能远程操作现在的会话"时,先答这个网关**,⛔ 别急着设计自研桥或反向代理。
⚠️ **但必须先看权限**:该网关能**起 agent run** + **改会话**(在你机器上执行任务)⇒ 任何让它经中继/公网可达的方案,**权限面远大于只读访问**,须先出权限影响评估(R5)。
#### 1.4.3 🔴 `/gateway` 斜杠命令 —— 凭据**不用重启**就能拿到(含二维码)
用户可在 WorkBuddy 会话里直接敲(`disableModelInvocation: true` ⇒ 只能用户敲、模型不能调):
🔴 **「在哪敲」有实证(2026-09-28 补)**:敲的地方 = **WorkBuddy 的对话输入框**(任务对话页)。依据 `@genie/agent-cli` 的 `sendAvailableCommandsUpdate()`:它先用一个**排除列表**(`/exit /help /config /sandbox /ide /hooks /theme /rename /agents /model /resume /mcp /permissions /tasks /plugin /status /bash /memory …`,共 31 条内建)过滤 `commandManager.getAll()`,再把剩下的作为 `available_commands_update` **推给桌面 App**;**`/gateway` 不在该排除列表里** ⇒ **会出现在斜杠菜单中**(同 `/remote-control`)。
⚠️ 所以"只能在 CLI 里敲"是**错的**;但它是**用户专属**(模型不能代调),且 `token` 子命令**要求 gateway 已在运行**(`"connected" !== status` ⇒ 直接返回「Gateway is not running」)。
⚠️ 想**不开 GUI**:也可在终端跑自带 CLI `…\WorkBuddy\resources\app.asar.unpacked\cli\bin\codebuddy`(`package.json` 的 bin = `codebuddy` / `cbc`,**不在 PATH**)——但它起的是**它自己的** gateway 实例,与 WorkBuddy.exe 那个不是同一个(⛔ 别指望用它给桌面那个换令牌)。
```
/gateway [status|stop|token|tunnel]
/gateway Start gateway with tunnel
/gateway status Show current status
/gateway stop Stop the gateway
/gateway token Regenerate access token
/gateway tunnel Start gateway with tunnel
```
| 命令 | 实际输出(读代码实证) |
|---|---|
| **`/gateway status`** | 树状:`Status / Mode / Local / Tunnel / **Web UI: <url>?password=<token>** / Webhook / Uptime` + **附二维码**(`formatQRCodeSection`)⇒ **令牌直接看得见** |
| `/gateway token` | `New token generated` + 带新令牌的 **Web UI 地址** + 二维码 |
| `/gateway`(无参)/ `tunnel` | 起 gateway(带隧道)并打印 `Gateway started:` 那一段(同样含带令牌的 Web UI 地址 + 二维码) |
| `/gateway stop` | 停 |
⇒ **结论:凭据不需要"设置 + 重启"** —— 它本来就在,`/gateway status` 能直接显示。
**那什么才需要重启?只有一件:把服务对局域网开放**(改 `--serve --host`)。
官方 `formatNetworkAccessHint` 原文(绑定回环时会打印):
```
• Currently bound to localhost (127.0.0.1)
• Only accessible from this machine
To allow access from other machines on the network:
1. Stop this gateway: /gateway stop
2. Start with: codebuddy --serve --host 0.0.0.0 --port <port>
3. Access via http://<your-ip>:<port>
Or use tunnel for secure public access: /gateway tunnel
```
🔑 两个要点:① 改绑定地址是**进程启动参数**(`--serve --host`)⇒ 在 WorkBuddy 里等价于重启宿主;② **但有免重启替代 —— `/gateway tunnel`**(运行期开隧道)。
⚠️ **判据纠正**:⛔ 别把"凭据要重启"当成前提(那是错的);只有"改监听地址"才牵扯重启。
#### 1.4.4 🔴🔴 直接给「另一个会话」派活 —— **有原生通道,别再只答"用定时器"**(2026-09-28 推翻旧结论)
**问题**:「能不能直接让另一个会话去执行任务?」(用户原话)
**旧答案(错的)**:「跨会话唯一通道 = 定时自动化」。**纠正**:宿主**原生就有**跨会话派活的 API,就在同一个 gateway 上 —— `jobs` 系列(智能体实例):
| 端点 | 官方摘要(原文) | 用途 |
|---|---|---|
| `GET /api/v1/jobs` | 列出智能体实例 | 看有哪些会话可派 |
| `POST /api/v1/jobs/{id}/reply` | **向智能体发送后续指令** | ⭐ **直接派活** |
| `GET /api/v1/jobs/resumable` | 列出可恢复的**归档**会话 | 找已归档的目标 |
| `POST /api/v1/jobs/resume` | **恢复归档会话为智能体** —— ⚠️ **带 `cwd` query 参数**("项目工作目录")+ body `{sessionId}` | ⭐ **按工作目录把归档会话拉回来** |
| `POST /api/v1/jobs/{id}/respawn` | `/stop` | 重新拉起 | 停止 | 唤醒 / 打断 |
| `GET /api/v1/jobs/{id}/transcript` | `/stream` | 读 transcript | **回放并尾随** | 看它干了什么 |
⇒ **判据**:问「能不能让别的会话干活」时,**先答这两条**(`resume` 带 cwd + `reply` 发指令),⛔ 别再只说"排个定时器"。
🔴 **2026-09-30 17:2x 实测补强(这条同时解除了"只能操作 live 会话"的限制)**:
| 端点 | 实测(本机 59486) |
|---|---|
| `GET /api/v1/sessions` | **全部 20 个会话**(`id/name/createdAt/updatedAt/messageCount/isCurrent`)⇒ **会话列表本来就不受 live 限制** |
| `GET /api/v1/jobs/resumable` | **20 条候选**(含多个**非 live** 会话)+ `hasMore`/`nextOffset` 分页 |
| `POST /api/v1/jobs/resume?cwd=…`(body `{sessionId}`) | 返回 `{id,state:"working",tempo:"idle",cwd,kind:"background",alive:true,sessionId,pid}` |
| `POST /api/v1/jobs/{id}/stop` | `{"stopped":true}` |
| `GET /api/v1/status` | `{status,busy,activeSessionId,runStatus}` ⇒ **T2「记原活会话并归还核对」的现成判据字段** |
🔑 **源码级依据**(`app.asar` → `/cli/dist/codebuddy-headless.js` 的 OpenAPI 原文):`jobs/resume` = **「恢复归档会话为智能体」**,description = **「使用 --resume 启动常驻 worker,**不重放 prompt**;恢复后以 **idle 状态**出现在智能体列表」**。
⇒ 两条关键含义:① **不重放 prompt**(拉起不自动跑一轮、不偷烧 token)② **它是 headless 常驻 worker**(`kind:"background"` + 独立 `pid`)⇒ **不接管桌面**、**不会把用户正在看的对话切走**。
⇒ 🔴 **因此"任意会话(含归档)"都能被拉起来派活** —— `reply`-only-live 的那条限制不成立。
⚠️ **三个新坑(⛔ 别再犯)**:
1. 🔴 **`/jobs/resume` 不校验 `sessionId`** —— 用全 0 假 id 探契约,它**照样起了一个真 worker 进程**(`pid=44444`)⇒ **它不是只读接口,⛔ 别当零副作用探针**;调用方必须自己先校验 id。(已 `stop` 清理,复核 `jobs=[]`。)
2. ⚠️ **`/jobs` 按 id 前缀聚合**:同 sessionId 的多个 worker 在列表里只出现 1 条 ⇒ **stop 后必须 `GET /jobs` 复核**。
3. ⚠️ **`cwd` 不强制正斜杠**(传 `e:\…` 也接受)。
⚠️ **前提**:① 需要 gateway 令牌(用户敲 `/gateway token`)② 目标会话要能以**智能体实例**形式出现(`/jobs` 里看得到;看不到就先 `resumable` → `resume`)。
⚠️ **`cwd` 是 query 而不是 body** —— 容易被漏(只在参数表里)。
⚠️ **两条路各有取舍**:`jobs` API = **立即、可指定 cwd、能续同一会话**;定时自动化 = **无需令牌、但延迟、且每次开新会话**。
⚠️ **顺带否掉一条**:`POST /api/v1/runs`("发起 Agent 执行")**只收 `{text, sender}`、没有 cwd** ⇒ 它只能在 gateway 自己的上下文里跑,**不能用来把活派给别的工作区** —— 曾误以为它是派活入口。
🔴🔴 **2026-10-02 09:2x 复核(三条通道同测,结论未变,但新钉死一条关键约束)**
| 通道 | 复核结果(本机 ai1net-dsh-server 口 `64623`) |
|---|---|
| `POST /api/v1/jobs/resume?cwd=…`(body `{sessionId}`) | ✅ **能拉起**:`200 {"id":"57f58ecf","state":"working","tempo":"idle","kind":"background","alive":true,"pid":6652,"sessionId":…,"cwd":…}`;`GET /jobs` 立刻 `n=0 → n=1`。**⛔ 不重放 prompt、⛔ 不烧 token、⛔ 不抢 writer**(`/sessions/live` 前后同一条、`writerOccupied` 不变)。`POST /jobs/{id}/stop` ⇒ `{"stopped":true}`,可逆。 |
| `POST /api/v1/jobs/{id}/reply` | ❌ **仍不 drain**:`{"delivered":true,"saved":false,"notice":"Reply sent"}`,但 **50 秒后** `jobs.updatedAt`/`tempo`/`transcript` 条数**三项全不动**(66 → 66)⇒ 与本节上表记载一致。 |
| `POST /api/v1/runs`(body `{"id":<sessionId>,"type":"message","text":…}`) | `202 {"runId":…,"status":"accepted"}` —— 接受,但语义仍是"绑该网关所服务的那个会话"。 |
🔴🔴 **本轮新钉死的约束(此前只暗示、未点明)**:`GET /api/v1/sessions/live` **是单数** —— 一个网关口只报**一条** live 会话,且它就是**桌面当前聚焦的那条**。
⇒ 同一工作区同一时刻**只有一个可投递目标**。
⇒ 推论:`follow_for_topic()` 就算解析出正确的跟进会话,只要它不是"当前聚焦"的那条,`sessions/{id}/reply` 就投不进(`follow-not-live`);而 `jobs/resume` 拉起的是 `kind:"background"` worker,**不进 live** ⇒ 也接不到 `sessions/{id}/reply`。
⇒ **净结论:跨会话"开一条**能收指令**的会话",实测只有排期一条路。`jobs` 族能给"活着的进程",给不了"可投递的会话"。**
⚠️ 别把 `alive:true` 当成"它能收消息" —— 这是两个独立的能力。
#### 1.4.5 「同侪通讯」工具(`MessageColleague` / `SpeakInChannel`)**不是**跨会话派活
宿主 CLI 里 `MessageColleague`(22)、`SendMessage`(71)、`SpeakInChannel`(29)、`TeamCreate`(12) 都真实存在,但它们是 **Agent Home / Agent Teams** 机制 —— 服务对象是**同一个会话内的团队同事**(`<teammate-message … kind="dm" channel="agent-home">` 信封、频道 `@某人`),**不是另一个对话框/另一个工作区的会话**。
⇒ 别拿它们当"给别的会话派活"用;跨会话用上面的 `jobs` API。
#### 🔴 1.4.6 第二条唤醒原语:**会话自己起的「一次性后台任务」**(2026-09-30 13:0x 四组实测)
**机制(工具契约原文,非推断)**:`run_in_background` 的任务 ——「when the command finishes you will be **automatically notified via a `<task-notification>` message in your next turn**」⇒ **任务完成 ⇒ 宿主往本会话投一条通知 ⇒ 本会话开新一轮**。
| # | 实测结论 | 证据(两线独立取证:任务侧自写时间戳 + 会话侧 `sessions.last_activity_at/updated_at`) |
|---|---|---|
| **1** | 🔴 **唤醒的触发 =「任务完成」这一个事件,与 stdout 输出无关** | A(有输出 `sleep 18`)与 B(**0 字节** `sleep 36`)**都被唤醒**;C 的中途 2 次输出**都没唤醒**(`last_activity_at` 恒定不动),只有 C 静默完成才唤醒 |
| **2** | ✅ 静默任务完全安全 | B / D 各只产生 **1 条**通知,无输出洪泛 |
| **3** | ✅ 长时长不会被沙箱回收 | D `sleep 300` **整 300 秒跑满**(12:57:55→13:02:55)照常唤醒 |
| **4** | ⛔ **「常驻循环 + 周期 echo」当不了脉冲** | 结论 1 的必然推论:`while true; do sleep 300; echo tick; done` **永不"完成" ⇒ 永不唤醒** |
⇒ **设计含义(关键)**:
- 想靠**后台任务自身**当脉冲 ⇒ **只能一次性**:`sleep N`(P-1 单发)/一轮起 K 条错峰(P-2 批量预排)/被唤醒后自己续下一棒(P-3 链式续棒,**每棒消耗一轮 AI**)。
- 想用**常驻循环** ⇒ 唤醒**不能**靠它自己,只能是**它在循环里主动调 gateway `reply`**(§1.4.1 那条)⇒ 两条线在这里汇合;且常驻**必须完全静默**(stdout 全重定向),否则撞 10 MiB diagnostic-log 天花板。
- ✅ **同时纠正一条旧因果**:09-29 的"看着像卡"**不是**"输出唤醒导致"(C 组证明中途输出不唤醒)⇒ 真因是**输出把转录推过 10 MiB ⇒ diagnostic-log dropped**。**毒药是"输出的量",不是"被唤醒"。**
- 🔴 **「持续运行」与「持续唤醒」是两件事(2026-09-30 13:09 实测)**:
- **能长跑** —— 后台任务进程**不随本轮结束被杀**(实测:上一轮起的常驻静默心跳循环,跨过本轮结束与下一次唤醒仍在跳:`hb 13:08:31 / 13:08:52 / 13:09:12`)。
- **但长跑≠唤醒** —— 同一实验里它吐过一次心跳,`sessions.updated_at` **没动**(常驻静默不唤醒)。
- ⛔ **自举续命两条路都堵**:任务内 `nohup … &` ⇒ **detached spawn 活不过工具调用边界**(日志 0 字节即死);任务内循环 ⇒ 不"完成" ⇒ 不唤醒。
- ⛔ **持久化兜底封死**:沙箱回收子进程 + 内置程序黑名单(`wsl/wslconfig/wmic/sc/reg/schtasks`)⇒ 本机**唯一可行的常驻起法 = 会话后台任务 + stdout 全重定向**。
- ⇒ **想让常驻也唤醒,只有"它在循环里主动投递 `reply`"这一条**(= §1.4.1 那条,两条线在这里汇合)。
- ✅ **链式续棒实测成立**:第 1 棒 `pulse fired 13:09:12`(唤醒)→ **在同一轮里起第 2 棒** → `fired 13:10:31`(**照样唤醒**)⇒ 「完成 → 唤醒 → 再创建下一棒」闭环。**每棒一轮 AI**(5 分钟 = 288 轮/天);现成工具 `.workbuddy/collab/wake-pulse.sh <秒数>`。
- ⚠️ 共同代价:**生命周期绑在该会话上** —— 会话关闭 / 宿主重启 ⇒ 任务与链一起没(对比:自动化是宿主侧持久排期,不受会话影响)。
📄 全文(含泳道图 + 事故链图 + P-1…P-4 方案表)⇒ `交付物/唤醒-会话自建后台任务方案评估-20260930.md`
### 1.3c 🔴 Hook **注册表会被整段清空 ⇒ 机制静默失效**(2026-09-30 实测事故)
**症状**:一个跑了半个月的钩子机制"突然不工作了",**没有任何报错**。
**真因(实物证据)**:`settings.json` 被别的事改过一次,**整个 `hooks` 段被写成空对象** ⇒ 所有闸门一起掉线;
之后只补回了其中两个 ⇒ 剩下的从那天起再没被拉起。
**三条判据(照着查,别猜)**:
1. **看备份的 mtime + 内容** —— 同目录的 `settings.json.bak-*` 一比对就知道**哪一刻清空的**
(本次:`bak-…-20260927-233116`,mtime `09-26 21:17`,`hooks = {}`)。
2. **看该钩子自己写的日志的最后一行时间** —— 那就是它**停止工作**的时刻(本次 09-26 19:19,即 4 天前)。
若日志是"从来就没有过",说明**从来没挂上**(另一回事)。
3. **看日志里的事件分布** —— 能直接判定"哪条事件路从来没被调用过"
(本次:`UserPromptSubmit` × 115 / `Stop` × 0 ⇒ 那个 `Stop` 钩子在本机**从未生效**)。
**🔴 加固(本次落地)**:**把"钩子注册表体检"做进开工第 0 步的状态脚本**(独立于钩子,否则自检本身也一起死)——
读 `settings.json` → 比对"关键闸门必须挂在哪条事件上" → 缺失显红 + 打印全部在册项 + 对每个钩子指向的 `.py` 做 `exists`。
**⚠️ 读配置**必须认 **`CODEBUDDY_CONFIG_DIR`**(本机实测 `E:\ProgramData\.workbuddy`);
写成 `~/.workbuddy` 或 `WORKBUDDY_CONFIG_DIR` 会**读到不存在的文件 ⇒ 异常被吞 ⇒ 自检恒静默**
(本机的 `stop-dialog-guard.py::path_health()` 就是这个毛病,白装了半个月)。
**⛔ 装钩子时**:命令**不要加 `-E`**(屏蔽 `PYTHONUTF8` ⇒ 含中文的载荷解码即炸 ⇒ 表现为"钩子从没被调用")。
📄 全文(含泳道图 + 事故链图)⇒ `交付物/接续机制未运行的根因与强化-20260930.md`
### 1.3d 🔴 钩子**热生效**(≠"装完要重启");且**闸门体检自己会瞎**(2026-09-30 实测)
**两条纠正(旧文档写错,实测推翻)**:
| 旧说法(脚本 docstring / 技能) | 实测 2026-09-30 |
|---|---|
| "hooks 是应用启动时快照 ⇒ 装完必须**完全重启** WorkBuddy" | ❌ 不成立:18:32 改 `settings.json` ⇒ 18:38 起 `UserPromptSubmit` 钩子就在**真实用户发言**上产出注入;`PreToolUse` 同理(改完下一次 Edit / Bash 即被记录)⇒ **改完立刻能自证** |
| 安装示例里给 `python -S **-E** <script>` | ⛔ **`-E` 是禁止项**(屏蔽 `PYTHONUTF8` ⇒ 含中文载荷 cp936 ⇒ 静默 fail-open);`-S` 可选、非必需 |
**判"某条闸门活没活"的唯一省事判据 = 看它自己日志的**最后一行时间**。已做进 `state.py` 的 `[闸门]` 段(一次调用打印 4 条日志最后时间):
```
[闸门] ✅ 关键闸门在册 | PreToolUse×4 SessionEnd×2 SessionStart×2 UserPromptSubmit×4
· 各闸门日志最后一行:收口=… 技能=… 限流=… 锁=…
```
🔴 **"在册"与"真的被调用过"是两件事** —— 陈旧时间 ⇒ 该闸门已掉线,别只看注册表就收工。
**闸门体检自己会瞎 —— 两处必修**(均在 `stop-dialog-guard.py`):
1. `path_health()` 读 `WORKBUDDY_CONFIG_DIR` / `~/.workbuddy`,本机真根却是 `CODEBUDDY_CONFIG_DIR` ⇒ **路径自检恒静默**。改:优先认 `CODEBUDDY_CONFIG_DIR`(旧变量留作回退)。
2. `guard_health()` 按"最后 40 条 DENY"统计、**不看时间** ⇒ 拿 4 天前的陈旧 DENY 每轮报「最近 N 次 Bash 被拦」,把「**闸门已停用**」伪装成「**闸门在乱拦**」。改:加 `_fresh()`,只认近 24h;取不到时间 ⇒ 不报。
⇒ 🔴 遇【门禁自检】告警**先核时间戳**,⛔ 别急着把 `bash-guard-mode` 写成 `off`。
**补回注册表时**:⛔ 别凭记忆抄 —— 去 `归档/**/settings.json.bak*` 找**唯一还留着完整 `hooks`** 的那份**逐字复原**(本次=09-24 06:01 的副本);
并核对 `matcher` 与脚本实际分支一致(`bash-output-guard` 必须 `Bash|Read`;只挂 `Bash` ⇒ 其中的 **Read>400 KB** 那条规则**静默失效**)。
📄 全文(含泳道图 + 事故链图)⇒ `交付物/补回三个闸门-20260930.md`
**补闸门会不会"误伤协作机制"?—— 实测四问四答(2026-09-30)**:
1. 协作链路(`wb-result-hook` / `decision_bridge` / `collabd.py`)**未改动、仍在册**;且**钩子是宿主起的子进程,不经 `PreToolUse`**(`PreToolUse` 只管 agent 的工具调用)⇒ 钩子自身行为不受影响。
2. **多钩子互不压制**:同一轮里【协作程序同步】(`wb-result-hook`)与 `skill-load-guard` 的 `additionalContext` **都投递到了** ⇒ UserPromptSubmit 的注入是**合并**不是抢占。
3. **锁闸门作用域**(决定"会不会挡住别人"):库外一律放行;**库内(文档库 / 代码仓)且路径不含 `.workbuddy` ⇒ 无锁时拦下、持锁时放行**;路径含 `.workbuddy` 段一律放行(协作数据落点全在豁免内)。
4. ⇒ **真正要通知用户的影响面**:恢复后,**任何会话改文档库/代码仓正文都必须先持锁**(这是 09-26 前本来的规则,属恢复非新增);自动化会话若直接改 `dsh-server-docs` 正文会失败 —— 绕法=先 `--claim-exec`/写库外工作区/落点放 `.workbuddy` 下。
## 2. 迁移评估法(四步,别跳)
1. **拆功能,不拆代码**:把源插件的能力列成清单(如 AA 的 12 项),⛔ 不要按文件/模块对照 —— 目标平台的扩展面与源平台不同构,按文件对照必然错。
2. **按"能力归属"分三层**(实证好用的分法):
- **本机层**(进程、本地文件、宿主会话读写)⇒ 看目标平台有无"能跑外部进程 + 能读宿主会话"的扩展面
- **账号与设备层**(登录、设备注册、凭据)⇒ 看有无 `account` / `openAuth` / `storage`
- **对外接入层**(被外部连入:配对服务端、反隧道、手机页面)⇒ 目标桌面应用**通常没有**,多半必须外部服务端
3. **逐条给三档判定**,⛔ 不许用"基本可行"糊过去:
- ✅ **可做**(有明确对位,能指名到具体扩展面)
- ⚠️ **半可做**(对位存在但语义要自建,或形态要退化)
- ⛔ **须外部服务端 / 做不到**(目标平台内无对应机制)
4. **收尾必写两节**:**推荐拼装路径**(谁做哪层)+ **卡点与未验证项**(尤其进程常驻性、未公开扩展面、材料缺口)。
### 2.5 判"迁不进"之后:退路是把功能落进**源平台自己的插件包**(2026-09-28 实例)
判为 ⛔ 的能力**不是就地放弃**,而是**改落点** —— 并入**源平台自己的插件包**当一块模块(本项目 = 把设备侧垫片并入 `@dsh-local/ai1net`,而不是新建一个独立插件包)。
⚠️ 落地前先过**三条硬判据**(缺一条就别并):
| # | 判据 | 为什么(都有实例) |
|---|---|---|
| 1 | 该包只许用**两端都存在**的扩展点交集(本项目 E1 明文:`inject = ['connection']` **必写**) | 源包要在**两端都装**(客户端壳 + 平台实例)⇒ 用了单端专有服务,另一端加载即挂 —— 例:垫片原 `inject = ['connection','webServer']`,而**桌面端没有 `webServer`** ⇒ 归并前必须去掉 |
| 2 | 表的**模块段**必须能唯一归到既有分段(本项目 `tenant / model / plugin / net / im`) | 表名带模块段 ⇒ **一次迁移牵动不了全部**、每模块可单独回滚;⛔ 不建"无模块段"的表 |
| 3 | 不触碰**已划出的红线边界**(本项目:「取身份/入网凭据 ⇒ 引导层,⛔ 不进包」) | 那条边界管的是**某一类凭据**(平台签发的入网凭据);⛔ 别把**本机服务的会话凭据**误当同类 —— **先分清"哪类凭据",再谈合不合规** |
⚠️ 两条流程纪律:① 加模块 = 改**冻结正文**(规格书)⇒ 出**条款草案**交该包所属线,⛔ 不擅自改冻结件;② 归并要迁出**别人的包** ⇒ 那仓若 **0 commit**,**先入库再迁**(回滚唯一保障)。
## 3. 三条反复踩到的坑
1. **把"普通进程"当成"能常驻"** —— MCP app 是 `stdio`,生命周期由宿主管;"让它常驻监听回环并主动拨出"必须**实测**,⛔ 不能按"它是普通进程"推断。
2. **把"有命名空间"当成"对你开放"** —— SDK 里有 ≠ 第三方能用(`buddyApps` 即例)。文档站只有用户手册时,一律判「未公开」。
3. **顺手把功能可行性当许可核查** —— 两者独立。**可行性评估不构成**复用他人代码的许可依据;真要复用,须另做一轮许可与供应链核查。
## 4. 判别清单(收尾自问)
- [ ] 有没有先确认「目标平台的内核是不是同源」?(WorkBuddy ≠ DSH)
- [ ] 功能清单是**按能力**列的还是**按文件**列的?
- [ ] 每条判定都指名到**具体扩展面**了吗,还是有"基本可行"?
- [ ] 对外接入层有没有被当成"可以本地解决"?
- [ ] 卡点节里有没有写**未验证项**(而非只写结论)?
- [ ] 判"做不到"之前,有没有先问过「**能不能只用钩子(`deny` + 理由 / `additionalContext`)表达**」?(见 §1.3 —— 这是最容易漏掉的一条通路)
- [ ] 判"迁不进"之后,有没有给**退路落点**(并进源平台插件包)并过 §2.5 的**三条判据**?