dsh-doubao-work-connect
把本机已经登录的豆包工作(DoubaoWork)客户端变成 DeepSeek Harness 里的一个模型 provider:
不用另外配账号、不用 API Key,直接借客户端自己的登录态调它自己的 /chat/completion。
装好并重启 DSH 后,模型选择器里会多出一个 「豆包工作」 分组,含三个模型:
| 模型 id | 显示名 | 说明 |
|---|---|---|
doubao |
豆包 2.1(快速) | 不思考,最快 |
doubao-think |
豆包 2.1(思考) | 默认一档思考,可由 DSH 的 reasoningEffort 调整 |
doubao-expert |
豆包 2.1(专家) | 默认最高档思考,最强 |
思考档位:三个模型都会把 DSH 传来的 reasoning_effort 映射到豆包的
need_deep_think(off→0、minimal/low→1、medium→2、high/xhigh/max→3);
不传时按模型 id 取默认值(doubao→0、doubao-think→1、doubao-expert→3)。
模型 id 里的档位只是默认值,实际请求以 DSH 的选择为准。
工具调用:豆包的客户端协议没有 OpenAI 的 tools 字段,所以插件用提示词模拟——
把工具目录注入系统指令,要求模型输出 <tool_call>{"name":…,"arguments":{…}}</tool_call>,
小门再把这段标记解析成标准 tool_calls(流式按 chunk 增量、跨 chunk 拼接)
并给出 finish_reason: "tool_calls"。这是尽力兼容,不是原生 function calling:
模型不一定每次都老实输出标记,复杂 schema 的遵循度不如真原生协议。
为什么它是「本地插件」
DSH 判断一个插件「存在」有三个彼此独立的机制,本地开发的插件三样都要做,缺一样就表现为 「我明明写了插件,DSH 里却没有」:
| 步骤 | 做什么 | 缺了会怎样 |
|---|---|---|
① 插件目录里的 node_modules 软链 |
让插件能 import 宿主包(@deepseek-ai/dsh-llm 等) |
启动时报 ERR_MODULE_NOT_FOUND |
② profile 的 node_modules junction |
让宿主能 resolve 到这个包 | 包根本找不到 |
③ dsh.profile.bundles 登记 |
启动时才会组装它的 cordis.patch.yml |
能 resolve,但 apply() 永远不被调用 |
④ dependencies 里写 link: |
只影响市场「已安装」列表是否显示 | 插件能跑,但 UI 里查无此插件 |
install.mjs 就是把 ①–④ 一次做完的脚本。手抄任何一步都会重现「装不上」的现象。
安装
node install.mjs
脚本做的事:建插件内 4 个宿主包软链 → 在 <profile>/node_modules 建 junction →
把包名加进 dsh.profile.bundles → 在 dependencies 写 link:<本目录> → 备份一份
package.json.<插件名>.bak。
装到哪个 profile:默认挑「装了 dsh-connect-workbuddy/dsh-connect-trae 的那个,
否则名字以 desktop 结尾的,否则第一个」。要装到别的 profile(例如网页端用的 web)
必须显式指定:
node install.mjs --profile web
0.1.1 修正:此前
resolveProfile()只按目录名猜,永远选 desktop,导致 直接跑node install.mjs装不进网页端 profile——网页端因此表现为插件完全不存在 (/plugins/dsh-doubao-work-connect/status直接 404)。现在支持--profile。
装完重启 DSH(Electron 应用,不是这个 CLI)。
本机实测:profile 的
package.json变更后宿主会热加载——不重启,22:25:08 时127.0.0.1:30812小门就自己起来了(宿主进程 21:16:28 启动,未重启)。 但热加载不是契约,重启一次最稳。
回滚:
node install.mjs --remove
前置条件
Windows 上的豆包工作桌面客户端(
DoubaoWork.exe),且已登录。客户端需要带调试口启动:
DoubaoWork.exe --remote-debugging-port=30813。 Chromium 只在启动时接受这个参数,所以插件发现「客户端在跑但调试口不通」时不会替你重启客户端 (autoRestartDoubao默认false,见下),而是报错并在错误信息里提示你:自己关掉豆包工作, 再用D:\Program Files\DoubaoWork\app\DoubaoWork.exe --remote-debugging-port=30813启动它 (注意根目录的DoubaoWork.exe只是启动器,给它传参不会传给主程序)。破坏性默认值变更(0.1.3):
autoRestartDoubao的默认值由true改为false。 旧默认值意味着插件会主动taskkill /F掉你正在用的豆包客户端,打断你手上的对话; 现在默认不动你的进程。要恢复旧行为需显式设autoRestartDoubao: true, 那会强杀你正在使用的客户端。网络能直连
www.doubao.com。
工作原理
- 读凭据:CDP 连到
127.0.0.1:30813,用Storage.getCookies拿登录 Cookie, 在页面里求值取localStorage的device_id/web_id/s_v_web_id。 磁盘上的 Cookie 库被客户端独占锁定,这条路走不通。 - 借签名:在客户端自己的页面里调
window.bdms.frontierSign({ url }), 拿回X-Bogus值,作为 query 参数拼到/chat/completion?_signature…上。 不重写反爬算法,客户端升级也不用改插件。 - 翻译:POST
www.doubao.com/chat/completion读 SSE。必须丢弃FULL_MSG_NOTIFY回声帧(那是自己发出的消息),否则答案开头会重复用户原话。 - 接进 DSH:插件在
127.0.0.1:30812起一个 OpenAI 兼容小门(只认真 Bearer, 只认 loopback 的 Host/Origin),再用ctx.llm.registerAdapter把doubao-work这个 provider 挂到ctx.llm上。DSH 的模型目录是现算的,注册完立刻可见。 - 读额度:走同一个 CDP 连接拿到的 Cookie,POST 豆包自己的
/alice/commerce/sale/subscription/quota/summary/,把 5 小时 / 7 天两个滚动窗口 折算成百分比。宿主侧经/plugins/dsh-doubao-work-connect/quota暴露给浏览器半, 由输入框左侧的小胶囊显示。
端口:调试口 30813 沿用上游参考仓库的默认值;小门 30812 是本插件自选(避开 30813)。
两者都可配置,小门被占用时会自动退回随机端口。
多开注意:桌面端和网页端 DSH 若同时运行、都装了本插件,它们会抢同一个调试口
30813、并争着用taskkill重启同一个豆包客户端。小门端口冲突会自动退回随机 端口,但调试口没有隔离机制。要么只在一个 profile 里启用,要么给其中一个显式配置 不同的debugPort——不过豆包客户端本身只会开一个调试口,所以实际上只能二选一。
配置项
都可选,改 profile 里的插件配置即可:
| 键 | 默认 | 说明 |
|---|---|---|
shimPort |
30812 |
小门监听端口 |
debugPort |
30813 |
客户端调试口 |
doubaoExePath |
自动探测 | DoubaoWork.exe 路径 |
autoRestartDoubao |
false |
调试口不通时自动 taskkill /F 重启客户端一次。默认关(0.1.3 起);打开会打断你正在用的客户端 |
reasoningBlockTypes |
[] |
block_type 归类为「思考」的白名单,默认空 ⇒ 一律当正文。该映射未经真机校准,拿到真实帧后再填,详见 CHANGELOG「已知限制」 |
autoDeleteConversation |
true |
每轮答完删掉豆包侧临时会话(即用即焚) |
额度显示
输入框左侧会有一个「豆包额度」胶囊,鼠标悬停展开明细:套餐名、每个窗口的
已用百分比与 已用/总量、以及窗口重置时间。点一下立即刷新。
- 数据源是豆包自己的订阅额度接口,不是估算值;两个窗口就是官方口径的 5 小时与 7 天滚动限额。
- 60 秒缓存一次,不做后台轮询轰炸;客户端没启动时显示「豆包未运行」,不会 为了读额度去把客户端拉起来。
- 豆包客户端没打开时胶囊是灰的;打开并登录后自动恢复。
对应的宿主路由(只接受来自本机回环地址的请求,供浏览器半读取额度):
GET /plugins/dsh-doubao-work-connect/quota
→ 200 {state:"ok", plan, personal:{windows:[…]}, enterprise:{windows:[]}, …}
→ 200 {state:"app-not-running"} 客户端没开
→ 202 {state:"loading"} 首次解析还没完成,稍后重试
说明(0.1.1 修正):文档此前写「需要 GUI 会话的 bearer」,实际实现只校验 Host/Origin 是否回环,不要求 Authorization。实测无凭据也能拿到 200。 这类本机路由无法区分「浏览器半」与「本机其他进程」,所以请把它当作 本机可见的只读接口,不要假设它有鉴权。
自查
两套脚本,都不改任何配置:
node test/smoke.mjs # 离线:假 ctx 走一遍 apply(),验注册/模型目录/小门鉴权。不需要豆包、不需要网络。
node test/live.mjs # 真机:小门可达 → 模型列表 → 非流式一轮 → 流式一轮。需要豆包已登录 + DSH 已加载本插件。
live.mjs 可带参:--port 30812、--model doubao-think。
也可以直接打小门(PowerShell 里 curl 是 Invoke-WebRequest 的别名,要写 curl.exe)。
令牌是每次启动随机生成的,不再是源码里的固定字符串;它在 DSH 启动插件时写到
<DSH_HOME>/dsh-doubao-work-connect/shim-token.txt,即本机的
~/.dsh/dsh-doubao-work-connect/shim-token.txt(插件卸载时删除)。
关于权限(0.1.3 更正):此前文档写这个文件是「0600」。实际上 Windows 上 Node 的
mode对文件权限是 no-op,写盘时传的权限位不生效;文件的实际权限跟随用户目录(~/.dsh)的 ACL。 早期 0.1.1 条目里「(0600,卸载即删)」的写法沿用了这个误述。这不影响它是机密这件事—— 读到它等于能借用你的豆包额度,别往外传、别提交进仓库。
$token = Get-Content "$env:USERPROFILE\.dsh\dsh-doubao-work-connect\shim-token.txt"
curl.exe -H "Authorization: Bearer $token" http://127.0.0.1:30812/v1/models
不带 Authorization 会得到 401,这是设计:小门只服务宿主进程,不是公开端口。
0.1.1 修正:此前小门令牌是写死在源码里的常量
doubao-connect-local,等于没有 密钥——任何本机进程读到这个公开仓库就能借用你的豆包额度。现在改为每进程随机。
插件还注册了两条 host 路由,可以用它们确认插件真的被宿主加载了:
GET /plugins/dsh-doubao-work-connect/status → {state, debugPort, userId, deviceId, webId, cookieCount}
GET /plugins/dsh-doubao-work-connect/quota → 见上节
在本机 DSH 0.2.1-rc.2 上实测这两条都是 200(宿主会转发插件的 host 路由;早期笔记里
「返回 404」的结论是在旧端口上测的,已作废)。status 返回 state:"signed-in" 即说明
CDP 连通、Cookie 与签名都拿到了。
诊断:插件到底加载了没有
/plugins/.../status 能回答一半问题——它 404 只说明「路由没挂上」,但不区分「插件没加载」
和「宿主没转发」。0.1.3 起多了一个更直接的判据:apply() 注册成功后会在
<DSH_HOME>/dsh-doubao-work-connect/host-heartbeat.json 写一份心跳,
字段为 version / package / pluginVersion / pid / startedAt / updatedAt / shimPort / debugPort,
每 30 秒刷新一次 updatedAt(卸载时只在 pid 属于自己时才删除):
Get-Content "$env:USERPROFILE\.dsh\dsh-doubao-work-connect\host-heartbeat.json"
看到 updatedAt 时间在往前走,就说明宿主确实加载并 apply 了本插件;
文件不存在或 updatedAt 停住,则插件没起来、或者进程已经死了。
注意:桌面端与网页端 DSH 会共用这一个文件(谁后写谁赢), 所以它一次只能反映最后写的那一端,不能证明"只有一端在跑"。 文件里的
shimPort是那一端的实际监听端口;想知道两端各在什么端口上跑, 更可靠的办法是看数据目录里的shim-token-<端口>.txt——每个活着的小门都会留一份自己的 (见「双端同时启用」一节)。
已知限制
- 模型列表是内置静态表(
upstream.js的FALLBACK_MODELS),豆包出新模型要手动加。 - 只有文字对话,图片输入未实现。
- 必须保持豆包工作客户端登录;客户端大版本更新后请求参数可能需要重新实录校准。
block_type→ 思考/正文的映射未经真机校准:默认全部当正文(reasoningBlockTypes默认[])。 这是一个保守选择——宁可少显示思考,也不把正文误判成思考。取证时该账号对/chat/completion一律返回710022004,连豆包客户端自己发出的请求也一样被拒,因此暂时拿不到带正文/思考的真实帧; 拿到真实帧后再用reasoningBlockTypes校准即可,不必改代码。- 一轮对话的上下文完全由 DSH 每次重发的
messages决定,插件侧不保存任何会话状态; 送上游前会按HISTORY_MAX_CHARS(48000 字符)从最老的消息开始裁剪。 - 工具调用是提示词模拟,不是豆包原生能力;见上文说明。
- usage 是估算值(按字符数 ÷ 2),不是豆包的官方计费口径;上游若回传了真实 usage 则优先用真实值。成本统计页的数字因此只能当参考。
max_tokens由插件侧按字符预算近似执行(不是上游硬截断),finish_reason会相应给出"length";temperature会透传,但豆包是否采纳取决于其服务端。
免责声明
本项目仅供个人学习与研究使用,只驱动使用者本人在本机已登录的豆包账号。 因使用本项目产生的任何后果由使用者自行承担。本项目与豆包、字节跳动、 DeepSeek 均无关联,未获其授权或认可。
来源与版权
本项目的代码骨架(upstream.js 与 credentials.js 的 CDP / 签名 / SSE 部分)
来自 marsma-101/dsh-doubao-connect(MIT),
其中 upstream.js 与本机所取的上游版本逐字节一致;credentials.js、index.js、
cordis.patch.yml 做了针对豆包工作(DoubaoWork.exe、doubaowork-chat 页面、
provider id doubao-work)的改造。因此 LICENSE 里保留的是上游的版权行
Copyright (c) 2026 marsma-101——这是 MIT 的要求,不是笔误。
双端同时启用
结论:不需要二选一,两端的端口隔离是自动的。 下面全部是 2026-10-20 的双端实测值
(桌面端 DSH 与 dsh web 同时运行、同一个 ~/.dsh、同一个豆包客户端):
| 项 | 桌面端 DSH | 网页端 DSH | 是否冲突 |
|---|---|---|---|
| host Web 端口 | 2646(动态分配,本次实例还有 2647/2648) |
8080 |
否 |
| 小门监听端口 | 30812 |
7112 |
否——30812 被桌面端先占,网页端 EADDRINUSE 后自动回落随机端口 |
| 小门令牌文件 | shim-token-30812.txt |
shim-token-7112.txt |
否——按端口分开 |
| 豆包调试口 | 30813 |
30813 |
否——两端都只做只读 CDP(读 Cookie / 调签名),不独占该端口 |
/plugins/.../status |
200 |
200 |
— |
跨端鉴权实测:拿桌面端的令牌敲 127.0.0.1:7112/healthz 得 401,反过来同样是 401;
各自用自己那份令牌都是 200。也就是说两端的小门是彼此独立的,不会互相借用额度。
几点必须知道的细节:
- 共享文件是「谁后写谁赢」:
shim-token.txt、shim-token.pid、host-heartbeat.json在两端之间是同一份。所以 0.1.3 起每个小门额外写一份shim-token-<实际端口>.txt, 心跳里的shimPort记的也是实际监听端口(不是配置里的 30812)—— 否则双端同时启用时,配置值会让人误以为"只有一端在跑"。 test/live.mjs请带--port:node test/live.mjs --port 7112。脚本会优先读shim-token-7112.txt;不加--port时默认 30812、拿桌面端的令牌去敲,正好敲对。- 桌面端的 host 端口不是固定的,本次是 2646;2647/2648 上的同名路由返回 401, 它们是同一个 Electron 应用里的其它监听器,不是 host Server。探测请用当前实际端口。
autoRestartDoubao默认已改为false:豆包在跑但没开调试口时,插件不会再taskkill /F掉它,而是报错提示你自己带--remote-debugging-port=30813重启。 双端同时启用时这一点尤其重要——否则杀掉客户端会同时打断两端。
No comments yet. Be the first to write one.