dsh-connect-doubao 0.2.0 → 0.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 +85 -0
- package/README.md +108 -6
- package/lib/cli-doctor.js +14 -2
- package/lib/cli-probe.js +1 -1
- package/lib/client.js +168 -29
- package/lib/{doubao-quota-XUFfDd1i.js → doubao-quota-Qj46Oa4D.js} +260 -43
- package/lib/doubao-status-NgMAwtxK.js +1463 -0
- package/lib/index.js +269 -21
- package/package.json +2 -1
- package/lib/doubao-status-C3wgMqnL.js +0 -773
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# 更新记录
|
|
2
|
+
|
|
3
|
+
本文件记录每个发布版本对用户可见的变化。版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 0.3.0 — 2026-11-02
|
|
8
|
+
|
|
9
|
+
> 相对 `0.2.0` 的 15 个提交。这一版把插件从「只能纯聊天」推到了「能生图、能续聊、
|
|
10
|
+
> 能让模型通过提示词层模拟去调工具」,并修掉了一个从项目第一天就存在的设置持久化缺陷。
|
|
11
|
+
|
|
12
|
+
### 新增
|
|
13
|
+
|
|
14
|
+
- **豆包生图(Seedream 5.0)** —— 模型选择器里新增「豆包 生图(Seedream 5.0)」。
|
|
15
|
+
生成结果以 Markdown 图片形式追加在回复正文里,可直接预览与保存。
|
|
16
|
+
实测一次请求返回 3 张 2848×1600 图片(右下角带「豆包 AI生成」水印)。
|
|
17
|
+
注意 URL 后缀是 `.jpeg` 但字节是 PNG,保存时按内容判格式。
|
|
18
|
+
- **模型清单自动发现** —— 不再硬编码模型名。插件直接读豆包客户端的本地缓存
|
|
19
|
+
(`menuConfV2`,V8 structured clone 编码),豆包上线新模型时清单会自动跟上。
|
|
20
|
+
缓存解析失败时自动回退到内置静态清单,`doctor` 会显示当前用的是哪一种。
|
|
21
|
+
「工作任务」专属模型按名称前缀自动隐藏。
|
|
22
|
+
- **真会话续聊** —— 同一个 DSH 会话的第二轮起只发送增量消息,由豆包服务端维护线程内存,
|
|
23
|
+
不再每轮重发全部历史。实测第二轮只发 20 个字符,服务端仍能答出上一轮的内容。
|
|
24
|
+
- **深度思考分离** —— 开启深度思考后,思考过程走 `reasoning_content` 而不是混进正文。
|
|
25
|
+
- **实验性工具调用(提示词层模拟)** —— 让豆包能驱动 DSH 的本地工具(读写文件等)。
|
|
26
|
+
默认**关闭**,设置卡片里有独立开关。实现方式是在提示词里注入工具契约,再把模型输出的
|
|
27
|
+
` ```doubao-tool ` 围栏解析回标准 `tool_calls`。实测 5 次调用、坏 JSON 0 次、5/5 轮收敛。
|
|
28
|
+
- **设置卡片新增工具开关段落** —— 与模型清单分开保存,避免「换模型」和「放开文件权限」
|
|
29
|
+
共用一次确认;关不掉的权限开关是比误开更坏的失败。
|
|
30
|
+
|
|
31
|
+
### 修复
|
|
32
|
+
|
|
33
|
+
- **设置保存失败(从项目第一天就存在)** —— 症状是点开关报
|
|
34
|
+
`保存失败:host did not persist the value`,或看起来保存成功但刷新后回退。
|
|
35
|
+
根因不在 schema:本插件的 fiber 在 DSH 0.2.1 上**永远停在 `state 1`(LOADING)**,
|
|
36
|
+
宿主因此从不给它挂 `runtime.Config`,`describe()` 直接过滤掉命名空间,任何写入都失败于
|
|
37
|
+
`No configurable plugin entry "dsh-connect-doubao"`。
|
|
38
|
+
**这意味着历史上这个插件的设置一次都没能落盘过,包括手工改 `cordis.patch.yml` 的那些。**
|
|
39
|
+
现在两个开关改存插件自己的 `~/.dsh/dsh-connect-doubao.settings.json`,
|
|
40
|
+
写入是先写临时文件再原子重命名,读取不依赖任何宿主服务在线。
|
|
41
|
+
- **工具契约导致幻觉** —— 温和措辞的契约下,模型会回「已完成修改:…」但**磁盘根本没动**。
|
|
42
|
+
这是最坏的失败模式:用户以为文件改了。契约已改为硬性规则(第一条回复必须是工具调用、
|
|
43
|
+
严禁在没有 `[tool result]` 时声称完成),实测从 0/2 提升到 2/2。
|
|
44
|
+
- **多步编辑互相污染** —— 局部替换链会让第 N 次编辑失效于第 N+1 次,最终文件出现重复行。
|
|
45
|
+
契约加入「每次修改后重新读取文件再决定下一步」后,同一任务 1 次 edit + 1 次 read 收敛。
|
|
46
|
+
- **回复首字丢失** —— 回复开头在 `STREAM_MSG_NOTIFY`、增量在 `STREAM_CHUNK`,
|
|
47
|
+
旧代码只取一边导致「1+1」变成「+1」。已抽出 `assembleReplyText` 统一拼接。
|
|
48
|
+
- **对话模式根本不通** —— `agent_mode` 常量写反(1/2 互相搞错),每一轮都被路由到
|
|
49
|
+
豆包的任务 agent,直接回 `710010702`。已按 CDP 抓到的真实请求体整体重写。
|
|
50
|
+
|
|
51
|
+
### 改进
|
|
52
|
+
|
|
53
|
+
- **改动说明改为逐条列出** —— 工具执行完成后不再只说「已修改」,而是按「哪个文件 → 改了什么 →
|
|
54
|
+
为什么」逐条说明,多处改动分别列出。
|
|
55
|
+
- **请求体形状有测试钉住** —— 18 个断言全部取自 CDP 抓包而非由被测常量反推,
|
|
56
|
+
防止 `agent_mode` 这类常量再次静默回归。
|
|
57
|
+
|
|
58
|
+
### 已知限制
|
|
59
|
+
|
|
60
|
+
- **选不到 2.1 Pro / 2.1 Lite**。服务端**不校验** `model_item_key`(四种真 key 和一个不存在的
|
|
61
|
+
key 耗时与长度无可测差异),模型由服务端按产品/模式/账号决定。目录已收缩为
|
|
62
|
+
「快速 / 2.1 Turbo·专家 / 生图」三项,Pro/Lite 只能在豆包客户端自己的「工作任务」里用。
|
|
63
|
+
- **「工作任务 · 本地电脑」调不到**。协议层字段(`runtime_type: 2`、`sandbox_id`、`device_name`)
|
|
64
|
+
都能发对,但执行者是需要 App 进程内 CUA 运行时的组件,外部客户端一律
|
|
65
|
+
`710010702`。插件默认走纯对话模式,`DSH_DOUBAO_TASK=1` 才尝试任务模式。
|
|
66
|
+
- **工具模拟只在豆包自身不具备的能力上可靠**。让它「查天气」时它会改用自带联网搜索,
|
|
67
|
+
不调用我们的工具。
|
|
68
|
+
- **Windows only**。凭据解密依赖 DPAPI。
|
|
69
|
+
|
|
70
|
+
### 测试
|
|
71
|
+
|
|
72
|
+
- 10 个测试文件、**112 个用例**全绿(其中本版新增 `store.test.ts` 15 例、
|
|
73
|
+
`save-route.test.ts` 11 例,覆盖文件合并语义、原子写入、字段白名单、Origin 校验与 500 契约)。
|
|
74
|
+
- `doctor` 五项全绿:凭据 29 cookie / 账号通过 / shim OK / 额度 / 模型发现。
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 0.2.0
|
|
79
|
+
|
|
80
|
+
- 修复对话模式完全不通(`agent_mode` 常量写反)。
|
|
81
|
+
- 请求体按 CDP 抓包重写。
|
|
82
|
+
|
|
83
|
+
## 0.1.1 / 0.1.0
|
|
84
|
+
|
|
85
|
+
- 首个可用版本:凭据复用(DPAPI + AES-GCM)、provider 注册、只读额度卡片。
|
package/README.md
CHANGED
|
@@ -30,7 +30,8 @@ Chromium cookie(通过 Windows DPAPI 在本进程内解密),把它当作
|
|
|
30
30
|
|
|
31
31
|
## 功能特性
|
|
32
32
|
|
|
33
|
-
-
|
|
33
|
+
- **豆包对话接入** —— 对话模式真正可用的两个条目(见下表),作为 DSH 的模型 provider。
|
|
34
|
+
- **生图(Seedream 5.0)** —— 模型选择器里选「豆包 生图」,回复即以 Markdown 图片返回真实生成图,可直接下载落盘。2026-11-02 实测端到端通过。
|
|
34
35
|
- **推理强度三档** —— 低 / 中 / 高,对应上游 `reasoning_effort` 实测值 **3 / 4 / 5**。
|
|
35
36
|
- **只读额度面板** —— 5 小时限额与周额度两条进度条,各带**重置倒计时**;设置面板里手动刷新 + 5 分钟自动刷新。
|
|
36
37
|
- **模型勾选清单** —— 勾选的条目会出现在 DSH 的模型选择器中,请求时按 `model_item_key` 指定模型。
|
|
@@ -105,6 +106,91 @@ Chromium cookie(通过 Windows DPAPI 在本进程内解密),把它当作
|
|
|
105
106
|
|
|
106
107
|
---
|
|
107
108
|
|
|
109
|
+
## 生图(技能轴,与模型轴正交)
|
|
110
|
+
|
|
111
|
+
豆包把「能力」做成 **skill**,走的是**同一个** `/chat/completion` 端点 —— 客户端里没有独立的文生图 HTTP 接口
|
|
112
|
+
(唯一的生成端点 `/creativity/canvas/exec` 是图片编辑器:抠图 / 扩图 / 分层,不是文生图)。
|
|
113
|
+
|
|
114
|
+
| 字段 | 值 |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| 位置 | `ext.input_skill`(**JSON 字符串**) |
|
|
117
|
+
| 内容 | `{"skill_id":"3","skill_type":3,"variables":{}}` |
|
|
118
|
+
| 关系 | `skill_id` 就是 `skill_type` 的十进制字符串;普通对话是 `"0"`/0 |
|
|
119
|
+
| 响应 | `block_type: 2074` → `content.creation_block.creations[].image.{image_ori,image_preview,image_thumb}` |
|
|
120
|
+
| 模型 | `placeholder.description` 显示 **Seedream 5.0 Flash** |
|
|
121
|
+
|
|
122
|
+
技能全表(客户端枚举原样抄录,非推测):`0` 自由对话、`2` 写作、`3` 生图、`4` 搜索、`5` 翻译、
|
|
123
|
+
`6` 阅读、`7` 网页、`8` 深度搜索、`9` 音乐、`11` 教育、`13` 代码执行、`16` 代码助手、`17` 视频、`18` PPT。
|
|
124
|
+
|
|
125
|
+
本插件目前只接了 **生图**:模型选择器里的「豆包 生图(Seedream 5.0)」是 DSH 侧的能力别名
|
|
126
|
+
(`image-gen`,**不是** `model_item_key`),选中后 shim 发一条普通对话轮并带上 `input_skill: imageGen`,
|
|
127
|
+
再把返回的图片以 `` 追加到正文。
|
|
128
|
+
|
|
129
|
+
**两个实测细节**:
|
|
130
|
+
|
|
131
|
+
- 图片 URL 的签名有效期极长(抓到的 `x-expires` 在 2037 年),但服务端会把 `.jpeg` 结尾的 URL
|
|
132
|
+
实际返回 **PNG 字节** —— 落盘时按内容判断格式,别信扩展名。
|
|
133
|
+
- 提示词会被服务端改写扩写(`creation_material_info.creation_prompt` 里是扩写后的版本),
|
|
134
|
+
所以产物质量与措辞由豆包决定,不是逐字照发。
|
|
135
|
+
|
|
136
|
+
验证脚本:`node scripts/verify-imagegen.mjs`(会真实消耗生图额度)。
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 模型清单是自动发现的
|
|
141
|
+
|
|
142
|
+
**插件不再硬编码模型表。** 桌面客户端把服务端下发的 `menuConfV2` 存在自己的 IndexedDB 里,
|
|
143
|
+
本插件直接读那份缓存 —— 所以豆包上线新模型后,**不需要等插件更新**就会出现在 DSH 里。
|
|
144
|
+
|
|
145
|
+
- 缓存值是 **V8 structured clone**(不是 JSON):字符串 = 标签 + LEB128 长度 + 数据,
|
|
146
|
+
`0x22` 是 Latin-1、`0x63` 是 UTF-16LE。**实测确认 `0x63` 的长度字段是字节数而不是字符数**
|
|
147
|
+
—— 按字符数会正好读多一倍,模型名后面会吃掉下一个字段的字节。
|
|
148
|
+
- 客户端给「工作任务」专属模型的名称自带前缀(`工作任务 Pro`、`工作任务 Auto`),
|
|
149
|
+
所以模式归属直接从缓存里就能读出来:带前缀的一律不开放(它们需要 App 内的 CUA 运行时)。
|
|
150
|
+
- 缓存读不到、或解析结果不可信时,**回落到内置清单**,绝不清空用户的选择器。
|
|
151
|
+
- 结果缓存 10 分钟;卡片上的「刷新」(POST)会丢弃记忆重新读盘。
|
|
152
|
+
- `doctor` 第 5 项会报告这次到底是从缓存发现的、还是回落的。
|
|
153
|
+
|
|
154
|
+
当前这台机器上的实测输出:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
[ ok ] models 客户端缓存 5 个模型,开放 3 个(「工作任务」专属的按名字前缀排除)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## 实验性:工具调用(默认关闭)
|
|
161
|
+
|
|
162
|
+
**豆包没有函数调用通道。** 带 OpenAI 风格的 `tools` 发请求,服务端会**静默忽略**
|
|
163
|
+
——2026-10-22 实测:HTTP 200、流正常,但它转而使用自己的服务端搜索技能(流里冒出
|
|
164
|
+
`block_type 10101` 的「正在搜索」块),从头到尾没引用过我们声明的函数。这和
|
|
165
|
+
`model_item_key` 不校验是同一类事:字段根本没被读。
|
|
166
|
+
|
|
167
|
+
所以工具调用只能**在提示词里模拟**:把工具清单写进提示词,让模型输出一个
|
|
168
|
+
```` ```doubao-tool ```` 围栏包一个 JSON,再解析回标准 `tool_calls`。
|
|
169
|
+
|
|
170
|
+
| 项 | 值 |
|
|
171
|
+
| --- | --- |
|
|
172
|
+
| 开关 | 设置卡片「实验性:工具调用」,默认**关闭** |
|
|
173
|
+
| 配置字段 | `toolEmulation: boolean` |
|
|
174
|
+
| 围栏格式 | ```` ```doubao-tool ``\n`{"name":"…","arguments":{…}}` |
|
|
175
|
+
| 解析结果 | `delta.tool_calls` + `finish_reason: "tool_calls"`(流式与非流式一致) |
|
|
176
|
+
| 工具结果回传 | 入站 `role: "tool"` 折叠成 `[tool result] <name>: <content>` |
|
|
177
|
+
|
|
178
|
+
**实测数据**(2026-11-02):
|
|
179
|
+
|
|
180
|
+
- 沙箱两工具循环:9 次调用、**0 次 JSON 格式错误**、5/5 轮收敛、平均 **11.5 秒/轮**、1 次重复调用
|
|
181
|
+
- 真机走 shim:第 1 轮 `finish_reason=tool_calls`、参数完整;回填结果后给出结论、无误围栏泄漏
|
|
182
|
+
- **反例**:让它「查天气」时它**不调用我们的工具**,而是用自带的联网搜索直接答
|
|
183
|
+
—— 所以这套只在豆包自身不具备的能力上可靠(读写文件、跑命令),在它已有技能
|
|
184
|
+
(搜索、生图、翻译)上会绕过。
|
|
185
|
+
|
|
186
|
+
**失败模式是重复调用**(没有原生 stop token),因此解析层内置去重,调用方仍需设置步数上限。
|
|
187
|
+
这是一套**模拟**,上游模型换了输出习惯就可能退化 —— 这是它默认关闭、并且每条注释都写明
|
|
188
|
+
「measured / simulated」的原因。
|
|
189
|
+
|
|
190
|
+
验证脚本:`node scripts/verify-tool-emulation.mjs`(只验证协议链路,不执行工具)。
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
108
194
|
## 工作原理
|
|
109
195
|
|
|
110
196
|
```
|
|
@@ -236,11 +322,27 @@ npm run check # 以上全部
|
|
|
236
322
|
客户端进程里的 CUA(`tasks-mode-resource` + `sbox`),外部客户端注册不进去,必然回
|
|
237
323
|
`710010702`。所以插件固定走对话模式;请求体里的 `general_task_param` 发的是客户端在普通
|
|
238
324
|
对话下的默认值 `{"server_do_compare":true}`。
|
|
239
|
-
9. **`model_item_key`
|
|
240
|
-
`
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
325
|
+
9. **`model_item_key` 服务端根本不校验,所以选不了模型。** 三条证据(2026-10-23):
|
|
326
|
+
1. CDP 抓到 App 选 2.1 Pro 时的真实请求:`model_item_key: "5"` + `mode_id: "3"` +
|
|
327
|
+
`agent_mode: 1` + `general_task_param.runtime_type: 2`(本地电脑)。前三项我们能复制,
|
|
328
|
+
最后一项是 App 进程内的 CUA 运行时,复制不了 —— 这正是 `710010702` 的来源。
|
|
329
|
+
2. 用 `mode_id=3` + `key=5` 在对话模式发请求:正常回答,但与 `key=0` 无可测差异
|
|
330
|
+
(耗时 15.1–17.6 s、长度 311–340 字,均在噪声范围内)。
|
|
331
|
+
3. **真正的对照**:把三处 `model_item_key` 全部改成一个不存在的值直接发送,服务端仍返回
|
|
332
|
+
200 与完整正常流,没有任何错误帧 —— 它不校验这个字段。
|
|
333
|
+
所以「在 DSH 里指定 2.1 Pro / 2.1 Lite」做不到:模型由服务端按产品/模式/账号决定。
|
|
334
|
+
真要用这两个模型,只能在豆包客户端自己的「工作任务」里用。
|
|
335
|
+
10. **本插件的设置不走宿主 settings,存在自己的 JSON 文件里。**
|
|
336
|
+
`~/.dsh/dsh-connect-doubao.settings.json`,由插件自己写入(先写临时文件再原子重命名),
|
|
337
|
+
读取也不依赖任何宿主服务在线。原因是宿主这条路对本插件**完全关闭**:它的 fiber 永远停在
|
|
338
|
+
`state 1`(LOADING,和 `hmr`、`credentials` 一样),因此 `entry.fiber.runtime.Config`
|
|
339
|
+
从不挂载 → 宿主 `describe()` 直接过滤掉这个命名空间 → 任何写入都以
|
|
340
|
+
`No configurable plugin entry "dsh-connect-doubao"` 失败。
|
|
341
|
+
**历史上这个插件的设置一次都没能落盘过**(包括手工改的),所以卡片会出现「点了没反应 /
|
|
342
|
+
`host did not persist the value` / 保存成功但刷新后回退」三种症状。
|
|
343
|
+
schema 里仍然保留 `asVolatile()`:那是宿主侧的正当声明,一旦上游修好 fiber 生命周期,
|
|
344
|
+
走宿主路径也不会再踩坑;只是在当前版本上它不再决定成败。`tests/store.test.ts` 和
|
|
345
|
+
`tests/save-route.test.ts` 钉住文件语义与路由安全边界。
|
|
244
346
|
|
|
245
347
|
---
|
|
246
348
|
|
package/lib/cli-doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { $ as DOUBAO_PROVIDER, A as probeAccount, F as REQUIRED_COOKIE_NAMES, Q as DOUBAO_MODELS, R as describeProfileLocation, V as loadDoubaoCredentials, et as DOUBAO_PROVIDER_DISPLAY_NAME, lt as discoverDoubaoModels, m as DoubaoUpstreamError, n as fetchQuota, r as findWindow, w as classifyDoubaoFailure, z as findDoubaoUserDataDir } from "./doubao-quota-Qj46Oa4D.js";
|
|
3
|
+
import { f as liveModels, h as createDoubaoAdapter, i as doubaoQuotaView, l as discoverAllCachedModels, n as DOUBAO_QUOTA_PATH, p as createDoubaoShim } from "./doubao-status-NgMAwtxK.js";
|
|
4
4
|
//#region src/cli-doctor.ts
|
|
5
5
|
/**
|
|
6
6
|
* `doctor` — verify the whole stack end to end without spending inference quota.
|
|
@@ -138,6 +138,18 @@ async function runDoctor() {
|
|
|
138
138
|
}
|
|
139
139
|
});
|
|
140
140
|
}
|
|
141
|
+
const cached = discoverAllCachedModels();
|
|
142
|
+
const offered = liveModels();
|
|
143
|
+
checks.push({
|
|
144
|
+
name: "models",
|
|
145
|
+
status: offered.length > 0 ? "ok" : "fail",
|
|
146
|
+
detail: cached.length > 0 ? `客户端缓存 ${cached.length} 个模型,开放 ${offered.length} 个(「工作任务」专属的按名字前缀排除)` : `读不到客户端缓存,回落到内置清单(${offered.length} 个)`,
|
|
147
|
+
data: {
|
|
148
|
+
source: cached.length > 0 ? "client-cache" : "static-fallback",
|
|
149
|
+
cached: cached.map((entry) => `${entry.key}=${entry.name}${entry.taskOnly ? "[工作任务]" : ""}`),
|
|
150
|
+
offered: offered.map((model) => `${model.id}=${model.name}`)
|
|
151
|
+
}
|
|
152
|
+
});
|
|
141
153
|
return {
|
|
142
154
|
ok: checks.every((check) => check.status === "ok"),
|
|
143
155
|
plugin: "dsh-connect-doubao",
|
package/lib/cli-probe.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { A as probeAccount, Q as DOUBAO_MODELS, R as describeProfileLocation, V as loadDoubaoCredentials, ft as findDoubaoModel, i as formatDuration, j as streamChat, m as DoubaoUpstreamError, n as fetchQuota, o as millisUntilReset, pt as findReasoningLevel, r as findWindow } from "./doubao-quota-Qj46Oa4D.js";
|
|
3
3
|
//#region src/cli-probe.ts
|
|
4
4
|
/**
|
|
5
5
|
* Doubao smoke probe.
|
package/lib/client.js
CHANGED
|
@@ -54,6 +54,16 @@ window.__ModuleLoader__.load({
|
|
|
54
54
|
}
|
|
55
55
|
/** Host-side route serving the catalog for the checklist. */
|
|
56
56
|
const DOUBAO_CATALOG_PATH = "/plugins/dsh-connect-doubao/catalog";
|
|
57
|
+
/**
|
|
58
|
+
* The Host-side settings WRITE route.
|
|
59
|
+
*
|
|
60
|
+
* The card cannot persist through its injected `settingsScope`: on this host
|
|
61
|
+
* line `set()` resolves successfully and stores nothing (see
|
|
62
|
+
* `DoubaoQuotaCard.persistField`). This endpoint does the write in the Host
|
|
63
|
+
* process with `settings.mutate` — the path `dsh-connect-workbuddy` and
|
|
64
|
+
* `dsh-connect-trae` also use.
|
|
65
|
+
*/
|
|
66
|
+
const DOUBAO_SAVE_PATH = "/plugins/dsh-connect-doubao/__save";
|
|
57
67
|
/** Settings field holding the ticked entry ids. */
|
|
58
68
|
const DOUBAO_ENABLED_MODELS_FIELD = "enabledModelIds";
|
|
59
69
|
/**
|
|
@@ -69,6 +79,15 @@ window.__ModuleLoader__.load({
|
|
|
69
79
|
if (!Array.isArray(value)) return void 0;
|
|
70
80
|
return value.filter((entry) => typeof entry === "string");
|
|
71
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Read the tool-emulation switch out of a settings snapshot.
|
|
84
|
+
*
|
|
85
|
+
* `undefined` (never saved) means OFF — the emulation is opt-in, so an
|
|
86
|
+
* unconfigured install must not silently start letting the model touch files.
|
|
87
|
+
*/
|
|
88
|
+
function toolEmulationOf(configured) {
|
|
89
|
+
return configured?.toolEmulation === true;
|
|
90
|
+
}
|
|
72
91
|
/** Clamp a percentage into the 0-100 range the bars render. */
|
|
73
92
|
function clampPercent(value) {
|
|
74
93
|
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
@@ -297,10 +316,15 @@ window.__ModuleLoader__.load({
|
|
|
297
316
|
const [saved, setSaved] = (0, react.useState)(false);
|
|
298
317
|
const [saveError, setSaveError] = (0, react.useState)(null);
|
|
299
318
|
/**
|
|
300
|
-
* The settings snapshot,
|
|
301
|
-
*
|
|
319
|
+
* The settings snapshot, used ONLY as a fallback.
|
|
320
|
+
*
|
|
321
|
+
* The authoritative copy is now the plugin's own file, read through
|
|
322
|
+
* `DOUBAO_SAVE_PATH` below: this plugin's fiber never reaches ACTIVE on DSH
|
|
323
|
+
* 0.2.1, so the host neither lists nor writes its namespace (`No configurable
|
|
324
|
+
* plugin entry`). A snapshot that cannot see a save is worse than no
|
|
325
|
+
* snapshot, hence the file wins when both are available.
|
|
302
326
|
*/
|
|
303
|
-
const
|
|
327
|
+
const scopedValue = (0, react.useMemo)(() => {
|
|
304
328
|
if (scope === void 0) return void 0;
|
|
305
329
|
try {
|
|
306
330
|
return scope.getSnapshot().value;
|
|
@@ -308,13 +332,30 @@ window.__ModuleLoader__.load({
|
|
|
308
332
|
return;
|
|
309
333
|
}
|
|
310
334
|
}, [scope, settingsRevision]);
|
|
311
|
-
|
|
335
|
+
/** Settings as persisted by the plugin itself. `undefined` = not read yet. */
|
|
336
|
+
const [stored, setStored] = (0, react.useState)(void 0);
|
|
337
|
+
const [storeError, setStoreError] = (0, react.useState)(null);
|
|
338
|
+
const loadSettings = (0, react.useCallback)(async () => {
|
|
312
339
|
try {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
340
|
+
const response = await fetch(DOUBAO_SAVE_PATH, { method: "GET" });
|
|
341
|
+
if (!response.ok) throw new Error(`HTTP ${String(response.status)}`);
|
|
342
|
+
const body = await response.json();
|
|
343
|
+
setStored(body.value);
|
|
344
|
+
setStoreError(null);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
setStoreError(error?.message ?? "unknown error");
|
|
347
|
+
setStored(scope === void 0 ? void 0 : scopedValue);
|
|
316
348
|
}
|
|
317
|
-
})
|
|
349
|
+
}, [scope, scopedValue]);
|
|
350
|
+
(0, react.useEffect)(() => {
|
|
351
|
+
loadSettings();
|
|
352
|
+
}, [loadSettings]);
|
|
353
|
+
const configured = stored ?? scopedValue;
|
|
354
|
+
/**
|
|
355
|
+
* Writes go through the plugin's own endpoint, so they do NOT depend on the
|
|
356
|
+
* host's `writable` flag — that flag is exactly what was false here.
|
|
357
|
+
*/
|
|
358
|
+
const canWrite = stored !== void 0 || storeError === null;
|
|
318
359
|
(0, react.useEffect)(() => {
|
|
319
360
|
if (scope === void 0) return;
|
|
320
361
|
return scope.subscribe(() => setSettingsRevision((value) => value + 1));
|
|
@@ -421,30 +462,55 @@ window.__ModuleLoader__.load({
|
|
|
421
462
|
setSaveError(null);
|
|
422
463
|
}, []);
|
|
423
464
|
/**
|
|
465
|
+
* Persist one settings field through the plugin's OWN endpoint.
|
|
466
|
+
*
|
|
467
|
+
* Neither `scope.set()` NOR `settings.mutate` can persist this plugin: its
|
|
468
|
+
* fiber never reaches ACTIVE on DSH 0.2.1, so the host reports
|
|
469
|
+
* `No configurable plugin entry "dsh-connect-doubao"` and drops every write.
|
|
470
|
+
* The endpoint writes a JSON file the plugin owns (write-temp-then-rename),
|
|
471
|
+
* and returns the value it now reads back — so a successful response is
|
|
472
|
+
* proof of persistence, not a claim.
|
|
473
|
+
*/
|
|
474
|
+
const persistField = (0, react.useCallback)(async (field, value) => {
|
|
475
|
+
let response;
|
|
476
|
+
try {
|
|
477
|
+
response = await fetch(DOUBAO_SAVE_PATH, {
|
|
478
|
+
method: "POST",
|
|
479
|
+
headers: { "Content-Type": "application/json" },
|
|
480
|
+
body: JSON.stringify({
|
|
481
|
+
field,
|
|
482
|
+
value
|
|
483
|
+
})
|
|
484
|
+
});
|
|
485
|
+
} catch (error) {
|
|
486
|
+
throw new Error(`host save endpoint unreachable: ${error?.message ?? String(error)}`);
|
|
487
|
+
}
|
|
488
|
+
const detail = await response.json().catch(() => ({ error: `HTTP ${String(response.status)}` }));
|
|
489
|
+
if (!response.ok || detail.ok !== true) {
|
|
490
|
+
const reason = `${String(detail.errorName ?? "")} ${String(detail.error ?? "")}`.trim();
|
|
491
|
+
throw new Error(reason === "" ? `HTTP ${String(response.status)}` : reason);
|
|
492
|
+
}
|
|
493
|
+
setStored(detail.value);
|
|
494
|
+
setStoreError(null);
|
|
495
|
+
return detail.value;
|
|
496
|
+
}, []);
|
|
497
|
+
/**
|
|
424
498
|
* Save the draft, then READ IT BACK.
|
|
425
499
|
*
|
|
426
|
-
* `
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
* is the only reliable confirmation, and here it matters more than usual: a
|
|
431
|
-
* save discards the draft, so a silently-failed write would throw away the
|
|
432
|
-
* user's edits while reporting success.
|
|
500
|
+
* The endpoint reporting `ok` is the commit; the read-back only confirms the
|
|
501
|
+
* page's own snapshot caught up. Here it matters more than usual because a
|
|
502
|
+
* save discards the draft — so a snapshot that never refreshes is reported
|
|
503
|
+
* rather than silently losing the user's edits.
|
|
433
504
|
*/
|
|
434
505
|
const onSave = (0, react.useCallback)(async () => {
|
|
435
|
-
if (
|
|
506
|
+
if (draft === void 0) return;
|
|
436
507
|
setBusy(true);
|
|
437
508
|
setSaveError(null);
|
|
438
509
|
const ids = [...draft];
|
|
439
510
|
try {
|
|
440
|
-
await
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
444
|
-
readBack = enabledModelIdsOf(scope.getSnapshot().value);
|
|
445
|
-
}
|
|
446
|
-
if (!(readBack !== void 0 && readBack.length === ids.length && ids.every((id) => readBack?.includes(id)))) {
|
|
447
|
-
setSaveError(t("models.saveFailed", { reason: "host did not persist the value" }));
|
|
511
|
+
const committed = enabledModelIdsOf(await persistField(DOUBAO_ENABLED_MODELS_FIELD, ids));
|
|
512
|
+
if (!(committed !== void 0 && committed.length === ids.length && ids.every((id) => committed?.includes(id)))) {
|
|
513
|
+
setSaveError(t("models.saveFailed", { reason: "已写入磁盘,但回读的值与提交的不一致" }));
|
|
448
514
|
return;
|
|
449
515
|
}
|
|
450
516
|
setSaved(true);
|
|
@@ -454,10 +520,32 @@ window.__ModuleLoader__.load({
|
|
|
454
520
|
setBusy(false);
|
|
455
521
|
}
|
|
456
522
|
}, [
|
|
457
|
-
|
|
523
|
+
persistField,
|
|
458
524
|
draft,
|
|
459
525
|
t
|
|
460
526
|
]);
|
|
527
|
+
/**
|
|
528
|
+
* The tool-emulation switch.
|
|
529
|
+
*
|
|
530
|
+
* Written immediately (no Save button) because it is a single independent
|
|
531
|
+
* toggle: bundling it into the checklist's draft would make a model-selection
|
|
532
|
+
* edit and a privilege change share one commit, and the user cannot tell which
|
|
533
|
+
* one they are confirming.
|
|
534
|
+
*
|
|
535
|
+
* Read back for the same reason the checklist is: the host's write can resolve
|
|
536
|
+
* while the value never lands (see `onSave`), and silently failing to turn OFF
|
|
537
|
+
* a capability that lets a model touch files is the worse error.
|
|
538
|
+
*/
|
|
539
|
+
const toolsOn = toolEmulationOf(configured);
|
|
540
|
+
const [toolsError, setToolsError] = (0, react.useState)(null);
|
|
541
|
+
const onToggleTools = (0, react.useCallback)(async (next) => {
|
|
542
|
+
setToolsError(null);
|
|
543
|
+
try {
|
|
544
|
+
if (toolEmulationOf(await persistField("toolEmulation", next)) !== next) setToolsError(t("models.saveFailed", { reason: "已写入磁盘,但回读的值与提交的不一致" }));
|
|
545
|
+
} catch (error) {
|
|
546
|
+
setToolsError(t("models.saveFailed", { reason: error?.message ?? "unknown error" }));
|
|
547
|
+
}
|
|
548
|
+
}, [persistField, t]);
|
|
461
549
|
const state = classifyQuotaView(quota);
|
|
462
550
|
const stateText = t(`account.${state}`);
|
|
463
551
|
const stateTone = state === "ok" ? "" : state === "exhausted" || state === "network" ? " dbq-bad" : " dbq-warn";
|
|
@@ -551,7 +639,7 @@ window.__ModuleLoader__.load({
|
|
|
551
639
|
className: "dbq-note",
|
|
552
640
|
children: t("models.routingNote")
|
|
553
641
|
}),
|
|
554
|
-
|
|
642
|
+
!canWrite && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
555
643
|
className: "dbq-note",
|
|
556
644
|
children: t("models.readOnly")
|
|
557
645
|
}),
|
|
@@ -630,6 +718,45 @@ window.__ModuleLoader__.load({
|
|
|
630
718
|
]
|
|
631
719
|
})
|
|
632
720
|
]
|
|
721
|
+
}),
|
|
722
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
723
|
+
className: "dbq-section",
|
|
724
|
+
children: [
|
|
725
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
726
|
+
className: "dbq-section-title",
|
|
727
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("tools.title") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
728
|
+
className: "dbq-count",
|
|
729
|
+
children: toolsOn ? t("tools.enabled") : t("tools.disabled")
|
|
730
|
+
})]
|
|
731
|
+
}),
|
|
732
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
733
|
+
className: "dbq-note",
|
|
734
|
+
children: t("tools.note")
|
|
735
|
+
}),
|
|
736
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
737
|
+
className: "dbq-model",
|
|
738
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
739
|
+
type: "checkbox",
|
|
740
|
+
checked: toolsOn,
|
|
741
|
+
disabled: !canWrite,
|
|
742
|
+
onChange: (event) => void onToggleTools(event.currentTarget.checked)
|
|
743
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
744
|
+
className: "dbq-model-main",
|
|
745
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
746
|
+
className: "dbq-model-name",
|
|
747
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("tools.toggle") })
|
|
748
|
+
})
|
|
749
|
+
})]
|
|
750
|
+
}),
|
|
751
|
+
toolsOn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
752
|
+
className: "dbq-note dbq-warn",
|
|
753
|
+
children: t("tools.warning")
|
|
754
|
+
}),
|
|
755
|
+
toolsError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
756
|
+
className: "dbq-error",
|
|
757
|
+
children: toolsError
|
|
758
|
+
})
|
|
759
|
+
]
|
|
633
760
|
})
|
|
634
761
|
]
|
|
635
762
|
});
|
|
@@ -673,7 +800,7 @@ window.__ModuleLoader__.load({
|
|
|
673
800
|
"account.profile": "配置目录",
|
|
674
801
|
"models.title": "模型",
|
|
675
802
|
"models.enabledCount": "已启用 {count} ↑",
|
|
676
|
-
"models.routingNote": "
|
|
803
|
+
"models.routingNote": "清单直接读自豆包客户端的缓存,豆包上线新模型时会自动出现,不需要等插件更新。「工作任务」专属的条目(2.1 Pro / Lite / Auto)由客户端内的运行时执行,外部客户端接不进去,已自动隐藏",
|
|
677
804
|
"models.refresh": "刷新",
|
|
678
805
|
"models.refreshing": "刷新中…",
|
|
679
806
|
"models.save": "保存",
|
|
@@ -690,7 +817,13 @@ window.__ModuleLoader__.load({
|
|
|
690
817
|
"models.tierExpert": "专家",
|
|
691
818
|
"models.readOnly": "当前宿主未提供设置写入通道,勾选不会生效",
|
|
692
819
|
"models.loadFailed": "模型目录读取失败",
|
|
693
|
-
"models.empty": "没有可用的模型条目"
|
|
820
|
+
"models.empty": "没有可用的模型条目",
|
|
821
|
+
"tools.title": "实验性:工具调用",
|
|
822
|
+
"tools.toggle": "让豆包驱动本地工具(读写文件、执行命令)",
|
|
823
|
+
"tools.note": "豆包的接口没有函数调用通道(声明的 tools 会被静默忽略),此开关改用提示词模拟:把工具清单写进提示词,再把回复里的围栏解析回标准 tool_calls。实测约 11.5 秒/轮,复杂任务可能退化,故默认关闭。",
|
|
824
|
+
"tools.warning": "开启后豆包可以读写这台机器上的文件。请确认你信任当前会话的权限范围。",
|
|
825
|
+
"tools.enabled": "已开启",
|
|
826
|
+
"tools.disabled": "已关闭"
|
|
694
827
|
};
|
|
695
828
|
/** English copy, typed to the Chinese key set. */
|
|
696
829
|
const en = {
|
|
@@ -736,7 +869,13 @@ window.__ModuleLoader__.load({
|
|
|
736
869
|
"models.tierExpert": "Expert",
|
|
737
870
|
"models.readOnly": "This host provides no settings write path; ticking has no effect",
|
|
738
871
|
"models.loadFailed": "Could not read the model catalog",
|
|
739
|
-
"models.empty": "No model entries available"
|
|
872
|
+
"models.empty": "No model entries available",
|
|
873
|
+
"tools.title": "Experimental: tool calling",
|
|
874
|
+
"tools.toggle": "Let Doubao drive local tools (read/write files, run commands)",
|
|
875
|
+
"tools.note": "Doubao's endpoint has no function-calling channel — a declared `tools` array is silently ignored — so this switch simulates it: the tool list goes into the prompt and the fenced block in the reply is parsed back into standard tool_calls. Measured ~11.5s per round; can degrade on complex tasks, hence off by default.",
|
|
876
|
+
"tools.warning": "When on, Doubao can read and write files on this machine. Make sure you trust the permission scope of the current session.",
|
|
877
|
+
"tools.enabled": "On",
|
|
878
|
+
"tools.disabled": "Off"
|
|
740
879
|
};
|
|
741
880
|
/** Locale namespace registered by the browser-plugin entry. */
|
|
742
881
|
const NAMESPACE = "settings.doubao";
|