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 CHANGED
@@ -81,25 +81,27 @@ Chromium cookie(通过 Windows DPAPI 在本进程内解密),把它当作
81
81
 
82
82
  ## 模型目录
83
83
 
84
- 目录读自豆包客户端自身的配置缓存(服务端下发给 app 的 `menuConfV2`),
85
- 不是我们臆测的。`model_item_key` 与 DSH 侧的模型 id **是同一个字符串**,
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
- **推理强度**:低 = `3`、中 = `4`、高 = `5`。
98
- 这三个值是通过**抓取客户端切换档位时重新序列化的配置 diff**读出来的,
99
- 并由客户端配置里的 `reasoning_effort_config` 独立确认了一次,不是推断的。
98
+ 「工作任务」的四条**故意不列**:它们的执行侧是豆包客户端进程内的 CUA 运行时
99
+ (`tasks-mode-resource` + `sbox`),外部客户端注册不进去,每一轮都会得到
100
+ `710010702`。与其在模型选择器里摆出选不中的条目,不如不摆。
100
101
 
101
- > 「即将下线」不是某个模型的属性 —— app 发布了**两个** Turbo 条目,
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 { A as REQUIRED_COOKIE_NAMES, D as probeAccount, H as DOUBAO_MODELS, I as loadDoubaoCredentials, K as discoverDoubaoModels, N as describeProfileLocation, P as findDoubaoUserDataDir, S as classifyDoubaoFailure, U as DOUBAO_PROVIDER, W as DOUBAO_PROVIDER_DISPLAY_NAME, m as DoubaoUpstreamError, n as fetchQuota, r as findWindow } from "./doubao-quota-12DrjjCQ.js";
3
- import { c as createDoubaoAdapter, n as DOUBAO_QUOTA_PATH, o as createDoubaoShim, r as doubaoQuotaView } from "./doubao-status-BU1AUzYj.js";
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 { D as probeAccount, H as DOUBAO_MODELS, I as loadDoubaoCredentials, N as describeProfileLocation, O as streamChat, X as findReasoningLevel, Y as findDoubaoModel, i as formatDuration, m as DoubaoUpstreamError, n as fetchQuota, o as millisUntilReset, r as findWindow } from "./doubao-quota-12DrjjCQ.js";
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": "勾选的条目会出现在 DSH 模型选择器中,请求时按 model_item_key 指定模型",
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": "Ticked entries appear in the DSH model picker and are sent as model_item_key",
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
- * Agent mode for ordinary chat.
44
+ * `option.agent_mode` / `ext.agent_mode`.
45
45
  *
46
- * The desktop app has a 对话/工作 switch that also lands in `agent_mode`:
47
- * ordinary chat sends 1, work mode sends 2. Only chat is implemented here.
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 CHAT_AGENT_MODE = 1;
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 six entries the desktop app publishes, with the keys it sends.
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
- * Read out of the client's own `menuConfV2` cache (M10) — the configuration
84
- * the server hands the app, not something reconstructed from the UI. Each entry
85
- * there carries its own `reasoning_effort_config` listing 低=3 / 中=4 / 高=5,
86
- * which is the SECOND of two independent confirmations of those values (the
87
- * first was the UI diff in M8 续二).
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
- * 「豆包 快速」 leads because it is what `default_select_mode_map.chat` points
90
- * at, so an unconfigured install reproduces the app's own default rather than
91
- * something that merely happens to work.
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 DOUBAO_MODELS = [
152
+ const DOUBAO_UNAVAILABLE_MODELS = [
94
153
  {
95
- id: "0",
96
- name: "豆包 快速",
97
- reasoning: true
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
- badge: "0921 新版",
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: "5",
113
- name: "豆包 2.1 Pro",
114
- reasoning: true
164
+ id: "9",
165
+ name: "自动",
166
+ reason: "同上(工作任务专属)"
115
167
  },
116
168
  {
117
169
  id: "4",
118
- name: "豆包 2.1 Turbo",
119
- badge: "即将下线",
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 dpapiUnprotect(cipher) {
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
- "$ok = [DshCrypt]::CryptUnprotectData([ref]$inBlob, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, 1, [ref]$outBlob)",
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: cipher.toString("base64")
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
- * Chromium holds an exclusive lock on the live database, so the file is copied
374
- * to a temp directory first and that copy is destroyed before returning. The
375
- * original profile is only ever read.
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
- copyFileSync(source, copy);
383
- const { DatabaseSync } = require_("node:sqlite");
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.prepare("SELECT host_key, name, encrypted_value FROM cookies WHERE encrypted_value IS NOT NULL").all().map((r) => ({
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 agentMode = options.agentMode ?? 1;
620
- const needDeepThink = needDeepThinkOf(level);
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: randomUUID(),
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: needDeepThink,
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
- * Stream one chat completion and return the assembled reply.
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 streamChat(prompt, options = {}) {
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
- messageId = payload.message?.message_id ?? messageId;
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 text = stripPromptEcho(texts.join(""));
937
- if (!text) throw new DoubaoUpstreamError(`Doubao returned an empty completion${lastEventId ? ` (last event id ${lastEventId})` : ""}.`);
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 { REQUIRED_COOKIE_NAMES as A, DOUBAO_DEFAULT_MODEL_ID as B, describeDoubaoFailure as C, probeAccount as D, parseFrames as E, invalidateCredentialCache as F, DOUBAO_REASONING_LEVELS as G, DOUBAO_MODELS as H, loadDoubaoCredentials as I, enabledModelsOf as J, discoverDoubaoModels as K, CHAT_AGENT_MODE as L, decryptCookieValue as M, describeProfileLocation as N, streamChat as O, findDoubaoUserDataDir as P, DEFAULT_REASONING_LEVEL_ID as R, classifyDoubaoFailure as S, invalidateRenewals as T, DOUBAO_PROVIDER as U, DOUBAO_MAX_TOKENS as V, DOUBAO_PROVIDER_DISPLAY_NAME as W, findReasoningLevel as X, findDoubaoModel as Y, needDeepThinkOf as Z, applyRenewals as _, invalidateQuotaCache as a, buildQueryUrl as b, ACCOUNT_ENDPOINT as c, DEFAULT_BOT_ID as d, DOUBAO_ERROR_HINTS as f, RUNTIME_VERSION as g, QUOTA_ENDPOINT as h, formatDuration as i, decodeCookiePlaintext as j, CREDENTIAL_TTL_MS as k, CHAT_ENDPOINT as l, DoubaoUpstreamError as m, fetchQuota as n, millisUntilReset as o, DOUBAO_VERSION as p, doubaoCatalogEntries as q, findWindow as r, normalizeWindow as s, QUOTA_TTL_MS as t, CLIENT_PARAMS as u, buildChatBody as v, extractModel as w, chat as x, buildHeaders as y, DOUBAO_CONTEXT_WINDOW as z };
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 { H as DOUBAO_MODELS, I as loadDoubaoCredentials, J as enabledModelsOf, N as describeProfileLocation, S as classifyDoubaoFailure, U as DOUBAO_PROVIDER, V as DOUBAO_MAX_TOKENS, W as DOUBAO_PROVIDER_DISPLAY_NAME, Y as findDoubaoModel, a as invalidateQuotaCache, m as DoubaoUpstreamError, n as fetchQuota, o as millisUntilReset, q as doubaoCatalogEntries, r as findWindow, x as chat, z as DOUBAO_CONTEXT_WINDOW } from "./doubao-quota-12DrjjCQ.js";
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 REQUIRED_COOKIE_NAMES, B as DOUBAO_DEFAULT_MODEL_ID, C as describeDoubaoFailure, D as probeAccount, E as parseFrames, F as invalidateCredentialCache, G as DOUBAO_REASONING_LEVELS, H as DOUBAO_MODELS, I as loadDoubaoCredentials, J as enabledModelsOf, K as discoverDoubaoModels, L as CHAT_AGENT_MODE, M as decryptCookieValue, N as describeProfileLocation, O as streamChat, P as findDoubaoUserDataDir, R as DEFAULT_REASONING_LEVEL_ID, S as classifyDoubaoFailure, T as invalidateRenewals, U as DOUBAO_PROVIDER, V as DOUBAO_MAX_TOKENS, W as DOUBAO_PROVIDER_DISPLAY_NAME, X as findReasoningLevel, Y as findDoubaoModel, Z as needDeepThinkOf, _ as applyRenewals, a as invalidateQuotaCache, b as buildQueryUrl, c as ACCOUNT_ENDPOINT, d as DEFAULT_BOT_ID, f as DOUBAO_ERROR_HINTS, g as RUNTIME_VERSION, h as QUOTA_ENDPOINT, i as formatDuration, j as decodeCookiePlaintext, k as CREDENTIAL_TTL_MS, l as CHAT_ENDPOINT, m as DoubaoUpstreamError, n as fetchQuota, o as millisUntilReset, p as DOUBAO_VERSION, q as doubaoCatalogEntries, r as findWindow, s as normalizeWindow, t as QUOTA_TTL_MS, u as CLIENT_PARAMS, v as buildChatBody, w as extractModel, x as chat, y as buildHeaders, z as DOUBAO_CONTEXT_WINDOW } from "./doubao-quota-12DrjjCQ.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-BU1AUzYj.js";
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
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-doubao",
3
3
  "displayName": "DSH Connect Doubao",
4
4
  "description": "把本机已登录的豆包(Doubao)会员额度接入 DeepSeek Harness,作为 Seed 模型 provider,并提供只读的双窗口额度概览。",
5
- "version": "0.1.1",
5
+ "version": "0.2.0",
6
6
  "license": "MIT",
7
7
  "keywords": [
8
8
  "dsh-plugin",