539 lines
64 KiB
Markdown
539 lines
64 KiB
Markdown
---
|
||||
|
|
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 的**三条判据**?
|