dsh-connect-doubao 0.4.0 → 0.5.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 CHANGED
@@ -1,245 +1,357 @@
1
- # 更新记录
2
-
3
- 本文件记录每个发布版本对用户可见的变化。版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。
4
-
5
- ---
6
-
7
- ## 0.4.0 — 2026-11-05
8
-
9
- > 稳定性与额度准确性批次:让「回答」在中断时说实话,让「停止」真的停下上游,让凭据不再经过
10
- > 子进程环境变量,让设置卡片不再对任何本机页面敞开;额度读数改成看「还剩多少」,并把一直在
11
- > 扣的活动赠送额度显示出来;卡片重做了一遍视觉。全部改动都带回归测试(新增 36 例,全仓
12
- > 187 例)。
13
-
14
- ### 修复
15
-
16
- - **断流被当成完整回答** —— 豆包服务端提前断开连接时,插件只把缓冲区里已收到的那半截
17
- 文本当结果返回,并照样写 `finish_reason: "stop"` 和 `[DONE]`。用户看到的是一个语法
18
- 通顺、语义中断的回答,没有任何迹象表明它被截断了。现在以 `SSE_REPLY_END` 作为唯一
19
- 的完成依据:连接先断而该帧未到,就报「回答不完整(已收到 N 字符)」而不是假装成功。
20
- 特意**不可重试** —— 重试会新开一条线程,把这条线程上的上下文丢掉,比重打一次更糟。
21
- - **停止按钮停不下上游** —— 客户端断开后没有任何东西通知豆包那边停止生成,那一轮会一直
22
- 跑到结束,额度照扣,答案没人看。现在响应关闭即 `abort()`,并顺着 `fetch` 传到 socket。
23
- 顺带补上空闲看门狗:`DOUBAO_STREAM_IDLE_TIMEOUT_MS`(300 秒)此前定义了却从没被使用,
24
- 流静默这么久会主动中止,而不是把请求永远挂着。
25
- - **所有认不出的失败都被说成网络问题** —— 分类器对未知错误一律 `network`,shim 又把它
26
- 标成可重试的 `TIMEOUT`,于是 400、鉴权失败、插件内部异常都会触发宿主的整套重试,
27
- 最后给出一句「检查网络」。现在按 HTTP 状态与失败来源分类(401/403 → 会话失效、
28
- 429 → 额度耗尽、4xx → 上游错误不可重试、5xx 与连接失败 → 可重试、其余 → 上游错误),
29
- 只有真正的传输失败才会被建议重试,错误信封里也带上 `x-should-retry`。
30
- - **深度思考内容永远为空** —— 帧处理层把 `onThinking` 回调漏掉了(shim 侧的
31
- `reasoning_content` 分支早就写好了),开启深度思考时只能看到答案、看不到推理过程。
32
- - **凭据明文进了子进程环境变量** —— DPAPI 调用把整个会话以 base64 塞进
33
- `DSH_DOUBAO_DPAPI_BLOB` 交给 PowerShell,存活了 `Add-Type` 编译那 0.5–1 秒;
34
- `...process.env` 还把本机所有 API key 一起带了过去。现在改为 stdin 传入、环境变量走
35
- 白名单,`powershell.exe` 也从 `System32` 绝对路径启动而不是靠 PATH 查找。
36
- - **一个坏 profile 会让后面所有候选都不再尝试** —— 候选遍历里 `decryptFromProfile` 没有
37
- 自己的 try/catch,某个目录的 `Local State` 损坏会直接冒到外层,把 D 盘上真正可用的
38
- profile 全跳过。现在逐个隔离,失败信息里带上具体目录。
39
-
40
- - **一次会话失效要等十二小时才自愈** —— 加密的凭据副本在磁盘上缓存 12 小时,服务端
41
- 已经判定失效的 session 会被反复读回来,每次请求都以同一种方式失败;用户按提示重新
42
- 登录,插件仍然拿旧副本,直到 TTL 到期。现在服务端说会话失效就删掉盘上副本,让重新
43
- 登录立刻生效。顺带挡住时钟回拨:缓存时间在未来(改过系统时间、双系统、主板电池没电)
44
- 一律视为过期,否则 TTL 永远算不出一个正数。
45
- - **额度接口不可用时每一轮对话都要多等一次失败** —— 额度是对话前的前置检查,而失败的
46
- 结果从来不进缓存,于是接口挂掉的每一轮都要再付一次注定失败的往返,正好压在首字节
47
- 之前。失败结果现在缓存 30 秒:够短,恢复延迟只有一轮;够长,能把这种反复打穿。
48
- - **设置卡片对本机任何网页敞开** —— 四条路由只校验 Origin 是不是 loopback,而浏览器
49
- 在**同源**请求里根本不发 Origin 头,于是「没有 Origin」被当成可信 —— 本机另一个端口
50
- 上的页面(或任何能解析到 127.0.0.1 的页面)都能改掉模型开关和数据目录。现在 Host
51
- 必须是 loopback(挡住把域名解析到 127.0.0.1 的 DNS rebinding),带
52
- `sec-fetch-site: cross-site` 的请求直接拒绝,写请求还必须带一个自定义头 —— 自定义头
53
- 会触发 CORS 预检,而这几条路由从不应答预检,跨源页面因此发不出这个请求。同源正常
54
- 使用完全不受影响。
55
- - **声明了类型声明文件,但那文件从来不存在** —— `package.json` 的 `types` 指向
56
- `lib/index.d.ts`,构建却关着 `dts`,任何按类型使用这个包的人都会碰到一个空引用。
57
- 现在真的生成声明文件(`lib/index.d.ts` 等 5 个),`types` 与 `exports` 两条路径
58
- 都对得上。同时把 `lib/` 移出版本管理:tsdown 用内容哈希给共享 chunk 命名,每构建
59
- 一次整批文件就改名,diff 里看不出任何真实改动;发布不受影响(`prepack` 会构建,
60
- `files` 从 tarball 里取 `lib`)。
61
-
62
- - **额度卡片的「重置时间」是编出来的** —— 这台账号的套餐只返回一个周额度,且窗口的
63
- `start_time`/`end_time` 都是 0(服务端不给时间戳)。旧代码于是拿「现在」当窗口起点、
64
- 加上一个名义周期当重置时刻,卡片显示「6 天后重置」—— 真实重置可能就在二十分钟后。
65
- 同一份载荷里还带着 `total_amount: "2,100"` / `used_amount: "0"` 这种带千分位的金额,
66
- 旧代码完全没读,一旦 `used_percent` 缺失就只能显示 0%。现在:窗口还没开始计时就照实
67
- 说「开始使用后计时」(豆包客户端本身就是这个措辞),有时间戳就显示倒计时,两者都没
68
- 有才说「重置时间未知」;金额解析后作为百分比的兜底来源。顺带修掉 `doctor` 把「这个
69
- 套餐没有 5 小时窗口」判成失败的误报 —— 账号形态不是插件故障,只有额度真正读不出来
70
- 才算失败。
71
- - **真正在消耗的那份额度没被显示** —— 额度接口返回三个 section,旧代码只读
72
- `window_limit_section`。这台账号的「活动赠送额度」(`incentive_quota_section`,
73
- 已用 782 / 1,600、11 月 21 日 09:46 到期)因此完全不出现在卡片上:用户看到「近 7 天
74
- 已用 0%」,以为额度一点没动,实际上已经在扣赠送额度了。现在两个 section 一起读,
75
- 赠送额度单独一行、用「已用 782 / 1,600 · 16d 后到期」这种带金额的写法,排在常规窗口
76
- 前面。
77
-
78
- ### 新增
79
- - **卡片改成看「还剩多少」,并自己走动** —— 进度条过去画的是消耗比例(越用越满),
80
- 与「还剩多少」的直觉相反;现在画剩余量,用完即为空,颜色阈值也按剩余量给。数字从
81
- 「已用 782 / 1,600」改成「剩 818 / 1,600」,倒计时由本地时钟每秒从绝对时刻重算 ——
82
- 既不用每秒请求,也不会因为标签页放久了而漂移。轮询从 5 分钟缩到 30 秒,且只在卡片
83
- 可见时进行,切回标签页立刻刷一次;轮询走 POST 绕过宿主 5 分钟缓存,否则每 30 秒
84
- 拿回的都是同一份读数。
85
- - **卡片重做了一遍视觉** —— 原来五个分区的高度、字号、边框几乎一样,眼睛找不到落点。
86
- 现在整张卡是一片带极淡高光的表面,分区之间用一条发丝线分隔而不是各自套框;每个额度
87
- 窗口是一块独立的面板,一行的重点只有一个:右边那个「还剩下多少」。倒计时和「已用
88
- X / Y」退到第二行,不再和主数字抢宽度。进度条改成带方向感的渐变并带一点辉光,数字
89
- 改用等宽字形 —— 每秒跳动的倒计时不会再让整行左右抖。
90
-
91
- ### 测试
92
- - `tests/upstream-terminal.test.ts`(12 例):终止帧、跨 chunk 拆帧、截断判定、思考透传、
93
- 失败分类与重试矩阵。
94
- - `tests/shim-lifecycle.test.ts`(5 例):客户端断开、正常结束后关闭、空闲超时、活动续命、
95
- 释放后不再触发。
96
- - `tests/auth-dpapi.test.ts`(4 例):源码级守住「明文不进 env」「不走 PATH」两条约束。
97
- - `tests/save-route.test.ts`(+4 例):缺写入头、非 loopback Host、跨站标记三种拒绝路径,
98
- 以及「同源 GET 不带 Origin 仍然放行」这条必须保持可用的例外。
99
- - `tests/quota.test.ts`(8 例):用真实载荷做固定数据 —— 只有周额度、时间戳为 0、
100
- 另含「活动赠送额度」组(782 / 1,600、只有到期时间、window_type 3);覆盖「不伪造
101
- 重置时刻」「未开始计时标注 startsOnUse」「赠送额度只报到期」「金额推算百分比」
102
- 「真实时间戳原样透传」五条契约。
103
-
104
- ## 0.3.1 — 2026-11-02
105
-
106
- > 相对 `0.3.0` 的 3 个提交。修「User Data 不在 C 盘就读不到登录信息」,并补上手动指定目录的
107
- > 入口 —— 之前只能靠环境变量。
108
-
109
- ### 新增
110
-
111
- - **手动指定豆包数据目录** —— 设置卡片新增「豆包数据目录」一段,带「选择目录…」按钮
112
- (桌面端弹系统原生文件夹对话框)和「改回自动探测」。装豆包时把数据目录改到 D 盘、
113
- 或事后手动搬走的人,现在可以自己指路,不用再设环境变量 `DSH_DOUBAO_PROFILE`。
114
- 段落里会显示当前用的是哪个目录,以及探测到的全部候选(带来源:
115
- `manual` / `env` / `appdata` / `process` / `scan`)。
116
- - 选中的路径在写入前会校验目录里是否有 `Default\Network\Cookies`,选错当场提示并**拒绝保存**;
117
- - 选中的目录后来被删掉或搬走,会自动退回自动探测,不会把凭据加载卡死;
118
- - 指到 `User Data` 本身或它的父目录(例如 `D:\Doubao`)都可以,后者会自动往下找一层。
119
- - 浏览器形态的宿主没有原生目录对话框,按钮会明确报错而不是静默失败。
120
-
121
- ### 修复
122
-
123
- - **User Data 不在 C 盘就读不到登录信息** —— 凭据查找曾硬编码
124
- `%LOCALAPPDATA%\Doubao\User Data`,把数据放到 D 盘(安装器提供这个选项,也有人事后手动搬)
125
- 的机器会报 `Doubao user profile not found`,而报错里显示的路径恰恰是用户根本没有的那个。
126
- 现在改为五层回退:卡片里手动选的目录 → 环境变量 `DSH_DOUBAO_PROFILE` → AppData 候选 →
127
- 运行中客户端的 `--user-data-dir` → 限时扫盘(深度 4、1.5 秒预算、跳过系统目录)。
128
- 顺序按**代价**而非准确度排:常见情况 4 次 `existsSync` 就命中,不会为 0.8 秒的进程探测
129
- 拖慢每次冷启动。返回候选**列表**而非单一路径,因为「复制而非移动」会留下空壳 profile —
130
- 凭据加载逐个尝试,取第一个真正解出可用会话的。
131
- - **手动指定的目录失效后会永久留空** —— 覆盖值现在每次都会重新校验,失效的选点自动落回
132
- 自动探测,不再遮蔽真正可用的 profile。
133
- - **提示文案里的 Windows 路径少了一层反斜杠** —— `Default\Network\Cookies` 写在单引号字符串里
134
- 会被当成转义序列,浏览器实际看到的是 `DefaultNetworkCookies`,正好是最需要准确的那句话。
135
- 已补转义,并加了 `scripts/check-locale-literals.mjs` 直接检查构建产物里的字面量
136
- (`npm run check` 会自动跑)。
137
-
138
- ### 测试
139
-
140
- - 11 个测试文件、**151 个用例**全绿(0.3.0 是 132 例)。新增覆盖:`profileDir` 的存取与非法
141
- 类型、`/__save` 对目录存在性的校验(选错目录必须 400 且不写盘)、`/profile` 只读路由
142
- (空结果返回 `null` 而不是空字符串)、手动覆盖的优先级与失效回退。
143
- - `doctor` 五项全绿;`doctor --json` 的 `credentials.data.profile` 会显示来源,
144
- 现在多一个 `via manual`。
145
-
146
- ---
147
-
148
- ## 0.3.0 — 2026-11-02
149
-
150
- > 相对 `0.2.0` 的 15 个提交。这一版把插件从「只能纯聊天」推到了「能生图、能续聊、
151
- > 能让模型通过提示词层模拟去调工具」,并修掉了一个从项目第一天就存在的设置持久化缺陷。
152
-
153
- ### 新增
154
-
155
- - **豆包生图(Seedream 5.0)** —— 模型选择器里新增「豆包 生图(Seedream 5.0)」。
156
- 生成结果以 Markdown 图片形式追加在回复正文里,可直接预览与保存。
157
- 实测一次请求返回 3 张 2848×1600 图片(右下角带「豆包 AI生成」水印)。
158
- 注意 URL 后缀是 `.jpeg` 但字节是 PNG,保存时按内容判格式。
159
- - **模型清单自动发现** —— 不再硬编码模型名。插件直接读豆包客户端的本地缓存
160
- (`menuConfV2`,V8 structured clone 编码),豆包上线新模型时清单会自动跟上。
161
- 缓存解析失败时自动回退到内置静态清单,`doctor` 会显示当前用的是哪一种。
162
- 「工作任务」专属模型按名称前缀自动隐藏。
163
- - **真会话续聊** —— 同一个 DSH 会话的第二轮起只发送增量消息,由豆包服务端维护线程内存,
164
- 不再每轮重发全部历史。实测第二轮只发 20 个字符,服务端仍能答出上一轮的内容。
165
- - **深度思考分离** —— 开启深度思考后,思考过程走 `reasoning_content` 而不是混进正文。
166
- - **实验性工具调用(提示词层模拟)** —— 让豆包能驱动 DSH 的本地工具(读写文件等)。
167
- 默认**关闭**,设置卡片里有独立开关。实现方式是在提示词里注入工具契约,再把模型输出的
168
- ` ```doubao-tool ` 围栏解析回标准 `tool_calls`。实测 5 次调用、坏 JSON 0 次、5/5 轮收敛。
169
- - **设置卡片新增工具开关段落** —— 与模型清单分开保存,避免「换模型」和「放开文件权限」
170
- 共用一次确认;关不掉的权限开关是比误开更坏的失败。
171
- - **手动指定豆包数据目录** —— 设置卡片新增「豆包数据目录」一段,带「选择目录…」按钮
172
- (桌面端弹原生文件夹对话框)和「改回自动探测」。装豆包时把数据目录改到 D 盘、或者事后
173
- 手动搬走的人,现在可以自己指路,不用再设环境变量。选中的路径在写入前会校验
174
- `Default\Network\Cookies` 是否存在,选错当场提示且不落盘;已选的目录后来被删掉会自动
175
- 退回自动探测。段落里会列出当前用着哪个目录、以及探测到的全部候选。
176
-
177
- ### 修复
178
-
179
- - **设置保存失败(从项目第一天就存在)** —— 症状是点开关报
180
- `保存失败:host did not persist the value`,或看起来保存成功但刷新后回退。
181
- 根因不在 schema:本插件的 fiber 在 DSH 0.2.1 上**永远停在 `state 1`(LOADING)**,
182
- 宿主因此从不给它挂 `runtime.Config`,`describe()` 直接过滤掉命名空间,任何写入都失败于
183
- `No configurable plugin entry "dsh-connect-doubao"`。
184
- **这意味着历史上这个插件的设置一次都没能落盘过,包括手工改 `cordis.patch.yml` 的那些。**
185
- 现在两个开关改存插件自己的 `~/.dsh/dsh-connect-doubao.settings.json`,
186
- 写入是先写临时文件再原子重命名,读取不依赖任何宿主服务在线。
187
- - **工具契约导致幻觉** —— 温和措辞的契约下,模型会回「已完成修改:…」但**磁盘根本没动**。
188
- 这是最坏的失败模式:用户以为文件改了。契约已改为硬性规则(第一条回复必须是工具调用、
189
- 严禁在没有 `[tool result]` 时声称完成),实测从 0/2 提升到 2/2。
190
- - **多步编辑互相污染** —— 局部替换链会让第 N 次编辑失效于第 N+1 次,最终文件出现重复行。
191
- 契约加入「每次修改后重新读取文件再决定下一步」后,同一任务 1 次 edit + 1 次 read 收敛。
192
- - **回复首字丢失** —— 回复开头在 `STREAM_MSG_NOTIFY`、增量在 `STREAM_CHUNK`,
193
- 旧代码只取一边导致「1+1」变成「+1」。已抽出 `assembleReplyText` 统一拼接。
194
- - **对话模式根本不通** —— `agent_mode` 常量写反(1/2 互相搞错),每一轮都被路由到
195
- 豆包的任务 agent,直接回 `710010702`。已按 CDP 抓到的真实请求体整体重写。
196
- - **User Data 不在 C 盘就读不到登录信息** —— 凭据查找曾硬编码
197
- `%LOCALAPPDATA%\Doubao\User Data`,把数据放到 D 盘(安装器提供这个选项,也有人事后手动搬)
198
- 的机器会报 `Doubao user profile not found`。现在改为五层回退:设置卡片里手动选的目录 →
199
- 环境变量 `DSH_DOUBAO_PROFILE` → AppData 候选 → 运行中客户端的 `--user-data-dir` → 限时扫盘。
200
- 顺序按**代价**而非准确度排:常见情况 4 次 `existsSync` 就命中,不会为 0.8 s 的进程探测
201
- 拖慢每次冷启动。返回的是候选**列表**而非单一路径,因为「复制而非移动」会留下空壳 profile —
202
- 凭据加载会逐个尝试,取第一个真正能解出可用会话的。
203
-
204
- ### 改进
205
-
206
- - **改动说明改为逐条列出** —— 工具执行完成后不再只说「已修改」,而是按「哪个文件 → 改了什么 →
207
- 为什么」逐条说明,多处改动分别列出。
208
- - **请求体形状有测试钉住** —— 18 个断言全部取自 CDP 抓包而非由被测常量反推,
209
- 防止 `agent_mode` 这类常量再次静默回归。
210
-
211
- ### 已知限制
212
-
213
- - **选不到 2.1 Pro / 2.1 Lite**。服务端**不校验** `model_item_key`(四种真 key 和一个不存在的
214
- key 耗时与长度无可测差异),模型由服务端按产品/模式/账号决定。目录已收缩为
215
- 「快速 / 2.1 Turbo·专家 / 生图」三项,Pro/Lite 只能在豆包客户端自己的「工作任务」里用。
216
- - **「工作任务 · 本地电脑」调不到**。协议层字段(`runtime_type: 2`、`sandbox_id`、`device_name`)
217
- 都能发对,但执行者是需要 App 进程内 CUA 运行时的组件,外部客户端一律
218
- `710010702`。插件默认走纯对话模式,`DSH_DOUBAO_TASK=1` 才尝试任务模式。
219
- - **工具模拟只在豆包自身不具备的能力上可靠**。让它「查天气」时它会改用自带联网搜索,
220
- 不调用我们的工具。
221
- - **Windows only**。凭据解密依赖 DPAPI。User Data 可以不在 C 盘(见上文五层回退)。
222
- 浏览器形态的宿主没有原生目录对话框,「选择目录…」会明确报错而不是静默失败,
223
- 那种环境请用 `DSH_DOUBAO_PROFILE`。
224
-
225
- ### 测试
226
-
227
- - 11 个测试文件、**151 个用例**全绿。本版新增三个文件:`store.test.ts` 20 例(文件合并语义、
228
- 原子写入、无临时文件残留、`profileDir` 存取与非法类型)、`save-route.test.ts` 20 例
229
- (字段白名单、Origin 校验、值形状、500 契约、`profileDir` 的路径存在性校验、`/profile`
230
- 只读路由)、`profile.test.ts` 25 例(五层探测优先级、去重、扫描深度与时间预算、
231
- 手动覆盖的优先与失效回退)。
232
- - `doctor` 五项全绿:凭据 29 cookie / 账号通过 / shim OK / 额度 / 模型发现。
233
- `doctor --json` 的 `credentials.data` 现在同时给出 `profile`(用的是哪个、`via env|appdata|
234
- process|scan|manual`)与 `candidates`(全部候选),定位"读不到登录信息"时先看这两项。
235
-
236
- ---
237
-
238
- ## 0.2.0
239
-
240
- - 修复对话模式完全不通(`agent_mode` 常量写反)。
241
- - 请求体按 CDP 抓包重写。
242
-
243
- ## 0.1.1 / 0.1.0
244
-
245
- - 首个可用版本:凭据复用(DPAPI + AES-GCM)、provider 注册、只读额度卡片。
1
+ # 更新记录
2
+
3
+ 本文件记录每个发布版本对用户可见的变化。版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。
4
+
5
+ ---
6
+
7
+ ## 0.5.0 — 2026-11-07
8
+
9
+ > 取凭据的路只剩一条:**浏览器扫码登录**。上一轮加的 `authMode` 开关(客户端 / 浏览器)
10
+ > 连同它背后的「解密豆包客户端 cookie」整条路径一起删掉 —— 不是弃用,是移除。理由是那条
11
+ > 路径有个从外面修不好的缺陷:豆包客户端运行时对 `Default\Network\Cookies` 持**独占锁**,
12
+ > 而本插件恰恰是为「客户端正在运行」的账号准备的;读不到就回退到 12 小时前的密封副本,
13
+ > 服务端早已轮换会话,于是每个请求都被上游拒绝,本地状态却还显示「已登录」——用户既看不
14
+ > 出问题也修不了。浏览器扫码没有这个死结:cookie 当场抓到、当场密封,不存在更旧的副本。
15
+ > 已登录的浏览器会话**不受影响**(密封文件名沿用 `doubao-credentials-browser.dpapi`)。
16
+
17
+ ### 破坏性变更
18
+
19
+ - **登录方式只剩「浏览器扫码登录」** —— 删除 `authMode` 设置项(config schema、自带设置文件、
20
+ 路由、卡片单选、中英文案),插件永远读浏览器会话,没有第二个选项可选。
21
+ - **删除 `/plugins/dsh-connect-doubao/profile` 路由与卡片里的「豆包数据目录」整节** ——
22
+ 客户端 profile 选择器已无意义;保存路由的写入白名单同步去掉 `profileDir`,避免出现
23
+ 「写进了磁盘但直到重启才生效」的半截状态。数据目录仍可通过设置文件的 `profileDir`
24
+ 或环境变量 `DSH_DOUBAO_PROFILE` 指定,它现在只用于模型清单缓存与任务模式工作区。
25
+
26
+ ### 移除
27
+
28
+ - `decryptFromProfile`、`decodeCookiePlaintext`、`decryptCookieValue`、SQLite 读取与
29
+ 多 profile 回退(`loadDoubaoCredentials` 不再遍历候选,也不再做 stale-while-error)。
30
+ - 12 小时密封缓存 `~/.dsh/doubao-credentials.dpapi`(`CREDENTIAL_CACHE_FILENAME` /
31
+ `saveCredentialCache` / `loadCredentialCache`)。**老版本留下的这个文件不再被读取**,
32
+ 可以手工删除;留着也不会被当成登录态。
33
+
34
+ ### 修复
35
+
36
+ - **账号状态把「未登录」误判成「网络失败」** —— 无登录态时的提示文案是中文,客户端
37
+ 的状态分类只匹配英文关键字,于是回落成「连接豆包失败:请检查网络」。现在中文的
38
+ 登录态/风控文案也参与分类。
39
+ - **卡片与文档不再声称「复用客户端登录态」** —— 副标题、账号状态文案、README 中英两版
40
+ 的前置条件、工作原理、平台支持、已知限制全部改成浏览器扫码,并写明不再解密客户端 cookie
41
+ 的原因。
42
+
43
+ ### 思考(同批)
44
+
45
+ - **深度思考默认开启** —— 以前每轮都是普通回合(`need_deep_think: 0`),模型选择器的
46
+ 低/中/高只改 `reasoning_effort`,对思考没有任何作用。现在非「工作任务」模型发
47
+ `need_deep_think: 3` + `ext.use_deep_think: "3"`;「工作任务」模型(Lite / Pro / 自动)
48
+ 保持官方客户端抓到的真实形态 `10001`。因为任务模型的形态是靠 `deepThink === undefined`
49
+ 区分的,shim 对它们特意**不传**布尔值,否则会被降级成聊天形态。
50
+ - **「豆包 快速」标记为不可推理** —— 实测深度思考全开、档位拉到高,它仍返回
51
+ `thinking: false` 且流里没有 `10040` 容器;同一请求换 2.1 Pro 立刻变 `thinking: true`。
52
+ 于是它不再出现在推理档位的可选范围里,卡片清单也不再给它列低/中/高。发现路径也要
53
+ 应用这条:客户端菜单把每条都标成可推理,所以判断放在发现之后,按实测清单收口。
54
+ - **不再谎报「选了档位就会思考」** —— 这是上面两条要一起改的原因:只改开关不改挡位,
55
+ 或只改挡位不改开关,都仍然看不到思考。
56
+
57
+ > **思考正文仍然看不到,这是接口限制。** 2026-11-07 在官方客户端打开深度思考发一条真实
58
+ > 消息并抓包对照:请求是 `need_deep_think: 10001`,响应里有 `10040` 思考容器和一个
59
+ > `10091` 计时块,但容器的子块只有 `summary: "正在思考"`,`text_block.text` 只出现在
60
+ > 正式答案里。也就是说连官方客户端都拿不到推理正文,插件无从转发。README 中英两版已写明。
61
+
62
+ ### 测试
63
+
64
+ - 新增:登录路由只接受 `{"action":"login"}`(其它 body 一律 400,且 POST 必须带写标记);
65
+ `src/doubao-auth.ts` 的客户端解密路径不得回归、只保留浏览器密封文件、`findDoubaoUserDataDir`
66
+ 仍对外可用。
67
+ - 新增:思考开关的上线形态 —— `deepThink: true` → `3`、`false` → `0`、
68
+ 「工作任务」未设置 → `10001`,以及「豆包 快速」不可推理。四条都钉在
69
+ `tests/thinking.test.ts` 里,防止以后有人「顺手统一」成同一个值。
70
+ - 同步:删除 `store.test.ts` 的 `authMode` 断言、`save-route.test.ts` 的 profile 路由与
71
+ `profileDir` 写入用例;`scripts/check-locale-literals.mjs` 改为「整张表逐条往返解码无损 +
72
+ 双语齐备」,因为原来被钉死的那两条 Windows 路径文案随本节一起删除了。
73
+
74
+ ---
75
+
76
+ ## 0.4.1 — 2026-11-05
77
+
78
+ > 「工作任务」模型接入批次:把豆包客户端里那三个**只能在工作任务模式跑**的条目
79
+ > (2.1 Lite `seed-lite-7b`、2.1 Pro `5`、自动 `9`)真正接到 DSH 里,走的正是会员/
80
+ > 活动付费额度。上一版把它们整个藏起来,理由是「外部客户端注册不进 CUA 运行时」——
81
+ > 那个理由对了一半:运行时确实接不进去,但它不是必需的,`tool_local` 的三条 HTTP
82
+ > 接口就够把一轮任务回合走完。新增 4 例回归测试(另改写 2 例旧的「必须隐藏」断言),
83
+ > 全仓 189 例。
84
+
85
+ ### 新增
86
+
87
+ - **豆包 2.1 Lite / 2.1 Pro / 自动 进入模型选择器** —— 选中任一条,请求自动切到
88
+ 「工作任务」模式(`mode_id: "3"`、`agent_mode: 1`、`need_deep_think: 10001`、
89
+ 新会话 `conversation_mode: 2`、`model_extra_params.input_token_limit: "256k"`,
90
+ `seed-lite-7b` 另带 `reasoning_effort`),`general_task_param` 带 `task_input_json`。
91
+ 实机跑通:Lite 与 Pro 各一次纯文本回合,约 5.8 s,`server_model_tag` 为 `38`。
92
+ - **`tool_local` 应答器** —— 任务模式下服务端会按 `${unique_key}:${device_id}:${aid}:message:${seq}`
93
+ 的顺序下发本地工作区指令;插件并发轮询 `msg/ack`,遇到 `command: 6003`(`cmd_call`,
94
+ 如 `local.workspace.apply`)就回 `upload_command_result` + `msg/complete`,遇到
95
+ `command: 6004`(`agent_finished`)收尾。**这是解开 `710010702` 的全部原因。**
96
+ - **`CHUNK_DELTA` 增量帧** —— 任务模式的正文增量以无 `block_type` 包裹的
97
+ `{ text, id }` 形式下发,按 `id` 分流到 `reasoning_content` / 正文。
98
+
99
+ ### 修复
100
+
101
+ - **选 `seed-lite-7b` / `5` 会被静默换成「豆包 快速」** —— `findDoubaoModel` 只在内置
102
+ 清单里查,查不到就回落到 `'0'`。现在三条任务模型都在清单里,不再回落。
103
+ - **`msg/ack` 批量查询必失败** —— `{ msg_ids: [0..6] }` 会整体报
104
+ `{"code":710010702}`,因为批里只要含一个服务端还没生成的 `seq`,整批就废。
105
+ 现在严格单条轮询,逐 `seq` 递进。
106
+ - **自动发现把工作任务模型全部隐藏** —— 上一版按名称前缀一律排除。现在改为只排除
107
+ `DOUBAO_UNAVAILABLE_MODELS`(仅剩「即将下线」的 `4`),并按内置清单补齐,避免本机
108
+ IndexedDB 只缓存了对话子集时任务模型消失。
109
+
110
+ ### 已知边界
111
+
112
+ - 纯文本、不触发本地工具的小回合,服务端会在 `ext` 里标 `general_agent_task_mode: "simple"`
113
+ 与 `mode_downgrade_reason: "success"`,此时**不扣**活动赠送额度(实测调用前后
114
+ `usedAmount` 恒为 782)。要消耗付费额度,得让它真的执行多步工具/文档任务。
115
+ - `msg/ack` 的 `seq` 没有可查询的上界,应答器在流结束后最多再补 8 次轮询即退出。
116
+
117
+ ---
118
+
119
+ ## 0.4.0 — 2026-11-05
120
+
121
+ > 稳定性与额度准确性批次:让「回答」在中断时说实话,让「停止」真的停下上游,让凭据不再经过
122
+ > 子进程环境变量,让设置卡片不再对任何本机页面敞开;额度读数改成看「还剩多少」,并把一直在
123
+ > 扣的活动赠送额度显示出来;卡片重做了一遍视觉。全部改动都带回归测试(新增 36 例,全仓
124
+ > 187 例)。
125
+
126
+ ### 修复
127
+
128
+ - **断流被当成完整回答** —— 豆包服务端提前断开连接时,插件只把缓冲区里已收到的那半截
129
+ 文本当结果返回,并照样写 `finish_reason: "stop"` 和 `[DONE]`。用户看到的是一个语法
130
+ 通顺、语义中断的回答,没有任何迹象表明它被截断了。现在以 `SSE_REPLY_END` 作为唯一
131
+ 的完成依据:连接先断而该帧未到,就报「回答不完整(已收到 N 字符)」而不是假装成功。
132
+ 特意**不可重试** —— 重试会新开一条线程,把这条线程上的上下文丢掉,比重打一次更糟。
133
+ - **停止按钮停不下上游** —— 客户端断开后没有任何东西通知豆包那边停止生成,那一轮会一直
134
+ 跑到结束,额度照扣,答案没人看。现在响应关闭即 `abort()`,并顺着 `fetch` 传到 socket。
135
+ 顺带补上空闲看门狗:`DOUBAO_STREAM_IDLE_TIMEOUT_MS`(300 秒)此前定义了却从没被使用,
136
+ 流静默这么久会主动中止,而不是把请求永远挂着。
137
+ - **所有认不出的失败都被说成网络问题** —— 分类器对未知错误一律 `network`,shim 又把它
138
+ 标成可重试的 `TIMEOUT`,于是 400、鉴权失败、插件内部异常都会触发宿主的整套重试,
139
+ 最后给出一句「检查网络」。现在按 HTTP 状态与失败来源分类(401/403 → 会话失效、
140
+ 429 → 额度耗尽、4xx → 上游错误不可重试、5xx 与连接失败 → 可重试、其余 → 上游错误),
141
+ 只有真正的传输失败才会被建议重试,错误信封里也带上 `x-should-retry`。
142
+ - **深度思考内容永远为空** —— 帧处理层把 `onThinking` 回调漏掉了(shim 侧的
143
+ `reasoning_content` 分支早就写好了),开启深度思考时只能看到答案、看不到推理过程。
144
+ - **凭据明文进了子进程环境变量** —— DPAPI 调用把整个会话以 base64 塞进
145
+ `DSH_DOUBAO_DPAPI_BLOB` 交给 PowerShell,存活了 `Add-Type` 编译那 0.5–1 秒;
146
+ `...process.env` 还把本机所有 API key 一起带了过去。现在改为 stdin 传入、环境变量走
147
+ 白名单,`powershell.exe` 也从 `System32` 绝对路径启动而不是靠 PATH 查找。
148
+ - **一个坏 profile 会让后面所有候选都不再尝试** —— 候选遍历里 `decryptFromProfile` 没有
149
+ 自己的 try/catch,某个目录的 `Local State` 损坏会直接冒到外层,把 D 盘上真正可用的
150
+ profile 全跳过。现在逐个隔离,失败信息里带上具体目录。
151
+
152
+ - **一次会话失效要等十二小时才自愈** —— 加密的凭据副本在磁盘上缓存 12 小时,服务端
153
+ 已经判定失效的 session 会被反复读回来,每次请求都以同一种方式失败;用户按提示重新
154
+ 登录,插件仍然拿旧副本,直到 TTL 到期。现在服务端说会话失效就删掉盘上副本,让重新
155
+ 登录立刻生效。顺带挡住时钟回拨:缓存时间在未来(改过系统时间、双系统、主板电池没电)
156
+ 一律视为过期,否则 TTL 永远算不出一个正数。
157
+ - **额度接口不可用时每一轮对话都要多等一次失败** —— 额度是对话前的前置检查,而失败的
158
+ 结果从来不进缓存,于是接口挂掉的每一轮都要再付一次注定失败的往返,正好压在首字节
159
+ 之前。失败结果现在缓存 30 秒:够短,恢复延迟只有一轮;够长,能把这种反复打穿。
160
+ - **设置卡片对本机任何网页敞开** —— 四条路由只校验 Origin 是不是 loopback,而浏览器
161
+ 在**同源**请求里根本不发 Origin 头,于是「没有 Origin」被当成可信 —— 本机另一个端口
162
+ 上的页面(或任何能解析到 127.0.0.1 的页面)都能改掉模型开关和数据目录。现在 Host
163
+ 必须是 loopback(挡住把域名解析到 127.0.0.1 的 DNS rebinding),带
164
+ `sec-fetch-site: cross-site` 的请求直接拒绝,写请求还必须带一个自定义头 —— 自定义头
165
+ 会触发 CORS 预检,而这几条路由从不应答预检,跨源页面因此发不出这个请求。同源正常
166
+ 使用完全不受影响。
167
+ - **声明了类型声明文件,但那文件从来不存在** —— `package.json` 的 `types` 指向
168
+ `lib/index.d.ts`,构建却关着 `dts`,任何按类型使用这个包的人都会碰到一个空引用。
169
+ 现在真的生成声明文件(`lib/index.d.ts` 等 5 个),`types` 与 `exports` 两条路径
170
+ 都对得上。同时把 `lib/` 移出版本管理:tsdown 用内容哈希给共享 chunk 命名,每构建
171
+ 一次整批文件就改名,diff 里看不出任何真实改动;发布不受影响(`prepack` 会构建,
172
+ `files` 从 tarball 里取 `lib`)。
173
+
174
+ - **额度卡片的「重置时间」是编出来的** —— 这台账号的套餐只返回一个周额度,且窗口的
175
+ `start_time`/`end_time` 都是 0(服务端不给时间戳)。旧代码于是拿「现在」当窗口起点、
176
+ 加上一个名义周期当重置时刻,卡片显示「6 天后重置」—— 真实重置可能就在二十分钟后。
177
+ 同一份载荷里还带着 `total_amount: "2,100"` / `used_amount: "0"` 这种带千分位的金额,
178
+ 旧代码完全没读,一旦 `used_percent` 缺失就只能显示 0%。现在:窗口还没开始计时就照实
179
+ 说「开始使用后计时」(豆包客户端本身就是这个措辞),有时间戳就显示倒计时,两者都没
180
+ 有才说「重置时间未知」;金额解析后作为百分比的兜底来源。顺带修掉 `doctor` 把「这个
181
+ 套餐没有 5 小时窗口」判成失败的误报 —— 账号形态不是插件故障,只有额度真正读不出来
182
+ 才算失败。
183
+ - **真正在消耗的那份额度没被显示** —— 额度接口返回三个 section,旧代码只读
184
+ `window_limit_section`。这台账号的「活动赠送额度」(`incentive_quota_section`,
185
+ 已用 782 / 1,600、11 月 21 日 09:46 到期)因此完全不出现在卡片上:用户看到「近 7 天
186
+ 已用 0%」,以为额度一点没动,实际上已经在扣赠送额度了。现在两个 section 一起读,
187
+ 赠送额度单独一行、用「已用 782 / 1,600 · 16d 后到期」这种带金额的写法,排在常规窗口
188
+ 前面。
189
+
190
+ ### 新增
191
+ - **卡片改成看「还剩多少」,并自己走动** —— 进度条过去画的是消耗比例(越用越满),
192
+ 与「还剩多少」的直觉相反;现在画剩余量,用完即为空,颜色阈值也按剩余量给。数字从
193
+ 「已用 782 / 1,600」改成「剩 818 / 1,600」,倒计时由本地时钟每秒从绝对时刻重算 ——
194
+ 既不用每秒请求,也不会因为标签页放久了而漂移。轮询从 5 分钟缩到 30 秒,且只在卡片
195
+ 可见时进行,切回标签页立刻刷一次;轮询走 POST 绕过宿主 5 分钟缓存,否则每 30 秒
196
+ 拿回的都是同一份读数。
197
+ - **卡片重做了一遍视觉** —— 原来五个分区的高度、字号、边框几乎一样,眼睛找不到落点。
198
+ 现在整张卡是一片带极淡高光的表面,分区之间用一条发丝线分隔而不是各自套框;每个额度
199
+ 窗口是一块独立的面板,一行的重点只有一个:右边那个「还剩下多少」。倒计时和「已用
200
+ X / Y」退到第二行,不再和主数字抢宽度。进度条改成带方向感的渐变并带一点辉光,数字
201
+ 改用等宽字形 —— 每秒跳动的倒计时不会再让整行左右抖。
202
+
203
+ ### 测试
204
+ - `tests/upstream-terminal.test.ts`(12 例):终止帧、跨 chunk 拆帧、截断判定、思考透传、
205
+ 失败分类与重试矩阵。
206
+ - `tests/shim-lifecycle.test.ts`(5 例):客户端断开、正常结束后关闭、空闲超时、活动续命、
207
+ 释放后不再触发。
208
+ - `tests/auth-dpapi.test.ts`(4 例):源码级守住「明文不进 env」「不走 PATH」两条约束。
209
+ - `tests/save-route.test.ts`(+4 例):缺写入头、非 loopback Host、跨站标记三种拒绝路径,
210
+ 以及「同源 GET 不带 Origin 仍然放行」这条必须保持可用的例外。
211
+ - `tests/quota.test.ts`(8 例):用真实载荷做固定数据 —— 只有周额度、时间戳为 0、
212
+ 另含「活动赠送额度」组(782 / 1,600、只有到期时间、window_type 3);覆盖「不伪造
213
+ 重置时刻」「未开始计时标注 startsOnUse」「赠送额度只报到期」「金额推算百分比」
214
+ 「真实时间戳原样透传」五条契约。
215
+
216
+ ## 0.3.1 — 2026-11-02
217
+
218
+ > 相对 `0.3.0` 的 3 个提交。修「User Data 不在 C 盘就读不到登录信息」,并补上手动指定目录的
219
+ > 入口 —— 之前只能靠环境变量。
220
+
221
+ ### 新增
222
+
223
+ - **手动指定豆包数据目录** —— 设置卡片新增「豆包数据目录」一段,带「选择目录…」按钮
224
+ (桌面端弹系统原生文件夹对话框)和「改回自动探测」。装豆包时把数据目录改到 D 盘、
225
+ 或事后手动搬走的人,现在可以自己指路,不用再设环境变量 `DSH_DOUBAO_PROFILE`。
226
+ 段落里会显示当前用的是哪个目录,以及探测到的全部候选(带来源:
227
+ `manual` / `env` / `appdata` / `process` / `scan`)。
228
+ - 选中的路径在写入前会校验目录里是否有 `Default\Network\Cookies`,选错当场提示并**拒绝保存**;
229
+ - 选中的目录后来被删掉或搬走,会自动退回自动探测,不会把凭据加载卡死;
230
+ - 指到 `User Data` 本身或它的父目录(例如 `D:\Doubao`)都可以,后者会自动往下找一层。
231
+ - 浏览器形态的宿主没有原生目录对话框,按钮会明确报错而不是静默失败。
232
+
233
+ ### 修复
234
+
235
+ - **User Data 不在 C 盘就读不到登录信息** —— 凭据查找曾硬编码
236
+ `%LOCALAPPDATA%\Doubao\User Data`,把数据放到 D 盘(安装器提供这个选项,也有人事后手动搬)
237
+ 的机器会报 `Doubao user profile not found`,而报错里显示的路径恰恰是用户根本没有的那个。
238
+ 现在改为五层回退:卡片里手动选的目录 → 环境变量 `DSH_DOUBAO_PROFILE` → AppData 候选 →
239
+ 运行中客户端的 `--user-data-dir` → 限时扫盘(深度 4、1.5 秒预算、跳过系统目录)。
240
+ 顺序按**代价**而非准确度排:常见情况 4 次 `existsSync` 就命中,不会为 0.8 秒的进程探测
241
+ 拖慢每次冷启动。返回候选**列表**而非单一路径,因为「复制而非移动」会留下空壳 profile —
242
+ 凭据加载逐个尝试,取第一个真正解出可用会话的。
243
+ - **手动指定的目录失效后会永久留空** —— 覆盖值现在每次都会重新校验,失效的选点自动落回
244
+ 自动探测,不再遮蔽真正可用的 profile。
245
+ - **提示文案里的 Windows 路径少了一层反斜杠** —— `Default\Network\Cookies` 写在单引号字符串里
246
+ 会被当成转义序列,浏览器实际看到的是 `DefaultNetworkCookies`,正好是最需要准确的那句话。
247
+ 已补转义,并加了 `scripts/check-locale-literals.mjs` 直接检查构建产物里的字面量
248
+ (`npm run check` 会自动跑)。
249
+
250
+ ### 测试
251
+
252
+ - 11 个测试文件、**151 个用例**全绿(0.3.0 是 132 例)。新增覆盖:`profileDir` 的存取与非法
253
+ 类型、`/__save` 对目录存在性的校验(选错目录必须 400 且不写盘)、`/profile` 只读路由
254
+ (空结果返回 `null` 而不是空字符串)、手动覆盖的优先级与失效回退。
255
+ - `doctor` 五项全绿;`doctor --json` 的 `credentials.data.profile` 会显示来源,
256
+ 现在多一个 `via manual`。
257
+
258
+ ---
259
+
260
+ ## 0.3.0 — 2026-11-02
261
+
262
+ > 相对 `0.2.0` 的 15 个提交。这一版把插件从「只能纯聊天」推到了「能生图、能续聊、
263
+ > 能让模型通过提示词层模拟去调工具」,并修掉了一个从项目第一天就存在的设置持久化缺陷。
264
+
265
+ ### 新增
266
+
267
+ - **豆包生图(Seedream 5.0)** —— 模型选择器里新增「豆包 生图(Seedream 5.0)」。
268
+ 生成结果以 Markdown 图片形式追加在回复正文里,可直接预览与保存。
269
+ 实测一次请求返回 3 张 2848×1600 图片(右下角带「豆包 AI生成」水印)。
270
+ 注意 URL 后缀是 `.jpeg` 但字节是 PNG,保存时按内容判格式。
271
+ - **模型清单自动发现** —— 不再硬编码模型名。插件直接读豆包客户端的本地缓存
272
+ (`menuConfV2`,V8 structured clone 编码),豆包上线新模型时清单会自动跟上。
273
+ 缓存解析失败时自动回退到内置静态清单,`doctor` 会显示当前用的是哪一种。
274
+ 「工作任务」专属模型按名称前缀自动隐藏。
275
+ - **真会话续聊** —— 同一个 DSH 会话的第二轮起只发送增量消息,由豆包服务端维护线程内存,
276
+ 不再每轮重发全部历史。实测第二轮只发 20 个字符,服务端仍能答出上一轮的内容。
277
+ - **深度思考分离** —— 开启深度思考后,思考过程走 `reasoning_content` 而不是混进正文。
278
+ - **实验性工具调用(提示词层模拟)** —— 让豆包能驱动 DSH 的本地工具(读写文件等)。
279
+ 默认**关闭**,设置卡片里有独立开关。实现方式是在提示词里注入工具契约,再把模型输出的
280
+ ` ```doubao-tool ` 围栏解析回标准 `tool_calls`。实测 5 次调用、坏 JSON 0 次、5/5 轮收敛。
281
+ - **设置卡片新增工具开关段落** —— 与模型清单分开保存,避免「换模型」和「放开文件权限」
282
+ 共用一次确认;关不掉的权限开关是比误开更坏的失败。
283
+ - **手动指定豆包数据目录** —— 设置卡片新增「豆包数据目录」一段,带「选择目录…」按钮
284
+ (桌面端弹原生文件夹对话框)和「改回自动探测」。装豆包时把数据目录改到 D 盘、或者事后
285
+ 手动搬走的人,现在可以自己指路,不用再设环境变量。选中的路径在写入前会校验
286
+ `Default\Network\Cookies` 是否存在,选错当场提示且不落盘;已选的目录后来被删掉会自动
287
+ 退回自动探测。段落里会列出当前用着哪个目录、以及探测到的全部候选。
288
+
289
+ ### 修复
290
+
291
+ - **设置保存失败(从项目第一天就存在)** —— 症状是点开关报
292
+ `保存失败:host did not persist the value`,或看起来保存成功但刷新后回退。
293
+ 根因不在 schema:本插件的 fiber 在 DSH 0.2.1 上**永远停在 `state 1`(LOADING)**,
294
+ 宿主因此从不给它挂 `runtime.Config`,`describe()` 直接过滤掉命名空间,任何写入都失败于
295
+ `No configurable plugin entry "dsh-connect-doubao"`。
296
+ **这意味着历史上这个插件的设置一次都没能落盘过,包括手工改 `cordis.patch.yml` 的那些。**
297
+ 现在两个开关改存插件自己的 `~/.dsh/dsh-connect-doubao.settings.json`,
298
+ 写入是先写临时文件再原子重命名,读取不依赖任何宿主服务在线。
299
+ - **工具契约导致幻觉** —— 温和措辞的契约下,模型会回「已完成修改:…」但**磁盘根本没动**。
300
+ 这是最坏的失败模式:用户以为文件改了。契约已改为硬性规则(第一条回复必须是工具调用、
301
+ 严禁在没有 `[tool result]` 时声称完成),实测从 0/2 提升到 2/2。
302
+ - **多步编辑互相污染** —— 局部替换链会让第 N 次编辑失效于第 N+1 次,最终文件出现重复行。
303
+ 契约加入「每次修改后重新读取文件再决定下一步」后,同一任务 1 次 edit + 1 次 read 收敛。
304
+ - **回复首字丢失** —— 回复开头在 `STREAM_MSG_NOTIFY`、增量在 `STREAM_CHUNK`,
305
+ 旧代码只取一边导致「1+1」变成「+1」。已抽出 `assembleReplyText` 统一拼接。
306
+ - **对话模式根本不通** —— `agent_mode` 常量写反(1/2 互相搞错),每一轮都被路由到
307
+ 豆包的任务 agent,直接回 `710010702`。已按 CDP 抓到的真实请求体整体重写。
308
+ - **User Data 不在 C 盘就读不到登录信息** —— 凭据查找曾硬编码
309
+ `%LOCALAPPDATA%\Doubao\User Data`,把数据放到 D 盘(安装器提供这个选项,也有人事后手动搬)
310
+ 的机器会报 `Doubao user profile not found`。现在改为五层回退:设置卡片里手动选的目录 →
311
+ 环境变量 `DSH_DOUBAO_PROFILE` → AppData 候选 → 运行中客户端的 `--user-data-dir` → 限时扫盘。
312
+ 顺序按**代价**而非准确度排:常见情况 4 次 `existsSync` 就命中,不会为 0.8 s 的进程探测
313
+ 拖慢每次冷启动。返回的是候选**列表**而非单一路径,因为「复制而非移动」会留下空壳 profile —
314
+ 凭据加载会逐个尝试,取第一个真正能解出可用会话的。
315
+
316
+ ### 改进
317
+
318
+ - **改动说明改为逐条列出** —— 工具执行完成后不再只说「已修改」,而是按「哪个文件 → 改了什么 →
319
+ 为什么」逐条说明,多处改动分别列出。
320
+ - **请求体形状有测试钉住** —— 18 个断言全部取自 CDP 抓包而非由被测常量反推,
321
+ 防止 `agent_mode` 这类常量再次静默回归。
322
+
323
+ ### 已知限制
324
+
325
+ - **选不到 2.1 Pro / 2.1 Lite**。服务端**不校验** `model_item_key`(四种真 key 和一个不存在的
326
+ key 耗时与长度无可测差异),模型由服务端按产品/模式/账号决定。目录已收缩为
327
+ 「快速 / 2.1 Turbo·专家 / 生图」三项,Pro/Lite 只能在豆包客户端自己的「工作任务」里用。
328
+ - **「工作任务 · 本地电脑」调不到**。协议层字段(`runtime_type: 2`、`sandbox_id`、`device_name`)
329
+ 都能发对,但执行者是需要 App 进程内 CUA 运行时的组件,外部客户端一律
330
+ `710010702`。插件默认走纯对话模式,`DSH_DOUBAO_TASK=1` 才尝试任务模式。
331
+ - **工具模拟只在豆包自身不具备的能力上可靠**。让它「查天气」时它会改用自带联网搜索,
332
+ 不调用我们的工具。
333
+ - **Windows only**。凭据解密依赖 DPAPI。User Data 可以不在 C 盘(见上文五层回退)。
334
+ 浏览器形态的宿主没有原生目录对话框,「选择目录…」会明确报错而不是静默失败,
335
+ 那种环境请用 `DSH_DOUBAO_PROFILE`。
336
+
337
+ ### 测试
338
+
339
+ - 11 个测试文件、**151 个用例**全绿。本版新增三个文件:`store.test.ts` 20 例(文件合并语义、
340
+ 原子写入、无临时文件残留、`profileDir` 存取与非法类型)、`save-route.test.ts` 20 例
341
+ (字段白名单、Origin 校验、值形状、500 契约、`profileDir` 的路径存在性校验、`/profile`
342
+ 只读路由)、`profile.test.ts` 25 例(五层探测优先级、去重、扫描深度与时间预算、
343
+ 手动覆盖的优先与失效回退)。
344
+ - `doctor` 五项全绿:凭据 29 cookie / 账号通过 / shim OK / 额度 / 模型发现。
345
+ `doctor --json` 的 `credentials.data` 现在同时给出 `profile`(用的是哪个、`via env|appdata|
346
+ process|scan|manual`)与 `candidates`(全部候选),定位"读不到登录信息"时先看这两项。
347
+
348
+ ---
349
+
350
+ ## 0.2.0
351
+
352
+ - 修复对话模式完全不通(`agent_mode` 常量写反)。
353
+ - 请求体按 CDP 抓包重写。
354
+
355
+ ## 0.1.1 / 0.1.0
356
+
357
+ - 首个可用版本:凭据复用(DPAPI + AES-GCM)、provider 注册、只读额度卡片。