dsh-connect-doubao 0.1.1 → 0.2.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/README.md +24 -12
- package/lib/cli-doctor.js +2 -2
- package/lib/cli-probe.js +1 -1
- package/lib/client.js +2 -2
- package/lib/{doubao-quota-12DrjjCQ.js → doubao-quota-XUFfDd1i.js} +471 -74
- package/lib/{doubao-status-BU1AUzYj.js → doubao-status-C3wgMqnL.js} +3 -2
- package/lib/index.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,25 +81,27 @@ Chromium cookie(通过 Windows DPAPI 在本进程内解密),把它当作
|
|
|
81
81
|
|
|
82
82
|
## 模型目录
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
豆包把模型按**模式**分组,`menu_list` 每个模式有自己的一份清单:
|
|
85
|
+
|
|
86
|
+
| 模式 | 可选条目 |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| 对话 | 豆包 快速(`0`)、豆包 2.1 Turbo「专家」(`3`) |
|
|
89
|
+
| 工作任务 | 自动(`9`)、2.1 Lite(`seed-lite-7b`)、2.1 Pro(`5`)、Turbo(`3`) |
|
|
90
|
+
|
|
91
|
+
**本插件只提供「对话」模式的两条**:
|
|
87
92
|
|
|
88
93
|
| id | 名称 | 徽章 | 推理强度 |
|
|
89
94
|
| --- | --- | --- | --- |
|
|
90
95
|
| `0` | 豆包 快速 | — | ✅ |
|
|
91
|
-
| `seed-lite-7b` | 豆包 2.1 Lite | `0921 新版` | ✅ |
|
|
92
96
|
| `3` | 豆包 2.1 Turbo | `专家` | ✅ |
|
|
93
|
-
| `5` | 豆包 2.1 Pro | — | ✅ |
|
|
94
|
-
| `4` | 豆包 2.1 Turbo | `即将下线` | ✅ |
|
|
95
|
-
| `9` | 自动 | — | ✅ |
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
「工作任务」的四条**故意不列**:它们的执行侧是豆包客户端进程内的 CUA 运行时
|
|
99
|
+
(`tasks-mode-resource` + `sbox`),外部客户端注册不进去,每一轮都会得到
|
|
100
|
+
`710010702`。与其在模型选择器里摆出选不中的条目,不如不摆。
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
**推理强度**:低 = `3`、中 = `4`、高 = `5`(由客户端切换档位时重新序列化的配置 diff 读出,
|
|
103
|
+
并由配置里的 `reasoning_effort_config` 独立确认)。深度思考开关本身走
|
|
104
|
+
`need_deep_think: 0`,档位只由 `reasoning_effort` 携带 —— 这是 2026-10-22 CDP 抓包更正的。
|
|
103
105
|
|
|
104
106
|
---
|
|
105
107
|
|
|
@@ -229,6 +231,16 @@ npm run check # 以上全部
|
|
|
229
231
|
5. **风控滑块无法自动处理。** 触发后需去豆包客户端手动发一条消息完成验证。
|
|
230
232
|
6. **图片输入未提供。** 本 provider 只发送文本块,不转发图片。
|
|
231
233
|
7. **单账号。** 没有账号池、没有区域切换、没有签到。
|
|
234
|
+
8. **「工作任务」跑不了。** `agent_mode` 的取值是 `2 = 普通对话`、`1 = 工作任务`(CDP 抓包实测,
|
|
235
|
+
2026-10-22)。任务模式下服务端会把每一轮交给 `Agent-GeneralTask`,而它的执行运行时是豆包
|
|
236
|
+
客户端进程里的 CUA(`tasks-mode-resource` + `sbox`),外部客户端注册不进去,必然回
|
|
237
|
+
`710010702`。所以插件固定走对话模式;请求体里的 `general_task_param` 发的是客户端在普通
|
|
238
|
+
对话下的默认值 `{"server_do_compare":true}`。
|
|
239
|
+
9. **`model_item_key` 目前不影响谁来回答。** 实测(2026-10-22):`0` / `3` / `5` /
|
|
240
|
+
`seed-lite-7b` 以及一个**刻意写错的 key**,回答长度和耗时都无法区分(12.3–14.0 s),
|
|
241
|
+
响应里的 `model_id` 恒为不透明标记 `38`。结论:对话模式下服务端按账号/模式路由,
|
|
242
|
+
不看这个字段。插件仍原样转发它(服务端会把值记在消息上),但**不要指望换 key 换模型**;
|
|
243
|
+
「工作任务」模式是否按 key 路由,因为它根本跑不通,未验证。
|
|
232
244
|
|
|
233
245
|
---
|
|
234
246
|
|
package/lib/cli-doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { c as createDoubaoAdapter, n as DOUBAO_QUOTA_PATH, o as createDoubaoShim, r as doubaoQuotaView } from "./doubao-status-
|
|
2
|
+
import { C as classifyDoubaoFailure, G as DOUBAO_MODELS, I as describeProfileLocation, K as DOUBAO_PROVIDER, L as findDoubaoUserDataDir, N as REQUIRED_COOKIE_NAMES, O as probeAccount, et as discoverDoubaoModels, m as DoubaoUpstreamError, n as fetchQuota, q as DOUBAO_PROVIDER_DISPLAY_NAME, r as findWindow, z as loadDoubaoCredentials } from "./doubao-quota-XUFfDd1i.js";
|
|
3
|
+
import { c as createDoubaoAdapter, n as DOUBAO_QUOTA_PATH, o as createDoubaoShim, r as doubaoQuotaView } from "./doubao-status-C3wgMqnL.js";
|
|
4
4
|
//#region src/cli-doctor.ts
|
|
5
5
|
/**
|
|
6
6
|
* `doctor` — verify the whole stack end to end without spending inference quota.
|
package/lib/cli-probe.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { G as DOUBAO_MODELS, I as describeProfileLocation, O as probeAccount, i as formatDuration, it as findReasoningLevel, k as streamChat, m as DoubaoUpstreamError, n as fetchQuota, o as millisUntilReset, r as findWindow, rt as findDoubaoModel, z as loadDoubaoCredentials } from "./doubao-quota-XUFfDd1i.js";
|
|
3
3
|
//#region src/cli-probe.ts
|
|
4
4
|
/**
|
|
5
5
|
* Doubao smoke probe.
|
package/lib/client.js
CHANGED
|
@@ -673,7 +673,7 @@ window.__ModuleLoader__.load({
|
|
|
673
673
|
"account.profile": "配置目录",
|
|
674
674
|
"models.title": "模型",
|
|
675
675
|
"models.enabledCount": "已启用 {count} ↑",
|
|
676
|
-
"models.routingNote": "
|
|
676
|
+
"models.routingNote": "本插件只提供「对话」模式的条目(快速 / 专家):「工作任务」专属的 2.1 Pro、2.1 Lite 由豆包客户端内的运行时执行,外部客户端接不进去",
|
|
677
677
|
"models.refresh": "刷新",
|
|
678
678
|
"models.refreshing": "刷新中…",
|
|
679
679
|
"models.save": "保存",
|
|
@@ -719,7 +719,7 @@ window.__ModuleLoader__.load({
|
|
|
719
719
|
"account.profile": "Profile path",
|
|
720
720
|
"models.title": "Models",
|
|
721
721
|
"models.enabledCount": "{count} enabled ↑",
|
|
722
|
-
"models.routingNote": "
|
|
722
|
+
"models.routingNote": "Only the 对话-mode entries are offered (快速 / 专家). The 「工作任务」-only models (2.1 Pro / 2.1 Lite) are executed by the Doubao desktop app runtime, which an external client cannot join",
|
|
723
723
|
"models.refresh": "Refresh",
|
|
724
724
|
"models.refreshing": "Refreshing…",
|
|
725
725
|
"models.save": "Save",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { createDecipheriv, createHash, randomUUID } from "node:crypto";
|
|
3
|
-
import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir, hostname, tmpdir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
//#region src/doubao-catalog.ts
|
|
@@ -41,12 +41,44 @@ const DOUBAO_CONTEXT_WINDOW = 256e3;
|
|
|
41
41
|
/** Generous output ceiling; the subscription quota, not this, is the real bound. */
|
|
42
42
|
const DOUBAO_MAX_TOKENS = 32e3;
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* `option.agent_mode` / `ext.agent_mode`.
|
|
45
45
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* VERIFIED AGAINST A CDP-CAPTURED REQUEST (2026-10-22, 对话 mode):
|
|
47
|
+
*
|
|
48
|
+
* agent_mode 2 → 普通对话
|
|
49
|
+
* agent_mode 1 → 「工作任务」 (where every turn is handed to
|
|
50
|
+
* `Agent-GeneralTask` and then fails 710010702 when the
|
|
51
|
+
* desktop client's CUA runtime is not the requester)
|
|
52
|
+
*
|
|
53
|
+
* The previous revision of this constant said 1 for chat and carried a comment
|
|
54
|
+
* claiming work mode sends 2 — BOTH were backwards, and sending 1 is exactly
|
|
55
|
+
* what made every request from this plugin look like a work task.
|
|
56
|
+
*/
|
|
57
|
+
const CHAT_AGENT_MODE = 2;
|
|
58
|
+
/** 「工作任务」 mode. Its executing runtime is the desktop app's own process. */
|
|
59
|
+
const TASK_AGENT_MODE = 1;
|
|
60
|
+
/**
|
|
61
|
+
* Which machine runs a 「工作任务」.
|
|
62
|
+
*
|
|
63
|
+
* Read off the desktop client's own enum (`ai-views` bundle, module 941658):
|
|
64
|
+
*
|
|
65
|
+
* RuntimeType_Cloud_VM = 1 → 「工作任务 · 云电脑」: ByteDance's VM executes
|
|
66
|
+
* RuntimeType_Local_PC = 2 → 「工作任务 · 本地电脑」: THIS machine executes
|
|
67
|
+
*
|
|
68
|
+
* The plugin uses 2 on purpose: the cloud VM's artefacts are unreachable from an
|
|
69
|
+
* external client, whereas the local runtime is this very machine — the same one
|
|
70
|
+
* the Harness already executes tools on.
|
|
48
71
|
*/
|
|
49
|
-
const
|
|
72
|
+
const TASK_RUNTIME_CLOUD_VM = 1;
|
|
73
|
+
const TASK_RUNTIME_LOCAL_PC = 2;
|
|
74
|
+
/**
|
|
75
|
+
* Sandbox authorisation level.
|
|
76
|
+
*
|
|
77
|
+
* `2` is what the client sends with its 「全部允许」 authorisation setting (read
|
|
78
|
+
* off the same captured task request). The cloud uses it to decide how freely it
|
|
79
|
+
* may plan file and command actions inside the declared workspace.
|
|
80
|
+
*/
|
|
81
|
+
const TASK_SANDBOX_AUTH_ALLOW_ALL = 2;
|
|
50
82
|
const DOUBAO_REASONING_LEVELS = [
|
|
51
83
|
{
|
|
52
84
|
id: "low",
|
|
@@ -78,51 +110,65 @@ function needDeepThinkOf(level) {
|
|
|
78
110
|
return 1e4 + level.level;
|
|
79
111
|
}
|
|
80
112
|
/**
|
|
81
|
-
* The
|
|
113
|
+
* The two entries DSH may offer.
|
|
114
|
+
*
|
|
115
|
+
* WHY ONLY TWO. Doubao groups models by MODE, and the model list is per-mode:
|
|
116
|
+
*
|
|
117
|
+
* 对话 → 豆包 快速 (0), 豆包 2.1 Turbo·专家 (3)
|
|
118
|
+
* 工作任务 → 自动 (9), 2.1 Lite (seed-lite-7b), 2.1 Pro (5), Turbo (3)
|
|
82
119
|
*
|
|
83
|
-
*
|
|
84
|
-
* the
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
120
|
+
* The 「工作任务」 group is not offered here because its requests are executed by
|
|
121
|
+
* the desktop client's own CUA runtime (`tasks-mode-resource` + `sbox`), which an
|
|
122
|
+
* external client cannot register; every such turn fails `710010702`. See the
|
|
123
|
+
* README's known limitations.
|
|
124
|
+
*
|
|
125
|
+
* MEASURED, 2026-10-22: `model_item_key` is forwarded verbatim (the server stores
|
|
126
|
+
* it on the message), but in 对话 mode it does NOT change what answers —
|
|
127
|
+
* 0 / 3 / 5 / seed-lite-7b AND a deliberately bogus key all returned answers of
|
|
128
|
+
* indistinguishable length and latency (12.3–14.0 s). So this list is honest
|
|
129
|
+
* about what the PICKER can offer; it does not claim per-key routing.
|
|
130
|
+
*
|
|
131
|
+
* 「豆包 快速」 leads because it is the app's own chat-mode default, so an
|
|
132
|
+
* unconfigured install reproduces the app rather than something that merely
|
|
133
|
+
* happens to work.
|
|
134
|
+
*/
|
|
135
|
+
const DOUBAO_MODELS = [{
|
|
136
|
+
id: "0",
|
|
137
|
+
name: "豆包 快速",
|
|
138
|
+
reasoning: true
|
|
139
|
+
}, {
|
|
140
|
+
id: "3",
|
|
141
|
+
name: "豆包 2.1 Turbo",
|
|
142
|
+
badge: "专家",
|
|
143
|
+
reasoning: true
|
|
144
|
+
}];
|
|
145
|
+
/**
|
|
146
|
+
* Entries that exist in the desktop app but are deliberately NOT offered here.
|
|
88
147
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
148
|
+
* Kept as data rather than a comment so the README and any future UI can state
|
|
149
|
+
* the reason from one place — and so re-adding one is a deliberate act that has
|
|
150
|
+
* to confront this note.
|
|
92
151
|
*/
|
|
93
|
-
const
|
|
152
|
+
const DOUBAO_UNAVAILABLE_MODELS = [
|
|
94
153
|
{
|
|
95
|
-
id: "
|
|
96
|
-
name: "豆包
|
|
97
|
-
|
|
154
|
+
id: "5",
|
|
155
|
+
name: "豆包 2.1 Pro",
|
|
156
|
+
reason: "「工作任务」专属:执行侧是客户端内的 CUA 运行时,外部客户端注册不进去"
|
|
98
157
|
},
|
|
99
158
|
{
|
|
100
159
|
id: "seed-lite-7b",
|
|
101
160
|
name: "豆包 2.1 Lite",
|
|
102
|
-
|
|
103
|
-
reasoning: true
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
id: "3",
|
|
107
|
-
name: "豆包 2.1 Turbo",
|
|
108
|
-
badge: "专家",
|
|
109
|
-
reasoning: true
|
|
161
|
+
reason: "同上(工作任务专属)"
|
|
110
162
|
},
|
|
111
163
|
{
|
|
112
|
-
id: "
|
|
113
|
-
name: "
|
|
114
|
-
|
|
164
|
+
id: "9",
|
|
165
|
+
name: "自动",
|
|
166
|
+
reason: "同上(工作任务专属)"
|
|
115
167
|
},
|
|
116
168
|
{
|
|
117
169
|
id: "4",
|
|
118
|
-
name: "豆包 2.1 Turbo",
|
|
119
|
-
|
|
120
|
-
reasoning: true
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
id: "9",
|
|
124
|
-
name: "自动",
|
|
125
|
-
reasoning: true
|
|
170
|
+
name: "豆包 2.1 Turbo(即将下线)",
|
|
171
|
+
reason: "客户端已用「即将下线」标注,不引导用户选择"
|
|
126
172
|
}
|
|
127
173
|
];
|
|
128
174
|
/**
|
|
@@ -243,11 +289,11 @@ const DPAPI_MAGIC = Buffer.from("DPAPI", "ascii");
|
|
|
243
289
|
* addon, so we shell out to the Windows Data Protection API. The master key
|
|
244
290
|
* travels back over stdout and never touches disk.
|
|
245
291
|
*/
|
|
246
|
-
function
|
|
292
|
+
function dpapiCall(mode, blob) {
|
|
247
293
|
const script = [
|
|
248
294
|
"$ErrorActionPreference = 'Stop'",
|
|
249
295
|
"$ProgressPreference = \"SilentlyContinue\"",
|
|
250
|
-
"$cs = 'using System; using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential)] public struct DshDataBlob { public int cbData; public IntPtr pbData; } public static class DshCrypt { [DllImport(\"crypt32.dll\", SetLastError = true)] public static extern bool CryptUnprotectData(ref DshDataBlob pDataIn, IntPtr szDataDescr, IntPtr pOptionalEntropy, IntPtr pvReserved, IntPtr pPromptStruct, int dwFlags, ref DshDataBlob pDataOut); }'",
|
|
296
|
+
"$cs = 'using System; using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential)] public struct DshDataBlob { public int cbData; public IntPtr pbData; } public static class DshCrypt { [DllImport(\"crypt32.dll\", SetLastError = true)] public static extern bool CryptUnprotectData(ref DshDataBlob pDataIn, IntPtr szDataDescr, IntPtr pOptionalEntropy, IntPtr pvReserved, IntPtr pPromptStruct, int dwFlags, ref DshDataBlob pDataOut); [DllImport(\"crypt32.dll\", SetLastError = true)] public static extern bool CryptProtectData(ref DshDataBlob pDataIn, IntPtr szDataDescr, IntPtr pOptionalEntropy, IntPtr pvReserved, IntPtr pPromptStruct, int dwFlags, ref DshDataBlob pDataOut); }'",
|
|
251
297
|
"Add-Type -TypeDefinition $cs",
|
|
252
298
|
"$bytes = [System.Convert]::FromBase64String($env:DSH_DOUBAO_DPAPI_BLOB)",
|
|
253
299
|
"$len = [int]$bytes.Length",
|
|
@@ -257,7 +303,11 @@ function dpapiUnprotect(cipher) {
|
|
|
257
303
|
"$inBlob.cbData = $len",
|
|
258
304
|
"$inBlob.pbData = $mem",
|
|
259
305
|
"$outBlob = New-Object DshDataBlob",
|
|
260
|
-
"
|
|
306
|
+
"if ($env:DSH_DPAPI_MODE -eq 'protect') {",
|
|
307
|
+
" $ok = [DshCrypt]::CryptProtectData([ref]$inBlob, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, 1, [ref]$outBlob)",
|
|
308
|
+
"} else {",
|
|
309
|
+
" $ok = [DshCrypt]::CryptUnprotectData([ref]$inBlob, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, 1, [ref]$outBlob)",
|
|
310
|
+
"}",
|
|
261
311
|
"if (-not $ok) { [Console]::Out.Write('DPAPI_CALL_FAILED'); exit 3 }",
|
|
262
312
|
"$res = New-Object byte[] $outBlob.cbData",
|
|
263
313
|
"[Runtime.InteropServices.Marshal]::Copy($outBlob.pbData, $res, 0, $outBlob.cbData)",
|
|
@@ -281,7 +331,8 @@ function dpapiUnprotect(cipher) {
|
|
|
281
331
|
windowsHide: true,
|
|
282
332
|
env: {
|
|
283
333
|
...process.env,
|
|
284
|
-
DSH_DOUBAO_DPAPI_BLOB:
|
|
334
|
+
DSH_DOUBAO_DPAPI_BLOB: blob.toString("base64"),
|
|
335
|
+
DSH_DPAPI_MODE: mode
|
|
285
336
|
}
|
|
286
337
|
});
|
|
287
338
|
const stdout = result.stdout ?? "";
|
|
@@ -304,6 +355,68 @@ function dpapiUnprotect(cipher) {
|
|
|
304
355
|
});
|
|
305
356
|
}
|
|
306
357
|
}
|
|
358
|
+
/** Convenience wrapper for the decrypt direction. */
|
|
359
|
+
function dpapiUnprotect(cipher) {
|
|
360
|
+
return dpapiCall("unprotect", cipher);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Why a cache exists at all.
|
|
364
|
+
*
|
|
365
|
+
* The Doubao client takes an EXCLUSIVE lock on `Default\Network\Cookies` while
|
|
366
|
+
* it is active, so an external process cannot read the session at all while the
|
|
367
|
+
* app runs — `EBUSY` on open, on copy, everywhere. That is the NORMAL state for
|
|
368
|
+
* this plugin: its whole point is to use the account whose app is signed in.
|
|
369
|
+
*
|
|
370
|
+
* So a successful read is sealed with Windows DPAPI (current user scope) and
|
|
371
|
+
* kept under the DSH home. The plaintext never touches disk — only the sealed
|
|
372
|
+
* blob does — and that blob cannot be opened by another Windows account or
|
|
373
|
+
* moved to another machine. It expires, because the server rotates session
|
|
374
|
+
* cookies.
|
|
375
|
+
*/
|
|
376
|
+
const CREDENTIAL_CACHE_FILENAME = "doubao-credentials.dpapi";
|
|
377
|
+
/** Long enough to survive a reboot with the app open, short enough to expire. */
|
|
378
|
+
const CREDENTIAL_CACHE_TTL_MS = 432e5;
|
|
379
|
+
function credentialCachePath() {
|
|
380
|
+
return join(homedir(), ".dsh", CREDENTIAL_CACHE_FILENAME);
|
|
381
|
+
}
|
|
382
|
+
/** Seal the current session to disk. Best-effort: never fails a request. */
|
|
383
|
+
function saveCredentialCache(credentials) {
|
|
384
|
+
if (!credentials.ok) return;
|
|
385
|
+
try {
|
|
386
|
+
const payload = JSON.stringify({
|
|
387
|
+
savedAt: Date.now(),
|
|
388
|
+
header: credentials.header,
|
|
389
|
+
pairs: [...credentials.pairs.entries()]
|
|
390
|
+
});
|
|
391
|
+
const sealed = dpapiCall("protect", Buffer.from(payload, "utf8"));
|
|
392
|
+
mkdirSync(dirname(credentialCachePath()), { recursive: true });
|
|
393
|
+
writeFileSync(credentialCachePath(), sealed);
|
|
394
|
+
} catch {}
|
|
395
|
+
}
|
|
396
|
+
/** Read the sealed cache, or `undefined` when absent/expired/unreadable. */
|
|
397
|
+
function loadCredentialCache() {
|
|
398
|
+
try {
|
|
399
|
+
const json = dpapiCall("unprotect", readFileSync(credentialCachePath())).toString("utf8");
|
|
400
|
+
const parsed = JSON.parse(json);
|
|
401
|
+
if (typeof parsed.savedAt !== "number") return void 0;
|
|
402
|
+
if (Date.now() - parsed.savedAt > CREDENTIAL_CACHE_TTL_MS) return void 0;
|
|
403
|
+
if (typeof parsed.header !== "string" || !Array.isArray(parsed.pairs)) return void 0;
|
|
404
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
405
|
+
for (const entry of parsed.pairs) {
|
|
406
|
+
if (!Array.isArray(entry) || entry.length !== 2) continue;
|
|
407
|
+
const [name, value] = entry;
|
|
408
|
+
if (typeof name === "string" && typeof value === "string" && name !== "") pairs.set(name, value);
|
|
409
|
+
}
|
|
410
|
+
if (REQUIRED_COOKIE_NAMES.filter((name) => !pairs.get(name)).length > 0) return void 0;
|
|
411
|
+
return {
|
|
412
|
+
ok: true,
|
|
413
|
+
header: parsed.header,
|
|
414
|
+
pairs
|
|
415
|
+
};
|
|
416
|
+
} catch {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
307
420
|
/**
|
|
308
421
|
* Strip the host-binding prefix from a decrypted cookie plaintext.
|
|
309
422
|
*
|
|
@@ -367,27 +480,63 @@ function findDoubaoUserDataDir() {
|
|
|
367
480
|
}
|
|
368
481
|
for (const candidate of candidates) if (existsSync(join(candidate, "Default", "Network", "Cookies"))) return candidate;
|
|
369
482
|
}
|
|
483
|
+
/** Block the current thread for `ms` (no async plumbing in this read path). */
|
|
484
|
+
function sleepSync(ms) {
|
|
485
|
+
const view = new Int32Array(new SharedArrayBuffer(4));
|
|
486
|
+
Atomics.wait(view, 0, 0, ms);
|
|
487
|
+
}
|
|
488
|
+
/** Select the decryptable cookie rows from an opened database. */
|
|
489
|
+
function selectCookieRows(db) {
|
|
490
|
+
return db.prepare("SELECT host_key, name, encrypted_value FROM cookies WHERE encrypted_value IS NOT NULL").all().map((r) => ({
|
|
491
|
+
host: String(r.host_key),
|
|
492
|
+
name: String(r.name),
|
|
493
|
+
encrypted: Buffer.from(r.encrypted_value)
|
|
494
|
+
}));
|
|
495
|
+
}
|
|
370
496
|
/**
|
|
371
497
|
* Read rows from the cookie jar.
|
|
372
498
|
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
499
|
+
* Two paths, because the live file is held by a running Chromium:
|
|
500
|
+
*
|
|
501
|
+
* 1. a read-only SQLite open on the ORIGINAL file — SQLite only needs a shared
|
|
502
|
+
* lock, so this normally works even while the app is signed in and running;
|
|
503
|
+
* 2. a throwaway COPY, retried a few times. Windows share-deny locks and
|
|
504
|
+
* on-access scanners make `copyFileSync` return `EBUSY` for a moment, and
|
|
505
|
+
* that transient error used to surface as "credentials unreadable" — which
|
|
506
|
+
* reads like a signed-out account and is the most misleading possible
|
|
507
|
+
* message for a file that is simply busy.
|
|
508
|
+
*
|
|
509
|
+
* The original profile is only ever read.
|
|
376
510
|
*/
|
|
377
511
|
function readCookieRows(userDataDir) {
|
|
378
512
|
const source = join(userDataDir, "Default", "Network", "Cookies");
|
|
513
|
+
const { DatabaseSync } = require_("node:sqlite");
|
|
514
|
+
for (let attempt = 0; attempt < 4; attempt += 1) try {
|
|
515
|
+
const db = new DatabaseSync(source, { readOnly: true });
|
|
516
|
+
try {
|
|
517
|
+
return selectCookieRows(db);
|
|
518
|
+
} finally {
|
|
519
|
+
db.close();
|
|
520
|
+
}
|
|
521
|
+
} catch {
|
|
522
|
+
if (attempt < 3) sleepSync(150);
|
|
523
|
+
}
|
|
379
524
|
const scratch = mkdtempSync(join(tmpdir(), "dsh-doubao-cookie-"));
|
|
380
525
|
const copy = join(scratch, "Cookies.db");
|
|
381
526
|
try {
|
|
382
|
-
|
|
383
|
-
|
|
527
|
+
let copied = false;
|
|
528
|
+
let lastError;
|
|
529
|
+
for (let attempt = 0; attempt < 6 && !copied; attempt += 1) try {
|
|
530
|
+
copyFileSync(source, copy);
|
|
531
|
+
copied = true;
|
|
532
|
+
} catch (error) {
|
|
533
|
+
lastError = error;
|
|
534
|
+
sleepSync(120);
|
|
535
|
+
}
|
|
536
|
+
if (!copied) throw new Error("the Doubao desktop client holds its Cookies database with an EXCLUSIVE lock (EBUSY), so no other process can read the session while it is in that state. Fully quit the Doubao client and retry: credentials are then cached in this process for 10 minutes, so the app can be reopened straight away.", { cause: lastError });
|
|
384
537
|
const db = new DatabaseSync(copy, { readOnly: true });
|
|
385
538
|
try {
|
|
386
|
-
return db
|
|
387
|
-
host: String(r.host_key),
|
|
388
|
-
name: String(r.name),
|
|
389
|
-
encrypted: Buffer.from(r.encrypted_value)
|
|
390
|
-
}));
|
|
539
|
+
return selectCookieRows(db);
|
|
391
540
|
} finally {
|
|
392
541
|
db.close();
|
|
393
542
|
}
|
|
@@ -427,6 +576,24 @@ function loadDoubaoCredentials(options = {}) {
|
|
|
427
576
|
pairs: /* @__PURE__ */ new Map()
|
|
428
577
|
};
|
|
429
578
|
}
|
|
579
|
+
if (!credentials.ok && cached$1?.credentials.ok === true) {
|
|
580
|
+
cached$1 = {
|
|
581
|
+
at: now,
|
|
582
|
+
credentials: cached$1.credentials
|
|
583
|
+
};
|
|
584
|
+
return cached$1.credentials;
|
|
585
|
+
}
|
|
586
|
+
if (!credentials.ok) {
|
|
587
|
+
const sealed = loadCredentialCache();
|
|
588
|
+
if (sealed !== void 0) {
|
|
589
|
+
cached$1 = {
|
|
590
|
+
at: now,
|
|
591
|
+
credentials: sealed
|
|
592
|
+
};
|
|
593
|
+
return sealed;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (credentials.ok) saveCredentialCache(credentials);
|
|
430
597
|
cached$1 = {
|
|
431
598
|
at: now,
|
|
432
599
|
credentials
|
|
@@ -602,6 +769,78 @@ var DoubaoUpstreamError = class extends Error {
|
|
|
602
769
|
}
|
|
603
770
|
};
|
|
604
771
|
/**
|
|
772
|
+
* Stable-per-process id that rides along as `client_env_id`.
|
|
773
|
+
*
|
|
774
|
+
* The client generates one per app environment; ours is per host process so a
|
|
775
|
+
* restart looks like a new environment, which is exactly what it is.
|
|
776
|
+
*/
|
|
777
|
+
const CLIENT_ENV_ID = randomUUID();
|
|
778
|
+
/** Whether a request asks for local-PC task mode. Default: OFF — see below. */
|
|
779
|
+
function taskModeEnabled(options) {
|
|
780
|
+
if (options.taskMode !== void 0) return options.taskMode;
|
|
781
|
+
const flag = process.env.DSH_DOUBAO_TASK;
|
|
782
|
+
return flag === void 0 ? false : /^(1|true|on|yes)$/i.test(flag.trim());
|
|
783
|
+
}
|
|
784
|
+
/** The directory this machine declares to the task runtime. */
|
|
785
|
+
function taskWorkspaceOf(options) {
|
|
786
|
+
return options.workspace ?? process.env.DSH_DOUBAO_WORKSPACE ?? process.cwd();
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Build `ext.general_task_param` — the field that makes a turn a 工作任务.
|
|
790
|
+
*
|
|
791
|
+
* Transcribed field-for-field from a real 「工作任务 · 本地电脑」 request the
|
|
792
|
+
* desktop client sent on 2026-09-15 (recovered from the app's own IndexedDB
|
|
793
|
+
* message store), then reduced to what is portable:
|
|
794
|
+
*
|
|
795
|
+
* thread_local_message_id the local message id being submitted
|
|
796
|
+
* client_option the workspace and the sandbox/authorisation policy
|
|
797
|
+
* runtime_type 2 — Local_PC; the cloud VM would be 1
|
|
798
|
+
* agent_task_param the same facts again, device-side
|
|
799
|
+
* agent_task_param_change all false: nothing is being switched mid-thread
|
|
800
|
+
*
|
|
801
|
+
* Paths are computed from THIS machine rather than copied, because the cloud
|
|
802
|
+
* plans against them: a workspace that does not exist here would make the task
|
|
803
|
+
* fail inside the runtime instead of at submit time. `local_device_id` and
|
|
804
|
+
* `local_app_id` reuse the frozen client identity already in {@link CLIENT_PARAMS}
|
|
805
|
+
* — the captured request carries the same two values, so they are the same
|
|
806
|
+
* install identity and not per-request state.
|
|
807
|
+
*/
|
|
808
|
+
function buildGeneralTaskParam(localMessageId, workspace) {
|
|
809
|
+
const userData = findDoubaoUserDataDir();
|
|
810
|
+
const agentWorkspace = userData === void 0 ? join(tmpdir(), "dsh-doubao-agent-workspace") : join(userData, "Default", ".doubao", "agent_mode", "workspace");
|
|
811
|
+
return {
|
|
812
|
+
thread_local_message_id: [localMessageId],
|
|
813
|
+
client_option: {
|
|
814
|
+
shared_folder_path: [workspace, agentWorkspace],
|
|
815
|
+
enable_sandbox: true,
|
|
816
|
+
os: process.platform === "win32" ? "Windows" : process.platform,
|
|
817
|
+
client_env_id: CLIENT_ENV_ID,
|
|
818
|
+
sandbox_id: `route-${randomUUID()}`,
|
|
819
|
+
workspace,
|
|
820
|
+
agent_workspace: {
|
|
821
|
+
agent_workspace: agentWorkspace,
|
|
822
|
+
local_skill_paths: [join(homedir(), "Doubao", "skills"), join(homedir(), ".agents", "skills")]
|
|
823
|
+
},
|
|
824
|
+
sandbox_auth_type: 2
|
|
825
|
+
},
|
|
826
|
+
runtime_type: 2,
|
|
827
|
+
need_modify_conversation: false,
|
|
828
|
+
agent_task_param: {
|
|
829
|
+
runtime_type: 2,
|
|
830
|
+
device_name: hostname(),
|
|
831
|
+
workspace,
|
|
832
|
+
local_device_id: CLIENT_PARAMS.device_id,
|
|
833
|
+
local_app_id: CLIENT_PARAMS.aid,
|
|
834
|
+
sandbox_auth_type: 2
|
|
835
|
+
},
|
|
836
|
+
agent_task_param_change: {
|
|
837
|
+
runtime_changed: false,
|
|
838
|
+
device_changed: false,
|
|
839
|
+
sandbox_auth_type_changed: false
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
605
844
|
* Build the request body Doubao's chat endpoint expects.
|
|
606
845
|
*
|
|
607
846
|
* The model and the reasoning effort are each written at TWO locations, because
|
|
@@ -616,11 +855,27 @@ var DoubaoUpstreamError = class extends Error {
|
|
|
616
855
|
function buildChatBody(prompt, options = {}) {
|
|
617
856
|
const model = findDoubaoModel(options.modelKey);
|
|
618
857
|
const level = findReasoningLevel(options.reasoningLevel);
|
|
619
|
-
const
|
|
620
|
-
|
|
858
|
+
const task = taskModeEnabled(options);
|
|
859
|
+
/**
|
|
860
|
+
* 2 = 普通对话, 1 = 「工作任务」.
|
|
861
|
+
*
|
|
862
|
+
* CDP-captured from the running client on 2026-10-22. This constant was the
|
|
863
|
+
* bug: the plugin sent 1 for chat, so the server answered every turn as a work
|
|
864
|
+
* task — `Agent-GeneralTask`, then `710010702` because the task runtime is the
|
|
865
|
+
* desktop app's own CUA process.
|
|
866
|
+
*/
|
|
867
|
+
const agentMode = options.agentMode ?? (task ? 1 : 2);
|
|
868
|
+
const localMessageId = randomUUID();
|
|
869
|
+
/**
|
|
870
|
+
* Mode id the model picker groups by. The captured chat request carries "1" in
|
|
871
|
+
* BOTH `conversation_init_ext` and `aggregate_params`; leaving it out lets the
|
|
872
|
+
* server choose a mode for us.
|
|
873
|
+
*/
|
|
874
|
+
const modeId = (process.env.DSH_DOUBAO_MODE_ID ?? "").trim() || "1";
|
|
875
|
+
const taskParam = task ? buildGeneralTaskParam(localMessageId, taskWorkspaceOf(options)) : { server_do_compare: true };
|
|
621
876
|
return {
|
|
622
877
|
messages: [{
|
|
623
|
-
local_message_id:
|
|
878
|
+
local_message_id: localMessageId,
|
|
624
879
|
message_status: 0,
|
|
625
880
|
content_block: [{
|
|
626
881
|
block_type: 1e4,
|
|
@@ -647,7 +902,7 @@ function buildChatBody(prompt, options = {}) {
|
|
|
647
902
|
answer_with_suggest: false,
|
|
648
903
|
agent_mode: agentMode,
|
|
649
904
|
tts_switch: false,
|
|
650
|
-
need_deep_think:
|
|
905
|
+
need_deep_think: 0,
|
|
651
906
|
click_clear_context: false,
|
|
652
907
|
from_suggest: false,
|
|
653
908
|
is_regen: false,
|
|
@@ -657,16 +912,48 @@ function buildChatBody(prompt, options = {}) {
|
|
|
657
912
|
disable_sse_cache: false,
|
|
658
913
|
select_text_action: "",
|
|
659
914
|
is_select_text: false,
|
|
915
|
+
resend_for_regen: false,
|
|
916
|
+
scene_type: 0,
|
|
917
|
+
unique_key: randomUUID(),
|
|
918
|
+
start_seq: 0,
|
|
919
|
+
need_create_conversation: true,
|
|
920
|
+
conversation_init_option: { need_ack_conversation: true },
|
|
921
|
+
conversation_init_ext: {
|
|
922
|
+
model_item_key: model.id,
|
|
923
|
+
reasoning_effort: String(level.level),
|
|
924
|
+
mode_id: modeId
|
|
925
|
+
},
|
|
926
|
+
regen_query_id: [],
|
|
927
|
+
edit_query_id: [],
|
|
928
|
+
regen_instruction: "",
|
|
929
|
+
no_replace_for_regen: false,
|
|
930
|
+
message_from: 0,
|
|
931
|
+
shared_app_name: "",
|
|
932
|
+
shared_app_id: "",
|
|
660
933
|
sse_recv_event_options: { support_chunk_delta: true },
|
|
934
|
+
support_lazy_fetch_stream: true,
|
|
935
|
+
is_ai_playground: false,
|
|
936
|
+
is_old_user: true,
|
|
937
|
+
general_task_param: taskParam,
|
|
938
|
+
recovery_option: {
|
|
939
|
+
is_recovery: false,
|
|
940
|
+
req_create_time_sec: Math.floor(Date.now() / 1e3),
|
|
941
|
+
append_sse_event_scene: 0
|
|
942
|
+
},
|
|
943
|
+
message_storage_type: 0,
|
|
944
|
+
related_deleted_message_ids: {},
|
|
945
|
+
connector_info_list: [],
|
|
661
946
|
model_config: {
|
|
662
947
|
model_item_key: model.id,
|
|
663
|
-
model_extra_params: {
|
|
664
|
-
memory_profile: "256k",
|
|
665
|
-
total_window_size: String(DOUBAO_CONTEXT_WINDOW)
|
|
666
|
-
},
|
|
948
|
+
model_extra_params: {},
|
|
667
949
|
reasoning_effort: level.level
|
|
668
950
|
},
|
|
669
951
|
aggregate_params: {
|
|
952
|
+
mention_skill_list: "[]",
|
|
953
|
+
mention_plugin_list: "[]",
|
|
954
|
+
mention_ext: "[{}]",
|
|
955
|
+
conversation_mode: "",
|
|
956
|
+
mode_id: modeId,
|
|
670
957
|
model_item_key: model.id,
|
|
671
958
|
agent_mode: String(agentMode),
|
|
672
959
|
reasoning_effort: String(level.level),
|
|
@@ -678,13 +965,31 @@ function buildChatBody(prompt, options = {}) {
|
|
|
678
965
|
conversation_id: options.conversationId ?? "",
|
|
679
966
|
bot_id: DEFAULT_BOT_ID,
|
|
680
967
|
last_section_id: "",
|
|
681
|
-
last_message_index: null
|
|
968
|
+
last_message_index: null,
|
|
969
|
+
local_permissions: [
|
|
970
|
+
{
|
|
971
|
+
permission_name: "ACCESS_COARSE_LOCATION",
|
|
972
|
+
status: 1
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
permission_name: "ACCESS_FINE_LOCATION",
|
|
976
|
+
status: 1
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
permission_name: "ACCESS_BACKGROUND_LOCATION",
|
|
980
|
+
status: 1
|
|
981
|
+
}
|
|
982
|
+
]
|
|
682
983
|
},
|
|
683
984
|
user_context: [],
|
|
684
985
|
ext: {
|
|
685
|
-
use_deep_think: String(needDeepThink),
|
|
686
986
|
agent_mode: String(agentMode),
|
|
987
|
+
use_deep_think: "0",
|
|
988
|
+
sub_conv_firstmet_type: "1",
|
|
989
|
+
general_task_param: JSON.stringify(taskParam),
|
|
990
|
+
collection_id: "",
|
|
687
991
|
is_finish: "1",
|
|
992
|
+
conversation_init_option: JSON.stringify({ need_ack_conversation: true }),
|
|
688
993
|
commerce_credit_config_enable: "0"
|
|
689
994
|
}
|
|
690
995
|
};
|
|
@@ -794,13 +1099,10 @@ function extractThinking(payload) {
|
|
|
794
1099
|
return false;
|
|
795
1100
|
}
|
|
796
1101
|
/**
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
-
* Rejects with `DoubaoUpstreamError` when the server answers with a business
|
|
800
|
-
* error frame — notably `710022004 rate limited`, which is risk control asking
|
|
801
|
-
* for a slider CAPTCHA rather than an authentication failure.
|
|
1102
|
+
* One attempt, no fallback. Split out so {@link streamChat} can retry a
|
|
1103
|
+
* task-mode call as plain chat when the task runtime is not attached.
|
|
802
1104
|
*/
|
|
803
|
-
async function
|
|
1105
|
+
async function streamOnce(prompt, options = {}) {
|
|
804
1106
|
const credentials = options.credentials ?? loadDoubaoCredentials();
|
|
805
1107
|
if (!credentials.ok) throw new DoubaoUpstreamError(credentials.reason ?? "Doubao credentials unavailable.");
|
|
806
1108
|
const missing = REQUIRED_COOKIE_NAMES.filter((name) => !credentials.pairs.get(name));
|
|
@@ -820,7 +1122,45 @@ async function streamChat(prompt, options = {}) {
|
|
|
820
1122
|
throw new DoubaoUpstreamError(`Doubao chat request failed with HTTP ${response.status}. ${detail.slice(0, 200)}`);
|
|
821
1123
|
}
|
|
822
1124
|
absorbSetCookies(response.headers);
|
|
823
|
-
return await consumeStream(response, options.callbacks);
|
|
1125
|
+
return await consumeStream(response, options.callbacks, { harvestNotifyText: taskModeEnabled(options) });
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Stream one completion and return the assembled reply.
|
|
1129
|
+
*
|
|
1130
|
+
* Rejects with `DoubaoUpstreamError` when the server answers with a business
|
|
1131
|
+
* error frame — notably `710022004 rate limited`, which is risk control asking
|
|
1132
|
+
* for a slider CAPTCHA rather than an authentication failure.
|
|
1133
|
+
*
|
|
1134
|
+
* TASK MODE FALLBACK. 「工作任务 · 本地电脑」 is the default, but the task agent
|
|
1135
|
+
* is executed by a runtime that only the desktop app attaches; when it is
|
|
1136
|
+
* absent the server answers the turn with `710010702 system error` instead of a
|
|
1137
|
+
* reply (verified: the stream still names `Agent-GeneralTask`, so the mode
|
|
1138
|
+
* itself was accepted). Rather than hand that placeholder to the user, the call
|
|
1139
|
+
* is retried ONCE as plain chat — and only when the failed attempt emitted no
|
|
1140
|
+
* text, so a fallback can never duplicate a partially streamed answer.
|
|
1141
|
+
*/
|
|
1142
|
+
async function streamChat(prompt, options = {}) {
|
|
1143
|
+
let emittedText = false;
|
|
1144
|
+
const wrapped = {
|
|
1145
|
+
onText: (delta) => {
|
|
1146
|
+
if (delta) emittedText = true;
|
|
1147
|
+
options.callbacks?.onText?.(delta);
|
|
1148
|
+
},
|
|
1149
|
+
onEvent: (event) => options.callbacks?.onEvent?.(event)
|
|
1150
|
+
};
|
|
1151
|
+
const wanted = taskModeEnabled(options);
|
|
1152
|
+
const call = (taskMode) => streamOnce(prompt, {
|
|
1153
|
+
...options,
|
|
1154
|
+
callbacks: wrapped,
|
|
1155
|
+
taskMode
|
|
1156
|
+
});
|
|
1157
|
+
try {
|
|
1158
|
+
return await call(wanted);
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
const taskRuntimeMissing = error instanceof DoubaoUpstreamError && (error.code === CODE_TASK_RUNTIME_UNAVAILABLE || /agent_err_real_answer/.test(error.message));
|
|
1161
|
+
if (!wanted || !taskRuntimeMissing || emittedText) throw error;
|
|
1162
|
+
return call(false);
|
|
1163
|
+
}
|
|
824
1164
|
}
|
|
825
1165
|
const renewals = /* @__PURE__ */ new Map();
|
|
826
1166
|
/**
|
|
@@ -864,20 +1204,42 @@ function applyRenewals(header) {
|
|
|
864
1204
|
for (const [name, value] of renewals) pairs.set(name, value);
|
|
865
1205
|
return [...pairs.entries()].map(([name, value]) => `${name}=${value}`).join("; ");
|
|
866
1206
|
}
|
|
867
|
-
async function consumeStream(response, callbacks) {
|
|
1207
|
+
async function consumeStream(response, callbacks, options = {}) {
|
|
868
1208
|
const reader = response.body?.getReader();
|
|
869
1209
|
if (!reader) throw new DoubaoUpstreamError("Doubao returned an empty response body.");
|
|
870
1210
|
const decoder = new TextDecoder();
|
|
871
1211
|
let buffer = "";
|
|
872
1212
|
const texts = [];
|
|
1213
|
+
/** Text carried by STREAM_MSG_NOTIFY instead of by chunk deltas. */
|
|
1214
|
+
const notifyTexts = [];
|
|
873
1215
|
let conversationId;
|
|
874
1216
|
let messageId;
|
|
875
1217
|
let model;
|
|
876
1218
|
let thinking = false;
|
|
877
1219
|
let lastEventId;
|
|
878
1220
|
let ended = false;
|
|
1221
|
+
/**
|
|
1222
|
+
* Every event name seen, in arrival order (deduped).
|
|
1223
|
+
*
|
|
1224
|
+
* Task mode answers with a richer event vocabulary than chat, so an empty
|
|
1225
|
+
* completion is usually "the text lives in an event we do not harvest yet"
|
|
1226
|
+
* rather than "the server said nothing". Naming the events turns an opaque
|
|
1227
|
+
* failure into a next step.
|
|
1228
|
+
*/
|
|
1229
|
+
const events = /* @__PURE__ */ new Set();
|
|
1230
|
+
/** The agent that served this turn, when the server names one. */
|
|
1231
|
+
let intentionAgent;
|
|
1232
|
+
/**
|
|
1233
|
+
* `expose_reason` of a task turn whose runtime was unreachable.
|
|
1234
|
+
*
|
|
1235
|
+
* Recorded instead of surfacing its placeholder text (「出了点问题,请稍后重试。」),
|
|
1236
|
+
* which reads like a transient glitch and hides the actual cause.
|
|
1237
|
+
*/
|
|
1238
|
+
let taskAgentError;
|
|
879
1239
|
const handle = (frame) => {
|
|
880
1240
|
const name = frame.event ?? "";
|
|
1241
|
+
if (name) events.add(name);
|
|
1242
|
+
if (process.env.DSH_DOUBAO_DEBUG_FRAMES === "1" && frame.data) process.stderr.write(`[frame ${name || "(none)"}] ${frame.data.slice(0, 1500)}\n`);
|
|
881
1243
|
if (name && callbacks?.onEvent) callbacks.onEvent(name);
|
|
882
1244
|
if (frame.id) lastEventId = frame.id;
|
|
883
1245
|
if (!frame.data) return;
|
|
@@ -901,9 +1263,26 @@ async function consumeStream(response, callbacks) {
|
|
|
901
1263
|
return;
|
|
902
1264
|
}
|
|
903
1265
|
if (name === "FULL_MSG_NOTIFY" || name === "STREAM_MSG_NOTIFY") {
|
|
904
|
-
|
|
1266
|
+
const record = payload;
|
|
1267
|
+
messageId = record.message?.message_id ?? messageId;
|
|
905
1268
|
model = model ?? extractModel(payload);
|
|
906
1269
|
thinking = thinking || extractThinking(payload);
|
|
1270
|
+
const ext = record.content?.ext ?? record.message?.ext ?? record.ext;
|
|
1271
|
+
if (ext !== void 0 && ext !== null && typeof ext === "object") {
|
|
1272
|
+
const agent = ext.intention_agent_name;
|
|
1273
|
+
if (typeof agent === "string" && agent !== "") intentionAgent = agent;
|
|
1274
|
+
const reason = ext.expose_reason;
|
|
1275
|
+
if (typeof reason === "string" && /agent_err_real_answer/.test(reason)) taskAgentError = reason;
|
|
1276
|
+
}
|
|
1277
|
+
if (name === "STREAM_MSG_NOTIFY") {
|
|
1278
|
+
const acc = { out: [] };
|
|
1279
|
+
harvestText(record.content?.content_block, acc);
|
|
1280
|
+
for (const piece of acc.out) {
|
|
1281
|
+
if (!piece) continue;
|
|
1282
|
+
notifyTexts.push(piece);
|
|
1283
|
+
if (callbacks?.onText) callbacks.onText(piece);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
907
1286
|
return;
|
|
908
1287
|
}
|
|
909
1288
|
if (name === "STREAM_CHUNK" || name === "CHUNK_DELTA") {
|
|
@@ -933,13 +1312,20 @@ async function consumeStream(response, callbacks) {
|
|
|
933
1312
|
} finally {
|
|
934
1313
|
reader.releaseLock();
|
|
935
1314
|
}
|
|
936
|
-
const
|
|
937
|
-
|
|
1315
|
+
const chunkText = texts.join("");
|
|
1316
|
+
const notifyText = notifyTexts.join("");
|
|
1317
|
+
const text = stripPromptEcho(chunkText.startsWith(notifyText) ? chunkText : notifyText + chunkText);
|
|
1318
|
+
if (taskAgentError !== void 0) throw new DoubaoUpstreamError(`Doubao accepted the task but its executing runtime was unreachable (${taskAgentError}). The 「工作任务」 runtime is attached by the desktop client; without it the agent has nowhere to run.`, CODE_TASK_RUNTIME_UNAVAILABLE);
|
|
1319
|
+
if (!text) {
|
|
1320
|
+
const seen = [...events];
|
|
1321
|
+
throw new DoubaoUpstreamError(`Doubao returned an empty completion${lastEventId ? ` (last event id ${lastEventId})` : ""}${seen.length > 0 ? `; events seen: ${seen.join(", ")}` : "; no events at all"}.`);
|
|
1322
|
+
}
|
|
938
1323
|
return {
|
|
939
1324
|
text,
|
|
940
1325
|
...model !== void 0 ? { model } : {},
|
|
941
1326
|
...conversationId !== void 0 ? { conversationId } : {},
|
|
942
1327
|
...messageId !== void 0 ? { messageId } : {},
|
|
1328
|
+
...intentionAgent !== void 0 ? { intentionAgent } : {},
|
|
943
1329
|
thinking
|
|
944
1330
|
};
|
|
945
1331
|
}
|
|
@@ -963,9 +1349,19 @@ async function chat(prompt, options = {}) {
|
|
|
963
1349
|
const CODE_SESSION_DEAD = 710012001;
|
|
964
1350
|
/** The server's business code for a risk-control slider challenge. */
|
|
965
1351
|
const CODE_RATE_LIMITED = 710022004;
|
|
1352
|
+
/**
|
|
1353
|
+
* The server's business code when a 「工作任务」 was accepted but its executing
|
|
1354
|
+
* runtime could not be reached (`expose_reason: agent_err_real_answer`).
|
|
1355
|
+
*
|
|
1356
|
+
* This is the signature of "the mode is right, the local runtime is not
|
|
1357
|
+
* attached" — the desktop client's CUA runtime registers itself; an external
|
|
1358
|
+
* client has nothing to register, so the agent has nowhere to run.
|
|
1359
|
+
*/
|
|
1360
|
+
const CODE_TASK_RUNTIME_UNAVAILABLE = 710010702;
|
|
966
1361
|
/** Map one upstream error onto its class. */
|
|
967
1362
|
function classifyDoubaoFailure(error) {
|
|
968
1363
|
if (error.rateLimited || error.code === CODE_RATE_LIMITED) return "rate_limited";
|
|
1364
|
+
if (error.code === CODE_TASK_RUNTIME_UNAVAILABLE) return "task_runtime_unavailable";
|
|
969
1365
|
if (error.code === CODE_SESSION_DEAD) return "session_dead";
|
|
970
1366
|
return "network";
|
|
971
1367
|
}
|
|
@@ -981,6 +1377,7 @@ const DOUBAO_ERROR_HINTS = {
|
|
|
981
1377
|
quota_exhausted: "豆包额度已用尽。请等待额度窗口重置,或在豆包客户端查看「订阅与额度管理」。",
|
|
982
1378
|
session_dead: "豆包登录态失效。请在豆包客户端重新登录,然后重试。",
|
|
983
1379
|
network: "连接豆包失败(网络或超时)。请检查网络后重试。",
|
|
1380
|
+
task_runtime_unavailable: "豆包把这个账号路由到了「工作任务」代理,而该任务的执行运行时只存在于豆包客户端进程里,外部客户端接不上。请打开豆包客户端,把输入框上方的开关切回「对话」,然后重试;需要多步执行的话,交给 DSH 自己的工具来做。",
|
|
984
1381
|
upstream: "豆包上游返回了未知错误。"
|
|
985
1382
|
};
|
|
986
1383
|
/** Build a user-facing message for a Doubao failure. */
|
|
@@ -1176,4 +1573,4 @@ function formatDuration(ms) {
|
|
|
1176
1573
|
return parts.join(" ") || "0s";
|
|
1177
1574
|
}
|
|
1178
1575
|
//#endregion
|
|
1179
|
-
export {
|
|
1576
|
+
export { TASK_SANDBOX_AUTH_ALLOW_ALL as $, taskModeEnabled as A, CHAT_AGENT_MODE as B, classifyDoubaoFailure as C, parseFrames as D, invalidateRenewals as E, decryptCookieValue as F, DOUBAO_MODELS as G, DOUBAO_CONTEXT_WINDOW as H, describeProfileLocation as I, DOUBAO_REASONING_LEVELS as J, DOUBAO_PROVIDER as K, findDoubaoUserDataDir as L, CREDENTIAL_TTL_MS as M, REQUIRED_COOKIE_NAMES as N, probeAccount as O, decodeCookiePlaintext as P, TASK_RUNTIME_LOCAL_PC as Q, invalidateCredentialCache as R, chat as S, extractModel as T, DOUBAO_DEFAULT_MODEL_ID as U, DEFAULT_REASONING_LEVEL_ID as V, DOUBAO_MAX_TOKENS as W, TASK_AGENT_MODE as X, DOUBAO_UNAVAILABLE_MODELS as Y, TASK_RUNTIME_CLOUD_VM as Z, applyRenewals as _, invalidateQuotaCache as a, needDeepThinkOf as at, buildHeaders as b, ACCOUNT_ENDPOINT as c, DEFAULT_BOT_ID as d, discoverDoubaoModels as et, DOUBAO_ERROR_HINTS as f, RUNTIME_VERSION as g, QUOTA_ENDPOINT as h, formatDuration as i, findReasoningLevel as it, taskWorkspaceOf as j, streamChat as k, CHAT_ENDPOINT as l, DoubaoUpstreamError as m, fetchQuota as n, enabledModelsOf as nt, millisUntilReset as o, DOUBAO_VERSION as p, DOUBAO_PROVIDER_DISPLAY_NAME as q, findWindow as r, findDoubaoModel as rt, normalizeWindow as s, QUOTA_TTL_MS as t, doubaoCatalogEntries as tt, CLIENT_PARAMS as u, buildChatBody as v, describeDoubaoFailure as w, buildQueryUrl as x, buildGeneralTaskParam as y, loadDoubaoCredentials as z };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as classifyDoubaoFailure, G as DOUBAO_MODELS, H as DOUBAO_CONTEXT_WINDOW, I as describeProfileLocation, K as DOUBAO_PROVIDER, S as chat, W as DOUBAO_MAX_TOKENS, a as invalidateQuotaCache, m as DoubaoUpstreamError, n as fetchQuota, nt as enabledModelsOf, o as millisUntilReset, q as DOUBAO_PROVIDER_DISPLAY_NAME, r as findWindow, rt as findDoubaoModel, tt as doubaoCatalogEntries, z as loadDoubaoCredentials } from "./doubao-quota-XUFfDd1i.js";
|
|
2
2
|
import { createProvider } from "@earendil-works/pi-ai";
|
|
3
3
|
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
4
4
|
import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
@@ -272,7 +272,8 @@ const KIND_STATUS = {
|
|
|
272
272
|
quota_exhausted: 429,
|
|
273
273
|
session_dead: 401,
|
|
274
274
|
network: 502,
|
|
275
|
-
upstream: 502
|
|
275
|
+
upstream: 502,
|
|
276
|
+
task_runtime_unavailable: 409
|
|
276
277
|
};
|
|
277
278
|
function writeJson(res, status, payload) {
|
|
278
279
|
const body = JSON.stringify(payload);
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import { a as registerDoubaoQuotaRoute, c as createDoubaoAdapter, i as registerDoubaoCatalogRoute, n as DOUBAO_QUOTA_PATH, o as createDoubaoShim, r as doubaoQuotaView, s as DOUBAO_STREAM_IDLE_TIMEOUT_MS, t as DOUBAO_CATALOG_PATH } from "./doubao-status-
|
|
1
|
+
import { $ as TASK_SANDBOX_AUTH_ALLOW_ALL, A as taskModeEnabled, B as CHAT_AGENT_MODE, C as classifyDoubaoFailure, D as parseFrames, E as invalidateRenewals, F as decryptCookieValue, G as DOUBAO_MODELS, H as DOUBAO_CONTEXT_WINDOW, I as describeProfileLocation, J as DOUBAO_REASONING_LEVELS, K as DOUBAO_PROVIDER, L as findDoubaoUserDataDir, M as CREDENTIAL_TTL_MS, N as REQUIRED_COOKIE_NAMES, O as probeAccount, P as decodeCookiePlaintext, Q as TASK_RUNTIME_LOCAL_PC, R as invalidateCredentialCache, S as chat, T as extractModel, U as DOUBAO_DEFAULT_MODEL_ID, V as DEFAULT_REASONING_LEVEL_ID, W as DOUBAO_MAX_TOKENS, X as TASK_AGENT_MODE, Y as DOUBAO_UNAVAILABLE_MODELS, Z as TASK_RUNTIME_CLOUD_VM, _ as applyRenewals, a as invalidateQuotaCache, at as needDeepThinkOf, b as buildHeaders, c as ACCOUNT_ENDPOINT, d as DEFAULT_BOT_ID, et as discoverDoubaoModels, f as DOUBAO_ERROR_HINTS, g as RUNTIME_VERSION, h as QUOTA_ENDPOINT, i as formatDuration, it as findReasoningLevel, j as taskWorkspaceOf, k as streamChat, l as CHAT_ENDPOINT, m as DoubaoUpstreamError, n as fetchQuota, nt as enabledModelsOf, o as millisUntilReset, p as DOUBAO_VERSION, q as DOUBAO_PROVIDER_DISPLAY_NAME, r as findWindow, rt as findDoubaoModel, s as normalizeWindow, t as QUOTA_TTL_MS, tt as doubaoCatalogEntries, u as CLIENT_PARAMS, v as buildChatBody, w as describeDoubaoFailure, x as buildQueryUrl, y as buildGeneralTaskParam, z as loadDoubaoCredentials } from "./doubao-quota-XUFfDd1i.js";
|
|
2
|
+
import { a as registerDoubaoQuotaRoute, c as createDoubaoAdapter, i as registerDoubaoCatalogRoute, n as DOUBAO_QUOTA_PATH, o as createDoubaoShim, r as doubaoQuotaView, s as DOUBAO_STREAM_IDLE_TIMEOUT_MS, t as DOUBAO_CATALOG_PATH } from "./doubao-status-C3wgMqnL.js";
|
|
3
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
4
|
//#region src/doubao-config.ts
|
|
5
5
|
/**
|
|
@@ -227,4 +227,4 @@ var src_default = {
|
|
|
227
227
|
apply
|
|
228
228
|
};
|
|
229
229
|
//#endregion
|
|
230
|
-
export { ACCOUNT_ENDPOINT, CHAT_AGENT_MODE, CHAT_ENDPOINT, CLIENT_PARAMS, CREDENTIAL_TTL_MS, Config, DEFAULT_BOT_ID, DEFAULT_REASONING_LEVEL_ID, DOUBAO_CATALOG_PATH, DOUBAO_CONTEXT_WINDOW, DOUBAO_DEFAULT_MODEL_ID, DOUBAO_ERROR_HINTS, DOUBAO_MAX_TOKENS, DOUBAO_MODELS, DOUBAO_PROVIDER, DOUBAO_PROVIDER_DISPLAY_NAME, DOUBAO_QUOTA_PATH, DOUBAO_REASONING_LEVELS, DOUBAO_SETTINGS_NS, DOUBAO_STREAM_IDLE_TIMEOUT_MS, DOUBAO_VERSION, DoubaoUpstreamError, ENABLED_MODEL_IDS_FIELD, PLUGIN_ID, PLUGIN_NAME, QUOTA_ENDPOINT, QUOTA_TTL_MS, REQUIRED_COOKIE_NAMES, RUNTIME_VERSION, apply, applyRenewals, buildChatBody, buildHeaders, buildQueryUrl, chat, classifyDoubaoFailure, createDoubaoAdapter, createDoubaoShim, decodeCookiePlaintext, decryptCookieValue, src_default as default, describeDoubaoFailure, describeProfileLocation, discoverDoubaoModels, doubaoCatalogEntries, doubaoQuotaView, effectiveIds, enabledIdsOf, enabledModelsOf, extractModel, fetchQuota, findDoubaoModel, findDoubaoUserDataDir, findReasoningLevel, findWindow, formatDuration, inject, invalidateCredentialCache, invalidateQuotaCache, invalidateRenewals, isConfigured, loadDoubaoCredentials, millisUntilReset, name, needDeepThinkOf, normalizeWindow, parseFrames, probeAccount, registerDoubaoCatalogRoute, registerDoubaoQuotaRoute, settingsNamespaceOf, streamChat };
|
|
230
|
+
export { ACCOUNT_ENDPOINT, CHAT_AGENT_MODE, CHAT_ENDPOINT, CLIENT_PARAMS, CREDENTIAL_TTL_MS, Config, DEFAULT_BOT_ID, DEFAULT_REASONING_LEVEL_ID, DOUBAO_CATALOG_PATH, DOUBAO_CONTEXT_WINDOW, DOUBAO_DEFAULT_MODEL_ID, DOUBAO_ERROR_HINTS, DOUBAO_MAX_TOKENS, DOUBAO_MODELS, DOUBAO_PROVIDER, DOUBAO_PROVIDER_DISPLAY_NAME, DOUBAO_QUOTA_PATH, DOUBAO_REASONING_LEVELS, DOUBAO_SETTINGS_NS, DOUBAO_STREAM_IDLE_TIMEOUT_MS, DOUBAO_UNAVAILABLE_MODELS, DOUBAO_VERSION, DoubaoUpstreamError, ENABLED_MODEL_IDS_FIELD, PLUGIN_ID, PLUGIN_NAME, QUOTA_ENDPOINT, QUOTA_TTL_MS, REQUIRED_COOKIE_NAMES, RUNTIME_VERSION, TASK_AGENT_MODE, TASK_RUNTIME_CLOUD_VM, TASK_RUNTIME_LOCAL_PC, TASK_SANDBOX_AUTH_ALLOW_ALL, apply, applyRenewals, buildChatBody, buildGeneralTaskParam, buildHeaders, buildQueryUrl, chat, classifyDoubaoFailure, createDoubaoAdapter, createDoubaoShim, decodeCookiePlaintext, decryptCookieValue, src_default as default, describeDoubaoFailure, describeProfileLocation, discoverDoubaoModels, doubaoCatalogEntries, doubaoQuotaView, effectiveIds, enabledIdsOf, enabledModelsOf, extractModel, fetchQuota, findDoubaoModel, findDoubaoUserDataDir, findReasoningLevel, findWindow, formatDuration, inject, invalidateCredentialCache, invalidateQuotaCache, invalidateRenewals, isConfigured, loadDoubaoCredentials, millisUntilReset, name, needDeepThinkOf, normalizeWindow, parseFrames, probeAccount, registerDoubaoCatalogRoute, registerDoubaoQuotaRoute, settingsNamespaceOf, streamChat, taskModeEnabled, taskWorkspaceOf };
|
package/package.json
CHANGED