dsh-connect-workbuddy 5.2.3 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/README.en.md +82 -0
- package/README.md +82 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +201 -7
- package/lib/{host-heartbeat-CgYyUwpM.js → host-heartbeat-_EvGiAek.js} +1 -1
- package/lib/index.d.ts +70 -22
- package/lib/index.js +1821 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,88 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.3.0 (2026-10-22)
|
|
4
|
+
|
|
5
|
+
> 新增能力属 **minor**,本次定版 **5.3.0**。**默认关闭,关闭时插件行为与 5.2.3 完全一致**:不多一个工具、不多一条指令、不多一个请求,provider 路由、模型、凭据与计费一律未变。
|
|
6
|
+
>
|
|
7
|
+
> 这一版是 PR #104 按维护者 13 条评审逐条重做的结果,**不是原样重提**。改动分三类:把「原版会真的伤到用户」的地方修掉、把原版站不住的论证改掉、把 runner 这条从未被测过的路径补上测试。
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **CLI 路线(可选):让本机已登录的 CodeBuddy CLI 直接干活**
|
|
12
|
+
- **两个入口**:`codebuddy_ask` 工具(模型自己调用)与 `/codebuddy` 指令(用户敲,**自动取当前会话工作区**,留空 = 评审未提交改动)。
|
|
13
|
+
- **结论回注会话模型**(`injectResult`,默认开):空闲则 `steer` 起一轮,运行中则 `inject` 排进下一步。回注文本首行带 `[dsh-connect-workbuddy · 由 /codebuddy 自动回注,不是用户输入]`,并明确写「不要为了再确认一次而重复调用」,防止「回注 → 模型再问 → 再回注」的烧额度循环。
|
|
14
|
+
- **凭据复用**:`CODEBUDDY_AUTH_TOKEN` → 插件已维护的 vault,**不需要再登录一次**。区域优先于新鲜度,`regionOrder` 默认 `cn,global`。
|
|
15
|
+
- **非破坏性装配**:`cliBridge.enabled` 默认 `false`;`tools` / `commands` 用 `ctx.inject([...])`(**不是** apply 期 `ctx.get()` 采样)等待宿主服务。
|
|
16
|
+
- **配置**:`cliBridge.{enabled,binary,model,permissionMode,effort,maxTurns,timeoutSecs,regionOrder,workingDir,allowedDirs,appendSystemPrompt,allowUnattended,maxReplyChars,injectResult,maxTurnsPerSessionHour,maxTurnsPerHour}`,逐键有默认值,硬上限 `maxTurns 100` / `timeoutSecs 1800` / `maxReplyChars 400000`。**`cliBridge` 是 volatile 字段**:改它只重建这一条路由,不 dispose 两个 provider 栈。
|
|
17
|
+
- **开关在卡片里,不用手改 YAML**:账号卡片的偏好区新增一个**全局**勾选「允许调用本机 CodeBuddy CLI 干活」,默认关闭。勾上后 `/codebuddy` 与 `codebuddy_ask` 一起出现,取消后一起消失;**关着时两个入口根本不注册**,不是注册后藏起来。写的是顶层 `cliBridge.enabled`(全局,与 cn/global 分区无关),只重建这一条路由,不 dispose provider 栈、不打断在飞请求。读取走 Host 的已提交值而不是浏览器镜像——后者在本命名空间会 stale,一个「看起来开了」的假值在这里等于谎报消费能力。
|
|
18
|
+
|
|
19
|
+
### Fixes(相对 PR #104 初版)
|
|
20
|
+
|
|
21
|
+
- **默认权限模式从 `acceptEdits` 改成 `dontAsk`,并且不采用评审建议的 `plan`。** `plan` 在 `-p` 下会发 `control_request` 等 stdin 回授权(`ExitPlanMode`);初版写完 prompt 就 `stdin.end()`,于是永远等不到回音,一路等到 `timeoutSecs`(默认 900 秒)才失败 —— 真机两次实测都是这样挂死的。`dontAsk` 则是只读工具放行、写工具被拒、且**不卡住**(真机实测:9.9 秒一轮,Glob/Bash 放行、Write 被拒并如实汇报)。同时 runner 现在对每一条 `control_request` 明确拒绝,但 **`interrupt` 按工具区分**:只有 `ExitPlanMode` 带 `interrupt: true`(不带则 `plan` 挂过 70 秒),其它工具不带——带了会让整轮中止(实测 `acceptEdits` 下拒绝 Bash → `is_error`、空回复、4.2 秒),不带则 CLI 换个工具继续(实测改用 PowerShell,12 秒正常出结论)。拒绝理由也不能以 `cancelled` / `timeout waiting for user response` / `this is an unattended automated session` 开头(CLI 会把这类理由当作 synthetic deny 忽略掉,工具继续跑)。
|
|
22
|
+
- **工作目录不再「传什么用什么」。** 初版把 `args.working_dir` 原样返回,配合默认写权限,模型可以让 agent 在 `/` 或 `~/.ssh` 里跑。现在:显式目录必须是**绝对路径**、**存在且是目录**、**落在允许根之内**(本会话工作区 / `workingDir` / `allowedDirs`),比较用 `realpath` 解析后的路径并按分隔符对齐边界(`/repo-evil` 不算 `/repo`)。工具入口的目录阶梯也从「宿主 `exec.cwd`」改成「**本会话工作区**(`execution.agent.session.header.cwd`)」——宿主给工具的 execution 里根本没有 `cwd` 字段,那一级在初版里永远是 `undefined`。
|
|
23
|
+
- **装配改用 `ctx.inject`。** 初版在 `apply` 期 `ctx.get('tools')` / `ctx.get('commands')` 采样一次,与本仓库自己写的警告("sampling ctx.get() once during apply silently loses all routes on Desktop")冲突;Desktop 上服务晚到位时两个入口会静默消失。现在与生图工具同一套写法。
|
|
24
|
+
- **`/codebuddy` 的文案不再说谎。** 初版描述写「不经过模型,不消耗 DSH 额度」,但 `injectResult` 默认开会 steer 唤醒会话模型 —— 默认配置下**真的花 DSH token**。描述与 README 现在都写明这一点,并指出 `injectResult: false` 才是完全不碰模型。
|
|
25
|
+
- **加配额闸门。** 初版没有次数/频率上限,只有 `/codebuddy` 侧的 `inFlight` 挡同会话并发,工具侧完全不设防,且回注文本还写「据此继续」,形成循环烧 CodeBuddy 配额的路径。现在 `TurnGate` 同时管工具与指令(**共享同一个门**,初版的 `inFlight` 只覆盖指令),每会话每小时 6 次、全场每小时 20 次(`0` = 不限),超限具名拒绝并写日志;工具描述里带成本信号。
|
|
26
|
+
- **换号只看凭据拒绝,且不再扫模型正文。** 初版 `looksLikeAuthFailure` 的正则作用在 `${errors}\n${text}`,把 assistant 的正文也扫了 —— 评审一段含 `HTTP 401` 的代码就会误判凭据被拒、整轮换号重跑。现在只看 `{isError, errors, subtype}`,并且 `catch` 里不再一律 `continue`:超时、崩溃、ENOENT 直接抛,只有明确 401 才换下一个账号(初版在 10 个 vault 账号下最坏会跑 10 × 900 秒)。
|
|
27
|
+
- **超时真的杀干净。** 初版只 `child.kill()` 直接子进程,CLI 派生的 `git` / `npm` 变孤儿。现在 POSIX 用 `detached: true` + `process.kill(-pid)` 并在一秒后升级 SIGKILL,Windows 用 `taskkill /pid <pid> /T /F`;收到 result 帧后先 `stdin.end()` 并给 3 秒优雅退出窗口(实测 96 ms 自行退出),超时才杀树。
|
|
28
|
+
- **缓冲有上限。** stdout 单行超 8 MiB、assistant 文本累计超 2M 字符、stderr 超 16K 字符都各自有界;初版 `stderr += chunk` 与 `buffer` / `assistantText` 累积都没有上限。
|
|
29
|
+
- **所有对外文本先脱敏。** 新增 `src/cli-bridge/redact.ts`:`CODEBUDDY_AUTH_TOKEN` 就在子进程 env 里,CLI 回显它就会进会话历史。stdout 文本、`errors`、stderr 尾 6 行、回注正文全部过一遍(精确匹配本轮 token + JWT/Bearer/`sk-`/`token=` 形态)。**没有复用 `redactUpstreamText`**,因为它末尾有 `.slice(0, 300)`,会把评审结论砍成 300 字符。
|
|
30
|
+
- **不再回退 PATH。** 初版探测不到就 `spawn('codebuddy')`(Windows 上 `codebuddy.cmd` + `shell: true`)—— PATH 上的任意二进制都能拿到 vault 令牌。现在探测失败**拒绝运行**并提示用 `cliBridge.binary` 指绝对路径;显式 `.cmd` / `.bat` 在 Windows 上直接拒绝(会经 `cmd.exe` 解释)。`shell: false` 全路径覆盖,`--model` / `--append-system-prompt` 不再有被 cmd.exe 切词的面。
|
|
31
|
+
- **探测清单补齐,且不再承诺 `cbc`。** 初版只覆盖 `%APPDATA%\npm` 等四条;现在加了 `~/.npm-global`、`/opt/homebrew`、`~/.local`、pnpm/yarn/volta 全局目录,以及 nvm / n / fnm 的 node 版本目录。文档不再写「自动探测 `codebuddy` / `cbc`」——`cbc` 是同一个 `bin/codebuddy` 的别名,探测的是包路径而不是命令名。
|
|
32
|
+
- **论证改成事实。** 初版与 README 都写「跟网关说话的那个进程真的是登录着的 CodeBuddy CLI,没有身份要伪造,所以伪装那条路被封时它还能用」。实际上代码是用 `CODEBUDDY_AUTH_TOKEN` 把 plugin vault 里的**同一个 WorkBuddy 账号**交给 CLI,身份没变,`11140` 是账号级的 —— 逃掉的只有客户端指纹层。README / 顶部注释 / CHANGELOG 现在都这么写,不再把它当「封号之后的免死金牌」。
|
|
33
|
+
- **`scratchDir()` 的固定共享目录只作为 `allow_unattended` 的最后一档**,不再是默认出路;`allowUnattended` 仍然默认 `false`。
|
|
34
|
+
|
|
35
|
+
- **`killTree()` 不再把 `pid ≤ 1` 当进程组 —— 这条测试曾经会把评审者的整个登录会话打死。** 原来只挡 `pid === undefined`,而 `process.kill(-pid)` 拿到的 `pid` 一旦是 `1`(测试里为了区分两个假子进程写的 `fakeChild(1)`)就退化成 `kill(-1)`:不是杀子进程树,而是**给当前用户能 signal 的所有进程发 SIGTERM**,一秒后还升级成 SIGKILL。实测(macOS)跑 `vitest run tests/cli-bridge-runner.spec.ts` 会让 launchd 记录 `node[<pid>]` 向 Finder / Dock / WindowManager / ControlCenter / 用户正开着的微信 / DSH 宿主同时发 SIGTERM;DSH 的 renderer 作为受害者退出后又被 `DesktopFatalRecovery` relaunch,用户视角就是「DSH 反复重启,还把微信一起关了」。现在 group-kill 只对 `Number.isInteger(pid) && pid > 1` 开放,其余退化成单进程 `child.kill()`;测试里的假 pid 从 `1/2` 改成 `51_001/51_002`,并新增回归用例(spy `process.kill`,断言永不出现非正数信号目标)。
|
|
36
|
+
|
|
37
|
+
### Fixes(同期并入 main:issue #102 / #108)
|
|
38
|
+
|
|
39
|
+
- **#102 没修完:`dsh-foreign` 的历史消息仍然把思考摊平成正文** —— 5.2.3 的 `alignAssistantIdentities()` 只改 `provider`,但 DSH 交给 pi-ai 的这条消息**根本到不了 provider 比较那一步之前就换了 api**。
|
|
40
|
+
- 真实链路:`dsh-llm-pi-ai` 的 `replayedAssistant()` 发现 `state.response.provider !== source.provider`(`workbuddy` vs `workbuddy-global`)就判定 replay 无效,改走 `foreignAssistant()`,而它把 `api` 写成 `dsh-foreign`、`provider` 取 `source.provider`(路由名)。于是 `isSameModel` 的两条比较**同时**失败;只补 `provider` 之后 `api` 仍然不等,thinking 照样被降级成 `text`。
|
|
41
|
+
- 实测(DSH 0.2.1-rc.2 + 本插件 5.2.3,真实 global 会话):只改 `provider` 时,一条 1514 字符的 thinking 块变成 1514 字符的**正文**;两个字段都修后恢复成 `thinking`。
|
|
42
|
+
- 修法:`alignAssistantIdentities()` 在改 `provider` 之外,**仅当 `api === 'dsh-foreign'`**(宿主回退标记,插件自己从不会发这个值)时把它改回 `openai-completions`;其他任何 api 原样保留。
|
|
43
|
+
- **为什么这不算伪造签名**:`foreignAssistant()` 造的 thinking 块**没有 `thinkingSignature`**,而 pi-ai 的请求组装只有存在签名时才会写 `reasoning_content`,所以该块根本不会进请求——与现在一样不发,只是不再变成可见正文。另外 `api` 只取自 `model.api`(请求组装全文不读消息自带的 `api`),改它不改变线上形状。
|
|
44
|
+
- 真正的换模型仍然降级:`isSameModel` 还要求 model id 相等(有测试钉住)。
|
|
45
|
+
|
|
46
|
+
- **(#108)输入框积分面板的失败原因被截断成半句话** —— 截图里那一行是「✗ 网关拦的是提示词本身,不是这个账号—」,到破折号就没了,它要解释的那半句永远看不到。
|
|
47
|
+
- 两个原因叠在一起,**缺一个都还能复现**:
|
|
48
|
+
1. **文案选错了**:面板那一行按设计是**短标签**(`availabilityMark` 的注释写明 "the SHORT form of the same fact"),但它取的是 `outcomeText()`,而 `channel-rejected` 在那里返回 `row.probeChannelRejected` —— **一段 88 字(中文)/ 179 字(英文)的解释**,本来是给账号池卡片那一列宽地方写的。面板这一列实际只有约 180px。
|
|
49
|
+
2. **CSS 是硬切不是省略**:面板的 switch 给账号名设了 `white-space:nowrap` + `overflow:hidden`(名字必须单行加省略号),而原因那一行**继承了这两条**。于是溢出部分被**直接切掉**,连省略号都没有 —— 硬切不带任何「后面还有」的信号,所以读起来像渲染坏了,而不是像被截断。
|
|
50
|
+
- 修法分两层:
|
|
51
|
+
- 新增 `outcomeMarkText()`:面板这一栏拿**结论**(`✗ 网关拦的是提示词`,新增 `row.probeChannelRejectedShort`),完整解释一个字不删 —— 它仍在行的 `title` 里(`availabilityLine`,含绝对时刻),也仍在账号池卡片上。一句话:**解释没有消失,只是不再挤在放不下它的那一格**。
|
|
52
|
+
- `.dsm-workbuddy-composer-panel-mark` 显式 `white-space:normal` + `overflow-wrap:anywhere`。这条针对**根因**:这一行是本行里唯一长度不受布局控制的文字(译文,两种语言相差 2 倍),所以它该**换行**而不是被切 —— 多一行是能读的结果,切断半个字不是。
|
|
53
|
+
- 同栏另外两个偏长的结论(`credential-rejected` 39 字、`unavailable` 19 字)现在会正常折行,不再被切。
|
|
54
|
+
|
|
55
|
+
- **(#108 后续)「被限流 · 约 2 小时后」补成「……后恢复」** —— 只有时长、没有动词,读者还得自己猜这两小时后会发生什么;而这一行整行的职责就是回答「这个账号现在能不能用」。
|
|
56
|
+
- 新增 `recoversInText()`(`src/client/remaining.ts`)与四个 `row.recovers*` 键,**数字、取整、空串契约与 `remainingText()` 完全一致**,只多一个动词:`约 2 小时后恢复` / `约 25 分钟后恢复` / `约 3 天后恢复` / `即将恢复`;英文 `recovers in about 2 h`。
|
|
57
|
+
- **只改面板,不改账号池卡片**,这是有意为之:卡片那句自带动词(`✗ 被限流 · 10/22, 14:47 之后可再用`),再叠「恢复」就成了 `之后可再用 · 约 2 小时后恢复`,一句话说两遍。所以两个函数并存,而不是给 `remainingText()` 加参数 —— 加了参数,那两个自带动词的调用点迟早会多长出一个。
|
|
58
|
+
- 随之调整了一条既有的跨界面契约用例:它原本断言两边「同一个 key 名逐字相同」,现在断言**同一测量算出同一个时长与取整**(把 `row.recovers*` 映射成 `row.remaining*` 后比对)。这才是那个测试真正要守的不变量:两面不会对「还剩多久」给出两个答案。
|
|
59
|
+
|
|
60
|
+
### Tests
|
|
61
|
+
|
|
62
|
+
- 新增 `tests/cli-bridge.spec.ts`(69 条)与 `tests/cli-bridge-runner.spec.ts`(17 条),共 **86 条**(含 `killTree` 的 `pid ≤ 1` 回归用例)。
|
|
63
|
+
- 开关本身另有两条:`tests/cli-bridge-switch.spec.ts`(`__save` 的合并语义:显式 false 生效、同级 cap 不被冲掉)与 `tests/cli-bridge-card-switch.spec.tsx`(读取只认显式 true、写入先 Host 后 scope、卡片默认关且点击只 POST 全局字段)。
|
|
64
|
+
- **`runner.ts` 第一次有测试。** 初版的 53 条全是纯逻辑与注册形状,`runOneTurn` / `runAgent` / `spawn` 一次都没被调用过 —— 而评审找到的第 5/6/7/8 条缺陷全在那个盲区里。新 spec 用 `vi.mock('node:child_process')` 造 fake child,断言的是模块自己声明的协议:
|
|
65
|
+
- `control_request` 的回包形状(`subtype:'success'` / `request_id` 原样回传 / `allowed:false`),且拒绝理由不以 synthetic-deny 前缀开头、result 帧前 **stdin 不 close**(这一条在修复前必红);`interrupt` 分两条:普通工具**不带**(带了会中止整轮)、`ExitPlanMode` **必带**(不带则 `plan` 挂死)
|
|
66
|
+
- 多 text block 拼接、tool_use 计数、result 帧的 usage / session_id / errors 解析
|
|
67
|
+
- 单行超 `MAX_FRAME_BYTES` 即失败、无 result 帧就退出时保留 stderr 尾、超时、宿主取消(精确文本 `cancelled by the host`)、CLI 路径不存在时**从未 spawn**
|
|
68
|
+
- 换号:只有 401 才换、**超时/崩溃不换**、最后一条 401 也不重试、空凭据列表报 `no credential worked`
|
|
69
|
+
- 脱敏:token 与 `Bearer …` 形态既不进正文也不进 errors
|
|
70
|
+
- 主 spec 的新增/重写部分:`resolveWorkingDir` 的 9 条真目录断言(不存在 / 是文件不是目录 / 相对路径 / 允许根之外 / `allowedDirs` 放行 / 前缀相同的兄弟目录拒绝)、`TurnGate` 的 7 条(假时钟注入、并发 busy、配额拒绝、窗口滑过恢复、`0` = 不限、`clear()`)、`resolveCliEntry` 的新签名与「显式路径失败不得回退 PATH」、`looksLikeAuthFailure` 的新签名。
|
|
71
|
+
- 全量:**116 文件 / 1920 条通过(另 2 条 skip)**;`tsc -p tsconfig.json` 与 `tsc -p tsconfig.client.json` 均 exit 0;`tsdown` build 成功(`lib/index.js` 446.54 kB)。
|
|
72
|
+
- **另做了一轮真机联调**(`@tencent-ai/codebuddy-code` 2.171.0,真实 vault 账号,非 mock),六项全过:①`dontAsk` 默认 9.9 秒一轮,Glob/Bash 放行、Write 被拒且如实汇报;②`acceptEdits` 下 Bash 被拒后 CLI 换 PowerShell 完成任务(12.2 秒、`ok:true`),验证 `interrupt` 不能对所有工具开;③`plan` 的 `ExitPlanMode` 被带 `interrupt:true` 拒掉后 12.8 秒结束,不再挂满 `timeoutSecs`;④`acceptEdits` 真的写文件,且落在我们指定的 cwd(`hello.txt` 内容一字不差);⑤6 秒超时 → 6.0 秒抛 `codebuddy exceeded its 6s budget`,**事后无残留 codebuddy 进程**(`taskkill /T` 生效);⑥凭据链真实可用:global 令牌得到 `401 Authentication required`、换 cn 后 8.1 秒成功,且全程 token 未进正文/errors。
|
|
73
|
+
|
|
74
|
+
### Tests(同期并入 main:issue #102 / #108)
|
|
75
|
+
|
|
76
|
+
- `tests/issue59-cross-region-thinking.spec.ts`:原「rewrites ONLY provider — api and model are untouched」用例改写为「只修宿主回退 api,原生 api 逐字节不动」。
|
|
77
|
+
- 新增 2 条:宿主真实形状(`api:'dsh-foreign'` + 无签名 thinking)能保住 thinking(修复前必红);provider 已对齐时 `dsh-foreign` 仍被单独修好(防止两个字段的修复互相短路)。
|
|
78
|
+
- 已验证:撤回 `src/adapter.ts` 的改动后,新增用例立刻转红(2 failed / 12 passed)。
|
|
79
|
+
- `tests/composer-credits-render.spec.tsx`(+3 条,`issue #108` 分节):用**真实中文 locale**(不是回显 key 的桩)断言面板打印的是 `✗ 网关拦的是提示词` 且长度 < 20;完整解释仍在行 `title` 里(含「第三方 agent 过滤」);样式表里 mark 一栏必须 `white-space:normal` + `overflow-wrap:anywhere`。
|
|
80
|
+
- 变异验证:撤回 `account-state.ts` + `styles.ts` 的改动 → 2 条立刻转红;单独把 locale 里的长文案改短(模拟「删掉解释」)→ 第 2 条转红。三条都真的在守卫,不是空转。
|
|
81
|
+
- 全量:**111 文件通过 / 1 跳过;1819 通过 / 2 跳过**;两个 tsconfig 均 exit 0;build 成功。
|
|
82
|
+
- `tests/remaining.spec.tsx`(+2 条):`recoversInText()` 与 `remainingText()` 对同一批时刻给出**相同数字与单位**(只差前缀);过去时刻返回空串。
|
|
83
|
+
- `tests/composer-credits-render.spec.tsx`:`WAIT` 正则改指 `row.recovers*`;跨界面契约用例改为比较「时长」而非「key 名」,并注明卡片为何不该加「恢复」。
|
|
84
|
+
- 变异验证:把 `availabilityMark` 里的 `recoversInText` 换回 `remainingText` → 上述 2 条立刻转红。
|
|
85
|
+
|
|
3
86
|
## 5.2.3 (2026-10-22)
|
|
4
87
|
|
|
5
88
|
### Fixes
|
package/README.en.md
CHANGED
|
@@ -53,6 +53,7 @@ It depends on the WorkBuddy client's own endpoints (not a public API), so the pl
|
|
|
53
53
|
- **Multi-candidate credential paths** — probes the platform defaults for macOS / Windows / Linux in turn, overridable by environment variable or directly in the card. **When nothing is found, the card lists which paths were probed and why each one failed**: five distinct reasons (absent / unreadable / no usable token / encrypted with no key available / belongs to the other region), so the easiest case to misdiagnose — signed in, but the desktop app is not present — says the app must be there instead of telling you to sign in again. The **belongs to the other region** reason is different in kind: that sign-in is real and usable, it is simply filed under the other tab, so the card states the fix outright — switch tabs, no need to sign in again. The list says it once: the paragraph states the conclusion, the collapsed list supplies the per-path detail, and the two never repeat each other.
|
|
54
54
|
- **Secure loopback shim** — one random port + in-process random secret per region; the real WorkBuddy token is never handed to pi-ai.
|
|
55
55
|
- **Command-line diagnostics** — `status` / `doctor` / `logout`, reporting sign-in and credits per region, without a browser.
|
|
56
|
+
- **CLI route (optional, off by default)** — Disguising requests as a CodeBuddy client to pass the channel check is an arms race, and it has already produced account-level restrictions (issue #98, code 11140). A second route skips that disguise: **let the locally signed-in CodeBuddy CLI run the task itself** — it reads files, runs git, edits code. Turn on `cliBridge.enabled` and the plugin gains a `codebuddy_ask` tool and a `/codebuddy` command, with the result injected back into the session model. While it is off, the plugin behaves **exactly** as before. **It bypasses the client-fingerprint layer, not the account layer**: it uses the same accounts from this plugin's vault, so it is not a get-out-of-jail card after a block. See [CLI route](#cli-route-optional-let-codebuddy-do-the-work).
|
|
56
57
|
|
|
57
58
|
## How it works
|
|
58
59
|
|
|
@@ -285,6 +286,87 @@ dsh plugin --profile web exec dsh-connect-workbuddy status
|
|
|
285
286
|
|
|
286
287
|
> 🔒 **The credential is the account**: that file is a directly usable session. Transfer it over an encrypted channel (`scp` / private network), tighten it with `chmod 600` once on the server, and keep it out of anything that gets packaged or committed.
|
|
287
288
|
|
|
289
|
+
## CLI route (optional): let CodeBuddy do the work
|
|
290
|
+
|
|
291
|
+
> Off by default (`cliBridge.enabled: false`). While it is off, the plugin behaves **exactly** as before: no extra tool, no extra command, no extra request.
|
|
292
|
+
|
|
293
|
+
This route uses CodeBuddy as a **tool** rather than as a model provider: the plugin exposes a `codebuddy_ask` tool and a `/codebuddy` command, and a local, already signed-in CodeBuddy CLI runs one real turn in its own process — reading files, running `git`, editing code — then hands the conclusion back.
|
|
294
|
+
|
|
295
|
+
**What this actually buys, stated plainly.** The provider route has to make a request look like a CodeBuddy client to pass the gateway's channel check; since 4.15.x that check matches an exact substring mid-prompt, and the plugin dodges it with zero-width spaces at punctuation boundaries. That is an arms race, and it is no longer free: issue #98 has produced an **account-level** restriction (`code 11140`, "the account shows abnormal behaviour and has been restricted").
|
|
296
|
+
|
|
297
|
+
The CLI route escapes the **client-fingerprint layer**, not the account layer. It hands the CLI `CODEBUDDY_AUTH_TOKEN` taken from this plugin's own vault — the same WorkBuddy accounts. So the claim "the process talking to the gateway really is the signed-in CodeBuddy CLI, there is no identity to forge, therefore the account cannot be restricted" **does not hold**: the identity is the same set of accounts, `11140` is account-level, and the same accounts are just as restricted on this route. What is genuinely bypassed is "does this request's client fingerprint look like CodeBuddy" — a real gain against the channel check, but not immunity from blocking. The two routes are complementary fallbacks, not a replacement.
|
|
298
|
+
|
|
299
|
+
### Prerequisites
|
|
300
|
+
|
|
301
|
+
- The CodeBuddy CLI is installed (npm package `@tencent-ai/codebuddy-code`; `codebuddy` and `cbc` are the same entry point). For a non-standard install, point `cliBridge.binary` at the absolute path.
|
|
302
|
+
- It is signed in. Credentials are resolved in this order: the `CODEBUDDY_AUTH_TOKEN` environment variable, then this plugin's **own** vault (`$DSH_HOME/workbuddy-vault/<region>/<account>.json`). In other words, **the accounts you already signed in for this plugin are reused directly — no second sign-in.** (Verified against the real CLI: `getAuthenticationAttributesSync` checks `CODEBUDDY_AUTH_TOKEN` ahead of its own sign-in file.)
|
|
303
|
+
- Region **outranks freshness**: a domestic token is valid against `copilot.tencent.com`, while a global token gets a 401 there (measured: `401 Authentication required ... (target: https://copilot.tencent.com)`). Hence the default `cn,global` order, falling through only when a region fails.
|
|
304
|
+
|
|
305
|
+
### Turning it on
|
|
306
|
+
|
|
307
|
+
```jsonc
|
|
308
|
+
{
|
|
309
|
+
"cliBridge": {
|
|
310
|
+
"enabled": true
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Every other key has a default. See the table below. Changing them does not need a host restart: `cliBridge` is a volatile field, so a change rebuilds only this route instead of reloading both provider stacks.
|
|
316
|
+
|
|
317
|
+
### The two entrances
|
|
318
|
+
|
|
319
|
+
| Entrance | Who triggers it | Working directory | Whose budget it spends |
|
|
320
|
+
| --- | --- | --- | --- |
|
|
321
|
+
| `codebuddy_ask` tool | the model decides to call it | the session workspace, or a `working_dir` the caller passes, or `cliBridge.workingDir` | CodeBuddy quota, plus the DSH tokens of the wake-up the result is injected into (`injectResult` is on by default) |
|
|
322
|
+
| `/codebuddy` command | a human types it | **always the current session workspace** | the same — the command itself skips the model, but by default the result is injected back into it |
|
|
323
|
+
|
|
324
|
+
An **empty** `/codebuddy` means "review the uncommitted changes in this workspace" (that invocation is forced to the read-only `dontAsk` permission mode and is told to run `git diff` itself and report only what it actually saw); **text after it** is sent through verbatim.
|
|
325
|
+
|
|
326
|
+
When the turn finishes, the conclusion is **injected back into the session model** (`cliBridge.injectResult`, on by default): an idle session is woken for a new turn, a running one gets it queued for its next step. **That step really spends DSH tokens** — set `injectResult` to `false` to leave the model untouched, and the `/codebuddy` result is then shown to you only. The injected text starts with `[dsh-connect-workbuddy · 由 /codebuddy 自动回注,不是用户输入]` so neither you nor the model mistakes it for a human message.
|
|
327
|
+
|
|
328
|
+
### Permissions: read-only by default
|
|
329
|
+
|
|
330
|
+
`permissionMode` defaults to **`dontAsk`**: read-only tools (Read/Glob/Grep) run, writes and command execution are refused, and the turn **does not hang** — the CLI ends the turn itself and reports which tools were denied.
|
|
331
|
+
|
|
332
|
+
**Why not `plan`**, the more conservative candidate and the default this review suggested: under `-p` (headless), `plan` emits a `control_request` and waits on stdin for an approval, and an unattended call has nobody at the terminal to click it. Left unhandled, the CLI waits out its own 60-second protocol timeout while the plugin waits out `timeoutSecs` (900 seconds by default) — both measured attempts hung exactly this way. Hence `dontAsk` as the default, plus an explicit denial of every `control_request` in the runner. **The denial is per tool.** Only `ExitPlanMode` carries `interrupt: true` (without it `plan` will not finish; measured hanging past 70 seconds). Every other tool (Bash, Write, …) must NOT carry it: with it the whole turn aborts (measured under `acceptEdits` — a denied Bash gave `is_error`, an empty reply, and a 4.2-second turn), while without it the CLI records the denial and picks a different tool, finishing normally (measured: it switched to PowerShell and succeeded in 12 seconds). To let it edit files for real, set `acceptEdits` explicitly.
|
|
333
|
+
|
|
334
|
+
### Working directory: a four-step ladder plus allowed roots
|
|
335
|
+
|
|
336
|
+
`codebuddy_ask` resolves its directory as: the caller's `working_dir` → **this session's workspace** (`session.header.cwd`) → `cliBridge.workingDir` → (only when `allow_unattended` is on) a scratch directory → otherwise it **refuses and explains the three ways out**.
|
|
337
|
+
|
|
338
|
+
Whichever step wins, the directory must pass three checks: **absolute**, **exists and is a directory**, and **inside an allowed root** (the session workspace, `cliBridge.workingDir`, `cliBridge.allowedDirs`). `allowedDirs` is a comma-separated list of absolute paths for directories outside the workspace. So a model that invents `/` or `~/.ssh` cannot get it executed, and `..` cannot walk out (the comparison uses resolved `realpath` and matches on path-separator boundaries, so `/repo-evil` is not inside `/repo`).
|
|
339
|
+
|
|
340
|
+
### Configuration
|
|
341
|
+
|
|
342
|
+
| Key | Default | Meaning |
|
|
343
|
+
| --- | --- | --- |
|
|
344
|
+
| `enabled` | `false` | The only switch. When `false`, this route registers nothing. |
|
|
345
|
+
| `binary` | `''` | Absolute path to the CLI executable. **When empty, only known install locations are probed and a miss refuses to run** — it never falls back to whatever `codebuddy` is on PATH (that binary would be handed the vault token). |
|
|
346
|
+
| `model` | `''` | Passed to `--model`; empty uses the CLI's own default. |
|
|
347
|
+
| `permissionMode` | `dontAsk` | `dontAsk` (default, read-only) / `plan` / `default` / `auto` / `acceptEdits` / `bypassPermissions`. The last two can write. |
|
|
348
|
+
| `effort` | `''` | `--effort`: `minimal` / `low` / `medium` / `high` / `xhigh` / `max`; empty uses the CLI default. |
|
|
349
|
+
| `maxTurns` | `10` | CLI turn limit for one task (hard cap 100). |
|
|
350
|
+
| `timeoutSecs` | `900` | Wall-clock timeout for one task, in seconds (hard cap 1800). |
|
|
351
|
+
| `regionOrder` | `cn,global` | Credential region priority. |
|
|
352
|
+
| `workingDir` | `''` | Default working directory for the tool entrance, and also one of the allowed roots. |
|
|
353
|
+
| `allowedDirs` | `''` | Extra allowed roots: comma-separated absolute paths. |
|
|
354
|
+
| `appendSystemPrompt` | `''` | Appended to the CLI system prompt; empty appends nothing. |
|
|
355
|
+
| `allowUnattended` | `false` | Allow falling back to a scratch directory when no working directory exists. Off by default: the CLI can read and write, so it should not run in a directory nobody named. |
|
|
356
|
+
| `maxReplyChars` | `60000` | Maximum characters handed back to the session model; longer replies are truncated with a note (hard cap 400000). |
|
|
357
|
+
| `injectResult` | `true` | Whether to inject the result back into the session model. Only `false` leaves the model completely untouched. |
|
|
358
|
+
| `maxTurnsPerSessionHour` | `6` | Turns per session per hour. `0` disables the cap. |
|
|
359
|
+
| `maxTurnsPerHour` | `20` | Turns across all sessions per hour. `0` disables the cap. |
|
|
360
|
+
|
|
361
|
+
### Boundaries
|
|
362
|
+
|
|
363
|
+
- **No shared context.** CodeBuddy is a separate agent and cannot see this conversation. It is good at "review this module independently" or "go investigate"; it is bad at "continue the thread I was on" — it has to re-read the repository.
|
|
364
|
+
- **Writing is gated, not sandboxed.** `dontAsk` is read-only by default; once you set `acceptEdits` it really does edit files in the workspace. The allowed roots constrain **where** it runs, not **what** it does there.
|
|
365
|
+
- **Two separate budgets.** CodeBuddy CLI quota and DSH model credit are unrelated. The command itself spends no DSH model tokens, but the default injection makes the session model take another turn — that one is real DSH usage. The tool description carries the cost signal, and the hourly caps exist to stop an "inject → model asks again → inject again" loop.
|
|
366
|
+
- **It fails instead of trying every account.** Only a recognised credential refusal (401 / `Authentication required` / `/login`) moves on to the next account; timeouts, crashes and a missing CLI are raised immediately — retrying a 900-second timeout across a dozen vault accounts is worse than the failure. The walk also no longer looks at what the model said: an early version scanned the CLI's **prose** too, so reviewing code that merely quoted `HTTP 401` discarded a good turn.
|
|
367
|
+
- **A timeout kills the whole process tree.** The CLI spawns `git` / `npm`, so killing only the direct child leaves orphans. POSIX signals the process group; Windows uses `taskkill /T`.
|
|
368
|
+
- **Restart needed.** New bundles and JS changes only take effect after a DSH host restart.
|
|
369
|
+
|
|
288
370
|
## Command line
|
|
289
371
|
|
|
290
372
|
```sh
|
package/README.md
CHANGED
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
- **凭据路径多候选** —— macOS / Windows / Linux 逐一探测默认位置,也支持环境变量与卡片内直接指定。**未登录时卡片会列出「探测过哪些路径、各自为什么没成」**:五档原因(不存在 / 读不了 / 没有可用 token / 字段已加密且拿不到密钥 / 属于另一个地区)分开呈现,因此「已经登录了、但桌面 App 不在场」这种最容易误判的情况会明说需要 App 在场,而不是叫你重新登录一次。**属于另一个地区**那一档尤其不同:登录信息是真实可用的,只是挂在另一个标签页下,卡片会直接给出结论「切换标签页即可,不用重新登录」。列表只讲一次——提示段落说结论,折叠列表给路径细节,两处不重复同一件事。
|
|
61
61
|
- **安全 loopback shim** —— 每区域一个随机端口 + 进程内随机 secret,真实 WorkBuddy token 不交给 pi-ai。
|
|
62
62
|
- **命令行诊断** —— `status` / `doctor` / `logout`,按区域报告登录与积分,无需浏览器即可确认宿主状态。
|
|
63
|
+
- **CLI 路线(可选,默认关闭)** —— 伪装成 CodeBuddy 客户端过渠道校验是一条军备竞赛的路,而且已经出现账号级限制(issue #98 的 code 11140)。另有一条路不做这层伪装:让**本机已登录的 CodeBuddy CLI 自己跑任务**,由它去读文件、跑 git、改代码。打开 `cliBridge.enabled` 后插件多出 `codebuddy_ask` 工具和 `/codebuddy` 指令,跑完把结论回注给会话模型。关闭时插件行为与之前**完全一致**。**它绕开的是客户端指纹层,不是账号层**:用的仍是本插件 vault 里的同一批账号,所以不该把它当成「封号之后的免死金牌」。详见 [CLI 路线](#cli-路线可选让-codebuddy-直接干活)。
|
|
63
64
|
|
|
64
65
|
## 工作原理
|
|
65
66
|
|
|
@@ -295,6 +296,87 @@ dsh plugin --profile web exec dsh-connect-workbuddy status
|
|
|
295
296
|
|
|
296
297
|
> 🔒 **凭据等同于账号**:这份文件里是可直接使用的登录态。传输时请走加密通道(`scp` / 私网),落到服务器后用 `chmod 600` 收紧权限,别放进任何会被打包或提交的目录。
|
|
297
298
|
|
|
299
|
+
## CLI 路线(可选):让 CodeBuddy 直接干活
|
|
300
|
+
|
|
301
|
+
> 默认关闭(`cliBridge.enabled: false`)。关闭时插件的行为与之前**完全一致**:不多一个工具、不多一条指令、不多一个请求。
|
|
302
|
+
|
|
303
|
+
这条路不把 CodeBuddy 当模型提供方,而是当**工具**:插件提供一个 `codebuddy_ask` 工具和一条 `/codebuddy` 指令,由本机已登录的 CodeBuddy CLI 在自己的进程里跑一轮真实任务——读文件、跑 `git`、改代码——再把结论交回来。
|
|
304
|
+
|
|
305
|
+
**这条路到底绕开了什么,说清楚。** 提供方那条路要让请求看起来像 CodeBuddy 客户端才能过网关的渠道校验,4.15.x 起那条校验已经升级成「按提示词中段做精确子串匹配」,靠句读处插零宽空格躲开——这是军备竞赛,而且不再免费:issue #98 里已经出现**账号级**限制(`code 11140`,「账号存在异常行为,已被限制使用」)。
|
|
306
|
+
|
|
307
|
+
CLI 路线逃掉的是**客户端指纹层**,不是账号层。它用 `CODEBUDDY_AUTH_TOKEN` 把本插件 vault 里的同一个 WorkBuddy 账号交给 CLI,所以「跟网关说话的是真的 CodeBuddy CLI 进程、没有身份要伪造、因此账号不会被限制」这句话**不成立**:身份仍然是同一批账号,`11140` 是账号级的,同一批账号在 CLI 路线上一样受限。真正被绕开的只有「这条请求的客户端指纹像不像 CodeBuddy」——这一层是真实的收益(渠道校验不会再因为提示词里有句读而拒你),但它不等于「免封」。两条路是互补的容错,不是替代。
|
|
308
|
+
|
|
309
|
+
### 前提
|
|
310
|
+
|
|
311
|
+
- 本机装了 CodeBuddy CLI(npm 包 `@tencent-ai/codebuddy-code`,`codebuddy` 与 `cbc` 是同一个入口)。装在非标准位置就用 `cliBridge.binary` 指绝对路径。
|
|
312
|
+
- 它已登录。插件按这个顺序找凭据:环境变量 `CODEBUDDY_AUTH_TOKEN` → 本插件**已经维护的**那份 vault(`$DSH_HOME/workbuddy-vault/<region>/<account>.json`)。也就是说,**你为了用这个插件登录过的账号,CLI 路线直接复用,不需要再登录一次。**(真机验证:CLI 的 `getAuthenticationAttributesSync` 把 `CODEBUDDY_AUTH_TOKEN` 排在自己的登录文件之前。)
|
|
313
|
+
- 区域**优先于新鲜度**:国内账号的令牌对 `copilot.tencent.com` 有效,国际版的令牌在那边会 401(真机实测:`401 Authentication required ... (target: https://copilot.tencent.com)`)。所以默认顺序是 `cn,global`,某个区域失败才换下一个。
|
|
314
|
+
|
|
315
|
+
### 打开
|
|
316
|
+
|
|
317
|
+
```jsonc
|
|
318
|
+
{
|
|
319
|
+
"cliBridge": {
|
|
320
|
+
"enabled": true
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
其余键都有默认值,不写就是默认。完整的键见下表。改这些键不需要重启宿主:`cliBridge` 是 volatile 字段,改动只重建这一条路由,不会重载两个 provider 栈。
|
|
326
|
+
|
|
327
|
+
### 两个入口
|
|
328
|
+
|
|
329
|
+
| 入口 | 谁触发 | 工作目录 | 花谁的额度 |
|
|
330
|
+
| --- | --- | --- | --- |
|
|
331
|
+
| `codebuddy_ask` 工具 | 模型自己决定调用 | 会话工作区;或调用方传的 `working_dir`;或 `cliBridge.workingDir` | CodeBuddy 的配额 + 那一次回注唤醒的 DSH token(`injectResult` 默认开) |
|
|
332
|
+
| `/codebuddy` 指令 | 用户敲 | **自动取当前会话的工作区** | 同上——指令本身不经过模型,但默认会把结论回注给模型 |
|
|
333
|
+
|
|
334
|
+
`/codebuddy` 后面**留空** = 让 CodeBuddy 评审当前工作区里未提交的改动(这种调用强制只读的 `dontAsk` 权限模式,并且要求它自己跑 `git diff`、只报确实看见的问题);**写了字** = 原样把这句话发过去。
|
|
335
|
+
|
|
336
|
+
跑完之后结论会**回注给会话模型**(`cliBridge.injectResult`,默认开):会话空闲时直接唤醒它起一轮,正在跑就排进它的下一步。**这一步真的花 DSH token**——把 `injectResult` 设成 `false` 才是「完全不碰模型」,那时 `/codebuddy` 的结论只显示给你看。回注的文本以一行 `[dsh-connect-workbuddy · 由 /codebuddy 自动回注,不是用户输入]` 开头,免得你(或者模型)把它当成自己打的字。
|
|
337
|
+
|
|
338
|
+
### 权限:默认只读
|
|
339
|
+
|
|
340
|
+
`permissionMode` 默认 **`dontAsk`**:只读工具(Read/Glob/Grep)自动放行,写文件与执行命令被拒,且**不卡住**——CLI 会自己结束这一轮并告诉你哪些工具被拒了。
|
|
341
|
+
|
|
342
|
+
**为什么不是 `plan`**(`plan` 是更保守的候选,也是本次评审建议的默认值):在 `-p`(headless)下 `plan` 会发一条 `control_request` 等 stdin 回授权,而无人值守的调用没有人在终端上点「同意」。不处理这条请求,CLI 会一直等到 60 秒的协议超时,插件则会一路等到 `timeoutSecs`(默认 900 秒)才失败——实测两次都是这样挂死的。所以默认取 `dontAsk`,同时在 runner 里把每一条 `control_request` 明确拒掉。**拒绝时要按工具区分**:只有 `ExitPlanMode` 带 `interrupt: true`(不带则 `plan` 不结束,实测挂过 70 秒);其它工具(Bash、Write…)**不能**带——带了会让整轮直接中止(实测 `acceptEdits` 下拒绝 Bash:`is_error`、空回复、4.2 秒收场),不带则 CLI 记下这次拒绝、换一个工具继续,整轮正常出结论(实测改用 PowerShell 后 12 秒成功)。想让它真的能改文件,显式设成 `acceptEdits`。
|
|
343
|
+
|
|
344
|
+
### 工作目录:四级阶梯 + 允许根
|
|
345
|
+
|
|
346
|
+
`codebuddy_ask` 的工作目录按顺序取:调用方传的 `working_dir` → **本会话的工作区**(`session.header.cwd`)→ `cliBridge.workingDir` → (只有 `allow_unattended` 打开时才)临时目录 → 以上都没有就**拒绝并说明三条出路**。
|
|
347
|
+
|
|
348
|
+
不管来源是哪一级,选中的目录都要过三关:**必须是绝对路径**、**必须存在且是一个目录**、**必须落在允许根之内**(本会话工作区、`cliBridge.workingDir`、`cliBridge.allowedDirs`)。`allowedDirs` 是逗号分隔的绝对路径列表,用来放行工作区之外的目录。这样「模型自己编一个 `/` 或 `~/.ssh` 传进来」不会被执行,`..` 也走不出去(比较的是 `realpath` 解析后的路径,并按路径分隔符对齐边界,`/repo-evil` 不算 `/repo` 的子目录)。
|
|
349
|
+
|
|
350
|
+
### 配置
|
|
351
|
+
|
|
352
|
+
| 键 | 默认 | 说明 |
|
|
353
|
+
| --- | --- | --- |
|
|
354
|
+
| `enabled` | `false` | 唯一的开关。`false` 时这个路由什么都不注册。 |
|
|
355
|
+
| `binary` | `''` | CLI 可执行文件的绝对路径。**留空时只按已知安装位置探测,找不到就拒绝运行**——不会回退到 PATH 上的 `codebuddy`(那个二进制会拿到 vault 令牌,不能随便认)。 |
|
|
356
|
+
| `model` | `''` | 传给 `--model`;留空用 CLI 自己的默认模型。 |
|
|
357
|
+
| `permissionMode` | `dontAsk` | `dontAsk`(默认,只读)/ `plan` / `default` / `auto` / `acceptEdits` / `bypassPermissions`。后两个有写权限。 |
|
|
358
|
+
| `effort` | `''` | `--effort`:`minimal` / `low` / `medium` / `high` / `xhigh` / `max`;留空用 CLI 默认。 |
|
|
359
|
+
| `maxTurns` | `10` | 单次任务的 CLI 轮数上限(硬上限 100)。 |
|
|
360
|
+
| `timeoutSecs` | `900` | 单次任务的墙钟超时,秒(硬上限 1800)。 |
|
|
361
|
+
| `regionOrder` | `cn,global` | 凭据区域的优先顺序。 |
|
|
362
|
+
| `workingDir` | `''` | 工具入口的默认工作目录,同时也是允许根之一。 |
|
|
363
|
+
| `allowedDirs` | `''` | 额外允许的根,逗号分隔的绝对路径。 |
|
|
364
|
+
| `appendSystemPrompt` | `''` | 追加到 CLI 系统提示末尾;留空不追加。 |
|
|
365
|
+
| `allowUnattended` | `false` | 允许在**没有任何工作目录**时退回一个临时目录。默认关:CLI 有读写权限,不该在没人指定的目录里跑。 |
|
|
366
|
+
| `maxReplyChars` | `60000` | 交回给会话模型的最大字符数,超出截断并注明(硬上限 400000)。 |
|
|
367
|
+
| `injectResult` | `true` | 跑完后是否把结论回注给会话模型。设为 `false` 才完全不碰模型。 |
|
|
368
|
+
| `maxTurnsPerSessionHour` | `6` | 每个会话每小时最多几次。`0` = 不限。 |
|
|
369
|
+
| `maxTurnsPerHour` | `20` | 全部会话每小时合计最多几次。`0` = 不限。 |
|
|
370
|
+
|
|
371
|
+
### 边界
|
|
372
|
+
|
|
373
|
+
- **不共享上下文。** CodeBuddy 是另一个 agent,它看不到这段对话。它适合「独立审一块代码」「跑一轮调查」;不适合「接着我刚才那个思路改」——那种事它得自己重新把仓库读一遍。
|
|
374
|
+
- **它有写权限的门槛,没有沙箱。** 默认 `dontAsk` 只读;调成 `acceptEdits` 之后它真的会改工作区里的文件。允许根只约束它在**哪个目录**里跑,不约束它在这个目录里做什么。
|
|
375
|
+
- **两套账。** CodeBuddy CLI 的配额和 DSH 的模型额度是两回事。指令本身不花 DSH 模型 token,但默认的回注会让会话模型多跑一轮,那一次是真的 DSH token。描述里写了成本信号,也设了每小时上限,防止「回注 → 模型再问 → 再回注」这种循环。
|
|
376
|
+
- **失败就停,不换着账号试。** 只有明确识别出凭据被拒(401 / `Authentication required` / `/login`)才会换下一个账号;超时、崩溃、找不到 CLI 都直接报错——十几个 vault 账号逐个重试一遍 900 秒,比失败本身更糟。换号也不再看模型说的话:早期版本把 CLI 的**正文**也扫了一遍,导致「评审一段包含 `HTTP 401` 的代码」被误判成凭据失效。
|
|
377
|
+
- **超时会连子进程一起杀。** CLI 会派生 `git` / `npm`,只 kill 直接子进程会留下孤儿。POSIX 上按进程组发信号,Windows 上走 `taskkill /T`。
|
|
378
|
+
- **要重启。** 新增的 bundle 与 JS 改动都要重启 DSH 宿主才生效。
|
|
379
|
+
|
|
298
380
|
## 命令行
|
|
299
381
|
|
|
300
382
|
```sh
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as defaultDesktopAuthCandidates, Ct as findWorkbuddyAppExecutableWithSource, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, _t as WORKBUDDY_APP_EXECUTABLE_ENV, a as readHostHeartbeat, b as WorkBuddyCredentialStore, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, jt as hostPeersInfo, k as legacyWorkbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, w as defaultDesktopAuthDirs, y as WORKBUDDY_AUTH_FILE_ENV } from "./host-heartbeat-
|
|
2
|
+
import { C as defaultDesktopAuthCandidates, Ct as findWorkbuddyAppExecutableWithSource, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, _t as WORKBUDDY_APP_EXECUTABLE_ENV, a as readHostHeartbeat, b as WorkBuddyCredentialStore, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, jt as hostPeersInfo, k as legacyWorkbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, w as defaultDesktopAuthDirs, y as WORKBUDDY_AUTH_FILE_ENV } from "./host-heartbeat-_EvGiAek.js";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/bin.ts
|