DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

EVA09 /

EVA09/dsh-codebuddy-acp

Verified

把本机 CodeBuddy CLI(ACP 模式)接入 DeepSeek Harness:模型选择、工具卡片、积分控制台

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@e0be65e1

dsh-codebuddy-acp

把本机 CodeBuddy Code CLI(@tencent-ai/codebuddy-code) 通过 ACP(Agent Client Protocol) 接入 DeepSeek Harness,作为 DSH 内部的模型/agent 后端。

装上之后:

  • 模型选择器里多出一个 CodeBuddy 分组,和普通 provider 一样选中即可;
  • 分组里的条目就是你账号下真实可用的 CodeBuddy 模型(本机实测 19 个,如 hy3、 deepseek-v4.1-flash、glm-5.3、kimi-k2.8-preview…),每个模型名后面直接标着 积分倍率(如 Claude Sonnet 4.6 ×1.2),一眼看出这一轮贵不贵;
  • 每个模型都能选思考档位(low / medium / high / xhigh / max,具体有哪几档由模型决定), 选中即生效,换档位不丢上下文;
  • 选中哪个,CodeBuddy 就用哪个模型跑,消耗的是你 CodeBuddy 账号里腾讯给的那份额度;
  • 侧栏多一个 CodeBuddy 控制台:看账号里真实的剩余积分、领每日积分、管理模型开关与顺序。
DSH 会话
  └─ ctx.llm 适配器(provider = codebuddy)
       └─ codebuddy --acp(每工作目录一个常驻进程,内含多条 ACP 会话)
            ├─ initialize → session/new → session/prompt
            ├─ session/set_config_option  → 切模型(model)/ 思考档位(thought_level)
            ├─ session/update → 正文/思考流、工具调用(含结果)、阶段、用量
            └─ session/request_permission、fs/* → 由 DSH 侧应答
  └─ ctx.tools 回放工具(codebuddy_tool)
       └─ 把 CodeBuddy 已执行完的工具结果交回 DSH → 会话里留下 tool/call + tool/result

工具调用怎么进 DSH(调用链)

CodeBuddy 的 agent 循环在它自己的进程里执行工具,ACP 只会把过程作为通知告诉客户端, 客户端既不能替它执行、也没有「把结果塞回去」的通道。所以「工具调用完整进 DSH」靠的是两半:

  1. 适配器发原生 tool-call 块:每收到一次 ACP tool_call,就在模型流里发一个独立的 tool-call 块(block-start + tool-call-delta + block-end),参数是 { title, kind, tool, callId, status, path, line, command, input }。
  2. 回放工具把结果交回 DSH:插件注册 codebuddy_tool,DSH 执行它时按 toolCallId 从结果桥里取出 CodeBuddy 的真实输出,原样返回。

于是会话里留下完整的 assistant/message(tool-call 块) → tool/call → tool/result 四段式链路,界面渲染成工具卡片:类别、标题、路径、状态、可展开的真实输出。 不会重复执行工具,也不会多跑一次模型请求。

卡片什么时候出现、长什么样

DSH 只在模型流结束之后才执行工具,所以卡片从「出现」到「完成」之间隔着整轮 ACP。 这带来两个观感问题,插件在两处分别处理:

  • 完成态(toolEmit: 'settle',默认):等 CodeBuddy 把这次调用结算掉(completed / failed)再发块。卡片一出现就是终态,多个工具按结算顺序逐个出现,不会一大串挂着 「执行中」。代价是工具在跑的那几十秒界面里没有卡片;想要「一看到就出现」就切 toolEmit: 'start',但要接受完成态等到轮末。
  • 区分度:所有 CodeBuddy 工具在 DSH 里共用一个回放工具名,DSH 自带「按工具名换图标/标题」 用不上。所以 client.js 按真实工具名(Read / Bash / Edit / Grep / Write…) 归到家族,各自一套徽章文案与配色(读取/命令/修改/写入/搜索/抓取/任务/待办/智能体), 左侧还有一条同色竖条;主行只放「工具名 + 主语」,路径可点、命令走等宽字体。

为什么结论要走「下一个 step」而不是跟工具块同一条消息

DSH 的 Chat 只把最后一个 step 中、且不含 tool-call 块的 assistant 回复当作「最终答案」 (ui-chat 的 latestAnswer:blocks.some(block => block.kind === 'tool-call') ? null : data)。 其余正文一律算过程内容,会随整轮折叠一起收起——这就是「CodeBuddy 的结论被折叠」的根因。

而 CodeBuddy 一轮 ACP prompt 里既跑工具又给出正文,原样塞进同一个 step 就必然混在一条消息里。 所以本插件的做法是:

  1. 这一轮只要发过 tool-call 块,正文先整段攒住(TurnEmitter.heldText),不流出去;
  2. 轮末把它存进会话运行态(SessionRecord.heldAnswer),并在本轮每条工具记录上写 holdAnswer: true;
  3. 回放工具看到 holdAnswer 就不调用 concludeTurn(),DSH 于是照常进入下一个 step;
  4. 下一个 step 里 DSH 没带新的用户消息,适配器就把攒住的正文作为一段干净的 text 块交出——那一步不含 tool-call 块,结论因此被认作最终答案,整轮折叠也收不走。

纯工具轮(正文根本没出现)仍然由回放工具 concludeTurn() 结束,不多花一次请求。 若 CodeBuddy 先吐了一句开场白再调工具,那段正文已经流出去了、收不回来,就整轮走老路径, 不会出现「半句在这条消息、半句在下一个 step」的撕裂。

回放工具只在「这一轮的模型是 CodeBuddy」时可见(注册时按 modelSelection.provider 过滤),换成别的模型时它既不在工具表里、也不进请求头。

图片:贴的截图真的到模型眼前(而不是一行 [图片附件])

修掉的 bug:DSH 消息里的图片是 { type:'image', attachment },attachment 只是引用 (同一份字节只存一次),真字节在 ctx.attachments 里。旧实现把整个图片块压成文本 [图片附件:name],于是「贴张截图问一句」等于让模型凭空猜——图片从来没发出去过。

现在的通路是:

DSH 消息 ImageBlock
  └─ host.readImage(ref, {width,height,maxBytes})     ← index.js 的宿主桥
       └─ ctx.attachments.readImageRequest(...)       ← 按目标降质/降尺寸;失败回落 readImage 原图
            └─ base64 + mimeType
                 └─ ACP session/prompt: [{type:'text'}, {type:'image',data,mimeType}…]

实测结论(2026-11-01,CodeBuddy CLI v2.178.1,probe/image-prompt.mjs + test/image-live.mjs):

场景 结果
text + image(纯红 64×64) 答「红色」 ✅
image + text(顺序相反) 答「红色」 ✅ 顺序无关
换纯蓝图 答「蓝色」 ✅ 不是默认猜测(还触发了 ViewImage)
阴性对照(不带图、同样的问题) 「没有收到图片」 ✅ 证明答案由图片导致
只给 uri、不给 data -32602 Invalid params ✅ data+mimeType 必填

适配器这边的几条约束:

  • 正文里留编号标号:图片块发在正文之后,正文对应位置写 [图片 1:a.png], 模型因此知道第几张图对应哪句话。
  • 没发出去的图会说清原因:没挂载 ctx.attachments、读失败、超过 maxImagesPerPrompt、 重放历史时被策略裁掉,标号会变成 [图片 2:b.png(未发送:…)]—— 模型不会误以为自己看见了图,这一轮也不会因此失败。
  • 被压缩省略的图(offloaded: true)没有字节可读,渲染成固定说明,不再当图片发。
  • 重放不重发历史截图:进程回收/换模型导致重开会话、需要重放整段历史时, 按 replayImages(默认 latest)只投最后一条用户消息的图。
  • 同一张图只发一份:附件是内容寻址的,重复出现不会翻倍 token。
  • 签名认图片身份:消息签名带上 attachmentId,否则「同一句话换一张图」会被判成 「已转发过」而不发新图(这是同一个 bug 的隐蔽变体)。
  • 只发位图:ACP 没有 svg 之类的通路,这类图退回占位符并注明。

会话标题、待办计划、审批(CodeBuddy → DSH 的另外三条通路)

工具之外,还有三样东西在 CodeBuddy 那边有、DSH 这边需要,都做成真实同步:

标题:用 CodeBuddy 起的名,而不是固定模板

CodeBuddy 跑完一轮会发 session_info_update.title(实测 "Plan and execute mjs file analysis"、 "List plugins directory and count .mjs files")。适配器把它转给 ctx.sessionTitle.rename(), DSH 侧因此写一条 source:{kind:'user'} 的 session/title —— 这条会钉住标题, 后续用户消息不再触发自动生成。

顺带修掉了一个老 bug:DSH 自己的标题 provider 发来的不是原始用户消息,而是 Generate the session title from this JSON array of human messages:[{...}] 模板帧, 旧实现直接截前 24 字,于是标题永远是 Generate the session tit…。 现在会先把 JSON 数组解出来取第一条 text,再退回本地截断。

待办:CodeBuddy 的任务清单直接变成 DSH 待办面板

关键实测结论:CodeBuddy 完全不发 ACP 标准的 plan 通知 —— 跑两个多步任务、 54 条 session_info_update,一条 plan 都没有。它用的是自己的工具 TaskCreate / TaskUpdate,任务清单藏在 tool_call_update._meta['codebuddy.ai/rawResponse'].todos:

{ "task": { "id": "1", "subject": "…", "activeForm": "…", "status": "pending" },
  "todos": [ { "id": "1", "content": "列出 plugins 目录", "activeForm": "…", "status": "pending" } ] }

所以以这个字段为准:每次任务清单变化 → 归一成 todo/write 快照 → DSH 的 todos 投影 → 界面待办面板。标准 ACP plan 通知的兼容通路也留着(万一将来改协议)。 去重按「内容+状态」指纹,同一份清单不会重复写事件。

审批:ACP 权限请求变成 DSH 原生审批框

CodeBuddy 在 permissionMode: default 下,跑命令这类敏感操作会发 session/request_permission(实测带 options:[allow_always / allow / reject])。 permissionPolicy: 'ask' 时适配器把它转成 ctx.approval.request():

DSH 审批结果 回给 CodeBuddy
allowed-once 选第一个 kind 以 allow 开头的 option
rejected 选第一个 kind 以 reject 开头的 option;没有就回 cancelled
cancelled / unavailable / 问不到 按 permissionAskFallback(默认 allow)

两个实测细节:

  • optionId 不能硬编码:实测是 allow_always / allow / reject,只能按 kind 选;
  • 请求里的 toolCall 没有 title:只有 _meta['codebuddy.ai/toolName'](Bash) 和 rawInput({command, description})。所以审批文案按「工具名 + 最关键参数」拼, 实测显示为 Bash: 查看工作目录顶层内容。
  • 会话的审批策略是 never 时不去问(DSH 那边会直接拒绝,问也白问),直接按兜底走。
  • 没有审批 UI 的组合(例如 dsh headless)会拿到 unavailable,同样按兜底走 —— 否则一次「没有界面」就会让整轮工具全部失败。

控制台页面(侧栏 CodeBuddy 图标)

区块 内容
KPI 账号真实剩余积分、今日 / 本月 / 累计的积分消耗、调用次数、Token
账号与积分 账号与站点域、额度包明细(剩余 / 已用 / 有效期)、每日积分状态与「领取每日积分」
模型管理 勾选启用的模型、上下移调整顺序、选默认模型,并显示每个模型的积分倍率;保存后 DSH 模型选择器立即生效
按模型用量 每个模型的倍率、今日 / 累计积分、调用次数、Token
最近调用 时间、模型、积分、Token;可清空账本
连接 Provider、常驻连接数、模型探测时间、数据目录

数据分两类:

  • 剩余积分 / 每日积分:来自 CodeBuddy 官方 billing 接口,凭据直接复用你已经登录过的 CodeBuddy CLI 登录态(%LOCALAPPDATA%\CodeBuddyExtension\Data\Public\auth\*.info)。 插件只读取它来发请求,不复制、不写回、不上报,插件自己的数据目录里没有任何 token; 页面拿到的也只是昵称与站点域这类摘要信息。
  • 积分用量:插件自己的记账。每轮 CodeBuddy 跑完,usage_update 里的 cost.amount / _meta.usage.credit 会累加进 $DSH_HOME/codebuddy-acp/usage.json; 模型策略存在 $DSH_HOME/codebuddy-acp/settings.json。

余额查询走(实测 2026-10-28 全部可用):

POST /v2/billing/meter/get-user-resource          额度包与剩余积分
POST /v2/billing/meter/checkin-activity-status    今日签到状态
POST /v2/billing/meter/daily-checkin              领取每日积分(只在页面点按钮时调用)

签到不做后台定时:插件不会在你看不见的时候替你去领,必须你点「领取每日积分」。 领取会改变账号余额,所以成功后插件会立即清掉余额缓存并重新拉一次。

积分倍率以账号侧服务端接口为准:

GET /v2/enterprises/personal/models     账号可用模型与真实倍率

这一点很关键,因为三个来源的口径并不一致(实测 deepseek-v4.1-flash):

来源 倍率
发行版包内 product.json(写死的静态表) x0.00(错,会显示成「免费」)
ACP session/new 的 availableModels x0.03
账号侧接口(插件采用) x0.11

所以插件先用 product.json 兜底模型可见集合,再用账号接口返回的倍率覆盖它; 账号接口拿不到时(未登录、断网)安全回落到目录自带倍率,不会丢模型也不会崩。 控制台模型表里倍率单独一列,DSH 模型选择器里直接跟在模型名后面(如 Deepseek-V4.1-Flash ×0.11;零倍率显示为「免费」)。

接口(同源,POST 需要 x-dsh-codebuddy-console: 1):

方法 路径 说明
GET /api/codebuddy-console/state 账号、额度、签到、模型目录、设置、用量汇总
GET /api/codebuddy-console/rates 轻量只读表:{ 模型 id: 积分倍率 },便于脚本核对
POST /api/codebuddy-console/settings body { models? },保存并立即生效
POST /api/codebuddy-console/refresh 重新探测模型目录
POST /api/codebuddy-console/reset 清空积分账本
POST /api/codebuddy-console/billing/refresh 立即刷新余额与签到状态
POST /api/codebuddy-console/billing/checkin 领取每日积分

端到端验证(本机实测,2026-10-28)

$ dsh --profile cbtest "用一句话回答:3+4等于几?"
3+4等于7。                          ← stdout(最终答复)
dsh: reasoning:
用户问:3+4等于几?用一句话回答。这是简单的数学题,3+4=7。用中文…   ← 思考流

链路:profile 组合 → ctx.llm 路由 codebuddy/hy3 → 适配器 → ACP session/prompt → CodeBuddy 用自己的登录态与额度调用模型 → 流式回传 → DSH 落盘并打印。

前置条件

  1. 本机已安装 CodeBuddy CLI:

    codebuddy --version
    
  2. CLI 已登录过(登录态在 ~/.codebuddy,插件不读取、不转发任何凭据):

    codebuddy        # 进入 TUI 后执行 /login,按提示用浏览器完成授权
    

关于「不要反复弹登录」

CodeBuddy 的 authenticate 不是「确认一下登录态」,而是一次真正的外部链接登录: CLI 日志里是 externalUriOpener.open,每次调用都会拉起浏览器。所以本插件的策略是惰性鉴权:

  1. 先 session/new;
  2. 只有它被 -32000 / error.data.category === "auth" 明确拒绝时,才补一次 authenticate;
  3. 同一进程只补一次。

只要本机登录态还在(CLI 启动时会 restore,日志里 hasAccessToken=true),session/new 直接通过,浏览器不会被拉起。这一点已实测:同一台机器连续多轮对话,除首次需要授权外, 后续 initialize → session/new → prompt 全程静默。

真的需要重新授权时,插件会把 CodeBuddy 下发的授权链接一并放进错误提示里, 并可用 authTimeoutMs 控制等待时间。

安装

先拿到代码(或直接 git clone 到任意目录):

git clone https://github.com/EVA09/dsh-codebuddy-acp.git

用 plugin_manager 的 install_bundle,目标是本目录的绝对路径:

action: install_bundle
target: <workspace>\plugins\dsh-codebuddy-acp

或者用 CLI 装进任意 profile:

dsh plugin --profile desktop add <workspace>\plugins\dsh-codebuddy-acp

装好后的 Loader 行:

行 id 包 作用
codebuddy-acp dsh-codebuddy-acp 在 ctx.llm 注册 provider 路由 codebuddy

装完或改了插件代码后,重启桌面端应用才会加载新的 JS 模块(HMR 只对新安装的包生效一次)。

卸载:plugin_manager → remove_bundle → dsh-codebuddy-acp。

在界面里怎么用

  1. 打开模型选择器,找到 CodeBuddy 分组。
  2. 选一个模型(首次建议 deepseek-v4.1-flash,x0.00 credits,最省)。
  3. 正常提问。界面上会看到:
    • 正文:CodeBuddy 的最终回答(流式);
    • 「思考」区:CodeBuddy 的推理片段(工具活动不再写进这里,它有自己的卡片);
    • 工具卡片:每次工具调用一张卡,显示类别(读取/修改/执行/搜索…)、CodeBuddy 的原始标题 (如 Read /path/package.json (1 - 3))、目标路径(可点击打开)、状态,展开后是 工具真实输出;
    • 用量:从 usage_update 映射出来的 input/output token(多轮模型请求累加,重复帧去重)。
  4. 思考档位:模型选择器里,CodeBuddy 分组下每个模型旁边可以选 low / medium / high / … (具体有哪几档由该模型决定,见下表)。选定后这一轮就以该档位跑; 中途换档不需要重开会话,适配器走 session/set_config_option {configId:"thought_level"}。
  5. 中途换模型:直接在选择器里换。适配器走 session/set_config_option {configId:"model"} (CodeBuddy 会校验 modelId),对话上下文保留,不会重开会话;换模型后档位会按新模型重新校准。

思考档位随模型变化(实测 v2.177.1)

模型 可用档位
deepseek-v4.1-flash / glm-5.3 / kimi-k2.8-preview low / high / max
hy3 / glm-5.2 / deepseek-v4-pro low / medium / high / xhigh
hy4-preview high(单档)
space-bunny / minimax-m3.1-flash low / medium / high / xhigh / max

权威来源是 session/new 的 models.availableModels[]._meta.effortOptions 与 configOptions.thought_level.options(本进程建过一次会话后即以此为准); 选择器打开时尚无会话,用 src/models.js 里的实测快照兜底。 写进非法档位不会让整轮失败——CodeBuddy 以 -32603 拒绝后适配器记一条 warn 并沿用当前档位。

配置

在 profile 的 cordis.patch.yml 里覆盖 codebuddy-acp 行的 config:

- id: codebuddy-acp
  name: dsh-codebuddy-acp
  config:
    permissionMode: bypassPermissions
    toolNarration: reasoning
键 默认 说明
provider codebuddy ctx.llm 上的路由名,也是选择器分组 id
displayName CodeBuddy 选择器分组标题
command codebuddy CLI 命令名或绝对路径
nodePath 自动探测 用哪个 node 起 JS 入口(桌面端 process.execPath 是 Electron,不能直接用)
args ['--acp'] 启动参数
permissionMode bypassPermissions 透传 --permission-mode:default / acceptEdits / bypassPermissions / plan / dontAsk / auto
permissionPolicy allow ACP session/request_permission 的回应策略:allow / deny / ask(弹 DSH 原生审批框)
permissionAskFallback allow permissionPolicy: 'ask' 但问不到用户时(无审批 UI、被取消、会话策略为 never)怎么办:allow / deny
syncTitle true 采用 CodeBuddy 的会话标题(session_info_update.title → DSH 标题)
syncPlan true 把 CodeBuddy 的任务清单同步成 DSH 待办面板(todo/write)
authMethod auto 鉴权方式;auto 优先 internal(微信,静默复用缓存凭据)
authTimeoutMs 120000 惰性鉴权的最长等待(首次要你在浏览器点授权)
billingAuthFile 自动探测 指定 CodeBuddy 登录态文件(*.info);留空则按平台默认位置查找。只读取,不写回
defaultModel 空 兜底模型
defaultThoughtLevel 空 DSH 没给 reasoningEffort 时用哪个思考档位:minimal / low / medium / high / xhigh / max / enabled;留空表示不改写 CodeBuddy 自己的设置
modelDiscovery both help / static / both / off:静态目录的来源
models 空 只显示这些模型 id,顺序即展示顺序
showAliases false 是否显示 default-model 这类角色档位别名(实测走 ACP 切换后拿不到回答,默认关)
useModelsJson true 合并 ~/.codebuddy/models.json 与项目级 models.json
contextWindow 200000 目录没给上下文窗口时的声明值(真实值优先取 models[]._meta.maxInputTokens)
maxTokens 32000 同上,输出上限声明值
systemPrompt off 把 DSH 系统提示词当前言交给 CodeBuddy:off / append / full
toolReplay true 把工具调用转成 DSH 原生 tool-call 块 + 回放工具(完整调用链);false 退回「文字播报」旧行为
toolName codebuddy_tool 回放工具在 DSH 工具表里的名字(改它要同步改 client.js 里的 TOOL_NAME)
toolInputLimit 4000 tool-call 参数里保留的 rawInput 预算(字符),超出转成预览
toolOutputLimit 12000 工具结果在会话里展示的字符上限(完整输出仍保留在工具体里)
toolNarration auto 旧模式的工具播报位置:auto(回放开启时自动关闭)/ reasoning / text / off
imageInput true 是否把 DSH 消息里的图片真的发给 CodeBuddy({type:'image',data,mimeType} 内容块);false 退回 [图片 N:name] 占位符
maxImageBytes 5242880 单张图投递的字节目标(5MB);附件服务据此降质/降尺寸,也是 imageRequestTarget 返回值
maxImagesPerPrompt 10 一次 prompt 最多投几张图;超出的只在正文留标号并注明原因
replayImages latest 重放完整历史时投哪些图:latest(只投最后一条用户消息)/ all / none
toolEmit settle 什么时候发 tool-call 块:settle(等 CodeBuddy 把这次调用结算完再发,卡片一出现就是「完成」,多个工具按结算顺序逐个出现)/ start(一看到就发,卡片早出现但完成态要等整轮 ACP 结束)
proxyFs true 把 ACP 的 fs/read_text_file、fs/write_text_file 代理给 DSH 文件服务(失败回落本机 fs)
proxyTerminal false 是否把 ACP 终端代理到本机(关闭时 CodeBuddy 自己执行命令)
startupTimeoutMs 60000 握手/建会话超时
turnTimeoutMs 0 单轮墙钟上限,0 不限
cancelGraceMs 3000 用户中断后先发 ACP session/cancel,再等 agent 把这一轮结算成 cancelled 的最长时间;超时才本地中断(0 = 只发通知不等回执)
disposeGraceMs 3000 进程退出宽限期(Windows 用 taskkill /T 兜底进程树)
env {} 追加给子进程的环境变量(如 CODEBUDDY_API_KEY)
logLevel warn silent / warn / info(info 会把每条 ACP 通知打到 Host 日志)

设计说明

  • 进程池:initialize ≈1.6s,鉴权按需最坏十几秒,所以按工作目录共享一个 codebuddy --acp 进程,进程内每条 DSH 会话各占一条 ACP 会话。进程内多轮 prompt 用 连接级互斥闸门串行,避免互相打断。
  • 一次 stream() = CodeBuddy 完整跑一轮:它有工具、权限、沙箱和自己的循环, 所以这里不把 DSH 的工具表交给它;但它的每一次工具调用都会转成 DSH 的 tool-call 块,并由回放工具带回真实结果(见上节「调用链」)。
  • 增量投喂:DSH 每一步都会重发整段历史。适配器为每条消息算签名,比对「已转发前缀」, 只把新增的用户消息发给已在运行的 ACP 会话;前缀对不上(压缩、回滚)才重放全部历史。
  • 换模型不重开:session/set_config_option {configId:"model", value}(有校验),失败才 退化成「新开 ACP 会话 + 重放历史」。不用 session/set_model——它不校验 modelId。
  • 思考档位是会话级的:thought_level 与 model 一样挂在 ACP 会话上,所以换档位不重开会话; 换模型时档位集合可能变化,因此会强制重新校准一次。enabled 是 CodeBuddy 的「跟随模型默认档」, 不参与 DSH 的档位列表,只作配置兜底值。
  • 块编排按时间顺序:思考串流、正文串流、每次工具调用独占一个块,块切换时立刻发 block-end(旧实现把整轮的思考与工具播报塞进一个块、所有 block-end 拖到轮末, 于是界面长时间不动、工具结果完全看不到)。
  • 占位帧先压住:实测 CodeBuddy 会先发一条 kind=other、标题只有 Read 的占位 tool_call,稍后才补一条带绝对路径与正确 kind 的同 id tool_call。适配器把占位帧 压在滞留队列里,等补全帧到了再一次性发块——否则卡片上只有 Read、没有路径。 但不能拿 rawInput 里的路径当信号:tool_call_update 的增量参数会先填上路径, 那样会提前发块(实测踩过)。
  • rawInput 是增量的:tool_call_update 里的 rawInput 每帧只带一部分字段, 必须合并而不是覆盖,否则最后一条只剩 {cwd} 之类。
  • 工具结果保留:tool_call_update 的 rawOutput / content 进结果桥, 由回放工具以 tool/result 交回 DSH;超长输出按 toolOutputLimit 截断。
  • 用量按 messageId 归并:一次 ACP prompt 里 CodeBuddy 会跑多轮模型请求,每轮各发一条 usage_update 结算,需要累加;但同一份结算会被重复推送两次,简单累加会把 token 翻倍, 所以以 messageId 为键 upsert(后到者为准,因为只有后到的那条带 cost)。
  • 用量:session/prompt 的结果里没有 usage,真值在 usage_update 的 _meta.usage(OpenAI 风格),这里映射成 DSH 的 inputTokens/outputTokens/reasoningTokens/cacheReadTokens。
  • 辅助调用不占进程:会话标题、上下文压缩(purpose: 'session-title' | 'compaction') 用确定性文本应答,不会为标题额外跑一轮 agent。

能力链接清单

CodeBuddy 原生能力 接入方式 状态
模型选择 账号可用模型目录 + session/set_config_option ✅ 实测
思考档位 DSH 的 reasoningEffort → session/set_config_option {configId:"thought_level"};resolveModel 按模型给出可用档位 ✅ 实测
账号级模型元数据 session/new 的 models.availableModels(含 maxInputTokens / effortOptions) ✅ 实测
流式回答 agent_message_chunk → text-delta ✅ 实测
思考过程 agent_thought_chunk → reasoning-delta ✅ 实测
图片输入 DSH ImageBlock → ctx.attachments 读字节 → session/prompt 的 {type:'image',data,mimeType} 内容块 ✅ 实测
工具调用可见性 tool_call / tool_call_update → 原生 tool-call 块 + codebuddy_tool 回放工具 + 客户端工具卡片 ✅ 实测
会话标题 session_info_update.title → ctx.sessionTitle.rename()(钉住标题) ✅ 实测
待办计划 TaskCreate/TaskUpdate 的 _meta['codebuddy.ai/rawResponse'].todos → todo/write → DSH 待办面板 ✅ 实测
权限模式 --permission-mode ✅ 实现
权限请求应答 session/request_permission → ctx.approval.request() → 原生审批框 ✅ 实测(ask 模式)
文件读写代理 fs/read_text_file / fs/write_text_file → ctx.fs(回落 node:fs) ✅ 可选
用量与 credits usage_update._meta.usage → DSH token meter ✅ 实测
剩余积分 / 每日积分 v2/billing/meter/*(复用本机 CLI 登录态) ✅ 实测
模型积分倍率 账号侧 /v2/enterprises/personal/models → ×0.11 ✅ 实测
取消 session/cancel(先通知 agent 再本地收口,见 cancelGraceMs) ✅ 实现
终端代理 terminal/* ❌ 默认关闭(CodeBuddy 自己执行命令)
斜杠命令(82 条) available_commands_update 目前只记日志 ⏳ 未做
会话恢复(loadSession) 未使用(DSH 侧有自己的持久化) ⏳ 未做
MCP 注入(session/new.mcpServers) 传空数组,用 CodeBuddy 自己的 MCP 配置 ✅ 不自建

安全与隐私

  • 不收集、不上传任何凭据:插件不内置任何 API Key,鉴权完全交给本机 CodeBuddy CLI (登录态由 CLI 自己维护在 ~/.codebuddy)。
  • 只读本机登录态:侧栏控制台要显示真实剩余积分 / 今日签到状态时,src/billing.js 会 只读本机 CLI 的 auth 文件取出 accessToken,用于向 CodeBuddy 官方接口查询一次余额; 不写回、不进日志、不转发到其他地址。
  • 没有遥测:会话内容只在你本机 ↔ CodeBuddy 之间流动。
  • 仓库不含抓包日志:probe/out/ 的原始 ACP 帧含账号标识(userId / 昵称)与本机绝对路径, 已在 .gitignore 中排除;要复现请用 probe/ 下的探针脚本自己抓。

已知限制

  • 角色档位别名不可用:default-model / fast-model 这类别名通过 ACP 切过去后, 模型请求会静默返回空结果(已实测),所以默认不展示;showAliases: true 可强制打开。
  • 角色档位 ≠ 账号模型:session/new 的 models.availableModels 才是可用集合, --help 里那份静态表是发行版本写死的,两者不一致。
  • 进程常驻:每个工作目录一个 CodeBuddy 进程(空闲 30 分钟或超过 4 个空闲连接时回收)。 子智能体共用同目录的进程。
  • DSH 侧不替 CodeBuddy 执行工具:ACP 没有「客户端执行工具」的通道,工具永远是 CodeBuddy 自己跑的。DSH 里看到的工具卡片是一次回放(codebuddy_tool 只交回结果、结束本轮), 所以卡片上的「耗时」反映不了 CodeBuddy 的真实执行过程(审批是另一条真实通路,见上)。
  • 工具审批要显式开:默认 permissionMode: bypassPermissions + permissionPolicy: allow, CodeBuddy 根本不问、也直接放行。想要 DSH 审批框必须两件事一起改: permissionMode: default(让它问)+ permissionPolicy: ask(让我们问用户)。 ask 模式下如果这个组合没有审批 UI(dsh headless),结果是 unavailable, 按 permissionAskFallback(默认 allow)走,不会卡住。
  • 待办来自 CodeBuddy 的任务工具:它不发标准 ACP plan,所以待办面板的内容取决于 模型有没有主动用 TaskCreate/TaskUpdate。不用这些工具时面板就是空的(正常)。
  • 图片要模型自己支持:能不能传图由模型目录的 supportsImages 决定(DSH 据此声明 inputModalities),非视觉模型即使贴图也只会收到占位符;重放历史默认只重发最新一张 (replayImages),所以「重开会话」后很早期的截图不在上下文里。
  • 图片走 JSON-RPC 内联 base64:没有 Files 上传通路,maxImageBytes(默认 5MB)是硬上限, 大图会先被附件服务降质/降尺寸再发,仍超限的会注明「未发送」。
  • 上下文压缩是本地确定性摘要,不是模型生成的摘要;CodeBuddy 侧上下文仍完整。
  • 首轮成本偏高:CodeBuddy 每轮都会带自己的系统提示与工具定义(约 2.7 万 input tokens)。

自测

不需要 DSH,直接驱动真实 CodeBuddy:

cd plugins\dsh-codebuddy-acp
node test/run.mjs --models                       # 只列模型(不发起对话)
node test/run.mjs "用一句话回答:1+1等于几?"      # 真跑一轮(默认取目录里第一个模型)
$env:CB_MODEL='hy3'; node test/run.mjs "1+1=?"    # 指定模型
$env:CB_LOG='info';  node test/run.mjs "1+1=?"    # 打印每条 ACP 通知

离线测试(不触网、不改变账号状态):

node test/billing.test.mjs               # 登录态解析、余额/签到/领取、失败降级、倍率解析
node test/rate-source.mjs                # 倍率以账号侧为准、覆盖静态表、失败安全回落
node test/reasoning-and-blocks.test.mjs  # 思考档位映射 + 多轮工具调用的块编排(回放实测帧)
node test/tool-replay.test.mjs           # 工具调用转原生块 + 回放工具取回结果 + 历史不回灌
node test/sync-title-plan-approval.test.mjs  # 标题/待办/审批三条同步通路(含 TaskCreate 真实帧)
node test/host-smoke.mjs                 # 控制台接口:state / rates / settings / refresh / reset
node test/client-smoke.mjs               # 浏览器半身:导出、四个槽位注册、整页渲染、工具卡片四态
node test/image-prompt.test.mjs          # 图片真的进 prompt:内容块/去重/上限/降级/签名(假连接,离线)
node test/host-integration.mjs           # 真实装配链路:index.js → 适配器/控制台/工具注册

想看 ACP 原始帧(例如确认某个通知到底发不发),用探针脚本直连 CodeBuddy:

node probe/capture-frames.mjs --mode plan "请用 TodoWrite 建立任务清单并执行"
# 会打印每条 session/update、每条 agent→client 请求(含 session/request_permission 完整参数)

tool-replay.test.mjs 用同一份真实帧序列断言「工具调用完整进 DSH」的每一步: 占位帧不抢跑、同一 toolCallId 只产生一个块、块的参数结构(title 在最前)、 结果进桥并被回放工具取回、concludeTurn() 只结算一次、历史投喂不再回灌工具参数与结果。

reasoning-and-blocks.test.mjs 用假 ACP 连接回放 2026-10-28 抓到的真实帧序列, 覆盖两个已修复的问题:档位是否真的写进 CodeBuddy(含非法档位不阻断、同档位不重复写)、 以及多轮工具调用的 block-start/block-end 是否配对且及时收口、工具输出是否保留、 重复 usage_update 是否不再翻倍。

image-prompt.test.mjs 用假连接 + 假附件服务断言「图片真的进 prompt」的每一步: image 内容块带规范 base64 与 mimeType、纯图片消息不被丢、没有图片服务时正文注明「未发送」、 读图失败不阻断整轮、上限与去重、换图后签名变化会重新发、压缩省略的图不占编号。

真实图片冒烟(会消耗一次额度,需要本机已登录 CodeBuddy):

node test/image-live.mjs                        # 用 probe/out/red-64x64.png,期望答「红色」
node test/image-live.mjs probe\out\blue-64x64.png   # 期望答「蓝色」(排除瞎猜)

真实接口冒烟(只读,用你本机登录态实打实查一次):

node test/billing-live.mjs    # 打印真实剩余积分、额度包与今日签到状态

端到端(真跑 DSH + 真跑 CodeBuddy 工具)

装好插件后,用装好的运行时把一轮「读文件」任务跑穿,然后看会话日志里有没有 assistant/message(tool-call) → tool/call → tool/result 四段:

# 1) 建一个专用 profile,并让默认模型走 CodeBuddy
dsh --profile headless --dump-config > $null      # 首次会生成 profile 目录
# $DSH_HOME\profiles\headless\cordis.patch.yml 写入:
#   - id: agent-default-model
#     name: "@deepseek-ai/dsh-agent-default-model"
#     config: { provider: codebuddy, model: deepseek-v4.1-flash }
dsh plugin --profile headless add <本目录绝对路径>

# 2) 跑一轮会触发工具的任务
dsh --profile headless --json "用 Read 工具读 <本目录>\package.json 的第 1-3 行,告诉我 name"

# 3) 期望在输出里看到 tool_call / tool_result 两类事件,且只有一次 step_end

实测(2026-10-29,CodeBuddy CLI v2.177.1,装好的 DSH 0.2.1-rc.3):

{"type":"tool_call","tool":"codebuddy_tool","input":{"title":"Read …\\package.json (1 - 3)","kind":"read","path":"…","status":"pending"}}
{"type":"tool_result","callId":"call_00_…","status":"completed","result":"✓ Read …\n\n   1→{\n   2→  \"name\": \"dsh-codebuddy-acp\","}
{"type":"status","phase":"step_end","turn":1,"step":1,"usage":{...}}   ← 只有一次模型请求

参考资料

  • docs/acp-findings.md:本机实测的 CodeBuddy ACP 协议笔记(握手顺序、鉴权四种方式、 session/update 19 种变体、模型/模式/配置项、usage 结构、错误结构与边界行为), 复跑用的探针脚本在 probe/。原始抓包帧(probe/out/*.jsonl)含账号标识与本机路径, 不入仓库;需要时用 probe/run-probe.mjs 自己抓一份。
  • 官方文档:https://www.codebuddy.ai/docs/zh/cli/acp

实现备注

本包不 import 任何 @deepseek-ai/* 运行时包。DSH 以链接方式安装本地 bundle 时这些包 不一定可解析;ctx.llm.registerAdapter 用方法调用校验适配器,结构满足 LlmAdapter 契约即可。

许可证

MIT。仓库内含一份 Apache-2.0 的 ACP schema 摘录(probe/acp-schema-excerpt.json), 归属与分发声明见 THIRD-PARTY-NOTICES.md。

—/ 5

No ratings yet

Verified DSH bundle

Commit e0be65e14513

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout