dsh-deepseek-web-login 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/CHANGELOG.md +3165 -0
- package/LICENSE +202 -0
- package/NOTICE +12 -0
- package/README.en.md +510 -0
- package/README.md +673 -0
- package/cordis.patch.yml +6 -0
- package/docs/assets/architecture.svg +264 -0
- package/docs/assets/banner-dsh-deepseek-web-login.jpg +0 -0
- package/docs/assets/hero.svg +53 -0
- package/docs/assets/login-flow.svg +307 -0
- package/docs/assets/panel-preview.svg +64 -0
- package/docs/assets/qq-group.jpg +0 -0
- package/docs/assets/screenshot-settings.png +0 -0
- package/docs/assets/screenshot-usage-stats.png +0 -0
- package/docs/assets/tool-bridge.svg +276 -0
- package/lib/client.js +2165 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +9585 -0
- package/lib/index.js.map +1 -0
- package/package.json +79 -0
- package/scripts/build.mjs +59 -0
- package/scripts/build.sh +6 -0
- package/scripts/make-dev-copy.mjs +49 -0
- package/scripts/prepare.mjs +25 -0
- package/tools/changelog-section.mjs +42 -0
- package/tools/inspect-session.mjs +223 -0
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["joinPath"],"sources":["../src/cookies.ts","../src/paths.ts","../src/accounts.ts","../src/auth.ts","../src/gate.ts","../src/context-feed.ts","../src/webapi.ts","../src/probe.ts","../src/protocol.ts","../src/adapter.ts","../src/browser-login.ts","../src/account-add.ts","../src/login.ts","../src/transport.ts","../src/net-diagnostics.ts","../src/ledger.ts","../src/update-check.ts","../src/version.ts","../src/session-journal.ts","../src/account-groups.ts","../src/index.ts"],"sourcesContent":["/**\n * cookie 过期信息的采集与解读。\n *\n * ## 为什么值得做\n *\n * 插件存下来的凭证里只有 `name=value` 串,**过期时间一直被丢掉** —— 于是\"登录态会不会\n * 因为长期不用而失效\"这件事在界面上完全没有可观察的线索。\n *\n * 实测(2026-09-12,只读、零额度):DeepSeek 的只读端点**不返回 set-cookie**,\n * token 也不轮换,所以**没有任何\"续期\"手段可做**;能做的只有**把过期时间采下来**,\n * 让\"不确定\"缩小一格 —— 至少能回答\"这些 cookie 里哪些本来就是会话级\"。\n *\n * 顺带一个实测结论(决定\"看什么\"):真正鉴权用的是 **`token`**,不是 cookie ——\n * 只发 token 不带 cookie 能通过,只发 cookie 不带 token 直接被拒(40002 Missing Token)。\n * 所以 cookie 的过期时间**不是**登录态寿命的上界;它只告诉我们浏览器侧会先丢哪些。\n *\n * ## 两种来源形状不同,都要认\n *\n * | 来源 | 字段 | 会话级的表示 |\n * | --- | --- | --- |\n * | CDP `Storage.getCookies`(真实 Edge/Chrome) | `expires` | `-1` |\n * | Electron `session.cookies.get()`(插件自开窗口) | `expirationDate` | 字段缺失 |\n *\n * 两者都以 **秒** 为单位(Unix epoch),本模块统一换算成**毫秒**。\n */\n\nexport interface CookieMeta {\n /** cookie 名。 */\n name: string\n /** 所属域(原样保留,便于排查域匹配问题)。 */\n domain: string\n /** 会话级:浏览器关闭即失效(服务端会话另有寿命,不受它约束)。 */\n session: boolean\n /** 持久级 cookie 的到期时间(**毫秒** epoch);会话级或未知 → 不出现该字段。 */\n expiresAt?: number\n}\n\n/**\n * 归一化一个 cookie 的过期形态。两种来源的字段名不同,这里统一。\n *\n * 判定顺序:显式 `session === true` 优先;否则看 `expires` / `expirationDate`。\n * 缺失、非数、`<= 0` **一律当会话级** —— CDP 对会话级给的就是 `-1`,\n * 而\"0\"在 Unix epoch 里也毫无意义,当成有效时间会算出 1970 年。\n */\nexport function readCookieExpiry(raw: any): { session: boolean; expiresAt?: number } {\n if (!raw || typeof raw !== 'object') return { session: true }\n if (raw.session === true) return { session: true }\n const seconds = Number(raw.expires ?? raw.expirationDate)\n if (!Number.isFinite(seconds) || seconds <= 0) return { session: true }\n return { session: false, expiresAt: Math.round(seconds * 1000) }\n}\n\n/**\n * 从任意来源的 cookie 数组里挑出目标域的,转成 `CookieMeta`。\n *\n * ⚠️ `filter` 由调用方给出、且**必须与该处拼 cookie 头的过滤条件逐字一致**:\n * 这些元信息要描述的正是\"请求里实际带上的那批 cookie\"。两条捕获路径原本的过滤条件\n * 不同(真实浏览器那条用 `deepseek`,Electron 那条用 `deepseek.com`),\n * 这里刻意不强行统一 —— 统一就等于改了请求头内容,而那个改动是没法靠单测兜住的。\n */\nexport function pickCookieMeta(\n cookies: readonly any[],\n filter: (domain: unknown) => boolean,\n): CookieMeta[] {\n const out: CookieMeta[] = []\n for (const raw of cookies ?? []) {\n const name = typeof raw?.name === 'string' ? raw.name : ''\n if (!name) continue\n const domain = String(raw?.domain ?? '')\n if (!filter(domain)) continue\n const { session, expiresAt } = readCookieExpiry(raw)\n out.push({ name, domain, session, ...(expiresAt !== undefined ? { expiresAt } : {}) })\n }\n return out\n}\n\n/**\n * 规整\"已经归一化过\"的 `CookieMeta` 数组(从磁盘读账号记录时用)。\n * 形状不对的条目直接丢掉,不抛错 —— 一份坏记录不该让整个账号库读不出来。\n */\nexport function normalizeCookieMetaList(raw: unknown): CookieMeta[] | undefined {\n if (!Array.isArray(raw)) return undefined\n const out: CookieMeta[] = []\n for (const item of raw) {\n if (!item || typeof item !== 'object') continue\n const name = typeof (item as any).name === 'string' ? (item as any).name : ''\n if (!name) continue\n const expiresRaw = Number((item as any).expiresAt)\n const hasExpiry = Number.isFinite(expiresRaw) && expiresRaw > 0\n const session = (item as any).session === true || !hasExpiry\n out.push({\n name,\n domain: typeof (item as any).domain === 'string' ? (item as any).domain : '',\n session,\n ...(session ? {} : { expiresAt: Math.round(expiresRaw) }),\n })\n }\n return out.length > 0 ? out : undefined\n}\n\nexport interface CookieLifeSummary {\n total: number\n sessionCount: number\n persistentCount: number\n /** 持久级里**最晚**到期的那个 —— 用来回答\"浏览器侧还能撑多久\"。 */\n latest?: { name: string; expiresAt: number; daysLeft: number }\n}\n\n/**\n * 汇总结论。**没有记录时返回 `undefined`**(而不是全 0 的对象)——\n * 调用方要区分\"没采到过期信息\"(老记录 / 手动粘 token)和\"采到了、全是会话级\",\n * 这两种情况的界面文案完全不同。\n */\nexport function summarizeCookieLife(\n metas: readonly CookieMeta[] | undefined,\n now: number = Date.now(),\n): CookieLifeSummary | undefined {\n const list = metas ?? []\n if (list.length === 0) return undefined\n const persistent = list.filter((item) => !item.session && Number.isFinite(item.expiresAt))\n let latest: CookieLifeSummary['latest']\n for (const item of persistent) {\n const expiresAt = item.expiresAt as number\n if (!latest || expiresAt > latest.expiresAt) {\n latest = { name: item.name, expiresAt, daysLeft: (expiresAt - now) / 86_400_000 }\n }\n }\n return {\n total: list.length,\n sessionCount: list.length - persistent.length,\n persistentCount: persistent.length,\n ...(latest ? { latest } : {}),\n }\n}\n\n/** 剩余时间的口语化写法。 */\nexport function describeRemaining(daysLeft: number): string {\n if (daysLeft <= 0) return '已过期'\n if (daysLeft >= 1) return `还剩 ${Math.floor(daysLeft)} 天`\n const hours = Math.max(1, Math.round(daysLeft * 24))\n return `还剩 ${hours} 小时`\n}\n\n/**\n * 一句话说清 cookie 的寿命构成(界面直接展示)。\n *\n * 三种情况分开写,因为它们的含义完全不同:\n * - 没记录 → 老记录 / 手动粘 token,下次重新登录会补上;\n * - 全是会话级 → 浏览器侧本来就没有到期时间,看它撑多久没意义;\n * - 有持久级 → 给出最晚那个的剩余时间(**只是浏览器侧的上界,不是登录态寿命**)。\n */\nexport function describeCookieLife(\n summary: CookieLifeSummary | undefined,\n now: number = Date.now(),\n): string {\n if (!summary) return '⚠️ 未记录(重新登录后会补上)'\n const parts = [`${summary.total} 项`]\n parts.push(summary.sessionCount > 0 ? `${summary.sessionCount} 会话级` : '无会话级')\n if (summary.persistentCount > 0) parts.push(`${summary.persistentCount} 持久级`)\n if (summary.latest) {\n // 注意:`summary.latest.daysLeft` 是按生成这份 summary 的时刻算的。\n // 调用方若隔了很久才展示,传 `now` 进来重算即可。\n const daysLeft = (summary.latest.expiresAt - now) / 86_400_000\n parts.push(`${summary.latest.name} ${describeRemaining(daysLeft)}`)\n }\n return parts.join(' · ')\n}\n","/**\n * 路径解析 —— 单独成文件,避免 auth.ts ↔ accounts.ts 互相 import 形成环。\n *\n * 约定:插件的所有本地状态都放在 `${DSH_HOME || ~/.dsh}/web-login/` 下,\n * **不进 settings/credentials 缝合口** —— 那里是通用配置面,不适合放网页端凭证。\n */\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\n/** DSH 主目录(与生态一致的解析顺序)。 */\nexport function resolveDshHome(): string {\n return process.env.DSH_HOME || join(homedir(), '.dsh')\n}\n\n/** 插件自己的状态目录。 */\nexport function webLoginDir(): string {\n return join(resolveDshHome(), 'web-login')\n}\n\n/**\n * 旧版单账号凭证文件(0.1.25 及以前只有这一个账号)。\n * 现在改为账号库,但**这个路径仍然要认得** —— 用来做一次性迁移。\n */\nexport function legacyAuthFilePath(): string {\n return join(webLoginDir(), 'deepseek-auth.json')\n}\n","/**\n * 账号库 —— 把「一个账号」升级成「一库账号,一键切换」。\n *\n * ## 为什么要做(2026-09-12,借鉴 workbuddy-switch)\n *\n * 原来只有一份凭证文件(`deepseek-auth.json`),换号的代价是:\n * **退出 → 清除浏览器分区 → 重新登录 → 等它捕获**,期间原来的号也回不去了。\n * 参考项目把\"账号\"当成一等公民管理(账号卡片、状态、临期高亮、导入导出),\n * 这里把其中适用的部分搬过来:**多账号并存 + 一键切换 + 导入导出**。\n *\n * 目录结构:\n * ```\n * <DSH_HOME>/web-login/\n * ├── accounts.json # 索引:{ activeId }(顺序与展示信息都在各账号文件里)\n * ├── accounts/acc_xxxx.json # 每个账号一份(WebAuth + 元信息),原子写 + 0600\n * └── deepseek-auth.json # 旧版单账号文件(只用于首次迁移)\n * ```\n *\n * ## ⚠️ 风险提示(必须让使用者看见,不只是写在文档里)\n *\n * 账号库让\"换号\"变得很容易,而**用多账号轮换规避单账号限流,是有代价的**:\n *\n * 1. 同一服务商会把多账号**关联**起来(同设备、同 IP、同指纹、彼此相近的行为模式)。\n * 一旦被判定为\"同一人的多开小号\",处置通常比单账号超频更重,且可能波及**全部**关联账号。\n * 2. 因此本插件**只提供手动切换**,刻意**不做自动轮换** ——\n * 真人不会在几分钟内换一个账号继续发消息,自动换号是极强的机器行为特征,\n * 与本插件在传输层/间隔/会话清理上\"降低机器可识别性\"的努力**直接冲突**。\n * 3. 账号库里的每个文件都含**可完整登录的凭证**(token + cookie)。\n * 导出的备份文件同样是明文 —— 分享给别人等于把账号给出去。\n *\n * 换句话说:这个功能的目标是「**在你自己的多个正常账号之间切换得更省事**」\n * (比如工作号/个人号),**不是**「靠轮换把限流绕过去」。\n */\nimport { normalizeCookieMetaList } from './cookies.ts'\nimport { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, chmodSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { randomUUID } from 'node:crypto'\nimport { legacyAuthFilePath, webLoginDir } from './paths.ts'\nimport type { WebAuth } from './auth.ts'\n\n/** 账号库索引。 */\ninterface AccountsIndex {\n version: number\n /** 当前生效的账号 id;不存在表示\"未选择账号\"。 */\n activeId?: string\n}\n\n/**\n * 一个账号 = 凭证(WebAuth 的全部字段)+ 元信息。\n *\n * 刻意**做成扁平结构**(而不是 `{ meta, auth }` 嵌套):这样它天然满足 `WebAuth`,\n * `readAuth()` 可以直接把它当凭证返回,适配器/登录流程一行都不用改。\n */\nexport interface AccountRecord extends WebAuth {\n /** 稳定 id(`acc_` + 8 位十六进制),库内唯一,与 token 无关(token 可能刷新)。 */\n id: string\n /** 用户可改的备注名(如「工作号」)。为空时界面显示掩码账号。 */\n label?: string\n /**\n * 所属分组 id(指向 `groups.json` 里的 `AccountGroup.id`)。\n *\n * 刻意**只存指针、不存组名**:组要能改名与排序,写进每条账号记录就得遍历全库;\n * 而组被删掉时这里会变成\"悬挂指针\",`partitionByGroup` 一律按「未分组」处理 ——\n * 所以删组不需要(也不应该)去逐个改账号文件。\n */\n groupId?: string\n /**\n * DeepSeek 服务端的 user id(来自 `users/current` 的 `data.id`)。\n * 用途:**同一账号重复捕获时更新而不是新增**,避免库里堆一堆同一个号。\n */\n serverId?: string\n /** 最近一次主动探活成功的时间(ISO)。见 `src/probe.ts`。 */\n lastVerifiedAt?: string\n /** 最近一次主动探活失败(保留原因,便于一眼看出是过期还是网络问题)。 */\n lastVerifyError?: { at: string; message: string }\n /**\n * 观测到的账号级限制(来自**生成请求被拒**,不是探活)。\n *\n * 更正(2026-09-12 实测):这里原先写\"受限期间 users/current 依然 200,所以探活探不出来\" ——\n * 返回 200 是对的,但**响应体里就带着 `chat: { is_muted, mute_until }`**,\n * 也就是说探活其实探得出来,只是目前还没接上。\n * 现在这个状态仍然只从**生成失败**里学到(失败信封里的 mute_until,见 webapi.ts 的 muteUntilMs)。\n */\n limit?: { untilMs: number; observedAt: string }\n}\n\nconst INDEX_VERSION = 1\n\nexport function accountsDir(): string {\n return join(webLoginDir(), 'accounts')\n}\n\nexport function accountsIndexPath(): string {\n return join(webLoginDir(), 'accounts.json')\n}\n\n/**\n * 挡住\"能跑出账号目录\"的 id。\n *\n * ⚠️ F01(2026-09-12 审计):旧实现直接 `join(accountsDir(), `${id}.json`)`,**没有任何校验**;\n * 而 `importAccounts` 会把**备份文件里的 id 原样当主键**,HTTP 路由也直接吃调用方传的 id。\n * 于是 id 写成 `../../../../Users/me/evil` 就能越界读、写、删账号目录之外的 JSON。\n *\n * 这里只挡\"危险字符\"而不强求格式:历史 id 形态不止一种(acc_ 前缀 + 8/16 位十六进制、\n * 迁移期还可能有过别的),按白名单收紧会误伤老账号。真正要拦的只有能穿越路径的那些。\n */\n/** 单次导入的账号条数上限(N01):避免一次导入对全库做重复扫描。 */\nexport const MAX_IMPORT_ACCOUNTS = 500\n\nfunction assertSafeAccountId(id: string): string {\n const text = String(id ?? '')\n if (!text || text.includes('\\0') || text === '.' || text === '..' || /[/\\\\]/.test(text)) {\n throw new Error(`账号 id 不合法(含路径分隔符或相对路径段):${JSON.stringify(text)}`)\n }\n return text\n}\n\nexport function accountFilePath(id: string): string {\n return join(accountsDir(), `${assertSafeAccountId(id)}.json`)\n}\n\n/** 新账号 id。用随机 id 而不是 token 哈希:token 会刷新,id 不该跟着变。 */\nexport function newAccountId(): string {\n return `acc_${randomUUID().replace(/-/g, '').slice(0, 8)}`\n}\n\n/** 原子写(临时文件 + 替换),非 Windows 下收紧权限到 0600。 */\nfunction writeJsonAtomic(file: string, value: unknown): void {\n mkdirSync(join(file, '..'), { recursive: true })\n const tmp = `${file}.tmp-${process.pid}`\n // 临时文件从创建起就是 0600:它装的是**可完整登录的凭证**,\n // 旧写法先按默认权限(受 umask 影响,常见 0644)落地、事后再 chmod,中间有暴露窗口。\n writeFileSync(tmp, JSON.stringify(value, null, 2), { encoding: 'utf8', mode: 0o600 })\n // ⚠️ F02(2026-09-12 审计):旧写法是「先 rmSync 目标文件,再 rename 过去」——\n // 一旦 rename 失败(磁盘满 / 占用 / 权限),**原文件已经没了**,凭证直接丢失。\n // 而在 Windows 上 `fs.renameSync` 本来就能直接覆盖已存在的目标文件\n // (实测 win32 + Node 22:renameSync 成功覆盖,目标变新内容,临时文件消失),\n // 所以那句 rmSync 既没必要、也是唯一的丢数据风险点。去掉之后:\n // rename 失败 → 清掉临时文件 → 原文件完好无损。\n try {\n renameSync(tmp, file)\n } catch (error) {\n try {\n rmSync(tmp, { force: true })\n } catch {}\n throw error\n }\n if (process.platform !== 'win32') {\n try {\n chmodSync(file, 0o600)\n } catch {}\n }\n}\n\nfunction readJson<T>(file: string): T | undefined {\n try {\n if (!existsSync(file)) return undefined\n return JSON.parse(readFileSync(file, 'utf8')) as T\n } catch {\n return undefined\n }\n}\n\nfunction readIndex(): AccountsIndex {\n const parsed = readJson<AccountsIndex>(accountsIndexPath())\n return {\n version: INDEX_VERSION,\n ...(typeof parsed?.activeId === 'string' && parsed.activeId ? { activeId: parsed.activeId } : {}),\n }\n}\n\nfunction writeIndex(index: Pick<AccountsIndex, 'activeId'>): void {\n writeJsonAtomic(accountsIndexPath(), { version: INDEX_VERSION, ...(index.activeId ? { activeId: index.activeId } : {}) })\n}\n\n/** 把任意对象规整成 AccountRecord(缺字段补默认值;凭证无效返回 undefined)。 */\nfunction normalizeRecord(raw: any, fallbackId?: string): AccountRecord | undefined {\n if (!raw || typeof raw !== 'object') return undefined\n const token = typeof raw.token === 'string' ? raw.token : ''\n if (!token) return undefined\n return {\n id: typeof raw.id === 'string' && raw.id ? raw.id : (fallbackId ?? newAccountId()),\n token,\n cookie: typeof raw.cookie === 'string' ? raw.cookie : '',\n hifDliq: typeof raw.hifDliq === 'string' ? raw.hifDliq : '',\n hifLeim: typeof raw.hifLeim === 'string' ? raw.hifLeim : '',\n wasmUrl: typeof raw.wasmUrl === 'string' ? raw.wasmUrl : '',\n userAgent: typeof raw.userAgent === 'string' ? raw.userAgent : '',\n ...(raw.extraHeaders && typeof raw.extraHeaders === 'object' ? { extraHeaders: raw.extraHeaders } : {}),\n capturedAt: typeof raw.capturedAt === 'string' ? raw.capturedAt : '',\n ...(raw.unverified === true ? { unverified: true } : {}),\n ...(raw.user && typeof raw.user === 'object' ? { user: raw.user } : {}),\n // cookie 过期构成:形状不对的条目在 cookies.ts 里被丢掉,不会让整条记录读不出来\n ...(() => {\n const meta = normalizeCookieMetaList(raw.cookieMeta)\n return meta ? { cookieMeta: meta } : {}\n })(),\n ...(typeof raw.label === 'string' && raw.label ? { label: raw.label } : {}),\n ...(typeof raw.groupId === 'string' && raw.groupId ? { groupId: raw.groupId } : {}),\n ...(typeof raw.serverId === 'string' && raw.serverId ? { serverId: raw.serverId } : {}),\n ...(typeof raw.lastVerifiedAt === 'string' ? { lastVerifiedAt: raw.lastVerifiedAt } : {}),\n ...(raw.lastVerifyError && typeof raw.lastVerifyError?.at === 'string'\n ? { lastVerifyError: { at: raw.lastVerifyError.at, message: String(raw.lastVerifyError.message ?? '') } }\n : {}),\n ...(raw.limit && Number.isFinite(raw.limit?.untilMs)\n ? { limit: { untilMs: Number(raw.limit.untilMs), observedAt: String(raw.limit.observedAt ?? '') } }\n : {}),\n }\n}\n\n/** 库里全部账号,按捕获时间倒序(最近捕获的在前)。 */\nexport function listAccounts(): AccountRecord[] {\n let names: string[] = []\n try {\n names = readdirSync(accountsDir()).filter((name) => name.endsWith('.json') && !name.includes('.tmp-'))\n } catch {\n return []\n }\n const records: AccountRecord[] = []\n for (const name of names) {\n const id = name.replace(/\\.json$/, '')\n // ⚠️ 0.1.82:`accountFilePath` 对空串 / `..` 这类非法 id 会直接抛(防路径穿越)。\n // 目录里一旦出现 `.json`、`..json` 这种文件名,不逐个兜住就会**整个账号库读不出来**\n // (列表空 + 路由 500),而原因只是一个无关文件。\n let record: AccountRecord | undefined\n try {\n record = normalizeRecord(readJson(accountFilePath(id)), id)\n } catch {\n continue\n }\n if (record) records.push(record)\n }\n records.sort((a, b) => String(b.capturedAt).localeCompare(String(a.capturedAt)))\n return records\n}\n\nexport function readAccount(id: string): AccountRecord | undefined {\n if (!id) return undefined\n return normalizeRecord(readJson(accountFilePath(id)), id)\n}\n\nexport function saveAccount(record: AccountRecord): void {\n writeJsonAtomic(accountFilePath(record.id), record)\n}\n\nexport function activeAccountId(): string | undefined {\n const { activeId } = readIndex()\n if (!activeId) return undefined\n // 索引指向的账号可能已被移除 —— 那就当\"未选择\",不要让调用方拿到悬空 id。\n // 也可能 id 本身不合法(索引文件被外部改过)→ 同理当\"未选择\",而不是让异常炸穿调用方。\n try {\n return existsSync(accountFilePath(activeId)) ? activeId : undefined\n } catch {\n return undefined\n }\n}\n\n/** 当前生效的账号(没有就返回 undefined)。 */\nexport function activeAccount(): AccountRecord | undefined {\n const id = activeAccountId()\n return id ? readAccount(id) : undefined\n}\n\nexport function setActiveAccount(id: string): boolean {\n if (!existsSync(accountFilePath(id))) return false\n writeIndex({ activeId: id })\n return true\n}\n\nexport function clearActiveAccount(): void {\n writeIndex({})\n}\n\nexport function updateAccount(id: string, patch: Partial<AccountRecord>): AccountRecord | undefined {\n const current = readAccount(id)\n if (!current) return undefined\n const next = normalizeRecord({ ...current, ...patch, id }, id)\n if (!next) return undefined\n saveAccount(next)\n return next\n}\n\n/**\n * 从账号库里移除一个账号(**删除凭证文件**)。\n *\n * 为什么不学其它可逆操作\"改名留档\":这里存的是**可完整登录的凭证**,\n * 「退出/移除」的语义就是\"这份凭证不该再留在磁盘上\" ——\n * 留一个 `.removed-<时间>` 的明文备份会让\"已登出\"变成谎话(安全上的倒退)。\n * 误删的保护交给两件事:界面上**二次确认**,以及账号库**导出备份**。\n */\nexport function removeAccount(id: string): boolean {\n const file = accountFilePath(id)\n if (!existsSync(file)) return false\n try {\n rmSync(file, { force: true })\n } catch {\n return false\n }\n if (readIndex().activeId === id) clearActiveAccount()\n return true\n}\n\n/**\n * 写入/更新一个账号的凭证(登录捕获、手动粘贴 token 都走这里)。\n *\n * 去重顺序:\n * 1. 有 `serverId` 且库里已有同 `serverId` → **更新那一条**(同一账号重新捕获);\n * 2. 否则 token 完全相同的记录 → 更新(serverId 还没拿到的场景);\n * 3. 都没有 → 新增。\n *\n * ⚠️ **凭证字段一律以本次传入的为准,不做合并**:调用方(例如登录流程)用\n * `writeAuth({ ...auth, unverified: true })` 表示\"这次没校验成功\",\n * 若沿用旧记录的字段,这条 `unverified` 会永远粘住、再也清不掉。\n * 需要跨次保留的只有元信息(备注名/探活时间/限制状态),所以只挑那几个字段继承。\n */\nexport function upsertAccount(auth: WebAuth, patch: Partial<AccountRecord> = {}): AccountRecord {\n // serverId 可能随 `patch` 传,也可能被调用方直接塞进 `auth` 里(登录流程习惯写\n // `writeAuth({ ...auth, user })`)—— 两种都得认,否则去重会静默失效、库里堆重复条目。\n const incoming = auth as Partial<AccountRecord>\n const serverId = patch.serverId ?? incoming.serverId\n\n const all = listAccounts()\n // ⚠️ 0.1.82:调用方显式给了 `patch.id` 就**优先认它**。重登路径(`commitCapturedAuth`)\n // 只传 `{ id: target }`,而重登**必然换 token**、捕获又常不带身份 ⇒ 下面两条都匹配不上、\n // 最后那条 token 匹配也落空 ⇒ `existing` 恒为 undefined ⇒ carried 白名单**全部落空**:\n // 备注名 / 分组 / serverId / 显示名被静默清空(每一次重登都会发生)。\n const existing =\n (patch.id ? all.find((item) => item.id === patch.id) : undefined) ??\n (serverId ? all.find((item) => item.serverId && item.serverId === serverId) : undefined) ??\n // 兼容旧记录(审计 F04):`serverId` 是后加的字段,老库里可能只存了 `user.id`。\n // 只认**没有 serverId** 的记录,免得跟上面那条抢匹配(那条才是权威身份键)。\n (serverId ? all.find((item) => !item.serverId && item.user?.id === serverId) : undefined) ??\n all.find((item) => item.token === auth.token)\n const id = patch.id ?? existing?.id ?? newAccountId()\n\n const carried: Partial<AccountRecord> = {}\n // ⚠️ 这个白名单决定「重新登录 / 导入」时哪些**用户附加的元数据**会被带过去。\n // 少了 groupId 就会出现\"重登一次,账号从组里掉出来\"这种很难查的 bug。\n for (const key of ['label', 'groupId', 'serverId', 'lastVerifiedAt', 'lastVerifyError', 'limit'] as const) {\n const value = (patch as any)[key] ?? (incoming as any)[key] ?? (existing as any)?.[key]\n if (value !== undefined) (carried as any)[key] = value\n }\n\n // ⚠️ `user` 必须单独做**深合并**,不能塞进上面那个 `??` 链里 ——\n // 它是服务端在**校验通过**时才给的「一次性」信息,而重登 / 导入时新凭证往往还是\n // `unverified`(拿不到 user)。若按\"新值整体覆盖\"处理,就会把原记录里可辨识的显示名抹成空:\n // 界面上的账号从「137******78」退化成「未识别账号 (acc_xxxx)」—— 2026-09-17 实测,\n // 用户看到的现象是\"发个消息账号就退出了\"。而 `serverId` 还在,说明这条记录本来是有身份的。\n // 这里按字段合并、新值优先、缺的字段沿用旧值。\n const userFromPatch = (patch as any)?.user\n const userFromIncoming = (incoming as any)?.user\n const mergedUser = {\n ...(existing?.user ?? {}),\n ...(userFromIncoming && typeof userFromIncoming === 'object' ? userFromIncoming : {}),\n ...(userFromPatch && typeof userFromPatch === 'object' ? userFromPatch : {}),\n }\n if (Object.keys(mergedUser).length > 0) carried.user = mergedUser as AccountRecord['user']\n\n const record = normalizeRecord({ ...auth, ...carried, id }, id)!\n saveAccount(record)\n return record\n}\n\n/**\n * 打包一份导出数据(含明文凭证 —— 调用方必须把风险讲给用户)。\n *\n * ⚠️ 这条数据现在有两条出口,安全姿态不同:\n * 1. `exportAccountsToFile()` + `POST /accounts/export`:**凭证不出宿主**,\n * 宿主自己写盘、只回传路径。始终保留,是回退路径。\n * 2. `POST /accounts/export-json`:把内容交给界面,由界面弹系统「另存为」写盘。\n * 为了让用户能自己选保存位置,这条路躲不开(理由见 index.ts 里那个路由的注释)。\n */\nexport function exportAccounts(): { version: number; exportedAt: string; warning: string; accounts: AccountRecord[] } {\n return {\n version: INDEX_VERSION,\n exportedAt: new Date().toISOString(),\n warning: '此文件含可完整登录的凭证(token + cookie),等同于账号本身,请勿分享或提交到仓库',\n accounts: listAccounts(),\n }\n}\n\n/**\n * 导出到**插件目录下的文件**并返回路径(`<web-login>/exports/accounts-<时间戳>.json`)。\n *\n * 这是回退路径:界面拿不到系统「另存为」(宿主未注入 File System Access、\n * 或弹框被平台拒绝)时用它,保证导出功能永不失效。\n * 优点是明文凭证不进 HTTP 响应体,只把**路径**回给界面。\n */\nexport function exportAccountsToFile(): { path: string; count: number } {\n const stamp = new Date().toISOString().replace(/[:.]/g, '-')\n const file = join(webLoginDir(), 'exports', `accounts-${stamp}.json`)\n writeJsonAtomic(file, exportAccounts())\n return { path: file, count: listAccounts().length }\n}\n\n/**\n * 导入(校验 + 去重 + 补 id)。返回新增/更新数量。\n *\n * ⚠️ **绝不相信备份文件自报的 `id`**(2026-09-13 第二轮审计 N01):\n * 旧实现用 `normalizeRecord(raw)` 时**不传 fallbackId 根本不生效** ——\n * `normalizeRecord` 内部仍是 `raw.id ?? fallbackId ?? newAccountId()`,\n * 于是备份里写同一个 `id` 的两条不同 token 账号会**互相覆盖**(不需要路径穿越)。\n * 现在导入一律**生成本地主键**,只有 token 命中才允许更新已有账号。\n *\n * `serverId` 同样不用于匹配:它是**备份自报**字段,不能拿它授权覆盖不同 token 的账号。\n * 代价:同账号刷新 token 的备份会多出一条,需要用户手动确认合并 —— 但保护了旧凭证。\n *\n * 上限 500 条,避免一次导入触发对全库的重复扫描;整批非事务,中途磁盘失败可能部分导入。\n */\nexport function importAccounts(payload: unknown): { imported: number; updated: number; skipped: number } {\n const list = Array.isArray(payload) ? payload : (payload as any)?.accounts\n if (!Array.isArray(list)) return { imported: 0, updated: 0, skipped: 0 }\n if (list.length > MAX_IMPORT_ACCOUNTS) throw new RangeError(`每批最多导入 ${MAX_IMPORT_ACCOUNTS} 个账号`)\n const records = listAccounts()\n const ids = new Set(records.map((r) => r.id))\n const tokens = new Map(records.map((r) => [r.token, r]))\n let imported = 0\n let updated = 0\n let skipped = 0\n for (const raw of list) {\n const candidate = normalizeRecord(raw)\n if (!candidate) {\n skipped += 1\n continue\n }\n const matched = tokens.get(candidate.token)\n let record: AccountRecord\n if (matched) {\n // 保留本地元信息(备注名、探活、限制状态),只替换凭证\n record = { ...matched, ...candidate, id: matched.id, label: candidate.label ?? matched.label }\n updated += 1\n } else {\n let id: string\n do {\n id = newAccountId()\n } while (ids.has(id))\n ids.add(id)\n record = { ...candidate, id }\n imported += 1\n }\n saveAccount(record)\n tokens.set(record.token, record)\n }\n // 一个账号都没有时,把导入进来的第一个设为当前(否则导入了却\"未选择账号\",很莫名其妙)\n if (!activeAccountId()) {\n const first = listAccounts()[0]\n if (first) setActiveAccount(first.id)\n }\n return { imported, updated, skipped }\n}\n\n/**\n * 迁移指针:最近一次迁移是否**完整**完成(失败原因留给调用方记日志)。\n *\n * 为什么需要它(审计 F21):以前迁移把旧文件 `rename` 成 `.migrated-<时间戳>` **留档**,\n * 于是\"退出登录\"删掉的是账号库里的记录,而那份**明文旧凭证**还躺在磁盘上、照样能登录 ——\n * \"已登出\"就成了一句谎话。现在成功迁移后**删掉源文件**,失败必须能被看见,不能静默。\n */\nlet lastMigrationError: string | undefined\n\n/** 最近一次迁移的失败原因(undefined = 没有失败)。 */\nexport function legacyMigrationError(): string | undefined {\n return lastMigrationError\n}\n\n/**\n * 一次性迁移:把 0.1.25 及以前的单账号文件搬进账号库。\n *\n * 旧文件在**确认新记录已落盘**之后被删除(不再留 `.migrated-*` 明文副本)。\n * 返回迁移出的账号(没有则 undefined);写入校验失败会抛错,由调用方记录。\n */\nexport function migrateLegacyAuth(): AccountRecord | undefined {\n const legacy = legacyAuthFilePath()\n const record = normalizeRecord(readJson(legacy))\n if (!record) return undefined\n // 已经在库里(同 token)就不重复导入\n const existing = listAccounts().find((item) => item.token === record.token)\n const saved = existing ?? { ...record, id: newAccountId() }\n if (!existing) saveAccount(saved)\n // 先确认这次迁移真的落盘了,再删源文件 —— 顺序反了就会丢凭证\n const verified = readAccount(saved.id)\n if (!verified || verified.token !== record.token) {\n throw new Error('迁移写入验证失败:账号库里的记录与旧凭证不一致')\n }\n rmSync(legacy)\n lastMigrationError = undefined\n if (!activeAccountId()) setActiveAccount(saved.id)\n return saved\n}\n\n/** 迁移只在\"库为空 且 旧文件在\"时跑一次。 */\nexport function migrateLegacyAuthIfNeeded(): AccountRecord | undefined {\n try {\n if (listAccounts().length > 0) return undefined\n if (!existsSync(legacyAuthFilePath())) return undefined\n return migrateLegacyAuth()\n } catch (error: any) {\n // 不静默:旧凭证可能还在磁盘上,调用方要把这句记进日志/界面\n lastMigrationError = `旧凭证未清除,迁移未完整完成:${error?.message ?? error}`\n return undefined\n }\n}\n\n\n/** 账号展示名:备注名优先,其次掩码账号,再次 id。 */\nexport function accountTitle(record: AccountRecord, mask: (raw: string) => string): string {\n if (record.label) return record.label\n const display = record.user?.display || record.user?.id || ''\n // 一个名字都没拿到时(刚捕获、还没校验过),别把**内部 id**(`acc_cd8e05ec`)当名字摆出来 ——\n // 用户看到一串 hex 只会以为是 bug(实测反馈)。写明\"未识别\",并留一小截后缀,\n // 这样多个未识别账号之间还能区分。\n if (!display) return `未识别账号(${record.id.replace(/^acc_/, '').slice(0, 8)})`\n return mask(display)\n}\n\n/** 库文件体积(供界面提示\"账号库占用\",也便于发现异常膨胀)。 */\nexport function accountsFootprint(): { count: number; bytes: number } {\n let bytes = 0\n let count = 0\n try {\n for (const name of readdirSync(accountsDir())) {\n if (!name.endsWith('.json') || name.includes('.tmp-')) continue\n count += 1\n try {\n bytes += statSync(join(accountsDir(), name)).size\n } catch {}\n }\n } catch {}\n return { count, bytes }\n}\n","/**\n * dsh-deepseek-web-login — 凭证存储与结构化错误。\n *\n * 登录凭证来自 chat.deepseek.com 网页端(浏览器窗口捕获或手动粘贴):\n * Bearer token + cookie + 反爬指纹头(x-hif-*)+ PoW WASM 地址。\n *\n * 存储已从「单个文件」升级成「**账号库**」(见 accounts.ts):本文件只保留\n * 类型 + 存取门面 —— `readAuth()` 取当前生效的账号、`writeAuth()` 写入并设为当前、\n * `clearAuth()` 移除当前账号。这样适配器、登录流程、诊断等**所有调用点一行都不用改**。\n *\n * 位置:`${DSH_HOME || ~/.dsh}/web-login/accounts/<id>.json`(插件自治,\n * 不进 settings/credentials 缝合口,避免敏感凭据落入通用配置面)。\n */\nimport { existsSync } from 'node:fs'\nimport type { CookieMeta } from './cookies.ts'\nimport {\n activeAccount,\n clearActiveAccount,\n readAccount,\n removeAccount,\n setActiveAccount,\n updateAccount,\n upsertAccount,\n} from './accounts.ts'\nimport { resolveDshHome } from './paths.ts'\n\n// resolveDshHome 搬去了 paths.ts(那是为了避开 auth 与 accounts 的 import 环)。\n// 这里原样再导出:外部既有代码(transport.ts / net-diagnostics.ts)不用改。\nexport { resolveDshHome }\n\n/** 一份已捕获的网页端登录凭证。 */\nexport interface WebAuth {\n /** chat.deepseek.com 的 Bearer token(网页端 localStorage userToken)。 */\n token: string\n /** deepseek.com 域 cookie 串(name=value; ...)。 */\n cookie: string\n /** 反爬指纹头(捕获自浏览器请求;缺省为空串)。 */\n hifDliq: string\n hifLeim: string\n /** PoW 求解器 WASM 地址(来自页面资源;缺省用已知默认)。 */\n wasmUrl: string\n /** 登录会话的浏览器 UA(补齐请求指纹)。 */\n userAgent: string\n /**\n * 浏览器真实请求头快照(accept-language / x-client-version 等,随网页端版本走)。\n * 复用它可让插件请求与网页端保持一致,避免硬编码版本号随 DeepSeek 升级失效。\n */\n extraHeaders?: Record<string, string>\n /** 捕获时间(ISO)。 */\n capturedAt: string\n /** 是否未经服务端校验(fail-open 落盘的凭证)。 */\n unverified?: boolean\n /** 已掩码的账号展示信息(可选)。 */\n user?: { id?: string; display?: string }\n /**\n * 捕获时顺手记下的 cookie 过期构成(可选)。\n *\n * 用途只有一个:把\"登录态到底还能撑多久\"从**完全不可观察**变成**至少能看一半**。\n * ⚠️ 别把它当登录态寿命来读 —— 实测真正鉴权用的是 `token`:\n * 只发 token 不带 cookie 能通过,只发 cookie 不带 token 直接被拒。\n * 所以这里面最晚的到期时间**只是浏览器侧的上界**,不是凭证失效时间。\n *\n * 老记录 / 手动粘 token 的账号没有这个字段(界面会说明\"未记录\")。\n */\n cookieMeta?: CookieMeta[]\n /**\n * 捕获时发现的「信息偏少」提示(可选;**不落进账号库**,只用于日志与诊断)。\n *\n * 存在的意义:把「这条凭证可能不完整」从\"事后翻 JSON 才能发现\"变成\"捕获当场就留了记录\"。\n * ⚠️ **它不是错误,也不代表不可用** —— 实测鉴权只用 token(见上面 cookieMeta 的说明),\n * 缺 cookie 本身是合法的。所以这里只记事实,**不做拦截、不标红、不改账号记录的标记**。\n */\n captureWarning?: string\n}\n\n/**\n * 捕获到的凭证有没有「信息偏少」的迹象。返回 undefined = 没发现。\n *\n * 判据刻意保守:**cookie 与 extraHeaders 同时为空**才算 ——\n * 不把\"cookie 为空\"单独当缺陷,因为鉴权只用 token(见 WebAuth.cookieMeta 的实测结论),\n * 手工粘 token 的账号本来就没有 cookie。两者同时为空,才说明\"这次捕获几乎什么都没抄到\"。\n *\n * 2026-09-21 的现场:`acc_2df7cf2f` 正是 cookie 与 extraHeaders 双空,\n * 且它 `capturedAt` 刷新后 19 秒就撞了 `code 9 / invalid ref file id`。\n * ⚠️ 但这只是**时间相关**,不是已证实的因果 —— 所以这里只留痕,不据此阻断。\n */\nexport function captureDefect(auth: Pick<WebAuth, 'token' | 'cookie' | 'extraHeaders'>): string | undefined {\n if (!String(auth.token ?? '').trim()) return undefined\n const hasCookie = !!String(auth.cookie ?? '').trim()\n const hasHeaders = !!auth.extraHeaders && Object.keys(auth.extraHeaders).length > 0\n if (hasCookie || hasHeaders) return undefined\n return '本次捕获只拿到 token(cookie 与请求头都为空)—— 若之后出现图片引用被拒(code 9)之类的异常,优先怀疑这份凭证'\n}\n\n/** 当前生效的登录凭证(没有选择账号 → undefined)。 */\nexport function readAuth(): WebAuth | undefined {\n return activeAccount()\n}\n\n/**\n * 写入/更新凭证。\n *\n * 语义:**写进去的那个就是接下来要用的那个** —— 所有调用点(浏览器捕获、\n * 手动粘 token、登录流程回填账号信息)表达的都是这个意思,所以这里顺带把它设为当前账号。\n * 同一个账号重复写入会**更新原记录**(按 serverId / token 去重,见 accounts.upsertAccount)。\n */\nexport function writeAuth(auth: WebAuth): void {\n const record = upsertAccount(auth)\n setActiveAccount(record.id)\n}\n\n\n/**\n * 把「**可信校验**得到的身份」归一进凭证:`user.id` → `serverId`(去重键)。\n *\n * ⚠️ 为什么必须有这一步(审计 F04):库里按 `serverId` 去重(同账号重新登录 → 更新而不是新增),\n * 但 `user.id` 原本只在登录时被塞进 `user` 字段、**从没写进 `serverId`** ——\n * 于是 token 一刷新,去重键就失效,同一个号在库里堆成好几条:\n * 真实登录路径上「两级去重」等于没接上(既有测试是手工传 `serverId` 才通过的)。\n *\n * 只在**服务端校验返回了身份**之后调用(浏览器捕获、分区恢复、手动 token、/status、探活)。\n * 备份文件自报的 id 不在这里采信 —— 那条路径由 `importAccounts` 单独把关。\n */\nexport function withVerifiedIdentity(\n auth: WebAuth,\n user: { id?: string; display?: string } | undefined,\n): WebAuth & { serverId?: string } {\n const display = typeof user?.display === 'string' && user.display ? user.display : undefined\n const serverId = typeof user?.id === 'string' && user.id ? user.id : undefined\n return {\n ...auth,\n unverified: undefined,\n ...(display || serverId ? { user: { ...auth.user, ...(display ? { display } : {}), ...(serverId ? { id: serverId } : {}) } } : {}),\n ...(serverId ? { serverId } : {}),\n }\n}\n\n/**\n * 已经在库里的记录:用**可信校验**的结果刷新它的元信息与身份键。\n *\n * 与 `withVerifiedIdentity` 的分工:那个用于「凭证还没落库/正在落库」,\n * 这个用于「记录已经在库里」(例如 `/status` 的迟到校验、探活)。\n * 两者都**不切换当前账号**、**不会新建记录** —— 审计 F05/N02 与 F04 是同一条纪律。\n *\n * 返回是否真的更新了(记录不存在、或 token 已变 → 什么都不做)。\n */\nexport function refreshVerifiedIdentity(\n id: string,\n token: string,\n user: { id?: string; display?: string } | undefined,\n): boolean {\n const record = readAccount(id)\n if (!record || record.token !== token) return false\n const display = typeof user?.display === 'string' && user.display ? user.display : undefined\n const serverId = typeof user?.id === 'string' && user.id ? user.id : undefined\n updateAccount(id, {\n ...(display || serverId\n ? { user: { ...record.user, ...(display ? { display } : {}), ...(serverId ? { id: serverId } : {}) } }\n : {}),\n ...(serverId ? { serverId } : {}),\n unverified: undefined,\n lastVerifiedAt: new Date().toISOString(),\n lastVerifyError: undefined,\n })\n return true\n}\n\n/**\n * 退出登录:把当前账号从库里**移除**(并清掉当前指针)。\n *\n * 库里还有其它账号时**刻意不自动切换**:自动换号会让人以为\"我只是登出了,\n * 怎么又用上另一个号了\"。界面会提示\"账号库里还有 N 个,点「切换」即可使用\",\n * 由人明确选择。\n */\nexport function clearAuth(): void {\n const active = activeAccount()\n if (active) removeAccount(active.id)\n clearActiveAccount()\n}\n\nexport function hasUsableAuth(auth: WebAuth | undefined): auth is WebAuth {\n return !!auth && typeof auth.token === 'string' && auth.token.length > 8\n}\n\n/**\n * 一条凭证校验失败的说明,是不是**授权类**失败(token 无效 / 过期)。\n *\n * 为什么必须分类(0.1.80):`lastVerifyError` 里既有授权类失败,也有网络抖动\n * (断网、超时、上游 5xx、429)。只有授权类才意味着\"这份凭证真的不能用了\" ——\n * 把网络抖动也当成账号死了,**一次断网就会把健康账号锁住**。\n * 所以这里用**白名单**:认不出来一律当\"不是授权失败\"(放行)。\n * 代价不对等:漏拦只是白跑一轮请求(下面还有 AUTH 兜底),误拦是功能坏了。\n *\n * 实测会出现的几种失败串(见 probe.ts / index.ts 的写入点):\n * · `Authorization Failed (invalid token)`(401 响应体)\n * · `users/current HTTP 401` / `HTTP 403`\n * · `DeepSeek 网页授权失败:… —— 登录态已过期或无效,请…重新登录`\n * 反例(**必须放行**):`fetch failed`、`The operation was aborted due to timeout`、\n * `users/current HTTP 500`、`users/current HTTP 429`。\n */\nexport function isAuthFailureMessage(message: unknown): boolean {\n const text = String(message ?? '')\n if (!text) return false\n if (/authorization failed|invalid token|unauthori[sz]ed/i.test(text)) return true\n if (/授权失败/.test(text)) return true\n if (/登录(态|状态)?[^。;;]{0,8}(过期|失效|无效)/.test(text)) return true\n // 明确的状态码形态:只认 \"HTTP 401/403\",不认裸数字(免得把端口号之类认成授权失败)\n return /HTTP\\s*40[13]\\b/i.test(text)\n}\n\n/**\n * 账号此刻是不是处于「已知授权失效」状态;是则返回那条失败说明。\n *\n * 判据与探活保持一致(见 probe.ts 的 lastProbeFailed):**失败时间晚于成功时间**\n * 才算\"当前处于失败态\" —— 只看有没有 `lastVerifyError` 会把\"失败过、后来成功了\"\n * 的账号误判成死的。\n *\n * ⚠️ `lastVerifiedAt` 缺省时用 `''` 参与比较:ISO 串与空串比必然为真(\"2\" > \"\"),\n * 这正是想要的 —— 从没成功过 + 有授权类失败 ⇒ 就是失效。别改成 `?? 'undefined'`,\n * 那个字符串以 'u' 开头,会把所有比较都判成假(这类比较的经典陷阱)。\n *\n * 为什么要做这件事(0.1.80,2026-09-21 现场):探活在 22:42 就判定了 token 失效,\n * 但**请求路径没人看这块牌子** ⇒ 22:50 还拿它去跑,14 张图逐个走一次 POW + 一次上传\n * = 28 次注定失败的请求,直到撞 AUTH 才停。判据本身早就写好了(lastProbeFailed),\n * 只是没有人调用它。\n */\nexport function staleAuthRecord(\n record:\n | { lastVerifyError?: { at?: unknown; message?: unknown } | null; lastVerifiedAt?: unknown }\n | undefined,\n): string | undefined {\n const failure = record?.lastVerifyError\n if (!failure) return undefined\n if (!(String(failure.at ?? '') > String(record?.lastVerifiedAt ?? ''))) return undefined\n if (!isAuthFailureMessage(failure.message)) return undefined\n const message = String(failure.message ?? '').trim()\n return message || '登录态已失效'\n}\n\n/**\n * 解包页面读回的 token(兼容裸字符串与 AppKit 包装 JSON)。\n *\n * ⚠️ 两个必须守住的边界(都是实测形态):\n * - 未登录时网页端返回的是 `{\"value\":null,\"__version\":\"0\"}` → 必须得到**空串**,\n * 绝不能把字符串 \"null\" 当 token(否则会拿垃圾 token 去请求,报 40003 让人一头雾水)。\n * - 旧版本网页端存的是裸 token 字符串 → 原样返回。\n */\nexport function unwrapStoredToken(raw: unknown): string {\n const text = String(raw ?? '').trim()\n if (!text) return ''\n if (text.startsWith('{')) {\n try {\n const parsed = JSON.parse(text)\n return typeof parsed?.value === 'string' ? parsed.value.trim() : ''\n } catch {\n return ''\n }\n }\n // 兜底:字面量 \"null\"/\"undefined\" 一律视为空\n return text === 'null' || text === 'undefined' ? '' : text\n}\n\n/** 掩码账号标识(保留可辨识部分,足以确认「是哪个号」而不泄露全量)。 */\nexport function maskIdentifier(raw: string): string {\n const value = String(raw || '').trim()\n if (!value) return ''\n // 幂等:网页端返回的 `user.display` **本身就是屏蔽过的**(形如 `192******27`、\n // `lidi*********+mn1@gmail.com`),记录里存的就是它。若在这里再屏蔽一次,会把\n // 「能区分两个号」的那截尾巴抹掉 —— 实测两个 Gmail 账号都变成 `lid***@gmail.com`,\n // 看着像同一个号,恰好违背本函数「保留可辨识部分」的目的。真账号标识里不会有连续三星号。\n if (value.includes('***')) return value\n const at = value.indexOf('@')\n if (at > 0) {\n const local = value.slice(0, at)\n const keep = Math.min(3, Math.max(1, local.length - 1))\n return `${local.slice(0, keep)}***${value.slice(at)}`\n }\n if (/^\\d{6,}$/.test(value)) return `${value.slice(0, 3)}****${value.slice(-4)}`\n if (value.length <= 4) return `${value[0]}***`\n return `${value.slice(0, 3)}***${value.slice(-2)}`\n}\n\n/**\n * 适配器边界错误。自带 `failure` 与 `code` 自有数据属性 ——\n * LlmRuntime.normalizeLlmFailure 通过自有属性(而非 instanceof)读取结构化\n * 失败信息,因此跨模块边界的自包含打包也能携带 code/status/retryAfter。\n */\nexport class AdapterLlmError extends Error {\n readonly failure: { message: string; code: string; status?: number; providerRetryAfterMs?: number }\n readonly code: string\n /**\n * 账号级限制的**解除时间**(毫秒时间戳),仅在 `user is muted` 时有值。\n *\n * 为什么要单独带一个字段:`providerRetryAfterMs` 是相对值(给重试策略用的),\n * 而\"记下这个账号被限到什么时候\"需要绝对值。让调用方去解析错误文案里的时间是不可靠的。\n */\n readonly mutedUntilMs?: number\n\n constructor(\n message: string,\n code: string,\n options: { status?: number; providerRetryAfterMs?: number; mutedUntilMs?: number; cause?: unknown } = {},\n ) {\n super(message)\n this.name = 'LlmError'\n this.code = code\n if (options.mutedUntilMs !== undefined) this.mutedUntilMs = options.mutedUntilMs\n this.failure = {\n message,\n code,\n ...(options.status !== undefined ? { status: options.status } : {}),\n ...(options.providerRetryAfterMs !== undefined ? { providerRetryAfterMs: options.providerRetryAfterMs } : {}),\n }\n // 原型链上设置 cause(Node 22 支持 options.cause,但为稳妥手动挂)\n if (options.cause !== undefined) (this as any).cause = options.cause\n }\n}\n\n/** 把 HTTP 状态映射为稳定错误码(对齐 dsh-llm 默认可重试码表:SERVER/RATE_LIMIT/TIMEOUT/TRANSPORT)。 */\nexport function httpErrorCode(status: number): string {\n if (status === 401 || status === 403) return 'AUTH'\n if (status === 429) return 'RATE_LIMIT'\n if (status === 402) return 'QUOTA'\n if (status >= 500) return 'SERVER'\n return 'PROVIDER_ERROR'\n}\n\n/** 解析 Retry-After(秒数或 HTTP-date),返回毫秒。 */\nexport function parseRetryAfterMs(raw: string | null | undefined): number | undefined {\n if (!raw) return undefined\n const text = String(raw).trim()\n if (/^\\d+$/.test(text)) return Math.max(1000, Number(text) * 1000)\n const parsed = Date.parse(text)\n if (!Number.isNaN(parsed)) return Math.max(1000, parsed - Date.now())\n return undefined\n}\n\nexport function existsFile(path: string): boolean {\n try {\n return existsSync(path)\n } catch {\n return false\n }\n}\n","/**\n * 请求闸门(request gate)—— 限制「同一账号上同时在飞的网页端请求」。\n *\n * 为什么需要它(2026-09-12 实测):\n * 从插件日志反推每次调用的起止时间,272 轮里发现 **16 对时间重叠**,\n * 特征非常清楚:一方是主回答(数百字、6~40 秒),另一方**只有 8~17 个字、耗时 1~3 秒**\n * —— 那是 DSH 的**会话标题生成**(`options.purpose === 'session-title'`)。\n * 也就是说,你还在等回答的时候,DSH 已经又往同一个账号发了一个短请求。\n *\n * 网页端同一账号**同时只能生成一条**,并发生成会被拒(`A message is being generated…`),\n * 更严重的是实测:双窗口并发生成不到 6 分钟就触发账号级限制(mute 1 天)。\n * 所以「并发」不是能白拿的吞吐,而是要主动规避的风险源。\n *\n * 两道约束:\n * 1. `allowConcurrent === false`(默认):**串行**,同一时刻只放行一个调用,其余排队(FIFO)。\n * 2. `minIntervalMs`:两次调用**之间**至少间隔这么久(按上一次「结束」时间算),\n * 把请求密度压下来 —— 这是防风控真正起作用的那一项。\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\n/**\n * 推荐的调用间隔:**随机区间 2~4 秒**(下限 / 上限)。\n *\n * 为什么是区间而不是固定值:固定间隔的方差≈0,在统计上就是「定时器特征」;\n * 人的操作间隔是有方差的。同类项目 cuckoo-code(从未被风控)用的正是 2000~4000ms 随机区间。\n */\nexport const DEFAULT_MIN_REQUEST_INTERVAL_MS = 2_000\nexport const DEFAULT_MAX_REQUEST_INTERVAL_MS = 4_000\n\n/**\n * 长任务保护:连续跑这么多次之后,强制长休一次。0 = 关闭。\n *\n * 为什么需要它:间隔只管\"两次之间空多久\",管不了\"一刻不停跑了多久\"。\n * 实测 2026-09-12 一个 SSH 插件开发任务,12 分钟里发了约 70 次请求(大量是只调工具、\n * 不说话的轮次),间隔设到 2~4 秒仍然全程零停顿 —— 那种形态比间隔大小更像脚本,\n * 当天该账号两次被临时限制(第二次长达 3 天)。\n */\nexport const DEFAULT_LONG_RUN_THRESHOLD = 15\n/**\n * 许可看门狗阈值(R3):宿主丢弃 generator(不再驱动也不 return)时,许可的 release 永远不会被调,\n * tail 永久卡死 —— 串行模式下此后所有请求排队不响应,只能重启 DSH。\n * 持有超过这个时长仍未释放的许可,会在下一次 acquire 时被强制收回并告警。\n * 15 分钟 >> 合法的长 turn(批量上传 90 张图约 7 分钟 + 生成几分钟),不会误杀。\n */\nexport const DEFAULT_LEASE_WATCHDOG_MS = 15 * 60_000\n/** 长休时长区间(1~3 分钟)。 */\nexport const DEFAULT_LONG_RUN_BREAK_MS: CleanupRange = { min: 60_000, max: 180_000 }\n/** 长休区间的合法范围(30 秒 ~ 10 分钟)。 */\nexport const LONG_RUN_BREAK_BOUNDS_MS: CleanupRange = { min: 30_000, max: 600_000 }\n/** 长休阈值的合法范围(0 = 关闭,上限 100 次)。 */\nexport const LONG_RUN_THRESHOLD_BOUNDS = { min: 0, max: 100 }\n\n/**\n * 每次请求发送的 prompt **字符上限**。\n *\n * 为什么它是防风的头号阀门(2026-09-13 实测):网页 API 无状态,**每一轮都要把整段转写重发**,\n * 所以转写越长、单次请求越贵。同一个会话里单次输入估算从 9.7k token 涨到 **293k**,\n * 180 次请求累计约 **2900 万 token**(这是我们自己的估算值,不是服务端账单,\n * 但\"每次都在重发历史\"是代码事实)。四个账号在两天内陆续被限制,体量是主要嫌疑。\n *\n * 边界取值理由:\n * - 上限就取**原来的默认值 150 万**:再大就有撑爆 1M 上下文的风险\n * (纯中文 150 万字符 ≈ 100 万 token)。\n * - 下限取**更早的默认值 12 万**:那是长期在用的值,说明这个量级还能干活(工具目录占约 5.6 万)。\n */\nexport const MAX_PROMPT_CHARS_BOUNDS = { min: 120_000, max: 1_500_000 } as const\n\n/**\n * **默认**上限 —— 0.1.76 起由 150 万降到 **40 万**。\n *\n * 它同时是一个**风控阀门**:网页端无状态,每一轮都要把整段转写重发,所以这个数字直接决定\n * 单次请求的体量。会话内实测单次输入从 9.7k token 一路涨到 293k,而 150 万字符(纯中文\n * ≈100 万 token)意味着默认就允许\"一次顶满 1M 上下文\" —— 四个账号两天内陆续被限制,\n * 体量是主要嫌疑。40 万 ≈27 万 token,够跑长任务,又不会让默认配置本身贴着天花板。\n *\n * ⚠️ 可调范围不变(见上面的 BOUNDS):确实需要更长的转写可以自己往上调,\n * 但要知道那是在拿账号的稳定换更长的记忆 —— 面板上那个旋钮的说明写了同一件事。\n */\nexport const DEFAULT_MAX_PROMPT_CHARS = 400_000\n\n/** 规整 prompt 字符上限:非数 → 默认;越界 → 夹到边界。 */\nexport function clampMaxPromptChars(value: number): number {\n if (!Number.isFinite(value)) return DEFAULT_MAX_PROMPT_CHARS\n return Math.max(MAX_PROMPT_CHARS_BOUNDS.min, Math.min(MAX_PROMPT_CHARS_BOUNDS.max, Math.round(value)))\n}\n\n/**\n * 一次请求最多带多少张图片(请求体里 `ref_file_ids` 的长度)。\n *\n * 为什么默认 24(2026-09-19 群友实测报告):网页端对这一批引用的数量有上限 ——\n * 最后一次成功是 40 张、第一次失败是 52 张,真值落在 (40, 52]。越过之后\n * `biz_code 10 / too many ref file` 会让**该会话此后每一轮都失败**(图还留在历史里,\n * 每轮重发都超标),用户唯一出路是丢掉整个会话。24 给已知安全线留了 16 张余量。\n *\n * `0` = 不限制 —— 留着这个取值只是给\"确实需要\"的人,但**别设**:那等于把 code 10 放回来。\n */\nexport const MAX_REF_IMAGES_BOUNDS = { min: 0, max: 100 } as const\nexport const DEFAULT_MAX_REF_IMAGES = 24\n\n/** 规整图片数量上限:非数 → 默认;越界 → 夹到边界。 */\nexport function clampMaxRefImages(value: number): number {\n if (!Number.isFinite(value)) return DEFAULT_MAX_REF_IMAGES\n return Math.max(MAX_REF_IMAGES_BOUNDS.min, Math.min(MAX_REF_IMAGES_BOUNDS.max, Math.round(value)))\n}\n\n/** 距上次请求超过这么久就算\"歇过了\",连续计数归零。 */\nconst LONG_RUN_IDLE_RESET_MS = 120_000\n\n/** 间隔可选的推荐档位(设置页的快捷按钮用):[下限, 上限]。 */\nexport const INTERVAL_PRESETS = [\n [1_500, 2_500],\n [2_000, 4_000],\n [5_000, 9_000],\n] as const\n/** 设置页滑块的取值上限。 */\nexport const MAX_INTERVAL_MS = 30_000\n\n// ── 会话清理的三个区间 ────────────────────────────────────────────────────\n//\n// 为什么这三个参数也要\"上下限 + 随机\":它们原来都是**固定值**(攒 8 个 / 等 90 秒 /\n// 逐个删时中间没有间隔)。固定值本身就是一种机器特征 —— 每次都攒到第 8 个就动手、\n// 每次都等 90 秒、删除请求像连发。真人不会这么精确。\n//\n// 默认值都围绕原来的固定值取一个区间(均值≈旧值),所以行为上没有突变,\n// 只是把\"精确\"换成了\"有方差\"。\n/** 一个可随机取值的区间(闭区间,单位由字段决定)。 */\nexport interface CleanupRange {\n min: number\n max: number\n}\n\n/** 攒够几个:默认 6~10(均值 8,等于旧默认)。 */\nexport const DEFAULT_CLEANUP_BATCH: CleanupRange = { min: 6, max: 10 }\n/** 从第一个会话入队起最多等多久:默认 60~120 秒(均值 90s,等于旧默认)。 */\nexport const DEFAULT_CLEANUP_DELAY_MS: CleanupRange = { min: 60_000, max: 120_000 }\n/** 两次删除之间的间隔:默认 0.8~2.5 秒。\n * 新增项 —— 批量删除不被服务端接受时会退化成\"逐个删\",原来那串请求中间**没有间隔**。 */\nexport const DEFAULT_CLEANUP_GAP_MS: CleanupRange = { min: 800, max: 2_500 }\n\n/** 各区间允许被设置到的范围(设置页滑块也按这个画)。 */\nexport const CLEANUP_BATCH_BOUNDS = { min: 1, max: 50 }\nexport const CLEANUP_DELAY_BOUNDS_MS = { min: 5_000, max: 600_000 }\nexport const CLEANUP_GAP_BOUNDS_MS = { min: 0, max: 60_000 }\n\n/**\n * 把任意输入规整成一个合法区间:非数忽略、按 bounds 夹住、**上下限颠倒时自动交换**。\n * 返回 undefined 表示\"这个字段不合法、当没给\"(调用方回落到默认)。\n */\nexport function normalizeCleanupRange(\n value: unknown,\n bounds: { min: number; max: number },\n): CleanupRange | undefined {\n if (!value || typeof value !== 'object') return undefined\n const raw = value as { min?: unknown; max?: unknown }\n const lo = Number(raw.min)\n const hi = Number(raw.max)\n if (!Number.isFinite(lo) || !Number.isFinite(hi)) return undefined\n const clamp = (n: number) => Math.min(bounds.max, Math.max(bounds.min, Math.floor(n)))\n // 用户把两个滑块拖反了不该报错,也不该让\"下限 > 上限\"这种状态活下去\n return { min: clamp(Math.min(lo, hi)), max: clamp(Math.max(lo, hi)) }\n}\n\nexport interface GateSettings {\n allowConcurrent: boolean\n /** 长任务保护:连续多少次请求后强制长休(0 = 关闭)。 */\n longRunThreshold: number\n /** 长休时长区间(毫秒);未设置时用内置默认。 */\n longRunBreakMs?: CleanupRange\n /** 间隔下限(毫秒)。与上限相等时退化为固定间隔。 */\n minRequestIntervalMs: number\n /** 间隔上限(毫秒)。实际等待在 [下限, 上限] 之间**随机**取值。 */\n maxRequestIntervalMs: number\n /**\n * 临时会话清理策略。它不参与节流逻辑,只是**搭同一份设置文件与同一个设置页**存储,\n * 实际执行在 webapi.ts 的 createSessionCleaner(类型放宽为字面量,避免循环依赖)。\n */\n sessionCleanup?: 'immediate' | 'deferred' | 'keep'\n /** deferred:攒够几个(个)。实际阈值**每轮清理重新随机抽**。 */\n cleanupBatch?: CleanupRange\n /** deferred:从第一个会话入队起最多等多久(毫秒)。每轮重新随机抽。 */\n cleanupDelayMs?: CleanupRange\n /** 两次删除之间的间隔(毫秒)。每次删除前重新随机抽。 */\n cleanupGapMs?: CleanupRange\n /**\n * 每次请求发送的 prompt 字符上限。**不参与节流逻辑** —— 和 sessionCleanup 一样,\n * 只是搭同一份设置文件(gate.json)与同一个设置页存储,真正的执行方是 adapter。\n */\n maxPromptChars?: number\n /** 一次请求最多带多少张图片(`ref_file_ids` 的长度;0 = 不限制)。见 MAX_REF_IMAGES_BOUNDS。 */\n maxRefImages?: number\n}\n\n/** 节流设置文件:`${DSH_HOME || ~/.dsh}/web-login/gate.json`(插件自治,与凭证同目录)。 */\nexport function gateSettingsPath(): string {\n const home = process.env.DSH_HOME || join(homedir(), '.dsh')\n return join(home, 'web-login', 'gate.json')\n}\n\n/**\n * 读设置页保存过的值。文件不存在/损坏都返回 undefined(回落到 cordis config)。\n * 优先级:**设置页(文件)> cordis config > 内置默认** —— 设置页是用户的显式操作,\n * 不该被配置文件里的旧值盖掉。\n */\nexport function readGateSettings(): Partial<GateSettings> | undefined {\n try {\n const file = gateSettingsPath()\n if (!existsSync(file)) return undefined\n const parsed = JSON.parse(readFileSync(file, 'utf8'))\n const out: Partial<GateSettings> = {}\n if (typeof parsed?.allowConcurrent === 'boolean') out.allowConcurrent = parsed.allowConcurrent\n if (Number.isFinite(parsed?.minRequestIntervalMs)) {\n out.minRequestIntervalMs = clampInterval(Number(parsed.minRequestIntervalMs))\n }\n if (Number.isFinite(parsed?.maxRequestIntervalMs)) {\n out.maxRequestIntervalMs = clampInterval(Number(parsed.maxRequestIntervalMs))\n }\n // 老配置(0.1.20 只存了 min)= 固定间隔语义:上限跟随下限\n if (out.minRequestIntervalMs !== undefined && out.maxRequestIntervalMs === undefined) {\n out.maxRequestIntervalMs = out.minRequestIntervalMs\n }\n const cleanup = parsed?.sessionCleanup\n if (cleanup === 'immediate' || cleanup === 'deferred' || cleanup === 'keep') {\n out.sessionCleanup = cleanup\n }\n const batch = normalizeCleanupRange(parsed?.cleanupBatch, CLEANUP_BATCH_BOUNDS)\n if (batch) out.cleanupBatch = batch\n const delay = normalizeCleanupRange(parsed?.cleanupDelayMs, CLEANUP_DELAY_BOUNDS_MS)\n if (delay) out.cleanupDelayMs = delay\n const gap = normalizeCleanupRange(parsed?.cleanupGapMs, CLEANUP_GAP_BOUNDS_MS)\n if (gap) out.cleanupGapMs = gap\n if (Number.isFinite(parsed?.longRunThreshold)) {\n const n = Math.round(Number(parsed.longRunThreshold))\n out.longRunThreshold = Math.max(\n LONG_RUN_THRESHOLD_BOUNDS.min,\n Math.min(LONG_RUN_THRESHOLD_BOUNDS.max, n),\n )\n }\n const lrb = normalizeCleanupRange(parsed?.longRunBreakMs, LONG_RUN_BREAK_BOUNDS_MS)\n if (lrb) out.longRunBreakMs = lrb\n if (Number.isFinite(parsed?.maxPromptChars)) {\n out.maxPromptChars = clampMaxPromptChars(Number(parsed.maxPromptChars))\n }\n if (Number.isFinite(parsed?.maxRefImages)) {\n out.maxRefImages = clampMaxRefImages(Number(parsed.maxRefImages))\n }\n return Object.keys(out).length > 0 ? out : undefined\n } catch {\n return undefined\n }\n}\n\nexport function writeGateSettings(settings: GateSettings): void {\n const file = gateSettingsPath()\n mkdirSync(join(file, '..'), { recursive: true })\n writeFileSync(file, JSON.stringify(settings, null, 2) + '\\n', 'utf8')\n}\n\n/** 把任意输入规整成合法间隔:非数 → 默认,负 → 0,超上限 → 上限。 */\nexport function clampInterval(value: number): number {\n if (!Number.isFinite(value)) return DEFAULT_MIN_REQUEST_INTERVAL_MS\n return Math.min(MAX_INTERVAL_MS, Math.max(0, Math.floor(value)))\n}\n\nexport interface RequestGateOptions {\n /** 允许同一账号并发(默认 false)。开启会恢复「标题与主回答同时发」的高风险行为。 */\n allowConcurrent?: boolean\n /** 两次调用之间的最小间隔(毫秒)。0 = 不限。 */\n minIntervalMs?: number\n /** 间隔上限(毫秒)。缺省且未给 minIntervalMs 时用默认上限;给了 minIntervalMs 则取同值(保持「固定间隔」老语义)。 */\n maxIntervalMs?: number\n /** 长任务保护:连续多少次请求后强制长休一次(0 = 关闭)。缺省 DEFAULT_LONG_RUN_THRESHOLD。 */\n longRunThreshold?: number\n /** 长休时长区间(毫秒)。缺省 DEFAULT_LONG_RUN_BREAK_MS。 */\n longRunBreakMs?: CleanupRange\n /** 随机源(单测注入用)。 */\n random?: () => number\n /**\n * 许可看门狗阈值(毫秒,R3)。缺省 DEFAULT_LEASE_WATCHDOG_MS。\n * 单测注入小值即可确定性触发强制回收。\n */\n leaseWatchdogMs?: number\n logger?: { info?: (msg: string) => void; warn?: (msg: string) => void; debug?: (msg: string) => void }\n /** prompt 字符上限(本模块不执行,只是存下来以便落盘与回显)。 */\n maxPromptChars?: number\n /** 一次请求最多带多少张图片(同上,本模块不执行,只是存下来以便落盘与回显)。 */\n maxRefImages?: number\n /**\n * 会话清理策略(本模块不执行,同样只是存下来以便落盘与回显)。\n *\n * ⚠️ 必须**传进来**(2026-09-14 修):设置页保存时写的是 `settings()` 的返回值,\n * 而这些字段若没初始化就永远是 `undefined` → 保存时被静默丢掉。\n * 后果是实测过的:用户只是调了下请求间隔,清理设置就被从 gate.json 里抹掉,\n * 重启后悄悄回到内置默认 —— 正是「拖了滑块却像没生效」那一类问题。\n */\n sessionCleanup?: GateSettings['sessionCleanup']\n cleanupBatch?: CleanupRange\n cleanupDelayMs?: CleanupRange\n cleanupGapMs?: CleanupRange\n /** 便于单测注入。 */\n now?: () => number\n sleep?: (ms: number) => Promise<void>\n}\n\nexport interface RequestGate {\n /** 取得放行许可;返回的函数必须调用一次(幂等)以释放并让出队首。 */\n /** signal 可在排队/等间隔期间取消(F11);取消时抛 AbortError 且闸门不会锁死。 */\n acquire(label?: string, signal?: AbortSignal): Promise<() => void>\n /** 当前状态(诊断/测试用)。 */\n stats(): { running: number; waiting: number; lastFinishedAt: number }\n /** 读取当前生效的节流设置。 */\n settings(): GateSettings\n /** 运行时改设置(设置页保存后调用);返回改完后的值。 */\n configure(next: Partial<GateSettings>): GateSettings\n}\n\nexport function createRequestGate(options: RequestGateOptions = {}): RequestGate {\n // 可运行时修改(设置页保存后立即生效,不必重启)\n let allowConcurrent = options.allowConcurrent === true\n let longRunThreshold = options.longRunThreshold ?? DEFAULT_LONG_RUN_THRESHOLD\n let longRunBreakMs: CleanupRange | undefined = options.longRunBreakMs\n /** 连续请求计数(长休后、或歇够了之后归零)。 */\n let consecutive = 0\n\n let minIntervalMs = clampInterval(\n options.minIntervalMs ?? (options.maxIntervalMs !== undefined ? options.maxIntervalMs : DEFAULT_MIN_REQUEST_INTERVAL_MS),\n )\n let maxIntervalMs = clampInterval(\n options.maxIntervalMs ?? (options.minIntervalMs !== undefined ? options.minIntervalMs : DEFAULT_MAX_REQUEST_INTERVAL_MS),\n )\n if (maxIntervalMs < minIntervalMs) maxIntervalMs = minIntervalMs\n const random = options.random ?? Math.random\n const now = options.now ?? (() => Date.now())\n // R3 看门狗:最近一次许可的发放时刻与它的 release 回调(仅串行模式跟踪)。\n const leaseWatchdogMs = options.leaseWatchdogMs ?? DEFAULT_LEASE_WATCHDOG_MS\n let leasedAt = 0\n let activeRelease: (() => void) | undefined\n /**\n * 默认等待:把定时器**挂在 promise 上**,好让 `waitOrAbort` 在取消/结算时把它清掉。\n *\n * ⚠️ 0.1.82 的教训:这里**不能**用 `timer.unref()` 来\"让进程不被拖住\" ——\n * 被 `await` 的 promise 一旦失去 ref,事件循环可能直接空转退出,\n * 表现为进程/用例挂在 `await` 上**永不结算**(实测两个用例当场挂死)。\n * 长休确实不该拖住宿主关停,但正确做法是\"取消时 clearTimeout\",不是 unref。\n */\n const sleep =\n options.sleep ??\n ((ms: number) => {\n let timer: ReturnType<typeof setTimeout>\n const promise = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, ms)\n }) as Promise<void> & { timer?: ReturnType<typeof setTimeout> }\n promise.timer = timer!\n return promise\n })\n const logger = options.logger\n\n /** 队尾:每个调用完成后才 resolve,保证 FIFO 且「上一个没结束就不放行下一个」。 */\n let tail: Promise<void> = Promise.resolve()\n let running = 0\n let waiting = 0\n let lastFinishedAt = 0\n /** 是否已经有调用结束过 —— 首次调用不该被间隔规则拖住。 */\n let hasFinished = false\n\n async function acquire(label = 'call', signal?: AbortSignal): Promise<() => void> {\n let releaseMine!: () => void\n const mine = new Promise<void>((resolve) => {\n releaseMine = resolve\n })\n const prev = tail\n tail = prev.then(() => mine)\n\n const aborted = (): Error => {\n const error = new Error(`「${label}」在闸门等待中被取消`)\n error.name = 'AbortError'\n return error\n }\n /** 让等待可被中断:abort 时立刻 reject,不等定时器/前序请求。 */\n const waitOrAbort = (inner: Promise<unknown>): Promise<void> => {\n // 取消或结算之后,别再让那个定时器继续跑(长休最长 180s,会拖住宿主关停)。\n // 注入的 sleep(单测用)没有 `.timer`,这里自然是个空操作。\n const clearInnerTimer = (): void => {\n const timer = (inner as { timer?: ReturnType<typeof setTimeout> })?.timer\n if (timer) clearTimeout(timer)\n }\n if (!signal) return inner.then(() => undefined)\n if (signal.aborted) {\n clearInnerTimer()\n return Promise.reject(aborted())\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n signal.removeEventListener('abort', onAbort)\n clearInnerTimer()\n reject(aborted())\n }\n signal.addEventListener('abort', onAbort, { once: true })\n inner.then(\n () => {\n signal.removeEventListener('abort', onAbort)\n clearInnerTimer()\n resolve()\n },\n (error) => {\n signal.removeEventListener('abort', onAbort)\n reject(error)\n },\n )\n })\n }\n\n // R3 看门狗(0.1.84):宿主丢弃 generator(不再驱动也不 return)时,许可的 release 永远不会被调,\n // tail 永久卡死。检查点放在每次 acquire 进入时 —— 不需要定时器(本项目 unref 有前科),\n // 锁死后用户的下一条消息自然会走到这里解锁。\n // ⚠️ 只在串行模式生效:并发模式不排队(tail 不被 await),且同时活着的许可可能不止一个,\n // 全局跟踪会把刚发的合法许可误判成泄漏。\n if (!allowConcurrent && running > 0 && activeRelease && now() - leasedAt > leaseWatchdogMs) {\n const heldMin = Math.round((now() - leasedAt) / 60000)\n logger?.warn?.(\n `deepseek-web: 闸门许可已持有 ${heldMin} 分钟未释放 —— 通常是宿主丢弃了进行中的流。` +\n '强制释放,以免后续请求永久排队。',\n )\n activeRelease()\n }\n\n waiting += 1\n try {\n if (signal?.aborted) throw aborted()\n // 串行:等前面所有调用结束。并发模式跳过这一步(间隔仍然生效)。\n if (!allowConcurrent) {\n if (running > 0 || waiting > 1) {\n logger?.debug?.(`deepseek-web: 「${label}」排队等待(前面还有 ${running} 个在跑 / ${waiting - 1} 个在等)`)\n }\n await waitOrAbort(prev)\n }\n\n // 长任务保护:先算\"是不是已经连着跑了很久\"。\n // 距上次结束已经过了很久 → 说明人是歇过的,连续计数归零。\n if (hasFinished && now() - lastFinishedAt > LONG_RUN_IDLE_RESET_MS) consecutive = 0\n const breakRange = longRunBreakMs ?? DEFAULT_LONG_RUN_BREAK_MS\n const needsBreak = longRunThreshold > 0 && consecutive > 0 && consecutive >= longRunThreshold\n\n // 间隔:在 [下限, 上限] 之间**随机**取值,按「上一次结束」时刻算\n // (不是上一次开始 —— 否则长回答之后的连环请求仍然很密)。每一次的等待都不同,避免定时器特征。\n const gap = needsBreak ? Math.round(breakRange.min + random() * Math.max(0, breakRange.max - breakRange.min)) : nextGap()\n if (hasFinished && (needsBreak || maxIntervalMs > 0)) {\n const waitMs = lastFinishedAt + gap - now()\n if (needsBreak) {\n logger?.info?.(\n `deepseek-web: 已连续 ${longRunThreshold} 次请求 —— 长休 ${Math.round(gap / 1000)}s 再继续(长任务保护:连续跑比间隔小更像脚本)`,\n )\n }\n if (waitMs > 0) {\n if (!needsBreak) {\n logger?.info?.(\n `deepseek-web: 距上次请求不足 ${gap}ms(区间 ${minIntervalMs}~${maxIntervalMs}),` +\n `等 ${Math.round(waitMs)}ms 再发「${label}」(防账号级限流)`,\n )\n }\n await waitOrAbort(sleep(waitMs))\n }\n }\n\n // 2026-09-13 审计 N08:清计数必须放在**长休真正走完且未被取消之后**。\n // 旧写法在 sleep 之前就 `consecutive = 0`,于是「取消一次长休」等于把休息债务一笔勾销 ——\n // 下一次请求直接绕过长休(把「闸门可取消」的正确修复和长任务保护组合出了旁路)。\n if (signal?.aborted) throw aborted()\n if (needsBreak) consecutive = 0\n\n } catch (error) {\n // 取消(或前序出错)时必须把自己从队列里摘掉:本节点的 mine 一旦不 resolve,\n // 后面排队的请求会在 tail 上永久卡住 —— 闸门就锁死了。\n releaseMine()\n throw error\n } finally {\n waiting -= 1\n }\n\n running += 1\n let released = false\n const release = () => {\n if (released) return\n released = true\n leasedAt = 0\n activeRelease = undefined\n running -= 1\n lastFinishedAt = now()\n hasFinished = true\n consecutive += 1\n releaseMine()\n }\n // R3:记录租约起始,供看门狗检查。\n leasedAt = now()\n activeRelease = release\n return release\n }\n\n /** 本次实际使用的间隔:区间内随机;上下限相等则固定。 */\n function nextGap(): number {\n if (maxIntervalMs <= minIntervalMs) return minIntervalMs\n return Math.round(minIntervalMs + random() * (maxIntervalMs - minIntervalMs))\n }\n\n /** 会话清理策略不在本模块实现,只借用设置文件存储(由宿主读取后交给 cleaner)。 */\n /** prompt 字符上限(同 cleanupMode:只是存着,执行在 adapter)。 */\n let maxPromptChars = clampMaxPromptChars(options.maxPromptChars ?? DEFAULT_MAX_PROMPT_CHARS)\n // 图片数量上限(同 maxPromptChars:只是存着,真正的执行在 adapter 的 uploadRequestImages)。\n let maxRefImages = clampMaxRefImages(options.maxRefImages ?? DEFAULT_MAX_REF_IMAGES)\n let cleanupMode = options.sessionCleanup\n // 会话清理的三个区间(同样不参与节流逻辑)。存在这里是为了**能落盘**:\n // writeGateSettings 写的是 settings() 的返回值,不存就丢。\n // ⚠️ 因此必须**从 options 初始化**(2026-09-14 修):否则保存设置页时,\n // `settings()` 里没有这几个字段 → 用户调个间隔就把清理设置一起冲没了。\n let cleanupBatch = normalizeCleanupRange(options.cleanupBatch, CLEANUP_BATCH_BOUNDS)\n let cleanupDelayMs = normalizeCleanupRange(options.cleanupDelayMs, CLEANUP_DELAY_BOUNDS_MS)\n let cleanupGapMs = normalizeCleanupRange(options.cleanupGapMs, CLEANUP_GAP_BOUNDS_MS)\n\n function settings(): GateSettings {\n return {\n allowConcurrent,\n minRequestIntervalMs: minIntervalMs,\n maxRequestIntervalMs: maxIntervalMs,\n ...(cleanupMode ? { sessionCleanup: cleanupMode } : {}),\n ...(cleanupBatch ? { cleanupBatch } : {}),\n ...(cleanupDelayMs ? { cleanupDelayMs } : {}),\n ...(cleanupGapMs ? { cleanupGapMs } : {}),\n longRunThreshold,\n ...(longRunBreakMs ? { longRunBreakMs } : {}),\n maxPromptChars,\n maxRefImages,\n }\n }\n\n function configure(next: Partial<GateSettings>): GateSettings {\n if (typeof next.allowConcurrent === 'boolean') allowConcurrent = next.allowConcurrent\n if (next.minRequestIntervalMs !== undefined) minIntervalMs = clampInterval(Number(next.minRequestIntervalMs))\n if (next.maxRequestIntervalMs !== undefined) maxIntervalMs = clampInterval(Number(next.maxRequestIntervalMs))\n if (next.sessionCleanup !== undefined) cleanupMode = next.sessionCleanup\n if (next.maxPromptChars !== undefined) maxPromptChars = clampMaxPromptChars(Number(next.maxPromptChars))\n if (next.maxRefImages !== undefined) maxRefImages = clampMaxRefImages(Number(next.maxRefImages))\n // 三个区间:非法的输入直接当\"没给\"(不报错、也不覆盖已有的有效值)\n if (next.cleanupBatch !== undefined) {\n const value = normalizeCleanupRange(next.cleanupBatch, CLEANUP_BATCH_BOUNDS)\n if (value) cleanupBatch = value\n }\n if (next.cleanupDelayMs !== undefined) {\n const value = normalizeCleanupRange(next.cleanupDelayMs, CLEANUP_DELAY_BOUNDS_MS)\n if (value) cleanupDelayMs = value\n }\n if (next.cleanupGapMs !== undefined) {\n const value = normalizeCleanupRange(next.cleanupGapMs, CLEANUP_GAP_BOUNDS_MS)\n if (value) cleanupGapMs = value\n }\n if (next.longRunThreshold !== undefined && Number.isFinite(next.longRunThreshold)) {\n longRunThreshold = Math.max(\n LONG_RUN_THRESHOLD_BOUNDS.min,\n Math.min(LONG_RUN_THRESHOLD_BOUNDS.max, Math.round(next.longRunThreshold)),\n )\n }\n if (next.longRunBreakMs !== undefined) {\n const value = normalizeCleanupRange(next.longRunBreakMs, LONG_RUN_BREAK_BOUNDS_MS)\n if (value) longRunBreakMs = value\n }\n // 设置页两个滑块可能拖出「上限 < 下限」,这里纠正(不报错,直接夹住)\n if (maxIntervalMs < minIntervalMs) maxIntervalMs = minIntervalMs\n logger?.info?.(\n `deepseek-web: 请求节流设置已更新 —— ${allowConcurrent ? '允许并发(不推荐)' : '串行'} · ` +\n `间隔 ${minIntervalMs}~${maxIntervalMs}ms(随机)` +\n (cleanupBatch ? ` · 清理阈值 ${cleanupBatch.min}~${cleanupBatch.max} 个` : '') +\n (cleanupDelayMs\n ? ` · 最长等待 ${Math.round(cleanupDelayMs.min / 1000)}~${Math.round(cleanupDelayMs.max / 1000)}s`\n : '') +\n (cleanupGapMs ? ` · 删除间隔 ${cleanupGapMs.min}~${cleanupGapMs.max}ms` : '') +\n ` · 长任务保护 ${longRunThreshold > 0 ? `每 ${longRunThreshold} 次长休 ${Math.round((longRunBreakMs ?? DEFAULT_LONG_RUN_BREAK_MS).min / 1000)}~${Math.round((longRunBreakMs ?? DEFAULT_LONG_RUN_BREAK_MS).max / 1000)}s` : '关闭'}`,\n )\n return settings()\n }\n\n return {\n acquire,\n stats: () => ({ running, waiting, lastFinishedAt }),\n settings,\n configure,\n }\n}\n","/**\n * 上下文投喂方式 —— 每轮到底给网页端发什么。\n *\n * 两种模式(2026-09-14 加,用户可切换):\n *\n * full 每轮重发全量 prompt(默认)。\n * webapi 每次 completion 都发 `parent_message_id: null`,每条消息都是会话里的\n * 根消息、没有父链 —— 服务端按消息树回溯上下文时回溯到空,**拿不到任何历史**。\n * 所以历史必须由我们自己每轮重发。DSH 的适配器契约本来就是无状态的\n * (每轮把完整 messages 交给我们),这个模式最稳、行为和 0.1.61 及以前完全一致。\n *\n * chained 链式投喂:只发**增量**,并把 `parent_message_id` 指向上一轮 assistant 的\n * message_id,让服务端自己按链维护上下文。\n * 依据(读参考实现 + 抓真实帧,不是推理):浏览器就是这么干的 ——\n * 参考实现里的 `nextParentMessageId = history?.parentMessageId ?? finalAssistantMessageId`\n * `interceptor/request-augmentation.ts` 里 `isFirstMessage = parent_message_id === null`\n * ⇒ 只有会话第一条的 parent 是 null,之后每轮都把上一条消息 id 当 parent 发上去。\n * 本轮 assistant 的 id 来自 SSE 首帧 `event: ready`\n * (`{\"request_message_id\":1,\"response_message_id\":2,...}`,实测样本见\n * `.workbuddy/tmp/shortq-r1-2026-09-14T04-17-41.sse`)。\n *\n * ⚠️ chained 的代价(必须知道,所以默认不开):\n * 模型能看到的工具协议、系统提示、历史,全都在**链首那条消息**里;一旦服务端侧\n * 把早期上下文丢掉(长会话/超窗),模型就没有协议可依 —— 可能直接不按 JSON 发工具调用。\n * 本模块的对策是\"能省则省、一有不确定就退回全量\":见 decideFeed 的判据。\n *\n * 本文件是**纯逻辑 + 一点设置读写**,不依赖 webapi,方便直接测(tests/check-context-feed.mjs)。\n */\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { resolveDshHome } from './auth.ts'\n\nexport type ContextMode = 'full' | 'chained'\n\n/** 默认每轮重发全量 —— 与 0.1.61 及以前的行为一致,不改动既有用户。 */\nexport const DEFAULT_CONTEXT_MODE: ContextMode = 'full'\n\nexport function normalizeContextMode(value: unknown): ContextMode | undefined {\n return value === 'full' || value === 'chained' ? value : undefined\n}\n\n/** 设置页展示用(纯文本,别写 markdown 星号)。 */\nexport const CONTEXT_MODE_HINT =\n '链式投喂:之后每轮只发新增内容,并把上一条回答挂到父消息上,让服务端自己维护上下文 —— ' +\n '请求体小得多、也更像真人连续对话。代价是工具协议只存在于链首那条消息里,' +\n '一旦服务端把早期上下文丢掉,模型可能不按约定格式发工具调用;本插件遇到任何不确定会自动退回全量重发。' +\n '每轮全量:最稳,行为和以前完全一致(网页端会看到每条消息都带着完整提示词)。'\n\n/** 链式投喂的链状态。只在「正在复用的那个会话」上有意义,会话轮换/失败即作废。 */\nexport interface ChainState {\n /** 链首发出去的固定头(系统 + 协议指令 + 工具目录)。头部变了就不能续链。 */\n head: string\n /** 已经发出去的历史条目(**完整**条目,不做过滤)——下一轮用它算增量。 */\n entries: readonly string[]\n /** 上一轮 assistant 的 message_id,作为下一轮的 `parent_message_id`。 */\n parentId: number\n /** 链所属的网页端会话。 */\n sessionId: string\n /** 链所属账号(凭证摘要)。切号后键不同,不能续链。 */\n accountKey: string\n}\n\nexport type FeedReason =\n | 'chained' // 发了增量\n | 'mode-full' // 配置就是全量\n | 'no-parts' // 调用方没给结构化 prompt,算不出增量\n | 'no-chain' // 还没有链(本轮要当链首)\n | 'new-session' // 本轮是新会话(复用的旧链不适用)\n | 'session-changed'\n | 'account-changed'\n | 'head-changed' // 系统提示/工具目录变了 —— 链首那份已经过期\n | 'not-appended' // 历史不是严格追加(被压缩/改写/回退)\n | 'empty-delta' // 没有新增内容(例如同一步的重试)\n | 'delta-too-long' // 增量本身超出预算,不值得为它冒险\n\nexport interface FeedInput {\n mode: ContextMode\n /** 本轮结构化 prompt 的三段:head = 固定头,entries = 历史条目,full = 今天那份字符串。 */\n head?: string\n entries?: readonly string[]\n full: string\n /** 本轮用的网页端会话,以及它是不是复用来的。 */\n sessionId: string\n accountKey: string\n reused: boolean\n /** 当前链(没有则 undefined)。 */\n chain?: ChainState\n /** serializePrompt 用的预算,用来给增量设一个上限。 */\n maxChars?: number\n}\n\nexport interface FeedDecision {\n /** 真正写进请求体的 prompt。 */\n prompt: string\n /** 真正写进请求体的 `parent_message_id`。 */\n parentMessageId: number | null\n /**\n * 本轮结束后(流正常跑完且拿到了 assistant message_id)应该建立/沿用的链。\n * `undefined` = 不建链(全量模式)。\n */\n next: Omit<ChainState, 'parentId'> | undefined\n /** 为什么这么决定 —— 只用于日志,排障时能一眼看出为什么没走上链式。 */\n reason: FeedReason\n}\n\n/** 严格前缀:prev 是 next 的前缀(含相等时不算\"追加\")。 */\nfunction isStrictPrefix(prev: readonly string[], next: readonly string[]): boolean {\n if (next.length <= prev.length) return false\n for (let i = 0; i < prev.length; i += 1) {\n if (prev[i] !== next[i]) return false\n }\n return true\n}\n\n/**\n * 决定本轮发什么。**纯函数**:不读文件、不看时间、不改全局状态。\n *\n * 判据宁可保守:只要能续链就发增量,任何一处不确定都退回\"全量 + parent=null\"\n * (退回去只是多花点 token,和以前行为一致;错续链则会让模型上下文错位,代价大得多)。\n */\nexport function decideFeed(input: FeedInput): FeedDecision {\n const full = input.full\n if (input.mode !== 'chained') return { prompt: full, parentMessageId: null, next: undefined, reason: 'mode-full' }\n\n const head = input.head\n const entries = input.entries\n if (typeof head !== 'string' || !Array.isArray(entries)) {\n return { prompt: full, parentMessageId: null, next: undefined, reason: 'no-parts' }\n }\n\n const restart = (reason: FeedReason): FeedDecision => ({\n prompt: full,\n parentMessageId: null,\n next: { head, entries: entries.slice(), sessionId: input.sessionId, accountKey: input.accountKey },\n reason,\n })\n\n if (!input.reused) return restart('new-session')\n const chain = input.chain\n if (!chain) return restart('no-chain')\n if (chain.sessionId !== input.sessionId) return restart('session-changed')\n if (chain.accountKey !== input.accountKey) return restart('account-changed')\n if (chain.head !== head) return restart('head-changed')\n if (!isStrictPrefix(chain.entries, entries)) return restart('not-appended')\n\n const delta = entries.slice(chain.entries.length).join('\\n\\n')\n if (delta.trim().length === 0) return restart('empty-delta')\n const cap = input.maxChars\n if (typeof cap === 'number' && Number.isFinite(cap) && cap > 0 && delta.length > cap) {\n return restart('delta-too-long')\n }\n return {\n prompt: delta,\n parentMessageId: chain.parentId,\n next: { head, entries: entries.slice(), sessionId: input.sessionId, accountKey: input.accountKey },\n reason: 'chained',\n }\n}\n\n// ── 设置:当前模式 + 落盘(照 transport.ts 那套)────────────────────────────\n\nlet currentMode: ContextMode = DEFAULT_CONTEXT_MODE\n\n/** 取当前生效的模式(即时生效,无需重启)。 */\nexport function currentContextMode(): ContextMode {\n return currentMode\n}\n\n/** 设置当前模式(设置页保存时立刻生效,无需重启)。 */\nexport function applyContextMode(mode: ContextMode): ContextMode {\n currentMode = mode\n return currentMode\n}\n\n/** 只给测试用:还原默认。 */\nexport function resetContextMode(): void {\n currentMode = DEFAULT_CONTEXT_MODE\n}\n\nexport function contextModeSettingsPath(): string {\n return join(resolveDshHome(), 'web-login', 'context-feed.json')\n}\n\nexport function readContextModeSetting(): ContextMode | undefined {\n try {\n const file = contextModeSettingsPath()\n if (!existsSync(file)) return undefined\n const parsed = JSON.parse(readFileSync(file, 'utf8'))\n return normalizeContextMode(parsed?.contextMode)\n } catch {\n return undefined\n }\n}\n\nexport function writeContextModeSetting(mode: ContextMode): void {\n const file = contextModeSettingsPath()\n mkdirSync(join(file, '..'), { recursive: true })\n writeFileSync(file, JSON.stringify({ contextMode: mode }, null, 2) + '\\n', 'utf8')\n}\n","/**\n * DeepSeek 网页版 (chat.deepseek.com) API 客户端:\n * PoW SHA3 WASM 求解 + chat_session 生命周期 + /chat/completion SSE 流式解析。\n *\n * 协议依据(2026 年多个活跃逆向实现交叉验证):\n * POST /api/v0/chat/create_pow_challenge {target_path} → data.biz_data.challenge\n * POST /api/v0/chat_session/create {} → data.biz_data.chat_session.id\n * POST /api/v0/chat_session/delete {chat_session_id}\n * POST /api/v0/chat/completion {chat_session_id, parent_message_id:null, prompt,\n * ref_file_ids:[], thinking_enabled, search_enabled,\n * model_type, action:null, preempt:false}\n * 请求头:Authorization: Bearer <token>、Cookie、x-hif-*、x-ds-pow-response\n * SSE 负载为 patch 流:\n * {\"v\":{\"response\":{...}}} 完整快照(fragments / content)\n * {\"p\":\"response/fragments\",\"o\":\"APPEND\",\"v\":{type,content}}\n * {\"p\":\"response/fragments/-1/content\",\"v\":\"…\"}\n * {\"p\":\"response/thinking_content\",\"v\":\"…\"} 旧格式:思考直连\n * {\"p\":\"response/content\",\"v\":\"…\"} 旧格式:正文直连\n * {\"v\":\"…\"} / {\"o\":\"APPEND\",\"v\":\"…\"} 承接上一个 path 的续段\n * {\"p\":\"response/status\",\"v\":\"FINISHED\"} 状态\n */\nimport { appendFileSync, mkdirSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { WebAuth } from './auth.ts'\nimport { AdapterLlmError, httpErrorCode, parseRetryAfterMs } from './auth.ts'\n// 默认区间来自 gate.ts —— 设置页的滑块边界与这里的默认值必须是**同一份**,否则界面显示的和实际跑的不是一回事。\nimport {\n DEFAULT_CLEANUP_BATCH,\n DEFAULT_CLEANUP_DELAY_MS,\n DEFAULT_CLEANUP_GAP_MS,\n type CleanupRange,\n} from './gate.ts'\n// 上下文投喂方式(全量 / 链式增量)——决策是纯函数,见 context-feed.ts 的模块注释。\nimport { currentContextMode, decideFeed, type ChainState, type FeedDecision, type FeedReason } from './context-feed.ts'\n\nexport const DS_BASE = 'https://chat.deepseek.com'\n\n/** PoW 求解器 WASM 的已知默认地址(页面资源捕获失败时兜底)。 */\nexport const DEFAULT_WASM_URL = 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm'\n\n/** 浏览器 UA 兜底(捕获失败时使用)。 */\nexport const FALLBACK_UA =\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'\n\nexport interface DsHeaders {\n [key: string]: string\n}\n\n/**\n * 组装一次网页端请求的头。\n * 优先复用登录时捕获的浏览器真实头(extraHeaders),再用最新登录态覆盖\n * authorization/cookie/指纹;user-agent 采用浏览器值(网页端接口需要浏览器指纹),\n * DSH 归属信息通过 `x-deepseek-harness` 头显式声明。\n */\nexport function buildDsHeaders(auth: WebAuth, referer?: string): DsHeaders {\n const headers: DsHeaders = {\n 'user-agent': auth.userAgent || FALLBACK_UA,\n accept: 'application/json, text/plain, */*',\n 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',\n 'content-type': 'application/json',\n origin: DS_BASE,\n referer: referer || `${DS_BASE}/`,\n 'x-client-platform': 'web',\n 'x-client-version': '2.0.0',\n 'x-app-version': '2.0.0',\n ...(auth.extraHeaders ?? {}),\n }\n headers.authorization = `Bearer ${auth.token}`\n headers['content-type'] = 'application/json'\n headers.origin = DS_BASE\n headers.referer = referer || `${DS_BASE}/`\n headers['user-agent'] = auth.userAgent || headers['user-agent'] || FALLBACK_UA\n headers['x-deepseek-harness'] = 'deepseek-harness (+https://github.com/deepseek-ai/deepseek-harness); provider=deepseek-web'\n // 这两个头由本插件按次生成/捕获,绝不复用快照里的旧值\n delete headers['x-ds-pow-response']\n if (auth.cookie) headers.cookie = auth.cookie\n else delete headers.cookie\n if (auth.hifDliq) headers['x-hif-dliq'] = auth.hifDliq\n if (auth.hifLeim) headers['x-hif-leim'] = auth.hifLeim\n return headers\n}\n\n// ── 响应信封(网页端常以 HTTP 200 + 业务错误码返回失败)────────\n\n/** 网页端统一信封:code===0 为成功;非 0 时 msg 是给用户看的诊断。 */\nexport function envelopeError(json: any): { code: number; msg: string } | undefined {\n if (!json || typeof json !== 'object') return undefined\n const code = (json as any).code\n if (typeof code === 'number' && code !== 0) {\n return { code, msg: String((json as any).msg ?? (json as any).message ?? 'unknown error') }\n }\n // ⚠️ 网页端把**真实业务错误**放在 data.biz_code 里,外层 code 依旧是 0。\n // 只认外层 code 的后果(实测 2026-09-11):服务端明明说得很清楚\n // {\"code\":0,\"msg\":\"\",\"data\":{\"biz_code\":1,\"biz_msg\":\"invalid chat session id\"}}\n // 却被降级成人人看不懂、而且**不可重试**的\n // `非流式响应(content-type: application/json):{...}` + MALFORMED_RESPONSE。\n // 认了 biz_code 之后,既能给出真原因,也能按原因做定向恢复(重建会话重试)。\n const bizCode = (json as any).data?.biz_code\n if (typeof bizCode === 'number' && bizCode !== 0) {\n const bizMsg = (json as any).data?.biz_msg\n const text = bizMsg === undefined || bizMsg === null || bizMsg === '' ? 'unknown error' : String(bizMsg)\n return { code: bizCode, msg: text }\n }\n return undefined\n}\n\n/**\n * 账号被临时限制判定(实测 2026-09-11):\n * {\"code\":0,\"data\":{\"biz_code\":5,\"biz_msg\":\"user is muted\",\n * \"biz_data\":{\"is_muted\":1,\"mute_until\":1789173841.894}}}\n * 这是**服务端对账号的限制**(免费网页端对高频自动化调用的静默限流),不是插件 bug:\n * 登录态有效、建会话也成功,只有 completion 被拒。必须把解除时间明确告诉用户,\n * 并且**不要空转重试** —— 否则每一轮都白发请求,还可能延长限制。\n */\nexport function isMutedError(biz: { code?: number; msg?: string } | undefined): boolean {\n return biz?.code === 5 || /user\\s+is\\s+muted|account\\s+is\\s+muted/i.test(String(biz?.msg ?? ''))\n}\n\n/** 从响应信封里读出解除限制的时间(ms);读不到返回 undefined。 */\nexport function muteUntilMs(json: any): number | undefined {\n const raw = (json as any)?.data?.biz_data?.mute_until\n const seconds = typeof raw === 'number' ? raw : Number(raw)\n if (!Number.isFinite(seconds) || seconds <= 0) return undefined\n return Math.round(seconds * 1000)\n}\n\n/** 被限制时的用户可读文案(带解除时间)。 */\nfunction mutedMessage(untilMs: number | undefined): string {\n if (untilMs === undefined) {\n return 'DeepSeek 网页端已临时限制本账号(user is muted),未给出解除时间。这期间任何网页模型调用都会失败;请等待解除,或改用官方 API key。'\n }\n const when = new Date(untilMs).toLocaleString('zh-CN', { hour12: false })\n const minutes = Math.max(1, Math.round((untilMs - Date.now()) / 60_000))\n return (\n `DeepSeek 网页端已临时限制本账号(user is muted):预计 ${when} 解除,约 ${minutes} 分钟后。` +\n '这期间任何网页模型调用都会失败(登录态本身有效、建会话也正常,只有发消息被拒);' +\n '请等待解除,或改用官方 API key。免费网页端对高频自动化调用会静默限流,刚跑过大量工具步骤的会话尤其容易被限。'\n )\n}\n\n/**\n * 「同一账号同时只能生成一条」的并发拒绝(实测 2026-09-11:两个 DSH 窗口共用同一网页账号,\n * 一个正在生成时另一个发请求即得此错:`A message is being generated, please try again later.`)。\n * 它**不是封号**(封号是 `user is muted`),但也无法立刻成功 ——\n * 归为可重试的 RATE_LIMIT,交由 dsh-llm-retry 稍后自动重发,而不是让整轮直接失败。\n */\nexport function isBusyGenerating(message: string): boolean {\n // R6(0.2.0):剔除英文裸短语 \"try again later\" —— 它是**限流**文案的常见尾巴\n // (\"too many requests, please try again later\"),留着会把限流误判成\"并发生成\",\n // 退避从 20s 渐长退化成 5s 固定,撞得更频。实测过的 busy 文案\n // (\"A message is being generated, please try again later.\")含 being generated,剔除安全。\n return /being generated|请稍后再试|稍后再试|正在生成/i.test(String(message ?? ''))\n}\n\n/**\n * 连续节流的状态:被限一次就退避久一点,别在限流窗口里反复撞。\n * (实测 2026-09-11 下午:同一个账号连续被限,5 次重试全落在窗口里 → 整轮失败。)\n */\n/**\n * 当前使用的 fetch 实现。\n *\n * 默认是 Node 的全局 fetch(undici)。宿主可以注入 **Electron 的 `net.fetch`** ——\n * 后者走 Chromium 原生网络库,能带来与真实浏览器一致的 TLS / HTTP2 指纹。\n * 为什么在意:实测 Node fetch 与 Chrome 的指纹差异是**结构性**的\n * (JA4 的 h1 vs h2、Node 无 GREASE、cipher 55 个 vs 15 个、扩展集合完全不同)。\n *\n * 注意:Electron 的 utility 进程里 `require('electron')` 只暴露 `net` 与 `systemPreferences`\n * (实测 2026-09-12),所以宿主只能注入 net.fetch,拿不到别的网络相关能力。\n */\nlet injectedFetch: typeof fetch | undefined\n\n/**\n * 实际发请求用的 fetch —— 刻意做成**每次现取**(`injectedFetch ?? fetch`),\n * 而不是在模块加载那一刻把全局 fetch 固化下来。\n *\n * 原因(2026-09-12 实测踩到):固化写法会让「模块加载之后再替换 globalThis.fetch」失效 ——\n * 单测正是用这种方式打桩,结果请求绕过了桩件、**真的发到了线上**\n * (拿回一个 INVALID_TOKEN,测试看着在验证错误分类,实际在打网络)。\n */\nfunction activeFetch(input: any, init?: any): Promise<Response> {\n return (injectedFetch ?? fetch)(input, init)\n}\n\n/** 注入 fetch 实现;传 undefined 还原为 Node 全局 fetch。 */\nexport function setFetchImpl(impl?: typeof fetch): void {\n injectedFetch = impl\n}\n\n/**\n * 当前生效的 fetch(诊断/检查更新这类**旁路请求**用它,从而与网页端请求走同一个传输层)。\n * 注意它已经做了\"每次现取\",直接当 fetch 用即可。\n */\nexport function currentFetch(input: any, init?: any): Promise<Response> {\n return activeFetch(input, init)\n}\n\n/** 当前用的是注入实现还是 Node 原生(诊断用)。 */\nexport function fetchImplKind(): 'injected' | 'node' {\n return injectedFetch ? 'injected' : 'node'\n}\n\nlet throttleStreak = 0\nlet lastThrottleAt = 0\n\n/** 取下一次节流退避(ms):20s 起、每次翻倍、上限 90s,并加 0~30% 抖动。 */\nexport function throttleBackoffMs(now: number = Date.now()): number {\n // 超过 5 分钟没被限,认为窗口已过,重新开始计数\n if (now - lastThrottleAt > 5 * 60_000) throttleStreak = 0\n const base = Math.min(20_000 * 2 ** throttleStreak, 90_000)\n const jitter = Math.round(base * 0.3 * Math.random())\n return base + jitter\n}\n\n/** 记录一次节流;返回本次应给的退避(ms)。 */\nfunction noteThrottled(now: number = Date.now()): number {\n if (now - lastThrottleAt > 5 * 60_000) throttleStreak = 0\n throttleStreak += 1\n lastThrottleAt = now\n return throttleBackoffMs(now)\n}\n\n/**\n * 账号级节流:「发得太频繁」。\n *\n * 实测 2026-09-11 16:11(SSE error 事件,不是 HTTP 429):\n * `消息发送过于频繁,请稍后重试`\n * ⚠️ 注意它和上面那条**差一个字**:并发拒绝写的是「请稍后再**试**」,节流写的是「请稍后**重**试」。\n * 之前只匹配前者,于是这条落到 PROVIDER_ERROR(**不可重试**)→ 整轮直接失败、只能手点「继续」。\n *\n * 与 `user is muted`(有明确解除时间)也不是一回事:节流是短时的,退避够久就能过去。\n * 退避给 20s(并发那条只给 5s):撞得越勤越可能延长限制。\n */\nexport function isThrottled(message: string): boolean {\n return /过于频繁|太频繁|操作频繁|too\\s+many\\s+requests|rate\\s*limit|稍后重试|限流/i.test(\n String(message ?? ''),\n )\n}\n\n/**\n * 会话失效判定:服务端用 biz_msg 表达「这个 chat_session_id 不存在/无效」。\n * 触发场景(实测):请求发出前会话已被删除(旧版把删除排在建会话之后 1.5s,\n * 而 PoW 求解 + 建连可能超过 1.5s),或服务端自行回收了闲置会话。\n * 这类失败**可以透明恢复**:本插件每次调用都是全新会话、不依赖服务端历史 → 换个会话重发即可。\n */\nexport function isInvalidSessionError(biz: { code?: number; msg?: string } | undefined): boolean {\n return /invalid\\s+chat\\s+session|chat\\s+session\\s+(?:not\\s+found|expired|invalid)|chat_session_id[^\\p{L}]{0,4}(?:无效|不存在|已过期|非法)|会话.{0,8}(?:无效|不存在|已过期)/iu.test(\n String(biz?.msg ?? ''),\n )\n}\n\n/** 业务错误码 → 稳定错误码(40003/40001:授权失败)。 */\nfunction bizErrorCode(code: number): string {\n if (code === 40003 || code === 40001) return 'AUTH'\n if (code === 429) return 'RATE_LIMIT'\n return 'PROVIDER_ERROR'\n}\n\n/**\n * `code 9` 在本项目里有**两个不同含义**,只能靠 msg 区分 —— 别只看数字:\n * · 上传文件时:`unsupported file type`(文件名后缀不被支持)⇒ 换个名字重传即可(见 imageUploadName)\n * · 发请求时:`invalid ref file id`(引用了一批服务端不认的文件 id)⇒ 需要降级重试\n *\n * 把后者单独识别出来,是为了让适配器能自救。这条错误的特点很恶劣:**带上图就失败**,\n * 而图留在历史里,之后每一轮重发都会再撞一次 ⇒ 用户除了丢掉整个会话没有别的出路。\n * 实测触发(2026-09-21):当前账号 `acc_2df7cf2f` 的凭证残缺(cookie 为空)时,\n * `new-session` 触发全量重发历史图 ⇒ code 9;而同一操作在另外两个 cookie 完整的账号上正常。\n */\nexport function isInvalidRefFileError(biz: { code: number; msg?: string } | undefined): boolean {\n return !!biz && biz.code === 9 && /ref\\s*file/i.test(String(biz.msg ?? ''))\n}\n\nfunction bizErrorMessage(code: number, msg: string): string {\n if (code === 40003 || code === 40001) {\n return `DeepSeek 网页授权失败:${msg} —— 登录态已过期或无效,请到「设置 → DeepSeek 网页登录」重新登录`\n }\n return `DeepSeek 网页端错误(code ${code}):${msg}`\n}\n\n// ── PoW 求解 ──────────────────────────────────────────────\n\ninterface PoWChallenge {\n algorithm: string\n challenge: string\n salt: string\n difficulty: string | number\n expire_at: string | number\n signature: string\n}\n\nlet wasmModuleCache: { url: string; promise: Promise<WebAssembly.Module> } | null = null\n/** 已验证可用/已发现的 WASM 地址(按凭证里记录的原值缓存,避免每次请求都探测)。 */\nlet resolvedWasmUrl: { key: string; url: string } | null = null\n\nasync function readOfficialResource(url: string, max: number, outer?: AbortSignal): Promise<Uint8Array<ArrayBuffer>> {\n const parsed = new URL(url)\n if (parsed.protocol !== 'https:' || parsed.username || parsed.password ||\n (parsed.port && parsed.port !== '443') ||\n (parsed.hostname !== 'deepseek.com' && !parsed.hostname.endsWith('.deepseek.com'))) throw new Error('非官方资源地址')\n const signal=outer?AbortSignal.any([outer,AbortSignal.timeout(15_000)]):AbortSignal.timeout(15_000)\n const resp=await activeFetch(parsed.href,{signal,redirect:'error'})\n if(!resp.ok||!resp.body){await resp.body?.cancel();throw new Error(`资源请求失败 HTTP ${resp.status}`)}\n const reader=resp.body.getReader();const chunks:Uint8Array[]=[];let size=0\n try {\n for(;;){const item=await reader.read();if(item.done)break;size+=item.value.byteLength\n if(size>max)throw new Error('资源超过字节上限');chunks.push(item.value)}\n } finally {try{await reader.cancel()}catch{};reader.releaseLock()}\n const bytes=new Uint8Array(size);let offset=0\n for(const chunk of chunks){bytes.set(chunk,offset);offset+=chunk.length}\n return bytes\n}\n\nasync function isReachable(url: string, outer?: AbortSignal): Promise<boolean> {\n if(!checkedWasmUrl(url))return false\n const signal=outer?AbortSignal.any([outer,AbortSignal.timeout(10_000)]):AbortSignal.timeout(10_000)\n let resp:Response|undefined\n try {resp=await activeFetch(url,{method:'GET',headers:{range:'bytes=0-0'},signal,redirect:'error'});return resp.ok}\n catch {if(outer?.aborted)outer.throwIfAborted();return false}\n finally {try{await resp?.body?.cancel()}catch{}}\n}\n\n/** 从网页端首页/JS chunk 里发现当前构建的 sha3 wasm 地址(哈希随版本变化)。 */\nasync function discoverWasmUrl(signal?: AbortSignal): Promise<string | undefined> {\n const decode = (bytes:Uint8Array)=>new TextDecoder().decode(bytes)\n const find = (text:string,base:string):string|undefined=>{\n for(const match of text.matchAll(/[^\"'\\s<>]*sha3[_a-z0-9.]*\\.wasm/gi)) {\n try{const url=checkedWasmUrl(new URL(match[0],base).href);if(url)return url}catch{}\n }\n return undefined\n }\n try {\n signal?.throwIfAborted()\n const html=decode(await readOfficialResource(`${DS_BASE}/`,2*1024*1024,signal))\n const direct=find(html,`${DS_BASE}/`);if(direct)return direct\n const scripts=[...html.matchAll(/(?:src|href)=\"([^\"]+\\.js)\"/g)].slice(0,8)\n for(const match of scripts){\n signal?.throwIfAborted()\n try{const url=new URL(match[1],`${DS_BASE}/`).href\n const found=find(decode(await readOfficialResource(url,8*1024*1024,signal)),url)\n if(found)return found\n }catch{if(signal?.aborted)signal.throwIfAborted()}\n }\n }catch{if(signal?.aborted)signal.throwIfAborted()}\n return undefined\n}\n\n/**\n * 解析可用的 PoW WASM 地址:凭证记录值 → 已知默认值 → 页面发现。\n * 结果按凭证原值缓存一次,避免每个请求都做探测。\n */\nexport async function resolveWasmUrl(auth: WebAuth, signal?: AbortSignal): Promise<string> {\n const key = auth.wasmUrl || ''\n if (resolvedWasmUrl?.key === key) return resolvedWasmUrl.url\n // 凭证里的地址先过白名单:不合法就丢弃(并告警),绝不拿去发请求\n const fromAuth = checkedWasmUrl(auth.wasmUrl)\n if (auth.wasmUrl && !fromAuth) {\n lastWasmUrlRejection = auth.wasmUrl\n }\n const candidates = [fromAuth, checkedWasmUrl(DEFAULT_WASM_URL)].filter((url): url is string => !!url)\n for (const url of candidates) {\n if (await isReachable(url, signal)) {\n resolvedWasmUrl = { key, url }\n return url\n }\n }\n const discovered = checkedWasmUrl(await discoverWasmUrl(signal))\n if (discovered) {\n resolvedWasmUrl = { key, url: discovered }\n return discovered\n }\n // 全都拿不到:宁可带着「地址不合法/找不到」的明确报错失败,也不要退回未校验的地址\n return fromAuth ?? checkedWasmUrl(DEFAULT_WASM_URL) ?? DEFAULT_WASM_URL\n}\n\n/**\n * F12(2026-09-12 审计):PoW WASM 地址的白名单校验。\n *\n * 为什么需要:`auth.wasmUrl` 主要来自**导入的账号备份**,可被构造成任意地址\n * (审计已复现:可打内网 / 云元数据 / file: 协议)。Electron 的 net.fetch 支持的\n * 协议比 Node fetch 更宽,不能把后者的协议限制当成统一边界。\n *\n * 为什么只限到 deepseek.com 而不是写死单个主机:默认地址里带内容哈希\n * (sha3_wasm_bg.7b9ca65ddd.wasm),官方一改就失效;而 `wasmUrl` 实际上**不是**\n * 浏览器抓来的(browser-login 里恒为空),页面发现(discoverWasmUrl)是唯一的\n * 兜底路径。所以保留发现能力,只把「能不能用」收白名单,既挡 SSRF 又留后路。\n */\nconst MAX_WASM_BYTES = 8 * 1024 * 1024\n\n/** 最近一次被白名单拒绝的凭证 wasmUrl(诊断/单测用;本模块无日志器,留状态而不是打日志)。 */\nexport let lastWasmUrlRejection: string | undefined\n\n/** 合法则返回规范化后的地址,否则返回 undefined(调用方负责回退并告警)。 */\nexport function checkedWasmUrl(raw: unknown): string | undefined {\n if (typeof raw !== 'string' || !raw) return undefined\n let url: URL\n try {\n url = new URL(raw)\n } catch {\n return undefined\n }\n if (url.protocol !== 'https:') return undefined\n if (url.username || url.password) return undefined\n if (url.port && url.port !== '443') return undefined\n const host = url.hostname.toLowerCase()\n // 挡住 169.254.169.254 / localhost / 内网 / 任意第三方,同时容忍未来换 CDN\n if (host !== 'deepseek.com' && !host.endsWith('.deepseek.com')) return undefined\n if (!url.pathname.toLowerCase().endsWith('.wasm')) return undefined\n return url.href\n}\n\nasync function loadWasmModule(wasmUrl: string): Promise<WebAssembly.Module> {\n const url=checkedWasmUrl(wasmUrl)\n if(!url)throw new Error('非法 WASM 地址')\n if(wasmModuleCache?.url===url)return wasmModuleCache.promise\n const promise=(async()=>WebAssembly.compile(await readOfficialResource(url,MAX_WASM_BYTES)))()\n wasmModuleCache={url,promise}\n promise.catch(()=>{\n if(wasmModuleCache?.promise===promise)wasmModuleCache=null\n if(resolvedWasmUrl?.url===url)resolvedWasmUrl=null\n })\n return promise\n}\n\n/**\n * 调用 DeepSeek 的 sha3_wasm_bg 求解 PoW。\n * wasm_solve(retptr, challengePtr, challengeLen, prefixPtr, prefixLen, difficulty);\n * prefix = `${salt}_${expire_at}_`;返回 float64 答案(取整)。\n */\nasync function solvePoW(challenge: PoWChallenge, wasmUrl: string): Promise<number> {\n const module = await loadWasmModule(wasmUrl)\n const instance = await WebAssembly.instantiate(module, { wbg: {} })\n const e = instance.exports as any\n if (typeof e.wasm_solve !== 'function' || typeof e.__wbindgen_export_0 !== 'function' || !e.memory) {\n throw new Error('PoW WASM exports missing (wasm_solve / __wbindgen_export_0 / memory)')\n }\n const encoder = new TextEncoder()\n const cBytes = encoder.encode(challenge.challenge)\n const pBytes = encoder.encode(`${challenge.salt}_${challenge.expire_at}_`)\n const cP = e.__wbindgen_export_0(cBytes.length, 1) >>> 0\n const pP = e.__wbindgen_export_0(pBytes.length, 1) >>> 0\n new Uint8Array(e.memory.buffer).set(cBytes, cP)\n new Uint8Array(e.memory.buffer).set(pBytes, pP)\n const sp = e.__wbindgen_add_to_stack_pointer(-16)\n e.wasm_solve(sp, cP, cBytes.length, pP, pBytes.length, Number(challenge.difficulty))\n const dv = new DataView(e.memory.buffer)\n const code = dv.getInt32(sp, true)\n const answer = dv.getFloat64(sp + 8, true)\n e.__wbindgen_add_to_stack_pointer(16)\n if (code === 0 || !Number.isFinite(answer) || answer <= 0) throw new Error(`PoW solve failed (code=${code})`)\n return Math.floor(answer)\n}\n\n/** 取得一次完成请求的 PoW 响应头值(base64 JSON)。 */\nexport async function createPowHeader(auth: WebAuth, targetPath: string, signal?: AbortSignal): Promise<string> {\n let resp: Response\n try {\n resp = await activeFetch(`${DS_BASE}/api/v0/chat/create_pow_challenge`, {\n method: 'POST',\n headers: buildDsHeaders(auth),\n body: JSON.stringify({ target_path: targetPath }),\n signal,\n })\n } catch (error: any) {\n throw new AdapterLlmError(`DeepSeek PoW challenge request failed: ${error?.message ?? error}`, 'TRANSPORT', { cause: error })\n }\n const text = await resp.text()\n if (!resp.ok) {\n const retryAfter = parseRetryAfterMs(resp.headers.get('retry-after'))\n throw new AdapterLlmError(\n `DeepSeek PoW challenge failed (HTTP ${resp.status})${text ? `: ${text.slice(0, 160)}` : ''}`,\n httpErrorCode(resp.status),\n { status: resp.status, ...(retryAfter !== undefined ? { providerRetryAfterMs: retryAfter } : {}) },\n )\n }\n let json: any\n try {\n json = JSON.parse(text)\n } catch {\n throw new AdapterLlmError('DeepSeek PoW challenge returned non-JSON', 'MALFORMED_RESPONSE', { status: resp.status })\n }\n const biz = envelopeError(json)\n if (biz) {\n throw new AdapterLlmError(bizErrorMessage(biz.code, biz.msg), bizErrorCode(biz.code), { status: resp.status })\n }\n const challenge: PoWChallenge | undefined = json?.data?.biz_data?.challenge\n if (!challenge?.challenge || !challenge?.salt || !challenge?.signature) {\n throw new AdapterLlmError(\n 'DeepSeek PoW challenge missing fields(登录态可能已过期,或被要求人机校验)',\n 'MALFORMED_RESPONSE',\n { status: resp.status },\n )\n }\n const wasmUrl = await resolveWasmUrl(auth, signal)\n const answer = await solvePoW(challenge, wasmUrl)\n const payload = JSON.stringify({\n algorithm: challenge.algorithm,\n challenge: challenge.challenge,\n salt: challenge.salt,\n answer,\n signature: challenge.signature,\n target_path: targetPath,\n })\n return Buffer.from(payload).toString('base64')\n}\n\n// ── 文件上传(图片输入)────────────────────────────────────\n//\n// 实测(2026-09):网页端看图不是「模型原生多模态入参」,而是网页把图片\n// 上传成文件(`/api/v0/file/upload_file`,PoW 场景 = 该路径),拿到\n// `data.biz_data.id`(形如 file-xxxx,model_kind=VISION)后在完成请求里用\n// `ref_file_ids` 引用。已验证:上传「左红右蓝」PNG 后模型准确回答「左红色,右=蓝色」。\n\nexport interface UploadedFile {\n fileId: string\n name?: string\n}\n\n/** 上传一张图片,返回 file_id。`data` 为原始编码字节(png/jpeg/webp/gif)。 */\nexport async function uploadImageFile(\n auth: WebAuth,\n input: { data: Uint8Array; mediaType: string; name?: string },\n signal?: AbortSignal,\n): Promise<UploadedFile> {\n const targetPath = '/api/v0/file/upload_file'\n const powHeader = await createPowHeader(auth, targetPath, signal)\n const headers: DsHeaders = { ...buildDsHeaders(auth) }\n // multipart 由 FormData 设定 boundary,必须去掉 content-type\n delete headers['content-type']\n headers['x-ds-pow-response'] = powHeader\n\n const form = new FormData()\n const bytes = input.data instanceof Uint8Array ? input.data : new Uint8Array(input.data as any)\n form.append('file', new Blob([bytes], { type: input.mediaType || 'image/png' }), input.name || 'image.png')\n\n let resp: Response\n try {\n resp = await activeFetch(`${DS_BASE}${targetPath}`, { method: 'POST', headers, body: form, signal })\n } catch (error: any) {\n throw new AdapterLlmError(`DeepSeek 图片上传失败:${error?.message ?? error}`, 'TRANSPORT', { cause: error })\n }\n const text = await resp.text()\n let json: any\n try {\n json = JSON.parse(text)\n } catch {\n json = undefined\n }\n if (!resp.ok) {\n throw new AdapterLlmError(\n `DeepSeek 图片上传失败 (HTTP ${resp.status})${text ? `: ${text.slice(0, 160)}` : ''}`,\n httpErrorCode(resp.status),\n { status: resp.status },\n )\n }\n const biz = envelopeError(json)\n if (biz) throw new AdapterLlmError(`DeepSeek 图片上传被拒(code ${biz.code}):${biz.msg}`, bizErrorCode(biz.code), { status: resp.status })\n const fileId = json?.data?.biz_data?.id ?? json?.data?.id\n if (typeof fileId !== 'string' || !fileId) {\n throw new AdapterLlmError('DeepSeek 图片上传未返回 file_id', 'MALFORMED_RESPONSE', { status: resp.status })\n }\n return { fileId, ...(input.name ? { name: input.name } : {}) }\n}\n\n// ── 会话 ──────────────────────────────────────────────────\n\n/** 新建一个网页端聊天会话,返回 chat_session_id。 */\nexport async function createChatSession(auth: WebAuth, signal?: AbortSignal): Promise<string> {\n let resp: Response\n try {\n resp = await activeFetch(`${DS_BASE}/api/v0/chat_session/create`, {\n method: 'POST',\n headers: buildDsHeaders(auth),\n body: '{}',\n signal,\n })\n } catch (error: any) {\n throw new AdapterLlmError(`DeepSeek session create failed: ${error?.message ?? error}`, 'TRANSPORT', { cause: error })\n }\n const text = await resp.text()\n if (!resp.ok) {\n const retryAfter = parseRetryAfterMs(resp.headers.get('retry-after'))\n throw new AdapterLlmError(\n `DeepSeek session create failed (HTTP ${resp.status})${text ? `: ${text.slice(0, 160)}` : ''}`,\n httpErrorCode(resp.status),\n { status: resp.status, ...(retryAfter !== undefined ? { providerRetryAfterMs: retryAfter } : {}) },\n )\n }\n let json: any\n try {\n json = JSON.parse(text)\n } catch {\n throw new AdapterLlmError('DeepSeek session create returned non-JSON', 'MALFORMED_RESPONSE', { status: resp.status })\n }\n const biz = envelopeError(json)\n if (biz) {\n throw new AdapterLlmError(bizErrorMessage(biz.code, biz.msg), bizErrorCode(biz.code), { status: resp.status })\n }\n const id = json?.data?.biz_data?.chat_session?.id || json?.data?.biz_data?.id\n if (typeof id !== 'string' || !id) {\n throw new AdapterLlmError('DeepSeek session create missing id', 'MALFORMED_RESPONSE', { status: resp.status })\n }\n return id\n}\n\n/** 尽力删除一个网页端会话(避免污染用户网页端聊天列表)。失败静默。 */\n// ── 会话清理:把「每轮建一个立刻删一个」的机器特征压下来 ──────────────\n//\n// 背景:一次模型调用要发 4 个请求 —— 建会话 → 取 PoW → completion → 删会话。\n// 其中「每轮新建一个临时会话、用完立刻删掉」是最强的机器行为特征之一(真人绝不会这样)。\n//\n// 为什么不能直接复用会话:DSH 每次把**全量历史**交给我们,而网页端会话是**有状态**的,\n// 复用会让服务端看到「历史 + 全量 prompt」两份上下文,迅速撑爆窗口。所以只能优化**删除侧**。\n//\n// immediate = 老行为:调用结束后 1.5s 删掉(每轮 1 个 DELETE 请求)\n// deferred = 默认:攒够 batchSize 个、或从第一个入队起等满 delayMs 才清理;\n// 清理时**先尝试一个请求批量删**(服务端支持的话 N 个会话只花 1 个请求),\n// 不支持则回退逐个删,并且此后不再尝试批量(只浪费一次)。\n// keep = 完全不删:请求数最少,但网页端会留下临时会话。\n\nexport type SessionCleanupMode = 'immediate' | 'deferred' | 'keep'\n\nexport interface SessionCleanupPolicy {\n mode: SessionCleanupMode\n /** 当前生效值:有区间时是\"这一轮抽到的\",没有区间时就是固定值(毫秒)。 */\n delayMs: number\n /** 当前生效值:攒够多少个立即清理(个)。 */\n batchSize: number\n /** 当前生效的删除间隔(毫秒)—— 相邻两个删除请求之间停多久。 */\n gapMs: number\n /**\n * 三对区间(可选)。给了就\"每轮 / 每次重新随机抽\",没给就保持固定值语义 ——\n * 这样老调用方(传死 batchSize / delayMs 的)行为完全不变。\n *\n * 为什么要有:**固定值本身就是机器特征** —— 每次都攒到第 8 个就动手、每次都正好等 90 秒、\n * 逐个删除时请求连发(中间 0 间隔)。真人不会这么精确。\n */\n batchRange?: CleanupRange\n delayRange?: CleanupRange\n gapRange?: CleanupRange\n}\n\nexport const DEFAULT_SESSION_CLEANUP: SessionCleanupPolicy = {\n mode: 'deferred',\n // 均值落在旧默认值上(90s / 8 个 / 1.5s),所以升级后行为没有突变,只是多了方差\n delayMs: Math.round((DEFAULT_CLEANUP_DELAY_MS.min + DEFAULT_CLEANUP_DELAY_MS.max) / 2),\n batchSize: Math.round((DEFAULT_CLEANUP_BATCH.min + DEFAULT_CLEANUP_BATCH.max) / 2),\n gapMs: Math.round((DEFAULT_CLEANUP_GAP_MS.min + DEFAULT_CLEANUP_GAP_MS.max) / 2),\n batchRange: DEFAULT_CLEANUP_BATCH,\n delayRange: DEFAULT_CLEANUP_DELAY_MS,\n gapRange: DEFAULT_CLEANUP_GAP_MS,\n}\n\n/**\n * 单个请求里最多塞多少个会话 id。\n *\n * 为什么要有:队列在\"清理很慢\"时可能积很多(比如你离开两小时后回来,一次 flush 要删几十个)。\n * 「一次请求删掉一大批」正是用户担心的事 —— 所以超过这个数就拆成多次,\n * 每次之间按随机间隔停一下。\n */\nconst MAX_IDS_PER_REQUEST = 20\n\n/**\n * 会话生命周期事件(2026-09-14)。\n *\n * 宿主用它维护一份「**还欠一次删除**」的会话清单并落盘(见 `session-journal.ts`),\n * 于是进程退出/被强杀之后,下次启动还能把这些会话补删掉 —— 在此之前,\n * 复用槽和待删队列只活在内存里,进程一走就静默丢失,网页端就会一直堆。\n *\n * 三个事件对应三种账:\n * - `leased` 会话进了复用槽(此刻**还没删**,所以是欠账);\n * - `queued` 会话进了待删队列(同样是欠账,只是排队了);\n * - `deleted` **确认删掉**(服务端接受了)→ 唯一能销账的信号。\n */\nexport type SessionLifecycleEvent =\n | { kind: 'leased'; auth: WebAuth; sessionId: string }\n | { kind: 'queued'; auth: WebAuth; sessionId: string }\n | { kind: 'deleted'; sessionId: string }\n\nlet sessionLifecycleHook: ((event: SessionLifecycleEvent) => void) | undefined\n\n/** 注册生命周期钩子(宿主启动时调一次即可;传 `undefined` 取消)。 */\nexport function setSessionLifecycleHook(hook?: (event: SessionLifecycleEvent) => void): void {\n sessionLifecycleHook = hook\n}\n\nfunction emitSessionLifecycle(event: SessionLifecycleEvent): void {\n try {\n sessionLifecycleHook?.(event)\n } catch {\n /* 钩子出错不能影响请求主流程 */\n }\n}\n\nexport interface SessionCleanerOptions {\n policy?: Partial<SessionCleanupPolicy>\n logger?: { info?: (msg: string) => void; debug?: (msg: string) => void }\n /** 单测注入。 */\n fetchImpl?: typeof fetch\n setTimeoutImpl?: (fn: () => void, ms: number) => any\n clearTimeoutImpl?: (t: any) => void\n /** 随机源。单测注入一个确定序列即可得到可重复的取值。默认 Math.random。 */\n randomImpl?: () => number\n}\n\nexport interface SessionCleaner {\n schedule(auth: WebAuth, sessionId: string): void\n /** 立即清理队列(测试 / 卸载时用)。 */\n flush(): Promise<void>\n pendingCount(): number\n policy(): SessionCleanupPolicy\n /** 运行时改策略(设置页保存后调用),返回改完后的值。 */\n configure(next: Partial<SessionCleanupPolicy>): SessionCleanupPolicy\n}\n\nexport function createSessionCleaner(options: SessionCleanerOptions = {}): SessionCleaner {\n const policy: SessionCleanupPolicy = {\n mode: options.policy?.mode ?? DEFAULT_SESSION_CLEANUP.mode,\n delayMs: Math.max(0, Math.floor(options.policy?.delayMs ?? DEFAULT_SESSION_CLEANUP.delayMs)),\n batchSize: Math.max(1, Math.floor(options.policy?.batchSize ?? DEFAULT_SESSION_CLEANUP.batchSize)),\n // 没给区间(老调用方 / 老配置)→ 间隔为 0,也就是**不加额外间隔**,保持老行为。\n // 只有显式配了 gapRange 才启用\"删一个歇一下\"。\n gapMs: Math.max(\n 0,\n Math.floor(options.policy?.gapMs ?? (options.policy?.gapRange ? DEFAULT_SESSION_CLEANUP.gapMs : 0)),\n ),\n // 区间是**可选**的:老调用方只传死 batchSize / delayMs 时,这里保持\"无区间\"= 固定值语义\n // (否则它们传的 3 会被默认区间 6~10 顶掉,单测与旧行为全乱)。\n ...(options.policy?.batchRange ? { batchRange: options.policy.batchRange } : {}),\n ...(options.policy?.delayRange ? { delayRange: options.policy.delayRange } : {}),\n ...(options.policy?.gapRange ? { gapRange: options.policy.gapRange } : {}),\n }\n /** 策略切换时按模式给默认延迟/批量(immediate 用老参数)。 */\n function applyModeDefaults(): void {\n if (policy.mode === 'immediate') {\n policy.delayMs = 1_500\n policy.batchSize = 1\n } else if (policy.mode === 'deferred' && policy.batchSize <= 1) {\n // 从「不删 / 立即」切回「延迟」时给一组新的随机值(不是写死的默认值)\n policy.delayMs = policy.delayRange\n ? pickInt(policy.delayRange)\n : DEFAULT_SESSION_CLEANUP.delayMs\n policy.batchSize = policy.batchRange\n ? pickInt(policy.batchRange)\n : DEFAULT_SESSION_CLEANUP.batchSize\n }\n }\n const doFetch = options.fetchImpl ?? fetch\n const setT = options.setTimeoutImpl ?? ((fn: () => void, ms: number) => setTimeout(fn, ms))\n const clearT = options.clearTimeoutImpl ?? ((t: any) => clearTimeout(t))\n const logger = options.logger\n\n let queue: { auth: WebAuth; sessionId: string }[] = []\n let timer: any\n /** 探测到服务端不接受批量删除后置位 —— 之后一律逐个删,不再浪费请求。 */\n let batchUnsupported = false\n\n const random = options.randomImpl ?? Math.random\n\n /** 在 [min, max] 里取整数(闭区间)。random 可注入,单测因此可重复。 */\n function pickInt(range: CleanupRange): number {\n const lo = Math.min(range.min, range.max)\n const hi = Math.max(range.min, range.max)\n if (hi <= lo) return lo\n // 注入的假随机源有可能返回 1,夹一下保证不越界\n return Math.min(hi, lo + Math.floor(random() * (hi - lo + 1)))\n }\n\n /**\n * 新的一轮清理开始(队列由空变非空)时重新抽:这一轮攒几个、最多等多久。\n *\n * 为什么按\"轮\"抽而不是每次都抽:阈值与等待时间要在一轮里保持稳定,\n * 否则\"攒够 6~10 个\"会退化成\"好像随时都在触发\"。每轮换一组,既有方差又不失节奏。\n */\n function rollCycle(): void {\n if (policy.mode !== 'deferred') return\n if (policy.batchRange) policy.batchSize = Math.max(1, pickInt(policy.batchRange))\n if (policy.delayRange) policy.delayMs = Math.max(0, pickInt(policy.delayRange))\n }\n\n /** 每次要发一个删除请求之前抽一次间隔(顺带记下当前值,供设置页显示)。 */\n function rollGap(): number {\n policy.gapMs = policy.gapRange ? Math.max(0, pickInt(policy.gapRange)) : Math.max(0, policy.gapMs)\n return policy.gapMs\n }\n\n /** 用注入的定时器睡一会儿(单测里就是\"等假表被触发\")。 */\n function sleep(ms: number): Promise<void> {\n if (!(ms > 0)) return Promise.resolve()\n return new Promise<void>((resolve) => {\n const handle = setT(() => resolve(), ms)\n ;(handle as any)?.unref?.()\n })\n }\n\n /** 装一次「到点清理」的表(已经装了就不动)。 */\n function armTimer(): void {\n if (timer !== undefined) return\n if (policy.mode === 'keep') return\n timer = setT(() => {\n void flush()\n }, Math.max(0, policy.delayMs))\n ;(timer as any)?.unref?.()\n }\n\n /**\n * 服务端\"看起来接受了\":HTTP ok 且响应体里没有业务错误信封。\n *\n * 网页端会在 HTTP 200 上裹一层 `{code, msg, data:{biz_code,biz_msg}}` ——\n * 只看 `resp.ok` 会把\"其实没删掉\"当成成功(F07 踩过这个坑)。\n */\n /**\n * 批量删除的响应该怎么定性(0.1.82)。\n *\n * 三态而不是两态,因为\"失败\"里混着两类完全不同的东西:\n * · `unsupported` —— 服务端**听懂了但拒绝**(4xx,或 HTTP 200 里裹业务错误信封,F07 形态)\n * ⇒ 可以永久关掉批量,之后逐个删;\n * · `transient` —— 5xx / 429 / 网关 HTML(非 JSON 正文)⇒ **瞬时**问题,\n * 据此永久关掉批量就会让此后每批退化成 N 个请求(自己把请求密度抬上去)。\n */\n async function classifyDeleteResp(resp: Response): Promise<'ok' | 'unsupported' | 'transient'> {\n if (!resp.ok) return resp.status >= 500 || resp.status === 429 ? 'transient' : 'unsupported'\n const text = await resp.text().catch(() => '')\n try {\n const json = text ? JSON.parse(text) : undefined\n if (json && envelopeError(json)) return 'unsupported'\n return 'ok'\n } catch {\n return 'transient'\n }\n }\n\n async function respLooksOk(resp: Response): Promise<boolean> {\n let ok = resp.ok\n if (ok) {\n const text = await resp.text().catch(() => '')\n try {\n const json = text ? JSON.parse(text) : undefined\n if (json && envelopeError(json)) ok = false\n } catch {\n ok = false\n }\n }\n return ok\n }\n\n /**\n * 删一个,返回**是否确认删掉** —— 删除回执要用来摘掉\"欠删除\"日志里的记录\n * (见 session-journal.ts:只有确认删掉才移记录,否则下次启动还来补删)。\n */\n async function deleteOne(auth: WebAuth, sessionId: string): Promise<boolean> {\n try {\n const resp = await doFetch(`${DS_BASE}/api/v0/chat_session/delete`, {\n method: 'POST',\n headers: buildDsHeaders(auth),\n body: JSON.stringify({ chat_session_id: sessionId }),\n signal: AbortSignal.timeout(10_000),\n })\n return await respLooksOk(resp)\n } catch {\n return false // 清理失败不影响主流程\n }\n }\n\n /**\n * 删一批:优先一个请求批量删;服务端不接受则**逐个删**。\n *\n * 逐个删时两个请求之间会停一个随机间隔 —— 原来这里是**连发**(一批 20 个就是 20 个连续请求),\n * 那是最像脚本的部分。\n */\n async function deleteChunk(batch: { auth: WebAuth; sessionId: string }[]): Promise<void> {\n if (batch.length === 0) return\n // ⚠️ F07(2026-09-12 审计):批量删除**只发一个凭证**(HTTP 请求只有一个 Authorization 头),\n // 若这一批里混了不同账号的会话,就等于「拿 A 的凭证去删 B 的会话」——\n // 轻则整批被服务端拒绝,重则 resp.ok 时被当成全部成功(旧代码 ok 就直接 return,\n // 不校验每个 id 是否真的删掉)。混号时退化为逐个删,逐个删用的是各自的 auth。\n const firstToken = batch[0].auth?.token\n const sameAccount = batch.every((item) => item.auth?.token === firstToken)\n if (batch.length > 1 && !batchUnsupported && sameAccount) {\n try {\n const resp = await doFetch(`${DS_BASE}/api/v0/chat_session/delete`, {\n method: 'POST',\n headers: buildDsHeaders(batch[0].auth),\n body: JSON.stringify({ chat_session_ids: batch.map((b) => b.sessionId) }),\n signal: AbortSignal.timeout(15_000),\n })\n const verdict = await classifyDeleteResp(resp)\n if (verdict === 'ok') {\n // 批量删只发一个请求,服务端接受即认为这一批都删掉了(它不逐个回报)。\n // 所以只对\"同账号 + 无业务错误\"的批次这样处理 —— 见上面的 F07 说明。\n for (const item of batch) emitSessionLifecycle({ kind: 'deleted', sessionId: item.sessionId })\n logger?.debug?.(`deepseek-web: 已批量清理 ${batch.length} 个临时会话(只用了 1 个请求)`)\n return\n }\n // ⚠️ 0.1.82:只有\"服务端明确说不行\"才能永久关掉批量 ——\n // 5xx / 429 / 网关 HTML / 非 JSON 正文都是**瞬时**问题,按旧写法一次抖动就\n // 让此后每批退化成 N 个请求(等于自己把请求密度抬上去)。\n if (verdict === 'unsupported') {\n batchUnsupported = true\n logger?.debug?.('deepseek-web: 服务端不接受批量删除会话,之后改为逐个删除')\n } else {\n logger?.debug?.(`deepseek-web: 批量删除本次失败(HTTP ${resp.status}),按瞬时问题处理,下次仍试批量`)\n }\n } catch {\n // 网络异常 ≠ 不支持,下次仍可尝试\n }\n }\n\n for (let i = 0; i < batch.length; i += 1) {\n if (i > 0) await sleep(rollGap())\n if (await deleteOne(batch[i].auth, batch[i].sessionId)) {\n emitSessionLifecycle({ kind: 'deleted', sessionId: batch[i].sessionId })\n }\n }\n logger?.debug?.(`deepseek-web: 已清理 ${batch.length} 个临时会话`)\n }\n\n /** 真正干活的清理。分片:队列积很多时也不一口气删完(见 MAX_IDS_PER_REQUEST 的说明)。 */\n async function doFlush(): Promise<void> {\n if (timer !== undefined) {\n clearT(timer)\n timer = undefined\n }\n const batch = queue\n queue = []\n try {\n if (batch.length === 0) return\n for (let i = 0; i < batch.length; i += MAX_IDS_PER_REQUEST) {\n if (i > 0) await sleep(rollGap())\n await deleteChunk(batch.slice(i, i + MAX_IDS_PER_REQUEST))\n }\n } catch (error: any) {\n // 清理失败不影响主流程\n logger?.debug?.(`deepseek-web: 会话清理出错(已忽略):${error?.message ?? error}`)\n } finally {\n // 清理期间可能又攒了新的:重新装表,否则它们会一直躺在队列里没人管(直到下次有会话入队)\n if (queue.length > 0) armTimer()\n }\n }\n\n /**\n * 立即清理队列。\n *\n * **串行化**:上一次还没删完时,这一次排在它后面等 —— 否则两轮 flush 的删除请求会交错发出,\n * 正是我们要避免的\"连发\"。排队的 flush 轮到自己时才取队列,所以能带上期间新攒的会话。\n */\n let chain: Promise<void> = Promise.resolve()\n function flush(): Promise<void> {\n chain = chain.then(doFlush, doFlush)\n return chain\n }\n\n function schedule(auth: WebAuth, sessionId: string): void {\n if (policy.mode === 'keep') return\n // 队列由空变非空 = 新的一轮开始 → 重新抽本轮的阈值与最长等待\n if (queue.length === 0) rollCycle()\n queue.push({ auth, sessionId })\n // 落账:这个会话此刻**还没删**(排着队)。宿主据此落盘,进程被强杀后下次启动补删。\n emitSessionLifecycle({ kind: 'queued', auth, sessionId })\n // 只有 deferred 才「攒够就立即清理」。immediate 始终走延迟 —— 保持老行为:\n // 调用结束后过一会儿才删,避免「流刚结束就紧跟一个 DELETE」这种过紧的节奏。\n if (policy.mode === 'deferred' && queue.length >= policy.batchSize) {\n void flush()\n return\n }\n armTimer()\n }\n\n function configure(next: Partial<SessionCleanupPolicy>): SessionCleanupPolicy {\n const modeChanged = next.mode !== undefined && next.mode !== policy.mode\n if (next.mode !== undefined) policy.mode = next.mode\n if (next.delayMs !== undefined) policy.delayMs = Math.max(0, Math.floor(next.delayMs))\n if (next.batchSize !== undefined) policy.batchSize = Math.max(1, Math.floor(next.batchSize))\n if (next.gapMs !== undefined) policy.gapMs = Math.max(0, Math.floor(next.gapMs))\n // 区间:给了就换;没给就保持原样(\"缺省 = 不随机\"的语义不能丢)\n for (const key of ['batchRange', 'delayRange', 'gapRange'] as const) {\n const value = next[key]\n if (value && Number.isFinite(value.min) && Number.isFinite(value.max)) {\n policy[key] = { min: Math.floor(Math.min(value.min, value.max)), max: Math.floor(Math.max(value.min, value.max)) }\n }\n }\n if (modeChanged) applyModeDefaults()\n if (policy.mode === 'keep') void flush() // 切到「不删」时把已排队的清掉,避免残留\n logger?.info?.(\n `deepseek-web: 会话清理策略已更新 —— ${policy.mode}` +\n (policy.mode === 'deferred'\n ? `(攒 ${policy.batchSize} 个或 ${Math.round(policy.delayMs / 1000)}s 后清理` +\n (policy.gapRange ? `;批量删除不受支持时逐个删,间隔 ${policy.gapMs}ms` : '') +\n ')'\n : ''),\n )\n return { ...policy }\n }\n\n return {\n schedule,\n flush,\n pendingCount: () => queue.length,\n policy: () => ({ ...policy }),\n configure,\n }\n}\n\n/** 默认清理器(immediate 语义,兼容旧调用方)。 */\nconst defaultCleaner = createSessionCleaner({\n policy: { mode: 'immediate', delayMs: 1_500, batchSize: 1 },\n})\n\nexport function scheduleDeleteSession(auth: WebAuth, sessionId: string): void {\n defaultCleaner.schedule(auth, sessionId)\n}\n\n/** 验证登录态:优先 users/current,端点不存在时退回 PoW challenge 探活。 */\n/**\n * 从 `users/current` 的 user 对象里挑一个**能看的账号标识**。\n *\n * 两个必须记住的坑(都是实测踩出来的,2026-09-12):\n *\n * 1. **不能用 `??` 串起来。** 接口对\"没设邮箱\"的账号会返回 `email: \"\"`,\n * 而空字符串**不是** nullish —— `\"\" ?? x` 的结果就是 `\"\"`,整条回退链当场被它挡住,\n * display 永远是空,界面只好退回去显示内部 id(`acc_cd8e05ec`)。\n * 所以必须按\"**有内容**\"取,跳过 undefined / null / 空白。\n *\n * 2. **字段名要和响应对齐。** 手机号是 `mobile_number`(不是 `mobile`),\n * 而且服务端返回的**已经是脱敏形态**(如 `183******78`),可以直接展示。\n *\n * 实测响应形状(只列相关字段):\n * { id, token, email: \"\", mobile_number: \"183******78\", area_code: \"+86\", chat: {...} }\n */\nexport function pickUserDisplay(user: any): string {\n const candidates = [\n user?.email,\n user?.mobile_number,\n user?.mobile,\n user?.phone,\n user?.username,\n user?.nickname,\n user?.name,\n ]\n for (const value of candidates) {\n if (value === undefined || value === null) continue\n const text = String(value).trim()\n if (text) return text\n }\n return ''\n}\n\n/**\n * 判定 `users/current` 的响应体**形状**是否可信。\n *\n * ⚠️ F09(2026-09-12 审计):旧代码在 `resp.json()` 抛错时把 json 置为 undefined,\n * 而 `envelopeError(undefined)` 返回 undefined,于是径直走到 `ok: true`,\n * 返回一个**空壳的 user({})**。也就是说:反爬页 / WAF 拦截页 / 空响应\n * —— 它们同样是 HTTP 200 —— 会被当成\"验证通过\"。\n *\n * 后果很实际:探活显示\"通过\"、账号看起来正常,\n * 0.1.31 加的「需要重新登录」按钮就永远不会触发;什么都没确认到,却说成功。\n * 只读零额度请求偶发失败的代价只是一次重试,远比\"误报成功\"划算。\n *\n * 抽成纯函数是为了能单测(validateAuth 要发网络请求,测不了这条分支)。\n */\n/**\n * 校验 `users/current` 的信封。**必须能辨认出一个用户身份**才算成功。\n *\n * 2026-09-13 第二轮审计 N09:旧实现只拒绝\"不是对象\"和\"data、code 都缺\",\n * 于是 `{code:0}`、`{data:null}`、`{code:\"401\",data:null}` 这类空壳/错型信封全部被判成功 ——\n * 而 `validateAuth` 之后又会回落空对象,导致\"校验成功\"与\"拿到有效身份\"脱节。\n * 现在:业务码必须是数值、data 必须是对象、且里面要能找到一个可辨认的用户字段。\n */\nexport function classifyAuthEnvelope(json: unknown): { ok: true } | { ok: false; error: string } {\n const fail = (error: string): { ok: false; error: string } => ({ ok: false, error })\n if (!json || typeof json !== 'object' || Array.isArray(json)) {\n return fail('users/current 响应不是 JSON 对象(可能是反爬页面或网关拦截)')\n }\n const obj = json as any\n // 业务码必须是**数值**:字符串 \"401\" 这种错型信封不能当成功\n if (typeof obj.code !== 'number') return fail('users/current 缺少数值业务码(形状不符)')\n const bizError = envelopeError(obj)\n if (bizError) return fail(bizError.msg)\n if (obj.code !== 0 || !obj.data || typeof obj.data !== 'object' || Array.isArray(obj.data)) {\n return fail('users/current 缺少用户数据(形状不符)')\n }\n if (obj.data.biz_code !== undefined && typeof obj.data.biz_code !== 'number') {\n return fail('users/current 内层业务码无效')\n }\n const payload = obj.data.biz_data ?? obj.data\n const user = payload?.user ?? payload\n if (!user || typeof user !== 'object' || Array.isArray(user)) return fail('users/current 用户形状无效')\n const hasId =\n (typeof user.id === 'string' && user.id.trim().length > 0) ||\n (typeof user.id === 'number' && Number.isFinite(user.id))\n const named = ['email', 'mobile_number', 'mobile', 'phone', 'username', 'nickname', 'name'].some(\n (k) => typeof user[k] === 'string' && user[k].trim(),\n )\n // 空壳信封(data 里什么都没有)不能算校验成功\n return hasId || named ? { ok: true } : fail('users/current 缺少可辨认的用户身份')\n}\n\nexport async function validateAuth(\n auth: WebAuth,\n signal?: AbortSignal,\n): Promise<{\n ok: boolean\n user?: { id?: string; display?: string }\n /** F1(0.2.0):users/current 自带的限流状态(chat.is_muted / mute_until),探活顺手带回。 */\n limit?: { muted: boolean; untilMs?: number }\n error?: string\n}> {\n try {\n const resp = await activeFetch(`${DS_BASE}/api/v0/users/current`, { headers: buildDsHeaders(auth), signal })\n if (resp.ok) {\n let json: any\n try {\n json = await resp.json()\n } catch {\n json = undefined\n }\n // 形状校验(纯函数,见 classifyAuthEnvelope 的注释)\n const verdict = classifyAuthEnvelope(json)\n if (!verdict.ok) return verdict\n const payload = json?.data?.biz_data ?? json?.data\n const user = payload?.user ?? payload ?? {}\n const display = pickUserDisplay(user)\n // F1(0.2.0):限流状态本来就躺在 users/current 的响应体里(2026-09-12 实测:\n // 受限期间 chat.is_muted / mute_until 有效)——顺手解析,探活就能\"提前看见被限\",\n // 而不是等生成请求撞 muted 才知道。chat 字段缺失时**不带** limit(不猜)。\n const chat = payload?.chat\n const untilRaw = chat?.mute_until ?? payload?.mute_until\n const untilSec = typeof untilRaw === 'number' ? untilRaw : Number(untilRaw)\n const untilMs = Number.isFinite(untilSec) && untilSec > 0 ? Math.round(untilSec * 1000) : undefined\n return {\n ok: true,\n ...(chat && typeof chat.is_muted === 'boolean'\n ? { limit: { muted: chat.is_muted === true, ...(untilMs ? { untilMs } : {}) } }\n : {}),\n user: {\n ...(user?.id !== undefined ? { id: String(user.id) } : {}),\n ...(display ? { display } : {}),\n },\n }\n }\n if (resp.status === 404) {\n await createPowHeader(auth, '/api/v0/chat/completion', signal)\n return { ok: true }\n }\n return { ok: false, error: `users/current HTTP ${resp.status}` }\n } catch (error: any) {\n return { ok: false, error: error instanceof Error ? error.message : String(error) }\n }\n}\n\n// ── SSE 流式解析 ──────────────────────────────────────────\n\nexport type WebStreamEvent =\n | { kind: 'thinking'; text: string }\n | { kind: 'text'; text: string }\n | { kind: 'status'; value: string }\n | {\n kind: 'finish'\n reason?: string\n /**\n * 服务端上报的**本消息**总 token 数(含 prompt + 回复 + 服务端自身开销)。\n * 实测 2026-09-13:9 字符 prompt → 38;12,424 字符 prompt → 6446;\n * 且同一会话第二轮仍只报自己的 38 → 是「本消息」而不是「会话累计」。\n * 拿不到时缺省(此时调用方退回按字符估算)。\n */\n totalTokens?: number\n }\n | {\n kind: 'error'\n message: string\n raw?: string\n /** 语义归类(如并发生成 → RATE_LIMIT),调用方据此决定重试 */\n code?: string\n retryAfterMs?: number\n /** RATE_LIMIT 细分:并发抢占(等对面写完)还是账号节流(等限流解除)—— 文案与退避都不同 */\n rateLimitKind?: 'concurrent' | 'throttled'\n }\n\ninterface Fragment {\n type: string\n content: string\n emitted: number\n}\n\nfunction isReasoningType(type: string): boolean {\n const t = type.toUpperCase()\n return t === 'THINK' || t === 'REASONING' || t === 'THINKING'\n}\n\n/** 把字节流切成行(SSE 帧以 \\n 分隔)。 */\nasync function* iterateLines(body: any): AsyncGenerator<string> {\n const decoder = new TextDecoder()\n let buffer = ''\n const drain = function* (): Generator<string> {\n let idx: number\n while ((idx = buffer.indexOf('\\n')) !== -1) {\n yield buffer.slice(0, idx).replace(/\\r$/, '')\n buffer = buffer.slice(idx + 1)\n }\n }\n if (typeof body?.getReader === 'function') {\n const reader = body.getReader()\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true })\n yield* drain()\n }\n } finally {\n try {\n reader.releaseLock?.()\n } catch {}\n }\n } else if (body?.[Symbol.asyncIterator]) {\n for await (const chunk of body) {\n buffer += decoder.decode(chunk, { stream: true })\n yield* drain()\n }\n }\n if (buffer.length > 0) yield buffer.replace(/\\r$/, '')\n}\n\n/**\n * 网页端 completion 负载的解析状态机(可单测:handle 返回待 yield 的事件)。\n *\n * ⚠️ 正确性模型(2026-09 事故修复):\n * 真实事故:模型回答了完整一段,DSH 里只显示「,」「不上」「了一圈」这类 1~3 字碎片,\n * 并伴随 EMPTY_RESPONSE 重试。根因是旧实现用「只增不减的 emitted 计数器」做去重,\n * 而快照会把派生文本重置为更短的内容 —— 计数器被撑大后保持高位,后续只在文本长度\n * 超过它时才吐字,于是前面的全丢、只剩余数的尾巴;余数为空又触发重试。\n *\n * 现在的规则:\n * ① **增量事件驱动发射**(fragment APPEND / -1/content / thinking_content / content / 裸 v)\n * ② **快照只做对账**:仅当候选文本是「已发射内容的严格延伸」时补差;\n * 更短(过期快照)或分歧(服务端重排/回退)一律忽略,绝不重置已发射内容\n * ③ 快照永远不会让已发射内容变小 → 不会丢字、不会因此触发假 EMPTY_RESPONSE\n */\nexport interface SseStateOptions {\n /**\n * 本轮请求是否开启了思考(对应请求体里的 `thinking_enabled`)。\n *\n * F25(2026-09-14,用真实帧复现):正常流的**首帧快照**一定带一个 `type: \"THINK\"`\n * 的 fragment(抓包实测 4 轮全如此),之后思考全部通过\n * `response/fragments/-1/content` 与「无 path 的裸续段」续写 —— 也就是说\n * **思考的归属完全依赖那份快照**。一旦快照没能进入状态机(整帧丢失,或服务端\n * 先发了 `fragments: []` 的快照),`fragments` 就一直是空的,而旧实现在\n * 「无 fragment 可续」时**无条件当正文发射** ⇒ 整段思考上屏。\n *\n * 实测后果:09-14 一轮 2062 字符、09-12 两轮 10526 / 4360 字符的全部进正文,\n * 且这些正文块开头都缺几个字(\"回答中文…\" 本该是 \"我们需要回答中文…\",\n * 缺掉的正是被丢弃的那份快照里的片段)—— 一个根因同时解释「整段上屏」与「缺开头」。\n *\n * 现在:开了思考却还没 fragment 时先**缓冲**,等 fragment 出现再定归属。\n * 未开思考时不缓冲(没有歧义),保持旧行为。\n */\n thinkingEnabled?: boolean\n /**\n * 本轮 assistant 的 message_id(链式投喂用)。\n *\n * 来源是流的**首帧** `event: ready`:`{\"request_message_id\":1,\"response_message_id\":2,...}`\n * (真实样本 `.workbuddy/tmp/shortq-r1-*.sse`)。下一轮就把它当 `parent_message_id` 发上去,\n * 服务端据此把新消息挂到上一条回答下面,于是历史由服务端维护、我们只发增量。\n *\n * 用回调而不是往 WebStreamEvent 里加一种 kind:新 kind 会流到 adapter 的事件消费处,\n * 那里对未知 kind 的处理没人守(多一种事件就多一处可能被当成\"未知\"丢掉)。\n */\n onResponseMessageId?: (id: number) => void\n}\n\n/** F28:思考的标准包装标签。孤儿兜底用(见 finish 里的判据)。 */\nconst THINKING_WRAPPER_RE = /<\\s*\\/?\\s*(analysis|summary|thinking|scratchpad|thought)\\b/i\n\n/**\n * F28 取证开关:把原始 SSE 逐行落盘,给「孤儿思考归正文」这类通道错位定论用。\n * 默认关;设环境变量 `DSH_WEB_LOGIN_DUMP_SSE=1` 开启(需重启 DSH 生效)。\n * 文件写到 `~/.dsh/deepseek-web/frames/<时间戳>-<序号>.sse`,逐行 append ——\n * 就算进程被强杀,已收到的帧也在盘上(F24 的教训:别用构造帧当证据,要抓真实帧)。\n */\nfunction dumpSinkPath(): string | null {\n if (process.env.DSH_WEB_LOGIN_DUMP_SSE !== '1') return null\n try {\n const dir = join(homedir(), '.dsh', 'deepseek-web', 'frames')\n mkdirSync(dir, { recursive: true })\n const stamp = new Date().toISOString().replace(/[:.]/g, '-')\n return join(dir, `${stamp}-${Math.random().toString(36).slice(2, 8)}.sse`)\n } catch {\n return null\n }\n}\n\nexport function createSseState(options: SseStateOptions = {}) {\n const fragments: Fragment[] = []\n /** fragments 派生文本(仅用于快照对账候选)。 */\n let fragmentsText = ''\n let fragmentsThinking = ''\n /** 直连格式的派生文本(仅用于快照对账候选)。 */\n let directText = ''\n let directThinking = ''\n /** 已发射的规范流(只增不减)。 */\n let outText = ''\n let outThinking = ''\n let divergences = 0\n let sink: 'fragments' | 'thinking' | 'content' | null = null\n /**\n * F25:通道未知的暂存文本。只在「请求开了思考、但还没出现任何 fragment」时使用\n * —— 正常流不会走到这里(首帧快照就带着 THINK fragment)。\n */\n let orphanBuffer = ''\n const thinkingEnabled = options.thinkingEnabled === true\n let pendingFinish: string | undefined\n let sawData = false\n /** 服务端上报的本消息 token 总量(见 WebStreamEvent 的 totalTokens 说明)。 */\n let totalTokens: number | undefined\n\n const emit = (out: WebStreamEvent[], kind: 'text' | 'thinking', delta: string): void => {\n if (!delta) return\n if (kind === 'text') outText += delta\n else outThinking += delta\n out.push({ kind, text: delta })\n }\n const emitText = (out: WebStreamEvent[], delta: string): void => emit(out, 'text', delta)\n const emitThinking = (out: WebStreamEvent[], delta: string): void => emit(out, 'thinking', delta)\n\n /**\n * F25:把暂存文本按**刚出现的 fragment 类型**归属并发射。\n *\n * 真实帧(2026-09-14 抓,4 轮同构)显示两条规律:\n * 1. 思考的 fragment 由**首帧快照**建立;正文的 fragment 由 `response/fragments`\n * 的 APPEND 建立(`fragments+1 [RESPONSE]`);\n * 2. 第一个 RESPONSE fragment 出现之前,流上的内容**全是思考**。\n *\n * 所以无论先到的是 THINK 还是 RESPONSE fragment,暂存的那段都应归思考 ——\n * 正文不会\"先于自己的 fragment\"出现在流上。这样即使快照整帧丢失,\n * 思考仍会回到思考通道,而不是被当成正文顶到用户脸上。\n */\n const settleOrphans = (out: WebStreamEvent[], firstType: string): void => {\n if (!orphanBuffer) return\n const text = orphanBuffer\n orphanBuffer = ''\n if (!isReasoningType(firstType)) {\n // 正文 fragment:它之前的内容只可能是思考(见上面的规律 2),仍归思考。\n }\n directThinking += text\n emitThinking(out, text)\n }\n\n /** 快照对账:只在候选是严格延伸时补差;过期/分歧忽略(宁可漏一次快照,也不吐乱码或丢字)。 */\n const reconcile = (out: WebStreamEvent[], kind: 'text' | 'thinking', candidate: string): void => {\n const current = kind === 'text' ? outText : outThinking\n if (!candidate || candidate === current) return\n if (candidate.startsWith(current)) {\n emit(out, kind, candidate.slice(current.length))\n return\n }\n if (current.startsWith(candidate)) return // 过期(更短)快照\n divergences += 1 // 分歧:忽略\n }\n\n /** 重建 fragments 派生文本(快照覆盖时用)。 */\n const rebuildFragmentText = (): void => {\n fragmentsText = ''\n fragmentsThinking = ''\n for (const fragment of fragments) {\n if (isReasoningType(fragment.type)) fragmentsThinking += fragment.content\n else fragmentsText += fragment.content\n }\n }\n /** 快照:整表替换 + 对账(不直接发射)。 */\n const replaceFragments = (list: any[], out: WebStreamEvent[]): void => {\n fragments.length = 0\n for (const f of list) {\n if (f && typeof f === 'object' && typeof f.content === 'string') {\n fragments.push({ type: String(f.type ?? 'RESPONSE'), content: f.content, emitted: 0 })\n }\n }\n rebuildFragmentText()\n sink = fragments.length > 0 ? 'fragments' : null\n // F25:快照迟到时(正常是流的第一帧),先把之前\"无归属\"的文本结算掉。\n if (fragments.length > 0) settleOrphans(out, fragments[0].type)\n }\n /** 增量:追加 fragment(其 content 属于新内容 → 直接发射)。 */\n const appendFragments = (incoming: any, out: WebStreamEvent[]): void => {\n const list = Array.isArray(incoming) ? incoming : incoming !== undefined ? [incoming] : []\n let settled = false\n for (const f of list) {\n if (!f || typeof f !== 'object' || typeof f.content !== 'string') continue\n const fragment: Fragment = { type: String(f.type ?? 'RESPONSE'), content: f.content, emitted: 0 }\n // F25:第一个 fragment 出现之前攒下的文本先定归属(必须在发射本 fragment 之前)。\n if (!settled) {\n settled = true\n settleOrphans(out, fragment.type)\n }\n fragments.push(fragment)\n if (isReasoningType(fragment.type)) {\n fragmentsThinking += fragment.content\n emitThinking(out, fragment.content)\n } else {\n fragmentsText += fragment.content\n emitText(out, fragment.content)\n }\n }\n sink = fragments.length > 0 ? 'fragments' : null\n }\n /** 增量:续写最后一个 fragment。 */\n const appendToLastFragment = (text: string, out: WebStreamEvent[]): void => {\n const fragment = fragments[fragments.length - 1]\n if (!fragment) {\n // 没有 fragment 可续 → 这段文字不是\"续写 fragment\",而是**当前通道的裸续段**,必须尊重 sink。\n //\n // F24(实测):思考阶段服务端就是分两步下发的 —— 先 `response/thinking_content` 发开头\n // 一小段(建立 sink='thinking'),再用 `response/fragments/-1/content` 发**思考的其余全部**。\n // 旧实现在这里无条件当正文发射,于是一整段思考被上屏:268 条 assistant 消息里 7 条中招\n // (5016~14877 字符),且这些正文块开头都缺 2~4 个字符(\" me analyze…\" 本该是\n // \"Let me analyze…\")—— 缺掉的正是先走 thinking 通道的那一小段,两处现象由这一分支同时解释。\n //\n // sink 未建立时**保持旧行为**(当正文):服务端也可能首帧就发 -1/content,\n // 那种情况没有通道信息可用,当正文是唯一合理的兜底(不能为了修这个而丢字)。\n if (sink === 'thinking') {\n directThinking += text\n emitThinking(out, text)\n return\n }\n if (sink === 'content') {\n directText += text\n emitText(out, text)\n return\n }\n // F25:请求开了思考、却还没有任何 fragment 可续 —— 正常流里首帧快照一定带 THINK\n // fragment,走到这里说明那份快照没能进入状态机(丢失 / fragments 为空)。\n // 这段文字极可能是思考的尾巴,先攒着,等 fragment 出现再定归属(见 settleOrphans)。\n // 旧实现在这里无条件当正文发射,就是\"整段思考上屏\"的直接原因。\n if (thinkingEnabled) {\n orphanBuffer += text\n return\n }\n directText += text\n emitText(out, text)\n return\n }\n fragment.content += text\n if (isReasoningType(fragment.type)) {\n fragmentsThinking += text\n emitThinking(out, text)\n } else {\n fragmentsText += text\n emitText(out, text)\n }\n }\n /** 增量:裸续段按当前 sink 归属。 */\n const appendSink = (text: string, out: WebStreamEvent[]): void => {\n if (sink === 'thinking') {\n directThinking += text\n emitThinking(out, text)\n } else if (sink === 'content') {\n directText += text\n emitText(out, text)\n } else if (sink === 'fragments') {\n appendToLastFragment(text, out)\n } else {\n // 0.1.82:`sink` 还没定(快照整帧丢失 / `fragments` 为空)时**不能丢字** ——\n // 与 `appendToLastFragment` 的兜底对齐。按本文件自己的纪律:\n // 两种错法代价不对等(当正文吐出去最多是难看,丢掉就是正文缺一段)。\n directText += text\n emitText(out, text)\n }\n }\n\n return {\n /** 负载处理(增量直接发射;快照只对账)。 */\n handlePayload(d: any, eventName?: string): WebStreamEvent[] {\n const out: WebStreamEvent[] = []\n sawData = true\n // 0) 首帧 `event: ready`:把本轮 assistant 的 message_id 交给调用方(链式投喂要用)。\n // 不 return:这里的判据只看字段本身,`ready` 的负载不会命中下面的任何分支,\n // 这样即使服务端某天不带 `event: ready` 那行,只要字段还在就照样能拿到。\n if (d && typeof d === 'object' && typeof (d as any).response_message_id === 'number') {\n options.onResponseMessageId?.((d as any).response_message_id)\n }\n // 1) 完整 response 快照\n if (d && typeof d === 'object' && d.v && typeof d.v === 'object' && d.v.response && typeof d.v.response === 'object') {\n const response = d.v.response\n if (Array.isArray(response.fragments)) {\n replaceFragments(response.fragments, out)\n // fragments 存在时以它为准;否则用 content\n if (fragments.length > 0) {\n reconcile(out, 'thinking', fragmentsThinking)\n reconcile(out, 'text', fragmentsText)\n }\n }\n if (typeof response.content === 'string') {\n directText = response.content\n sink = 'content'\n if (fragments.length === 0) reconcile(out, 'text', directText)\n }\n if (response.finish_reason !== undefined && response.finish_reason !== null) {\n pendingFinish = String(response.finish_reason)\n }\n return out\n }\n // 2) 模型错误事件:按语义归类(并发生成 → 可重试的 RATE_LIMIT),调用方据此决定重试还是报错\n if (d && typeof d === 'object' && d.type === 'error') {\n const message = typeof d.content === 'string' ? d.content : typeof d.message === 'string' ? d.message : 'model error'\n const event: WebStreamEvent & { raw?: string } = {\n kind: 'error',\n message,\n ...(d.finish_reason !== undefined ? { raw: String(d.finish_reason) } : {}),\n }\n if (isBusyGenerating(message)) {\n event.code = 'RATE_LIMIT'\n event.retryAfterMs = 5_000\n event.rateLimitKind = 'concurrent'\n } else if (isThrottled(message)) {\n // 账号级节流(「消息发送过于频繁,请稍后重试」):连续被限就退避渐长,别一直撞\n event.code = 'RATE_LIMIT'\n event.retryAfterMs = noteThrottled()\n event.rateLimitKind = 'throttled'\n }\n out.push(event)\n return out\n }\n // 3) SSE 命名事件(title 忽略;toast 视为提示性错误)\n if (eventName === 'toast') {\n const message = d && typeof d === 'object' ? (d.content ?? d.message ?? JSON.stringify(d)) : String(d)\n const full = `DeepSeek toast: ${String(message).slice(0, 200)}`\n const event: WebStreamEvent = { kind: 'error', message: full }\n if (isBusyGenerating(full)) {\n event.code = 'RATE_LIMIT'\n event.retryAfterMs = 5_000\n event.rateLimitKind = 'concurrent'\n } else if (isThrottled(full)) {\n event.code = 'RATE_LIMIT'\n event.retryAfterMs = noteThrottled()\n event.rateLimitKind = 'throttled'\n }\n out.push(event)\n return out\n }\n if (eventName === 'title') return out\n // 4) 顶层 finish_reason\n if (d && typeof d === 'object' && d.finish_reason !== undefined && d.finish_reason !== null) {\n pendingFinish = String(d.finish_reason)\n return out\n }\n const path: string | undefined = d?.p\n const value = d?.v\n if (typeof path === 'string') {\n switch (path) {\n case 'response/fragments':\n appendFragments(value, out)\n return out\n case 'response/fragments/-1/content': {\n if (typeof value === 'string') {\n appendToLastFragment(value, out)\n // F24:只有**真的续到 fragment 上**才把 sink 切到 'fragments'。\n // fragments 为空但已建立 thinking/content 通道时必须保持原通道 ——\n // 否则紧接着的裸续段会因 sink='fragments' 又退回\"当正文\",等于没修。\n const keepChannel = fragments.length === 0 && (sink === 'thinking' || sink === 'content')\n if (!keepChannel) sink = 'fragments'\n }\n return out\n }\n case 'response/fragments/-1/elapsed_secs': {\n // F27:这是「刚结束的是一段**思考**」的直接证据 —— 真实帧里它紧跟 THINK fragment\n // 出现、值就是该段思考的耗时(正文 fragment 上的这个字段是 null)。\n // 快照丢失时,它能告诉我们暂存的那段文本到底属于哪个通道。\n if (typeof value === 'number' && value > 0) settleOrphans(out, 'THINK')\n return out\n }\n case 'response/thinking_content':\n if (typeof value === 'string') {\n directThinking += value\n emitThinking(out, value)\n sink = 'thinking'\n }\n return out\n case 'response/content':\n if (typeof value === 'string') {\n directText += value\n emitText(out, value)\n sink = 'content'\n }\n return out\n case 'response/finish_reason':\n if (typeof value === 'string') pendingFinish = value\n return out\n case 'accumulated_token_usage':\n // 兼容:万一服务端直接以顶层路径下发(真实样本是裹在 response/BATCH 里的,见上)。\n // ⚠️ 快照里的那个字段初始恒为 0(status 还是 WIP),**不要**拿它当结果 ——\n // 判定脚本第一版就是取了末尾快照的 0,结论整个反过来。\n if (typeof value === 'number' && Number.isFinite(value)) totalTokens = value\n return out\n case 'response/status':\n if (typeof value === 'string') {\n out.push({ kind: 'status', value })\n if (value === 'FINISHED') pendingFinish = pendingFinish ?? 'FINISHED'\n }\n return out\n case 'response': {\n if (Array.isArray(value)) {\n for (const op of value) {\n if (op && typeof op === 'object' && op.p === 'fragments' && op.o === 'APPEND' && op.v !== undefined) {\n appendFragments(op.v, out)\n }\n // 真实形态(2026-09-13 抓包):\n // {\"p\":\"response\",\"o\":\"BATCH\",\"v\":[{\"p\":\"accumulated_token_usage\",\"v\":38}, …]}\n // 它在**内层 op** 里,不是顶层 case —— 第一版补丁插错层级,测试直接抓到。\n if (op && typeof op === 'object' && op.p === 'accumulated_token_usage') {\n if (typeof op.v === 'number' && Number.isFinite(op.v)) totalTokens = op.v\n }\n }\n }\n return out\n }\n default:\n return out\n }\n }\n // 5) 无 path 的续段:承接当前 sink\n if (typeof value === 'string' && value.length > 0) appendSink(value, out)\n return out\n },\n /** 对外入口(负载已直接发射增量,这里只做兜底对账)。 */\n handle(d: any, eventName?: string): WebStreamEvent[] {\n return this.handlePayload(d, eventName)\n },\n /** 流结束:产出 finish(若确实收到过数据)。 */\n finish(): WebStreamEvent[] {\n const out: WebStreamEvent[] = []\n // F27(2026-09-14,修订 F25 的兜底):整轮都没等到任何 fragment 时**按正文收尾**。\n //\n // F25 当初按\"开了思考就归思考\"收尾,理由是\"避免以正文的名义补发思考\"。但那个方向\n // 有一个更糟的失败模式:**万一暂存里其实是正文,回答就从界面上消失了** ——\n // 用户只看到一段思考、正文空白,会以为模型没回答(实测反馈:\"正文的内容卡在思考里\")。\n // 两种错法的代价不对等:\n // · 思考被显示到正文 → 内容都在,用户读得懂这是思考;\n // · 正文被吞进思考 → 用户看不到回答,是明确的功能故障。\n // 所以无线索时一律归正文。有 fragment 的正常/异常路径不受影响(由 settleOrphans 结算)。\n if (sawData && orphanBuffer) {\n const text = orphanBuffer\n orphanBuffer = ''\n // F28(2026-09-14,第二次修订兜底):兜底方向维持 F27 的\"归正文\",但**加一条标签判据** ——\n // 孤儿文本以 <analysis>/<summary> 这类**思考的标准包装**为主体时归思考。\n // 依据(实测 [998]/[1091] 两条消息):text 块 27397 字**整块**都是 analysis+summary 复盘、\n // 没有一句对用户说的话 —— 模型不会把整条回答写成纯复盘 ⇒ 那是思考。\n // 反过来,普通正文几乎不会以这些标签为主体,误伤面很小。\n // 其余无线索的孤儿仍按正文收尾(F27 的原则不变:看不到回答比看到思考更糟)。\n if (thinkingEnabled && THINKING_WRAPPER_RE.test(text)) {\n directThinking += text\n emitThinking(out, text)\n } else {\n directText += text\n emitText(out, text)\n }\n }\n if (!sawData) return out\n out.push({ kind: 'finish', reason: pendingFinish, ...(totalTokens !== undefined ? { totalTokens } : {}) })\n return out\n },\n /** 诊断:已发射正文/思考长度与快照分歧次数(单测与排查用)。 */\n stats(): { text: string; thinking: string; divergences: number; orphanLen?: number; totalTokens?: number } {\n return {\n text: outText,\n thinking: outThinking,\n divergences,\n ...(orphanBuffer ? { orphanLen: orphanBuffer.length } : {}),\n ...(totalTokens !== undefined ? { totalTokens } : {}),\n }\n },\n }\n}\n\n/** 解析 /chat/completion 的 SSE 字节流,产出增量文本/思考事件。 */\nexport async function* parseWebSse(body: any, options?: SseStateOptions): AsyncGenerator<WebStreamEvent> {\n const state = createSseState(options)\n let eventName = ''\n /**\n * F13(2026-09-12 审计):SSE 规范允许一个事件里出现**多个** `data:` 行,\n * 收齐后要用 `\\n` 拼接再整体解析。旧实现逐行 `JSON.parse`,一旦服务端把一个\n * JSON 拆到多行(或 payload 里本身含换行),每行都解析失败 → 被\n * `catch { continue }` 静默丢弃,表现为「流突然断了/少了一段」且无任何报错。\n */\n let dataLines: string[] = []\n const flushData = (): { events: WebStreamEvent[]; done: boolean } => {\n if (dataLines.length === 0) return { events: [], done: false }\n const data = dataLines.join('\\n').trim()\n dataLines = []\n if (data.length === 0) return { events: [], done: false }\n if (data === '[DONE]') return { events: Array.from(state.finish()), done: true }\n let parsed: any\n try {\n parsed = JSON.parse(data)\n } catch {\n return { events: [], done: false }\n }\n return { events: Array.from(state.handle(parsed, eventName)), done: false }\n }\n\n // F28 取证:开关开着就把原始帧逐行落盘(见 dumpSinkPath 的说明),失败不影响主流程。\n const dumpPath = dumpSinkPath()\n for await (const line of iterateLines(body)) {\n if (dumpPath) {\n try {\n appendFileSync(dumpPath, line + '\\n')\n } catch {\n /* 取证是尽力而为 */\n }\n }\n if (line.length === 0) {\n // 空行 = 事件结束\n const flushed = flushData()\n for (const event of flushed.events) yield event\n if (flushed.done) return\n eventName = ''\n continue\n }\n if (line.startsWith(':')) continue\n if (line.startsWith('event:')) {\n // 新事件名出现 = 上一个事件结束(有些实现不补空行,这里也要收口)\n const flushed = flushData()\n for (const event of flushed.events) yield event\n if (flushed.done) return\n eventName = line.slice(6).trim()\n continue\n }\n if (line.startsWith('data:')) {\n dataLines.push(line.slice(5).trim())\n continue\n }\n }\n // 流结束:末尾可能没有空行,攒下的 data 不能丢\n const tail = flushData()\n for (const event of tail.events) yield event\n if (tail.done) return\n for (const event of state.finish()) yield event\n}\n\n// ── 完成请求 ──────────────────────────────────────────────\n\n// ── 会话复用(2026-09-12 实测判定后新增)────────────────────\n\n/**\n * 复用一个网页端会话最多发多少次请求,超过就换一个新的;0 = 关闭复用(回到「每请求一个会话」)。\n *\n * 为什么可以复用(**实测**,不是推理):\n * 每次 completion 都发 `parent_message_id: null` → 每条消息都是会话里的**根消息**、没有父链,\n * 服务端按消息树回溯上下文时回溯到空。判定实验(2026-09-12):同一会话先发\n * 「记住编号 ZC-7391-KX,只回 OK」→ 得到 `OK`;再问「编号是什么」→ 答 `不知道`。\n * 证明同会话历史**不会**进入上下文。\n * (0.1.21 注释里「复用会让上下文翻倍」的说法是未经实测的推理,已被这次实验推翻。)\n *\n * 收益:实测 2026-09-12 一天建了 182 个网页端会话(峰值 74 个/小时、最密 8 个/分钟),\n * 因为每个 DSH 回合 = 建一个会话、用完再删一个 —— 真人不会这样建删对话。\n * 复用后建会话数降到「轮次 / N」。\n */\nexport const DEFAULT_SESSION_REUSE_TURNS = 20\n\n/** 复用槽:同一账号当前可复用的会话。key 是凭证摘要(不进日志、不拿明文当键)。 */\n/**\n * 复用槽。`cleanup` 记录**这个会话归谁回收**(2026-09-13 审计 N04):\n * 切号时旧槽要交回**原账号**的清理回调,不能用当前账号的去删别人的会话。\n */\nlet reuseSlot: { key: string; sessionId: string; turns: number; cleanup?: (id: string) => void } | undefined\n\n/**\n * 链式投喂的链状态(2026-09-14)。只跟随**正在复用的那个会话**:\n * 会话轮换、切号、请求失败/取消、流被污染,都会让它作废 —— 下一轮自动退回全量重发。\n * 判定逻辑在 context-feed.ts(纯函数),这里只负责\"喂进去 + 按结果记下来\"。\n */\nlet contextChain: ChainState | undefined\n\n/**\n * 服务端**已知**的图片 file_id(0.1.83):本会话内已经随请求发出去过的那批。\n *\n * 只用于链式投喂:走 `chained`(有父链)时服务端会回溯历史、那张图已经在它的上下文里,\n * 于是每轮重带整批是纯冗余 —— 真机实测见 tests/probe-image-chain.mjs(两张不同布局的图、\n * 第二轮都不带 `ref_file_ids`,仍都答对四个角)。\n * 会话换掉就整批作废:新会话没有那份历史。\n */\nlet sentRefIds = new Set<string>()\n/** `sentRefIds` 归属的会话 id(见上)。 */\nlet sentRefIdsSession: string | undefined\n\n/**\n * 上一次上报过的决策原因(0.1.63)。链式投喂的决策每轮都在做,\n * 但\"原因\"通常连续几百轮都不变 —— 只在**变化时**上报,日志才不会被刷满,\n * 同时\"哪一轮开始退回全量、为什么\"又一定能看见。\n */\nlet lastFeedReason: FeedReason | undefined\n\n/** 丢弃当前的链(会话退役/测试隔离用)。 */\nexport function resetContextChain(): void {\n contextChain = undefined\n lastFeedReason = undefined\n}\n\n/** 给状态页看:当前链式投喂是否真的在跑(没用链式就返回 undefined)。 */\nexport function contextChainInfo(): { sessionId: string; turns: number; parentId: number } | undefined {\n if (!contextChain) return undefined\n return {\n sessionId: contextChain.sessionId,\n turns: contextChain.entries.length,\n parentId: contextChain.parentId,\n }\n}\n\n/** 凭证摘要:只用来判断「是不是同一个账号」。不做安全用途、不落日志。 */\nfunction accountKey(auth: WebAuth): string {\n const raw = `${auth?.token ?? ''}|${auth?.cookie ?? ''}`\n let hash = 2166136261\n for (let i = 0; i < raw.length; i += 1) {\n hash ^= raw.charCodeAt(i)\n hash = Math.imul(hash, 16777619)\n }\n return (hash >>> 0).toString(36)\n}\n\ninterface SessionLease {\n sessionId: string\n reused: boolean\n}\n\nasync function leaseSession(\n auth: WebAuth,\n signal: AbortSignal,\n transport: CompletionTransport,\n maxTurns: number,\n cleanup?: (id: string) => void,\n): Promise<SessionLease> {\n signal.throwIfAborted()\n const key = accountKey(auth)\n const limit = Number.isFinite(maxTurns) ? Math.max(0, Math.floor(maxTurns)) : DEFAULT_SESSION_REUSE_TURNS\n // 关闭复用:每次都要新会话(调用方会自己回收)\n if (limit === 0) return { sessionId: await transport.createSession(auth, signal), reused: false }\n if (reuseSlot && reuseSlot.key === key && reuseSlot.turns < limit) {\n reuseSlot.turns += 1\n return { sessionId: reuseSlot.sessionId, reused: true }\n }\n // ⚠️ N04:轮换/切号时,旧槽必须交给**它自己的** cleanup 归还。\n // 旧实现只在\"同账号轮换\"时返回 retired、切号时直接覆盖旧槽 ——\n // 后者等于把旧会话的清理归属丢掉了(永远不会有人删它)。\n const previous = reuseSlot\n const sessionId = await transport.createSession(auth, signal)\n if (signal.aborted) {\n // 建会话期间被取消:这个会话还没人认领,就地回收,别留垃圾\n try {\n cleanup?.(sessionId)\n } catch {}\n signal.throwIfAborted()\n }\n reuseSlot = { key, sessionId, turns: 1, ...(cleanup ? { cleanup } : {}) }\n // 落账:这个会话进了复用槽,此刻**还没删**。宿主据此落盘 —— 否则进程被强杀时\n // 槽里的会话(每次退出必留一个)永远没人回收。\n emitSessionLifecycle({ kind: 'leased', auth, sessionId })\n if (previous) {\n try {\n previous.cleanup?.(previous.sessionId)\n } catch {}\n }\n return { sessionId, reused: false }\n}\n\n/** 把某个会话从复用槽里摘掉(会话失效 / 请求失败时调用,下次会新建)。 */\nexport function retireSession(sessionId?: string): void {\n if (!sessionId || (reuseSlot && reuseSlot.sessionId === sessionId)) reuseSlot = undefined\n // 会话被退役 ⇒ 它的链也失效(留着会让下一轮\"续\"到一个已经不存在的父消息上)。\n if (!sessionId || contextChain?.sessionId === sessionId) contextChain = undefined\n}\n\n/**\n * 卸载/退出时的收尾:把复用槽里的会话**交回它自己的清理回调**,然后清空槽。\n *\n * 为什么必须单独有这个函数(2026-09-14):`retireSession()` 只是把槽清掉,\n * 排队删除是槽里那个 `cleanup` 干的活 —— 直接清槽等于把待删的会话一起丢了,\n * 它就会永远留在网页端(每次退出必留一个,实测就是这样堆起来的)。\n *\n * 返回被退役的 sessionId(没有则 `undefined`),调用方可以据此记账/打日志。\n * 注意它**只排队**、不等删除完成;删不掉的部分由 `session-journal.ts` 兜底,\n * 记录只在\"确认删掉\"时才被摘掉,所以强杀也能在下次启动补删。\n */\nexport function disposeSessionReuse(): string | undefined {\n const slot = reuseSlot\n contextChain = undefined\n // 0.1.83:图片的\"服务端已知\"集合同样归会话所有,会话退役就作废\n sentRefIds = new Set()\n sentRefIdsSession = undefined\n if (!slot) return undefined\n reuseSlot = undefined\n try {\n slot.cleanup?.(slot.sessionId)\n } catch {\n /* 排队失败不影响卸载 */\n }\n return slot.sessionId\n}\n\n/** 只给测试用:清空复用槽。 */\nexport function resetSessionReuse(): void {\n reuseSlot = undefined\n contextChain = undefined\n // 决策回执的状态也是模块级的(见 lastFeedReason),一并清掉,测试之间才互不干扰\n lastFeedReason = undefined\n // 0.1.83:图片的\"服务端已知\"集合也是模块级的,一并清掉\n sentRefIds = new Set()\n sentRefIdsSession = undefined\n}\n\n/** 链式投喂的决策回执(见 CompletionParams.onContextFeed)。 */\nexport interface FeedReport {\n /** 决策原因 —— chained = 真的发了增量;其余都是\"退回全量\"的具体理由。 */\n reason: FeedReason\n /** true = 本轮只发了增量;false = 本轮重发了全量 prompt。 */\n chained: boolean\n /** 实际写进请求体的 prompt 长度(chained 时就是增量大小)。 */\n promptChars: number\n}\n\nexport interface CompletionParams {\n prompt: string\n /**\n * 链式投喂用:`prompt` 的结构化拆分(`head` = 系统+协议+工具目录,`entries` = **未截断**的历史条目)。\n * 不传 = 算不出\"新增了哪几条\",只能走全量 —— 适配器两条序列化路径都要传,\n * 漏传会让链式模式静默退化成全量(靠 tests/check-bundle.mjs 的产物断言守)。\n */\n promptParts?: { head: string; entries: readonly string[]; maxChars?: number }\n /**\n * 链式投喂的决策回执(0.1.63)。**只在决策原因变化时**回调一次,\n * 用来回答\"这一轮到底发了增量,还是退回全量、因为哪条判据\"——\n * 没有它,0.1.62 的链式投喂在日志里是完全不可见的。\n */\n onContextFeed?: (report: FeedReport) => void\n thinkingEnabled: boolean\n searchEnabled?: boolean\n modelType: 'default' | 'expert' | 'vision'\n /** 已上传文件的 file_id(图片输入:随请求引用,模型据此看图)。 */\n refFileIds?: readonly string[]\n /** 与 refFileIds **一一对应**的内容寻址 key(attachmentId,sha256:…)。\n * 0.2.0:sentRefIds 改按 key 记账 —— uploadCache 驱逐重传会换 fileId,\n * 按 id 记账会把同一张图记成\"两张\",重新打开每轮重发的口子。 */\n refKeys?: readonly string[]\n signal?: AbortSignal\n idleTimeoutMs?: number\n /**\n * 建连阶段(建会话 + PoW + 等到响应头)的整体期限,默认 45 秒。\n * 抽成可注入是为了能离线验证「建连挂住必须被中断」——这段原本没有统一限时,\n * 是审计 F10 指出的\"可无限等待\"(idle watchdog 要到响应头之后才启动)。\n */\n connectTimeoutMs?: number\n /** 同一会话复用的轮次上限(0 = 每请求一个会话,用完即删)。 */\n sessionReuseTurns?: number\n onDeleteSession?: (sessionId: string) => void\n}\n\n/**\n * 会话/请求的可注入传输层(默认就是真实实现)。\n * 抽出来是为了能在单测里确定性地复现「会话失效 → 重建重试」与「删除时机」这两条路径,\n * 不必真的打网络(这两处正是反复出问题的地方)。\n */\nexport interface CompletionTransport {\n createSession: (auth: WebAuth, signal?: AbortSignal) => Promise<string>\n powHeader: (auth: WebAuth, targetPath: string, signal?: AbortSignal) => Promise<string>\n}\n\nconst defaultTransport: CompletionTransport = { createSession: createChatSession, powHeader: createPowHeader }\n\n/**\n * 打开一次 completion 请求(建会话 + PoW + 发送),返回可用的会话与响应。\n *\n * 非 SSE 响应(HTTP 200 上裹着业务错误信封)在这里统一裁决:\n * - 会话失效(invalid chat session id)→ **换一个新会话透明重试一次**(用户无感);\n * - 其它业务错误 → 按业务码抛出(AUTH / RATE_LIMIT / PROVIDER_ERROR…)。\n */\nasync function openCompletion(\n auth: WebAuth,\n params: CompletionParams,\n signal: AbortSignal,\n transport: CompletionTransport,\n): Promise<{ sessionId: string; resp: Response; feed: FeedDecision }> {\n let lastFailure: AdapterLlmError | undefined\n for (let attempt = 0; attempt < 2; attempt++) {\n const lease = await leaseSession(\n auth,\n signal,\n transport,\n params.sessionReuseTurns ?? DEFAULT_SESSION_REUSE_TURNS,\n params.onDeleteSession,\n )\n const sessionId = lease.sessionId\n // 链式投喂:决定本轮发全量还是增量、parent 指向谁。判据在 context-feed.ts(纯函数):\n // 模式=chained 且「复用了同一会话 + 条目严格追加 + 头部/账号都没变」才发增量,\n // 任何一条不满足都退回全量 + parent=null(= 0.1.61 及以前的行为)。\n const feed = decideFeed({\n mode: currentContextMode(),\n ...(params.promptParts\n ? {\n head: params.promptParts.head,\n entries: params.promptParts.entries,\n ...(params.promptParts.maxChars !== undefined ? { maxChars: params.promptParts.maxChars } : {}),\n }\n : {}),\n full: params.prompt,\n sessionId,\n accountKey: accountKey(auth),\n reused: lease.reused,\n ...(contextChain ? { chain: contextChain } : {}),\n })\n // 决策回执(0.1.63):只在原因变化时上报一次,让日志能回答\"这一轮为什么没走增量\"。\n if (feed.reason !== lastFeedReason) {\n lastFeedReason = feed.reason\n params.onContextFeed?.({\n reason: feed.reason,\n chained: feed.parentMessageId !== null,\n promptChars: feed.prompt.length,\n })\n }\n // ── 0.1.83:图片只发\"服务端还没见过的\" ────────────────────────────────\n // 旧行为每轮都带整批:`adapter.ts` 的 `refFileIds: rounds === 0 ? refFileIds : []`\n // 本是给「自动续写」用的判据(第 2 轮不重带),而**链式投喂每轮都是新的 streamImpl 调用**\n // ⇒ `rounds` 恒为 0 ⇒ 顺带每轮都重挂一遍(网页端每条新消息下都挂一批图、会话内引用单调累积)。\n //\n // 判据放在**决策点**(`decideFeed` 之后),而不是用\"上一轮是什么\"去预测:\n // · 有父链 ⇒ 服务端手里已有 ⇒ 只发新增的那几张(多数轮是 0 张)\n // · 全量重发(restart / 新会话 / 切号 / head 变了)⇒ 服务端手里没有 ⇒ 发全部\n // ⚠️ 不能写成\"chained 就完全不带\":用户**这一轮新贴**的图只存在于增量里,漏了就是功能坏。\n if (sentRefIdsSession !== sessionId) {\n sentRefIds = new Set()\n sentRefIdsSession = sessionId\n }\n // 0.2.0:每项携带着与 id 一一对应的 key(缺省退回 id 本身,向后兼容)。\n // sentRefIds 按 **key** 记账:fileId 会随 uploadCache 驱逐重传而变,\n // key 是内容寻址的稳定身份 —— \"服务端见没见过这张图\"不该取决于这一次用哪个 fileId 引用。\n const askedRefItems = (params.refFileIds ?? []).map((id, index) => ({\n id,\n key: params.refKeys?.[index] ?? id,\n }))\n const refItemsToSend =\n feed.parentMessageId !== null ? askedRefItems.filter((item) => !sentRefIds.has(item.key)) : askedRefItems\n const refIdsToSend = refItemsToSend.map((item) => item.id)\n\n let resp: Response\n try {\n resp = await activeFetch(`${DS_BASE}/api/v0/chat/completion`, {\n method: 'POST',\n headers: {\n ...buildDsHeaders(auth, `${DS_BASE}/a/chat/s/${sessionId}`),\n accept: 'text/event-stream',\n 'x-ds-pow-response': await transport.powHeader(auth, '/api/v0/chat/completion', signal),\n },\n body: JSON.stringify({\n chat_session_id: sessionId,\n // 链式投喂时是上一条 assistant 的 message_id;全量模式恒为 null(根消息、无父链)。\n parent_message_id: feed.parentMessageId,\n prompt: feed.prompt,\n ref_file_ids: refIdsToSend,\n thinking_enabled: params.thinkingEnabled,\n search_enabled: params.searchEnabled ?? false,\n model_type: params.modelType,\n action: null,\n preempt: false,\n }),\n signal,\n })\n } catch (error: any) {\n retireSession(sessionId)\n params.onDeleteSession?.(sessionId)\n // ⚠️ N04:**先放行已经是 AdapterLlmError 的错误**。\n // 旧写法无条件包成 TRANSPORT,会把 PoW/网络层带出来的 AUTH / RATE_LIMIT\n // 等结构化分类抹掉 —— 宿主于是按\"可重试的传输错误\"处理本该停止重试的情况。\n if (error instanceof AdapterLlmError) throw error\n if (params.signal?.aborted) throw new AdapterLlmError('DeepSeek web request aborted by caller', 'ABORTED', { cause: error })\n throw new AdapterLlmError(`DeepSeek web request failed: ${error?.message ?? error}`, 'TRANSPORT', { cause: error })\n }\n\n if (!resp.ok) {\n const text = await resp.text().catch(() => '')\n const code = httpErrorCode(resp.status)\n const retryAfter = parseRetryAfterMs(resp.headers.get('retry-after'))\n const hint =\n code === 'AUTH'\n ? ' —— 网页登录态可能已过期,请到「设置 → DeepSeek 网页登录」重新登录'\n : code === 'RATE_LIMIT'\n ? ' —— 网页端频控(免费额度),稍后重试即可'\n : ''\n retireSession(sessionId) // 失败即弃,下次换新会话\n params.onDeleteSession?.(sessionId)\n throw new AdapterLlmError(\n `DeepSeek web completion failed (HTTP ${resp.status})${text ? `: ${text.slice(0, 200)}` : ''}${hint}`,\n code,\n { status: resp.status, ...(retryAfter !== undefined ? { providerRetryAfterMs: retryAfter } : {}), cause: new Error(text) },\n )\n }\n if (!resp.body) {\n retireSession(sessionId)\n params.onDeleteSession?.(sessionId)\n throw new AdapterLlmError('DeepSeek web completion returned no body', 'EMPTY_RESPONSE')\n }\n\n // HTTP 200 也可能是「业务错误信封」或 HTML 挑战页 —— 非 SSE 一律先当错误处理\n const contentType = String(resp.headers.get('content-type') ?? '')\n if (contentType.includes('text/event-stream')) {\n // 请求已被服务端接受 ⇒ 这批 file_id 进了它的上下文(下一轮起不必再带)。\n // ⚠️ 只在**接受之后**记:失败/被拒的请求不算,免得下一轮误以为服务端已经拿到了。\n for (const item of refItemsToSend) sentRefIds.add(item.key)\n return { sessionId, resp, feed }\n }\n\n const text = await resp.text().catch(() => '')\n let parsed: any\n try {\n parsed = JSON.parse(text)\n } catch {}\n const biz = envelopeError(parsed)\n const muted = isMutedError(biz)\n const busy = !muted && !!biz && isBusyGenerating(biz.msg)\n const untilMs = muteUntilMs(parsed)\n const failure = biz\n ? new AdapterLlmError(\n muted\n ? mutedMessage(untilMs)\n : busy\n ? 'DeepSeek 网页端同一账号同时只能生成一条消息(另一个窗口/标签页正在用同一账号生成)。这一步会自动重试;若两个窗口都要用网页模型,建议其中一个换 provider 或换账号。'\n : bizErrorMessage(biz.code, biz.msg),\n muted || busy\n ? 'RATE_LIMIT'\n : isInvalidSessionError(biz)\n ? 'TRANSPORT'\n : isInvalidRefFileError(biz)\n ? 'INVALID_REF_FILE'\n : bizErrorCode(biz.code),\n {\n status: resp.status,\n // 解除时间远大于重试策略的上限 → dsh-llm-retry 会直接放弃重试(而不是空转打请求)\n ...(muted && untilMs !== undefined ? { providerRetryAfterMs: Math.max(0, untilMs - Date.now()) } : {}),\n // 绝对值单独带一份:宿主会把它记到账号上,在设置页显示倒计时\n ...(muted && untilMs !== undefined ? { mutedUntilMs: untilMs } : {}),\n ...(busy ? { providerRetryAfterMs: 5_000 } : {}),\n },\n )\n : new AdapterLlmError(\n `DeepSeek 网页端返回了非流式响应(content-type: ${contentType || 'unknown'}):${text.slice(0, 200)}`,\n 'MALFORMED_RESPONSE',\n { status: resp.status },\n )\n retireSession(sessionId) // 这个会话已经废了,顺手回收,不留垃圾\n params.onDeleteSession?.(sessionId)\n if (attempt === 0 && biz && isInvalidSessionError(biz)) {\n lastFailure = failure\n continue\n }\n throw failure\n }\n throw lastFailure ?? new AdapterLlmError('DeepSeek 网页端无法建立可用会话', 'PROVIDER_ERROR')\n}\n\n/**\n * 发起一次网页版完成请求并流式产出事件;会话在**流结束之后**尽力删除。\n *\n * ⚠️ 删除时机是这个模块最容易被写错的地方(2026-09-11 实测故障):\n * 旧实现把 `onDeleteSession` 放在**建会话之后立刻**调用,而它内部是「延迟 1.5s 删除」,\n * 于是会话可能在 completion 请求发出之前就被自己删掉 —— 若 PoW 求解 + 建连超过 1.5s,\n * 服务端回\n * {\"code\":0,\"msg\":\"\",\"data\":{\"biz_code\":1,\"biz_msg\":\"invalid chat session id\"}}\n * 更隐蔽的是「生成进行到一半会话消失」,服务端可能直接掐断流 —— 表现就是回答说半句就停、\n * 工具调用没收全(正是我们一直在追的那类截断)。\n * 现在删除只发生在 finally(流正常结束、报错或调用方中止都算),会话在整个请求期间都活着。\n */\n/** 复用模式下的\"飞行互斥\":保证同一时刻只有一个复用请求在跑,避免轮换撞上并发。 */\nlet reuseFlightTail: Promise<void> = Promise.resolve()\n\nexport async function* streamWebCompletion(\n auth: WebAuth,\n params: CompletionParams,\n transport: CompletionTransport = defaultTransport,\n): AsyncGenerator<WebStreamEvent> {\n const controller = new AbortController()\n const signal = params.signal ? AbortSignal.any([params.signal, controller.signal]) : controller.signal\n const rawLimit = params.sessionReuseTurns ?? DEFAULT_SESSION_REUSE_TURNS\n const limit = Number.isFinite(rawLimit) ? Math.max(0, Math.floor(rawLimit)) : DEFAULT_SESSION_REUSE_TURNS\n\n let release: (() => void) | undefined\n let sessionId: string | undefined\n let iterator: AsyncGenerator<WebStreamEvent> | undefined\n let body: any\n let complete = false\n let poisoned = false\n let timer: ReturnType<typeof setTimeout> | undefined\n /** 本轮实际发出去了什么(链式投喂据此记账;见 finally 里的链更新)。 */\n let sentFeed: FeedDecision | undefined\n /** 首帧 `event: ready` 给的 assistant message_id —— 就是下一轮的 parent_message_id。 */\n let responseMessageId: number | undefined\n /** 同一个会话只回收一次(复用/轮换/失败三条路径可能都想回收它)。 */\n const deleted = new Set<string>()\n const cleanup = (id: string): void => {\n if (deleted.has(id)) return\n deleted.add(id)\n try {\n params.onDeleteSession?.(id)\n } catch {}\n }\n /** 让等待可被取消:abort 时立刻 reject,不等定时器/对端。 */\n const wait = <T>(promise: Promise<T>): Promise<T> =>\n new Promise((resolve, reject) => {\n if (signal.aborted) {\n promise.catch(() => {})\n reject(signal.reason)\n return\n }\n const abort = () => {\n signal.removeEventListener('abort', abort)\n reject(signal.reason)\n }\n signal.addEventListener('abort', abort, { once: true })\n promise.then(\n (value) => {\n signal.removeEventListener('abort', abort)\n resolve(value)\n },\n (error) => {\n signal.removeEventListener('abort', abort)\n reject(error)\n },\n )\n })\n const arm = (ms: number, message: string): void => {\n if (timer) clearTimeout(timer)\n timer = setTimeout(() => controller.abort(new AdapterLlmError(message, 'TIMEOUT')), ms)\n ;(timer as any).unref?.()\n }\n\n /**\n * F10:外层拥有本次调用创建过的**全部**会话。\n *\n * 建连阶段没有统一限时时,`createSession` 之后、响应头之前的任何失败都会让\n * 「已建出来但还没人认领」的会话漏在服务端;`openCompletion` 内部的失败分支只覆盖\n * 它自己 catch 得到的错误。超时/取消会**放弃**进行中的 `openCompletion`(不再等它),\n * 所以必须在这里兜住它后续才返回的那些会话。\n */\n const owned = new Set<string>()\n let finalized = false\n const tracked: CompletionTransport = {\n ...transport,\n createSession: async (value, sig) => {\n const id = await transport.createSession(value, sig)\n if (finalized) {\n // 已经收尾了才建出来(超时之后仍在跑的建连)→ 立刻归还,别留给下一次请求\n retireSession(id)\n cleanup(id)\n } else {\n owned.add(id)\n }\n return id\n },\n }\n\n try {\n signal.throwIfAborted()\n // N04:复用开启时全程串行(含跨账号)—— 共享槽位要一致的并发状态;\n // 关闭复用时保持原并发模式(每次自己建会话,彼此独立)。\n if (limit > 0) {\n const previous = reuseFlightTail\n const mine = new Promise<void>((resolve) => {\n release = resolve\n })\n reuseFlightTail = previous.then(\n () => mine,\n () => mine,\n )\n await wait(previous)\n }\n const connectMs =\n Number.isFinite(params.connectTimeoutMs) && (params.connectTimeoutMs as number) > 0\n ? Math.min(params.connectTimeoutMs as number, 600_000)\n : 45_000\n arm(connectMs, `DeepSeek 建立流超时(${connectMs}ms)`)\n // F10:**用 wait() 包住** —— 建连期限靠 controller.abort 生效,而 abort 只对\n // 「肯配合 signal 的传输」立即生效。包一层之后,即使底层 promise 永远不结算,\n // 等待也会在 abort 的瞬间结束(否则「限时」形同虚设,会一直挂在 await 上)。\n const opened = await wait(\n openCompletion(\n auth,\n { ...params, sessionReuseTurns: limit, onDeleteSession: cleanup },\n signal,\n tracked,\n ),\n )\n sessionId = opened.sessionId\n sentFeed = opened.feed\n body = opened.resp.body\n if (timer) clearTimeout(timer)\n iterator = parseWebSse(body, {\n thinkingEnabled: params.thinkingEnabled,\n onResponseMessageId: (id) => {\n responseMessageId = id\n },\n })\n const idle =\n Number.isFinite(params.idleTimeoutMs) && (params.idleTimeoutMs as number) > 0\n ? Math.min(params.idleTimeoutMs as number, 600_000)\n : 120_000\n for (;;) {\n arm(idle, `DeepSeek 流等待超时(${idle}ms)`)\n const item = await wait(iterator.next())\n if (timer) clearTimeout(timer)\n if (item.done) {\n complete = true\n break\n }\n if (item.value.kind === 'error') poisoned = true\n yield item.value\n }\n } catch (error: any) {\n if (params.signal?.aborted) throw new AdapterLlmError('请求已取消', 'ABORTED', { cause: error })\n if (controller.signal.aborted && controller.signal.reason instanceof AdapterLlmError) throw controller.signal.reason\n if (error instanceof AdapterLlmError) throw error\n throw new AdapterLlmError('DeepSeek 流请求失败', 'TRANSPORT', { cause: error })\n } finally {\n if (timer) clearTimeout(timer)\n controller.abort()\n // 主链不等待不合作的假/自定义流(否则仍会阻塞所有复用请求)\n if (iterator) {\n void iterator\n .return(undefined)\n .catch(() => {})\n .finally(() => {\n if (body && !body.locked) void body.cancel().catch(() => {})\n })\n }\n // N04:**提前结束(调用方 return / 取消)也必须退役会话**。\n // 旧实现只在 HTTP 失败分支退役,stream generator 被提前 return 时不退役 ——\n // 于是下一次请求会接着用一个\"上一条流还没消费完\"的会话。\n // F10:改成遍历本次创建过的**全部**会话;唯一放过的只有「正常跑完且仍在复用」的那个。\n finalized = true\n for (const id of owned) {\n if (id === sessionId && complete && !poisoned && limit > 0) continue\n retireSession(id)\n cleanup(id)\n }\n // 链式投喂的记账(2026-09-14):只有「流正常跑完 + 没被污染 + 拿到了本轮的\n // assistant message_id」才把这链接上;其余(报错/取消/提前 return/没收到 ready)\n // 一律作废 —— 下一轮 decideFeed 会看到\"没有链\",自动退回全量重发。\n // 注意这里按**每一次请求**记账(一轮里可能有首轮 + 续写轮多次调用),不是按 DSH 回合:\n // 续写轮的增量与 parent 正是靠这次记账才对得上。\n if (sentFeed?.next && complete && !poisoned && typeof responseMessageId === 'number') {\n contextChain = { ...sentFeed.next, parentId: responseMessageId }\n } else if (contextChain && (!sessionId || contextChain.sessionId === sessionId)) {\n contextChain = undefined\n }\n release?.()\n }\n}\n","/**\n * 登录态主动探活 —— 在任务跑到一半之前发现登录态失效。\n *\n * 借鉴 workbuddy-switch 的「Token 保活」。它的做法是\"操作前不足阈值就刷新 + 每日无条件刷新一次\",\n * 但我们这边**没有 refresh token 可刷**:网页端 token 只能靠重新登录(浏览器捕获)拿新的。\n * 所以能做的只有**尽早发现失效**:\n *\n * - 启动后延迟一小会儿探一次(覆盖\"隔天打开 DSH\"这个最常见的过期场景);\n * - 之后每 N 分钟探一次(默认 30 分钟,可关);\n * - 探活走**只读**的 `users/current`(零额度、实测 ~430ms),**不生成任何内容**;\n * 端点不可用时 `validateAuth` 自己会退回 PoW challenge 探活。\n *\n * 为什么值得:登录态过期现在的表现是\"几十步的任务跑到一半突然失败\"。\n * 一次只读探活的成本可以忽略,换来的是提前知道 —— 而且探活失败**只提示、不阻断**。\n *\n * ⚠️ 更正(2026-09-12 实测):本文档原先写\"受限期间探不出限制状态\",**这是错的**。\n * `users/current` 的响应体里就带着 `chat: { is_muted, mute_until }` —— 受限期间这个字段是有效的,\n * 所以理论上探活**能**提前发现限制(目前尚未接上:限制状态仍由生成失败时的\n * `mute_until` 记入 accounts 的 limit 字段,见 webapi.ts 的 muteUntilMs)。\n */\nimport { hasUsableAuth, staleAuthRecord, type WebAuth } from './auth.ts'\nimport { listAccounts, updateAccount } from './accounts.ts'\nimport { validateAuth } from './webapi.ts'\n\nexport interface ProbeOutcome {\n ok: boolean\n at: string\n error?: string\n /** 探活成功时顺带带回来的账号身份(用来补全显示名,见下面的写回)。 */\n user?: { id?: string; display?: string }\n /** F1(0.2.0):顺带带回来的限流状态(users/current 的 chat.is_muted / mute_until)。 */\n limit?: { muted: boolean; untilMs?: number }\n}\n\ninterface ProbeLogger {\n info?: (message: string) => void\n warn?: (message: string) => void\n}\n\n/**\n * 探一次。返回 `undefined` 表示\"没什么可探的\"(未登录)。\n *\n * 结果写回**发起探活时那个账号**(按 token 匹配)——\n * 探活期间用户可能已经切号,绝不能把结果写到新账号头上。\n */\nexport async function probeOnce(auth: WebAuth | undefined, logger?: ProbeLogger): Promise<ProbeOutcome | undefined> {\n if (!hasUsableAuth(auth)) return undefined\n const at = new Date().toISOString()\n const target = listAccounts().find((item) => item.token === auth.token)\n\n let outcome: ProbeOutcome\n try {\n const result = await validateAuth(auth, AbortSignal.timeout(20_000))\n outcome = result.ok\n ? {\n ok: true,\n at,\n ...(result.user ? { user: result.user } : {}),\n ...(result.limit ? { limit: result.limit } : {}),\n }\n : { ok: false, at, error: result.error ?? '校验未通过' }\n } catch (error: any) {\n outcome = { ok: false, at, error: error?.message ?? String(error) }\n }\n\n if (target) {\n if (outcome.ok) {\n // 成功:记录时间、并**清掉**上一次的失败(`undefined` 会被规范化为\"字段不存在\")。\n //\n // 顺带把身份信息写回:捕获那一刻可能还没校验过(比如刚\"登录新账号\"加进来的),\n // 于是列表只能显示内部 id。探活是零额度的只读调用,正好用来把显示名补上 ——\n // 这样库里闲置的账号也会自己\"长出名字\",不用等用户手动刷新。\n // 用「旧值打底 + 新值覆盖」合并:新一次只有 id、没有 display 时,\n // 不要把原来已经拿到的好名字冲掉(pickUserDisplay 保证空值不会写进 key)。\n // 0.1.61:`unverified: false` 必须显式清 —— 否则这个\"捕获时未校验\"的标记会永久粘住\n // (normalizeRecord 只保留 `=== true` 的,传 false 就自然消失)。\n // 粘住的后果实测(2026-09-14):账号库 5/5 全挂「未校验」,连\"最近校验 6 分钟前\"\n // 的那个也挂着 —— 标与数据自相矛盾、信息量归零,真出问题时反而看不出来。\n const patch: Record<string, unknown> = {\n lastVerifiedAt: at,\n lastVerifyError: undefined,\n unverified: false,\n }\n // F1(0.2.0):探活顺手把限流状态写回 —— 「被限到 X」提前出现在账号徽章上,\n // 而不是等生成请求撞 muted 才知道;is_muted=false 时**清掉**旧标记(提前看见解除)。\n // muted=true 但服务端没给解除时间时不写:没有可展示的,写成 untilMs:0 徽章也不亮。\n if (outcome.limit) {\n patch.limit =\n outcome.limit.muted && outcome.limit.untilMs\n ? { untilMs: outcome.limit.untilMs, observedAt: at }\n : undefined\n }\n if (outcome.user) {\n patch.user = { ...(target.user ?? {}), ...outcome.user }\n // 审计 F04:探活走的也是**可信校验**(只读 users/current),\n // 拿到的 user.id 要落成去重键,否则同一账号重登时又会新增一条。\n const verifiedId = (outcome.user as { id?: unknown }).id\n if (typeof verifiedId === 'string' && verifiedId) patch.serverId = verifiedId\n }\n updateAccount(target.id, patch as any)\n } else {\n updateAccount(target.id, { lastVerifyError: { at, message: String(outcome.error ?? '') } })\n }\n }\n\n if (outcome.ok) {\n logger?.info?.(`deepseek-web: 登录态探活通过(${target?.id ?? '未知账号'})`)\n } else {\n logger?.warn?.(`deepseek-web: 登录态探活失败 —— ${outcome.error}(可能已过期,建议重新登录)`)\n }\n return outcome\n}\n\nexport interface ProbeLoopOptions {\n /** 间隔毫秒;<= 0 表示关闭(不创建任何定时器)。 */\n intervalMs: number\n /** 首次探活的延迟,默认 20 秒 —— 别和 DSH 启动时的其它工作抢时间。 */\n initialDelayMs?: number\n /** 提供一个\"当前凭证\"的取值函数(每次探活都重新取,切号后自动跟着变)。 */\n getAuth: () => WebAuth | undefined\n logger?: ProbeLogger\n}\n\n/**\n * 启动定时探活,返回停止函数。\n *\n * 用 setTimeout 串行链而不是 setInterval:探活本身要花几百毫秒,\n * setInterval 在网络慢时会堆叠出并发探活(而这些请求算在同一账号头上)。\n */\nexport function startProbeLoop(options: ProbeLoopOptions): () => void {\n if (!(options.intervalMs > 0)) return () => {}\n\n let stopped = false\n let timer: ReturnType<typeof setTimeout> | undefined\n\n const tick = async (): Promise<void> => {\n if (stopped) return\n try {\n await probeOnce(options.getAuth(), options.logger)\n } catch {\n // probeOnce 内部已经兜了;这里再兜一层,保证循环不会因为一次异常而停摆\n }\n if (stopped) return\n timer = setTimeout(tick, options.intervalMs)\n // 不阻止进程退出\n ;(timer as any)?.unref?.()\n }\n\n timer = setTimeout(tick, options.initialDelayMs ?? 20_000)\n ;(timer as any)?.unref?.()\n\n return () => {\n stopped = true\n if (timer) clearTimeout(timer)\n }\n}\n\n/**\n * 最近一次探活是否失败。\n *\n * 不能只看\"有没有 lastVerifyError\" —— 成功时我们会清掉它,但反过来,\n * 一个较早的失败之后可能又有一次成功(lastVerifiedAt 更新、lastVerifyError 被清),\n * 所以**按时间比**:失败时间晚于成功时间才算\"当前处于失败态\"。\n */\nexport function lastProbeFailed(auth: WebAuth | undefined): boolean {\n if (!auth) return false\n const record = listAccounts().find((item) => item.token === auth.token)\n if (!record?.lastVerifyError) return false\n return String(record.lastVerifyError.at) > String(record.lastVerifiedAt ?? '')\n}\n\n/**\n * 当前凭证是否处于「已知**授权**失效」状态;是则返回那条失败说明。\n *\n * 与 `lastProbeFailed` 的分工:那个回答\"探活最近是不是失败过\"(**只用于展示/日志**),\n * 这个回答\"这份凭证现在还能不能用\"(**用于请求前拦截**,见 adapter.ts)。\n * 差别就在**失败类型**:断网、超时、5xx 也是\"探活失败\",但那种情况下凭证是好的,\n * 拦下来会误伤健康账号。纯判据在 `auth.ts` 的 `staleAuthRecord`(可单测),\n * 这里只负责按 token 找到对应的账号记录。\n */\nexport function staleAuthMessage(auth: WebAuth | undefined): string | undefined {\n if (!auth) return undefined\n const record = listAccounts().find((item) => item.token === auth.token)\n return staleAuthRecord(record)\n}\n","/**\n * 提示词协议层:\n * 1) 把 DSH 的消息词汇(system / user / assistant / tool-result / reasoning / tool-call)\n * 序列化成网页端可吃的单段 prompt(网页 API 只有 `prompt` 字符串,无 tools 字段)。\n * 2) 工具调用桥:网页模型没有原生 function calling,改用「JSON 协议 + 流式解析」——\n * 指令要求模型只输出 {\"tool_calls\":[{\"name\":…,\"arguments\":{…}}]},\n * 本模块在流式文本上做 hold-back 扫描,命中即转成 tool-call 块,不命中则原样透传正文。\n */\nimport { randomUUID } from 'node:crypto'\nimport { AdapterLlmError } from './auth.ts'\n\nexport interface ToolSchemaLike {\n name: string\n description: string\n parameters: Record<string, unknown>\n}\n\nexport interface ToolCallRequest {\n id: string\n name: string\n /** 原始 JSON 字符串(DSH ToolCallBlock.arguments 语义)。 */\n arguments: string\n}\n\n/** 单次 push/flush 的产出。 */\nexport interface FilterOutput {\n text: string\n calls: ToolCallRequest[]\n /**\n * 捕获到的协议块**无法解析**(raw = 原文,mode = 标记家族)。\n * ⚠️ 这个字段的意义:绝不再把这种残留当正文吐出去 —— 它既不是模型想说的话,\n * 又会被 Web GUI 的 markdown 渲染器当成垃圾(实测 2026-09:泄漏文本里的\n * `$ErrorActionPreference='…'` 被渲染成 KaTeX 行内公式 → 用户看到「一个字符一行 +\n * 弯引号」的乱码)。调用方据此决定「重试」或「给一句人话提示」。\n *\n * `reason` 区分失败形态,用于诊断(实测日志只留前 400 字符,看不到后半段的坏点):\n * - `unbalanced`:块没配平/没收全 —— 多半是流被服务端 60s 上限截断,不是模型写错;\n * - `unparsable`:块是完整的,但结构不符(漏括号、引号没转义等);\n * - `echo` :载荷里裹着转写回声(`[Tool Result for …]` 等)—— 模型在**复述历史**,\n * 不是真在调用。此类**必须丢弃**:实测抓到过一个 8152 字符、含 15 条\n * 「调用」的载荷,全部是历史回放,执行它等于把旧命令重跑一遍;\n * - `oversize` :超过捕获上限,放弃。\n */\n rejected?: { raw: string; mode: 'json' | 'xml'; reason?: 'unbalanced' | 'unparsable' | 'oversize' | 'echo' }\n}\n\n/**\n * 单个工具描述的上限。\n *\n * 2026-09-12 从 400 提到 3200。理由:DSH 实际下发 61 个工具,其中 17 个描述超过 400 字符,\n * 而**被砍掉的恰恰是最要紧的部分** —— `pwsh` 的 3010 字符里有 2610 个字符在讲\n * 「沙箱拒绝(file access denied)是策略判定、不是命令的 bug,别换个方式重试」\n * 「命名管道不可用时 stdio:'pipe' 的 spawn 会报 EPERM,同样别换方式」\n * 「只读沙箱下 .NET 静态调用 / Add-Type / COM / 反射会失败」这类**遇错该怎么办**的指引;\n * `workflow` 的 2500 字符里是 agent() / pipeline() / parallel() 的钩子签名。\n * 把它们砍掉,模型一遇错就只能瞎猜 —— 实测这两个工具正是 rejected.jsonl 里失败最多的。\n */\nconst MAX_DESCRIPTION_CHARS = 3_200\n\n/**\n * 工具目录(一节)的总预算。\n *\n * 2026-09-12 从 24_000 提到 56_000。理由:实测 DSH 下发 61 个工具、不截描述时共需\n * **50,942 字符**,旧预算只装得下 35 个。更糟的是**截断是按字母序发生的**(工具按名排序),\n * 于是 `write`(w)、`web_search`、`web_fetch`、`subagent`、`todo_write`、`skill`、`read_image`、\n * 全部 `ssh_*`/`sftp_*` 被砍,而极少用的 `db_tx_rollback`、`db_list_connections` 反而留下。\n * 取 56_000 留约十分之一余量(够再添几个中等大小的工具);再超就走下面的\"列出名字\"兜底。\n * 不至于撑爆上下文:DeepSeek 网页端上下文 1M token,而实际生效的 maxChars 是\n * **150 万字符**(`index.ts` 的 `maxPromptChars` 默认值;这里 12 万那个旧注释已过时 ——\n * 2026-09-13 核对时发现写的还是旧默认值,容易让人误判 prompt 体量)。\n */\nconst MAX_TOOLS_SECTION_CHARS = 56_000\nconst HOLD_BACK_CHARS = 24\nconst MAX_CAPTURE_CHARS = 256 * 1024\n\n/** 工具调用协议指令(固定文本,进 prompt 前缀,保持前缀缓存友好)。 */\n/**\n * head(system + 工具协议 + 工具目录)在 prompt 里最多占的比例。\n * 0.62 是 0.1.33 调的:实测 61 个工具时 head 约 6.35 万字符,而 0.45 × 12 万 = 5.4 万装不下。\n * 转写仍余约 5.6 万字符 —— 历史可以截,工具定义不可以。\n */\nconst HEAD_RATIO = 0.62\n/** 协议段拼接时的固定开销(换行、`---` 分隔、省略标记等)。 */\nconst PROTOCOL_SLACK_CHARS = 96\n\nexport const TOOL_PROTOCOL_INSTRUCTIONS = `# Tool Calling Protocol\n\nYou can call tools to complete the user's task. When you need a tool, output ONLY a single JSON object, with no other text before or after it:\n\n{\"tool_calls\":[{\"name\":\"<tool-name>\",\"arguments\":{<json-arguments>}}]}\n\nRules:\n1. Put every tool you want to run in the \"tool_calls\" array (usually exactly one; a batch is allowed).\n2. Stop immediately after that JSON object. The runner executes the call(s) and returns the results to you as the next message.\n3. Never fabricate, guess, or simulate tool output — always wait for the real result.\n4. When no tool is needed, answer normally in plain text and do NOT emit that JSON.\n5. \"arguments\" must be valid JSON (double-quoted strings, no trailing commas). When a value is a Windows path, escape backslashes as \\\\\\\\ (e.g. \"C:\\\\\\\\Users\\\\\\\\me\"); an unescaped single backslash makes the whole object unparsable. Close every brace: the call object and its \"arguments\" object each need their OWN closing \"}\" — one missing \"}\" makes the whole batch unparsable and the call will be discarded.\n5b. Two things break the JSON most often — check them before you emit:\n (a) QUOTES INSIDE A VALUE. A shell/PowerShell command very often contains double quotes, e.g. Get-ChildItem \"$env:USERPROFILE\\\\.dsh\". Every such inner double quote MUST be escaped as \\\\\" inside the JSON string. An unescaped one ends the string early and discards the whole call.\n (b) LINE BREAKS INSIDE A VALUE. Never put a real line break inside a string; write \\\\n instead. When a command needs several statements, join them with \";\" on ONE line, or use \\\\n escapes — do not paste them as actual newlines. Prefer single quotes inside commands to reduce escaping.\n6. Do NOT use XML/HTML-like markup for tool calls: no angle-bracket wrapper tags (no <tool_calls>, <invoke>, <parameter>), and none of the private delimiter-prefixed variants some DeepSeek surfaces use. The JSON object above is the ONLY accepted format. Markup is not just ignored — it leaks into the visible transcript (and into the web conversation) as broken output.\n7. Always answer in the same language the user writes in (these instructions are English only for precision; the JSON itself is language-neutral).\n8. NEVER reproduce the transcript. Do not restate previous turns, \"[Tool Result …]\" blocks, tool output, or the current prompt. Emit ONLY the calls you want to run right now. A payload that replays earlier calls or embeds tool results is discarded and costs a retry — measured case: a model emitted 15 replayed calls inside one 8152-char payload, and every one of them had to be thrown away.\n9. Keep each batch SMALL — at most 3 calls, and prefer exactly 1. If you need more, send them in successive steps. Long payloads are the ones that most often come out malformed.\n10. Each call must be able to run on its own: no shared shell variables across calls, no dependence on another call in the same batch.`\n\n/** 判定「这是一段要执行的程序」的最小长度:短于它的多半只是行内提及某个 API。 */\nconst MIN_TOOL_PROGRAM_CHARS = 80\n\n/**\n * 检测「模型把工具程序写成了正文」——而不是作为工具调用发出。\n *\n * 现场(2026-09-17 11:00,`--F-Code-DSH-Code-gongji--` 会话):第三方 preset(染神)\n * 的 `tool-bootstrap.mjs` 注入了一条 PTC 说明 ——「你在 Programmatic Tool Calling 模式,\n * 所有动作必须通过 run_code 写 TypeScript 程序完成」;同 preset 的 persona 还写着\n * `One complete deliverable per turn: numbered steps or code blocks`。\n * 于是模型把 run_code 的 code 参数**原样贴进了正文**:三轮里一次工具调用都没发出\n * (`toolCallCount` 始终为 0),agent loop 判定回合结束 → 用户看到「它停下来了」。\n * 模型自己在 reasoning 里也承认:\"我把 TypeScript 代码写成了正文文本,而不是作为工具调用发出\"。\n *\n * 判据:正文里出现**围栏代码块**且块内含 `tools.<name>(…)` 形态的调用 —— 那是 PTC\n * 程序体的特征(正常回答不会这么写)。没有围栏时只认带 `await` 的形态,更严格,\n * 免得把「提到某个 API」错判成「写了程序」。\n *\n * ⚠️ 这**只是判据**:调用方还要确认「本轮零工具调用」,否则健康的程序化调用轮会被误伤。\n */\nexport function looksLikeUnexecutedToolProgram(text: string): boolean {\n const source = String(text ?? '')\n if (!source) return false\n const blocks: string[] = []\n const fence = /```[^\\n]*\\n([\\s\\S]*?)```/g\n let match: RegExpExecArray | null\n while ((match = fence.exec(source)) !== null) blocks.push(match[1])\n if (blocks.length === 0) {\n return /await\\s+tools\\.[A-Za-z_$][\\w$]*\\s*\\(/.test(source)\n }\n return blocks.some(\n (block) => block.length >= MIN_TOOL_PROGRAM_CHARS && /tools\\.[A-Za-z_$][\\w$]*\\s*\\(/.test(block),\n )\n}\n\nfunction truncate(text: string, max: number): string {\n if (text.length <= max) return text\n return `${text.slice(0, max - 3)}...`\n}\n\n/** 渲染工具目录(含 JSON Schema)。 */\nexport function buildToolSection(\n tools: readonly ToolSchemaLike[] | undefined,\n maxChars: number = MAX_TOOLS_SECTION_CHARS,\n): string {\n if (!tools || tools.length === 0) return ''\n const parts: string[] = ['', '## Available tools']\n // 预算取「内置上限」与「调用方给的额度」的较小值。\n // 调用方(serializePrompt)会按剩余空间算一个动态额度传进来 —— 见下面 F18 的注释。\n let budget = Math.max(0, Math.min(MAX_TOOLS_SECTION_CHARS, maxChars))\n for (let index = 0; index < tools.length; index += 1) {\n const tool = tools[index]\n let schemaText = ''\n try {\n schemaText = JSON.stringify(tool.parameters ?? {})\n } catch {\n schemaText = '{}'\n }\n const block = [\n '',\n `### ${tool.name}`,\n truncate(String(tool.description ?? '').replace(/\\s+/g, ' ').trim(), MAX_DESCRIPTION_CHARS),\n `Parameters (JSON Schema): ${schemaText}`,\n ].join('\\n')\n if (budget - block.length < 0) {\n // 预算用完。**必须把剩下的工具名说出来**:旧写法只有一句\n // \"(remaining tools omitted for length)\",模型连\"还有哪些工具存在\"都不知道,\n // 只能盲猜名字和参数 —— 实测 61 个工具里 26 个就是这样静默消失的。\n // 同时明确要求它别猜参数,改为向用户确认。\n const rest = tools\n .slice(index)\n .map((item) => String(item?.name ?? ''))\n .filter(Boolean)\n parts.push(\n `\\n(⚠️ The following ${rest.length} tools are NOT described above (omitted for length): ` +\n `${rest.join(', ')}. If you need one of them, ask the user for its exact parameters — ` +\n 'do NOT guess them.)',\n )\n break\n }\n budget -= block.length\n parts.push(block)\n }\n return parts.join('\\n')\n}\n\nfunction flattenText(blocks: readonly any[] | undefined, out: string[] = []): string[] {\n for (const block of blocks ?? []) {\n if (!block || typeof block !== 'object') continue\n if (block.type === 'text' && typeof block.text === 'string') out.push(block.text)\n else if (block.type === 'tool-result' && Array.isArray(block.content)) flattenText(block.content, out)\n }\n return out\n}\n\n/**\n * 逐张产出图片占位标记,**按图片在消息里的实际顺序**。\n *\n * 为什么要分「带上」与「略过」(0.1.77,群友实测报告 `code 10 / too many ref file`):\n * 图片是**请求级**的(一次请求用 `ref_file_ids` 带一批),而网页端对这一批的数量有上限 ——\n * 实测最后一次成功是 40 张、第一次失败是 52 张,真值落在 (40, 52]。超长会话里我们只发\n * 最近的 N 张,更早的会被略过;此时若标记仍一律写 `[image attached]`,模型就会**以为它\n * 收到了那些图**,然后对着没送出去的图瞎猜。所以分两种标记写。\n *\n * ⚠️ **必须按顺序逐个产出,不能写成「N 个 attached 再 M 个 omitted」** ——\n * 那样标记的先后就不再对应图片的时间先后,模型会搞不清被省略的是哪几张。\n *\n * `kept` 为 undefined 时全部算「已发出」(= 0.1.77 之前的行为,续写轮与既有单测走这条路);\n * 图没有 `attachmentId` 时也算「已发出」—— 那种情况判断不了,保守起见别把真发出去的标成省略。\n */\nfunction blockImageMarks(\n blocks: readonly any[] | undefined,\n kept: ReadonlySet<string> | undefined,\n): string[] {\n const marks: string[] = []\n const walk = (list: readonly any[] | undefined): void => {\n for (const block of list ?? []) {\n if (!block || typeof block !== 'object') continue\n if (block.type === 'image') {\n const key = String(block.attachment?.attachmentId ?? '')\n marks.push(!kept || !key || kept.has(key) ? '[image attached]' : '[earlier image omitted]')\n } else if (block.type === 'tool-result' && Array.isArray(block.content)) walk(block.content)\n }\n }\n walk(blocks)\n return marks\n}\n\n/** 按出现顺序收集消息里的图片附件引用(含 tool-result 内嵌图片)。 */\nexport function collectImageRefs(messages: readonly any[] | undefined): any[] {\n const refs: any[] = []\n const walk = (blocks: readonly any[] | undefined): void => {\n for (const block of blocks ?? []) {\n if (!block || typeof block !== 'object') continue\n if (block.type === 'image' && block.attachment) refs.push(block.attachment)\n else if (block.type === 'tool-result' && Array.isArray(block.content)) walk(block.content)\n }\n }\n for (const message of messages ?? []) {\n if (!message || typeof message !== 'object') continue\n walk(Array.isArray(message.content) ? message.content : undefined)\n }\n return refs\n}\n\n/** mediaType → 文件名后缀。服务端按**后缀**判类型(不看 multipart 里的 content-type)。 */\nconst IMAGE_EXT_BY_MEDIA_TYPE: Record<string, string> = {\n 'image/png': 'png',\n 'image/jpeg': 'jpg',\n 'image/jpg': 'jpg',\n 'image/webp': 'webp',\n 'image/gif': 'gif',\n}\n\n/** 服务端认得的图片后缀。不在此列的一律重建名字,别把 bmp/tiff 之类原样发过去再被拒一次。 */\nconst KNOWN_IMAGE_EXT = new Set(['png', 'jpg', 'jpeg', 'webp', 'gif'])\n\n/**\n * 上传时该用的文件名。\n *\n * 为什么必须归一(2026-09-15 真机 A/B,三组对照,同一份 PNG 字节只改名字):\n * `image.png` ✅ / `<64位hex>.png` ✅ / **纯 64 位 hex ❌ `code 9 unsupported file type`** /\n * 不给 name(走缺省 image.png)✅\n * ⇒ 服务端**按文件名后缀**判类型,content-type 说了不算。\n * 而宿主给 `tool/result` 内嵌图片的 `name` 正是**纯 sha256(没有后缀)**,\n * `user/message` 与 `agent/inbox` 给的是 `image.png` —— 于是\"凡是经工具返回的图一律传不上去\":\n * 本机 09-14 的 36 次 + 09-15 的 6 次被拒,全部是这个原因(会话日志里\n * `tool/result` 的 name 无一例外是 64 位 hex,user/message 无一例外带 .png)。\n *\n * 所以这里只保留**受支持的后缀**,其余按 mediaType 重建为 `image.<ext>`\n * ——保证发出去的文件名总是声明了一个服务端支持的类型。\n */\nexport function imageUploadName(name: unknown, mediaType: unknown): string {\n const ext = IMAGE_EXT_BY_MEDIA_TYPE[String(mediaType ?? '').trim().toLowerCase()] ?? 'png'\n const raw = typeof name === 'string' ? name.trim() : ''\n // 只取基名:万一宿主给的是路径,别把目录带进 multipart 的文件名\n const base = raw.split(/[\\\\/]/).pop() ?? ''\n const matched = /\\.([a-z0-9]{2,5})$/i.exec(base)\n if (matched && KNOWN_IMAGE_EXT.has(matched[1].toLowerCase())) return base\n return `image.${ext}`\n}\n\n/** 把一条 assistant 消息里的 tool-call 块渲染回协议 JSON(供历史学习格式)。 */\nfunction renderToolCalls(blocks: readonly any[]): string | null {\n const calls = (blocks ?? []).filter((block) => block?.type === 'tool-call')\n if (calls.length === 0) return null\n const payload = {\n tool_calls: calls.map((call) => {\n let args: unknown = {}\n try {\n args = call.arguments ? JSON.parse(call.arguments) : {}\n } catch {\n args = { _raw: String(call.arguments ?? '') }\n }\n return { name: String(call.name ?? ''), arguments: args }\n }),\n }\n return JSON.stringify(payload)\n}\n\n/** 中间截断:保留开头(任务/协议)与结尾(最近回合),并把省略标记计入预算。 */\nfunction truncateMiddle(text: string, maxChars: number, tailRatio = 0.7): string {\n if (text.length <= maxChars) return text\n const RESERVE = 64 // 标记串预留(\"...[N chars omitted]...\" 远小于此)\n const budget = Math.max(0, maxChars - RESERVE)\n const tail = Math.floor(budget * tailRatio)\n const head = Math.max(0, budget - tail)\n const dropped = text.length - head - tail\n const marker = `\\n\\n...[${dropped} chars omitted]...\\n\\n`\n return `${text.slice(0, head)}${marker}${text.slice(text.length - tail)}`\n}\n\nexport interface SerializeOptions {\n system?: string\n messages: readonly any[]\n tools?: readonly ToolSchemaLike[]\n maxChars?: number\n /**\n * 本次请求**真正会带上**的图片 key 集合(各图的 `attachmentId`)。\n *\n * 给了它就按它区分「这张图在不在本次请求里」,占位标记相应写成\n * `[image attached]`(带上了)或 `[earlier image omitted]`(被长度控制略过)。\n *\n * **不给**(undefined)= 一律按「带上了」处理 —— 那是 0.1.77 之前的行为。\n * 续写轮与既有单测走的就是这条路,prompt 形态必须逐字节不变。\n */\n keptImageKeys?: ReadonlySet<string>\n}\n\n/**\n * 序列化为网页端单段 prompt。\n * 结构:system → 工具协议与目录 → 对话转写(User:/Assistant:/[Tool Result])。\n */\nexport interface PromptParts {\n /** 固定头:system + 协议指令 + 工具目录(可能为空串)。 */\n head: string\n /** 历史条目(**未截断**)。链式投喂按它算增量,所以不能受截断影响。 */\n entries: string[]\n /** 真正发出去的那份字符串(超预算时是截断后的)—— 与 serializePrompt 的返回值一致。 */\n full: string\n}\n\nexport function serializePrompt(options: SerializeOptions): string {\n return serializePromptParts(options).full\n}\n\n/**\n * 与 serializePrompt 同源,但额外交出 `head` 与未截断的 `entries`。\n *\n * 为什么需要(2026-09-14,链式投喂):增量 = 本轮条目减去上一轮条目,必须拿\n * **结构化**的条目数组去比前缀;而最终字符串可能被 truncateMiddle 从中间截过,\n * 用字符串切前缀会把截断位置算错(截断点之后的\"新增\"其实是被挖掉的中段)。\n */\nexport function serializePromptParts(options: SerializeOptions): PromptParts {\n const maxChars = options.maxChars ?? 120_000\n // N07:小数/NaN/极小值直接拒绝,别让它们一路漂到后面的算术里(`new Array(小数)` 这类坑在旁边就有)\n if (!Number.isSafeInteger(maxChars) || maxChars < 128) {\n throw new RangeError('maxChars 必须为至少 128 的整数')\n }\n const system = String(options.system ?? '').trim()\n // ⚠️ F18(2026-09-12 审计):**按剩余空间给工具目录算预算**,而不是\"先渲染满、事后截 head\"。\n // 旧做法是先把工具目录渲染到 5.6 万字符,再发现 head 超过 `maxChars * 比例`,\n // 于是 `truncateMiddle` 从**中间**挖掉一块 —— 留下的是**残缺的 JSON Schema**:\n // 模型会照着半截定义猜参数,比\"干脆不列这个工具\"更糟;而且 maxChars 越小时越容易触发。\n // 现在反过来:先把 system 与协议指令的固定开销扣掉,剩下的才是工具目录能用的额度。\n // 装不下就走 buildToolSection 自己的兜底(列出被省略的工具名),head 永远完整。\n const toolBudget = Math.max(\n 0,\n Math.floor(maxChars * HEAD_RATIO) - system.length - TOOL_PROTOCOL_INSTRUCTIONS.length - PROTOCOL_SLACK_CHARS,\n )\n const toolSection = buildToolSection(options.tools, toolBudget)\n const protocol = toolSection ? `\\n\\n${TOOL_PROTOCOL_INSTRUCTIONS}${toolSection}` : ''\n\n const lines: string[] = []\n for (const message of options.messages ?? []) {\n if (!message || typeof message !== 'object') continue\n const blocks: any[] = Array.isArray(message.content) ? message.content : []\n if (message.role === 'system') {\n const text = flattenText(blocks).join('')\n if (text.trim()) lines.push(`[System]\\n${text}`)\n continue\n }\n if (message.role === 'assistant') {\n const text = flattenText(blocks).join('')\n const renderedCalls = renderToolCalls(blocks)\n if (renderedCalls) lines.push(`Assistant: ${renderedCalls}`)\n else if (text.trim()) lines.push(`Assistant: ${text}`)\n continue\n }\n // user 角色:可能是纯文本,也可能携带 tool-result 块\n const toolResults = blocks.filter((block) => block?.type === 'tool-result')\n const text = flattenText(blocks.filter((block) => block?.type !== 'tool-result')).join('')\n const imageMarks = blockImageMarks(blocks, options.keptImageKeys)\n const images = imageMarks.length\n if (text.trim() || (toolResults.length === 0 && images === 0) || images > 0) {\n // 图片本体由调用方上传后经 ref_file_ids 附在请求上;这里只放可定位的占位标记,\n // 让模型知道「图几」对应哪条消息(顺序与 uploadedImages 收集顺序一致)。\n //\n // 0.1.77:被长度控制略过的那些**不能也写 `[image attached]`** —— 逐张按实发情况写,\n // 模型才知道哪些是真有的(见 blockImageMarks 的说明)。\n const imageNote = imageMarks.length > 0 ? `\\n${imageMarks.join(' ')}` : ''\n lines.push(`User: ${text}${imageNote}`)\n }\n for (const result of toolResults) {\n const body = flattenText(result.content).join('') || '(no output)'\n const errorMark = result.isError ? ' [ERROR]' : ''\n lines.push(`[Tool Result${errorMark} for ${String(result.toolCallId ?? '')}]\\n${body}`)\n }\n }\n\n const transcript = lines.join('\\n\\n')\n const head = system ? `${system}${protocol}` : protocol.trim()\n const merged = transcript ? `${head}\\n\\n---\\n\\n${transcript}` : head\n\n if (merged.length <= maxChars) return { head, entries: lines, full: merged }\n\n // ⚠️ N07(2026-09-13 第二轮审计):**固定头(system + 协议 + 工具目录)必须完整**,\n // 不能再按 `maxChars * HEAD_RATIO` 去截它。\n // 旧写法是 `headBudget = min(head.length, floor(maxChars * 0.62))`:\n // 只要 system 长到超过预算的 62%,就会从**中间**被挖掉一块 ——\n // 于是\"预算明明够放完整 system\"的请求,也会静默丢掉系统指令(模型照着残缺策略干活)。\n // 现在:固定头独占它的长度,剩余预算全给历史;**固定头自己放不下就明确报错**。\n // 代价:以前\"静默丢指令还能生成\"的请求现在会失败 —— 这是有意的,丢失系统指令比失败更糟。\n const separator = transcript ? '\\n\\n---\\n\\n' : ''\n const budget = maxChars - head.length - separator.length\n if (budget < 128) {\n throw new AdapterLlmError(\n '系统/工具定义超出 prompt 预算,请减少固定输入或扩大上限',\n 'CONTEXT_WINDOW_EXCEEDED',\n )\n }\n return { head, entries: lines, full: head + separator + truncateMiddle(transcript, budget, 0.7) }\n\n}\n\n// ── 流式工具调用过滤器 ────────────────────────────────────\n\n/** 完整 JSON 调用标记:{\"tool_calls\": 或 {\"tool_call\": (允许空白)。 */\nconst MARKER_RE = /\\{\\s*\"tool_calls?\"\\s*:/\n/**\n * XML 风格调用标记(实测:思考模式下模型偶尔改用这套标记,形如\n * `<tool_calls><invoke name=\"read\"><parameter name=\"file_path\">…</parameter></invoke></tool_calls>`;\n * 亦兼容 DeepSeek 自家的 DSML 前缀与 `dsml-` 连字符变体)。\n *\n * ⚠️ 2026-09-10 实测泄漏样本(真正的乱码来源):模型把 DSML 前缀写成**重复的全角竖线**,\n * 且包裹标签名退化成 `calls`:\n * `<` + `||` + `DSML` + `||` + ` ` + `calls>`\n * 旧写法只容忍单个竖线(`[||]`),于是 `<` 后吃掉一个 `|` 就要求紧跟 `DSML`,\n * 却撞上第二个 `|` → 整个标记认不出来 → 不进捕获态 → 原样进正文 → GUI 渲染成乱码。\n * 现在竖线按 `+` 容忍(含全角/半角混用),并把 `calls` 也列入包裹标签名。\n */\nconst DSML_PREFIX = '(?:[||]+\\\\s*DSML\\\\s*[||]+\\\\s*)?'\nconst WRAPPER_NAMES = 'tool_calls|tool_call|function_calls|calls'\nconst XML_STARTER_RE = new RegExp(`<\\\\s*${DSML_PREFIX}(?:dsml-)?(${WRAPPER_NAMES}|invoke)\\\\b`, 'i')\n/** 代码围栏收尾(模型常把调用块放进 ``` 里)。 */\nconst FENCE_TAIL_RE = /\\n?[ \\t]*```[a-zA-Z0-9]*[ \\t]*\\n?$/\nconst FENCE_HEAD_RE = /^[ \\t]*\\n?```[ \\t]*\\n?/\n\n/**\n * 开/收标签前缀(宽容写法)。严格解析与宽容解析**必须共用同一套**,否则会出现\n * 「findXmlToolCallEnd 认得出收尾、parseXmlToolCalls 认不出 invoke」→ 整块被降级成正文泄漏。\n * 覆盖:`< invoke`(标签名带空白)、单/重复竖线的 DSML 前缀(含全角)、`<dsml-invoke>`。\n */\nconst TAG_OPEN_PREFIX = `<\\\\s*${DSML_PREFIX}(?:dsml-)?`\nconst TAG_CLOSE_PREFIX = `<\\\\/\\\\s*${DSML_PREFIX}(?:dsml-)?`\nconst XML_CLOSE_NAMES = `parameter|invoke|${WRAPPER_NAMES}`\n\n/**\n * 归一化 DSML 噪声 → 标准标签。\n * 竖线支持**重复与全角**(实测样本是双全角竖线),并连带吃掉其后的空白,\n * 让标签名紧跟在 `<` 之后(`<` + 前缀 + ` ` + `invoke` → `<invoke`)。\n */\nfunction normalizeDsml(text: string): string {\n return text\n .replace(new RegExp(`<(/?)${DSML_PREFIX}`, 'gi'), '<$1')\n .replace(/<\\s*dsml-/gi, '<')\n .replace(/<\\/\\s*dsml-/gi, '</')\n}\n\n/**\n * JSON 调用标记前缀(用于跨包 hold-back 判断)。\n * ⚠️ 2026-09 事故:真实分块会把标记切成 `{\"tool` + `_calls\":[{\"name\":…` 两半。\n * 旧实现比较时多拼了一个引号(`{'{\"' + body}`,而 body 已含前引号 → `{\"\"tool`),\n * 于是「末尾是潜在前缀」永远判 false → 半截标记被当正文吐出去、后半个再也拼不回完整标记\n * → 整个 JSON 泄漏成正文。修复见 partialMarkerSuffixLength。\n */\nconst JSON_MARKER_STARTERS = ['{\"tool_calls\"', '{\"tool_call\"']\n\n/** XML 标记前缀(用于跨包 hold-back 判断)。`calls` 是实测出现的退化包裹名。 */\nconst XML_MARKER_STARTERS = ['<tool_calls', '<tool_call', '<function_calls', '<calls', '<invoke', '<dsml-tool_calls', '<dsml-invoke']\n\n/**\n * 判断 text 末尾是否是(可能的)标记前缀 —— 决定是否 hold back。\n * @returns 需要保留在缓冲区里的尾部字符数(0 = 无需保留)\n */\nfunction partialMarkerSuffixLength(text: string): number {\n const LIMIT = 32\n const from = Math.max(0, text.length - LIMIT)\n const raw = text.slice(from)\n // 候选起点:最后一个 `{` 或 `<`(在**原始**切片上定位,保证 held 长度与原文对齐)\n const braceAt = raw.lastIndexOf('{')\n const angleAt = raw.lastIndexOf('<')\n const startAt = Math.max(braceAt, angleAt)\n if (startAt === -1) return 0\n const held = raw.length - startAt\n const normalized = normalizeDsml(raw.slice(startAt))\n\n if (normalized.startsWith('{')) {\n if (MARKER_RE.test(normalized)) return 0 // 已是完整标记,交给捕获逻辑\n const body = normalized.replace(/^\\{\\s*/, '').replace(/\\s+/g, '')\n // body 已含前引号(如 `\"tool`)→ 与 starter 比较时应拼 `{` + body\n const ok = JSON_MARKER_STARTERS.some((starter) => starter.startsWith(`{${body}`))\n return ok ? held : 0\n }\n if (normalized.startsWith('<')) {\n if (XML_STARTER_RE.test(normalized)) return 0 // 已是完整标记\n const lower = normalized.toLowerCase().replace(/\\s+/g, '')\n if (XML_MARKER_STARTERS.some((starter) => starter.startsWith(lower))) return held\n // ⚠️ DSML 前缀可能**只到了一半**:`<|DSML` 还差最后一个竖线,\n // 于是 normalizeDsml(要求「竖线 DSML 竖线」)认不出来 → 上面这条 hold 判定失效。\n // 2026-09-12 实测泄漏路径正是这里:pending 停在半截前缀时 hold 判定给了 0,\n // 半截标记被当正文吐出,随后几个字符补成 `<|DSML|calls>` 就成了正文里的乱码。\n // 兜底:把候选里的竖线与 `dsml` 一并擦掉再看是不是某个 starter 的前缀。\n const loose = lower.replace(/[||]|dsml/g, '')\n if (/^<\\/?[a-z_]*$/.test(loose) && XML_MARKER_STARTERS.some((starter) => starter.startsWith(loose))) {\n return held\n }\n return 0\n }\n return 0\n}\n\n/** 从 index 0 起抽取一个配平的 JSON 对象;不完整返回 null。 */\nexport function extractBalancedJson(text: string): { json: string; end: number } | null {\n if (text[0] !== '{') return null\n let depth = 0\n let inString = false\n let escape = false\n for (let i = 0; i < text.length; i++) {\n const ch = text[i]\n if (escape) {\n escape = false\n continue\n }\n if (ch === '\\\\' && inString) {\n escape = true\n continue\n }\n if (ch === '\"') {\n inString = !inString\n continue\n }\n if (inString) continue\n if (ch === '{') depth += 1\n else if (ch === '}') {\n depth -= 1\n if (depth === 0) return { json: text.slice(0, i + 1), end: i + 1 }\n }\n }\n return null\n}\n\n/** 读取一个 XML 属性值(支持双引号/单引号/裸值)。 */\nfunction readAttr(attrs: string, name: string): string | undefined {\n const re = new RegExp(`\\\\b${name}\\\\s*=\\\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\\\s>/]+))`, 'i')\n const match = re.exec(attrs)\n if (!match) return undefined\n return match[1] ?? match[2] ?? match[3]\n}\n\n/**\n * 宽容 JSON 解析。实测场景:模型把 Windows 路径写成 `\"D:\\apps\\DSH\"`(单个反斜杠,\n * 非法转义),JSON.parse 直接抛错 → 工具调用解析失败、整段标记被当正文吐给用户。\n * 先试原样;失败则修补:非法转义补成字面反斜杠、字符串内裸换行转义、去尾逗号。\n */\nexport function parseJsonLenient(text: string): unknown {\n try {\n return JSON.parse(text)\n } catch {}\n for (const candidate of jsonRepairCandidates(text)) {\n try {\n const parsed = JSON.parse(candidate)\n if (parsed !== undefined) return parsed\n } catch {}\n }\n return undefined\n}\n\n/**\n * 依次尝试的修复候选(只在原样解析失败时使用)。顺序有讲究:\n *\n * 先跑「路径尾反斜杠」启发式(`\"…\\app.asar\\\"` 里的 `\\\"` 是转义引号 → 字符串不终止,\n * 必须在切分字符串**之前**修,否则整个字符串范围都会错),再跑字符串级修复。\n *\n * 字符串级修复的智能规则:若某字符串里出现**非法转义**(如 `\\A`),说明模型是「原样写出」\n * 未转义的反斜杠 —— 此时该字符串内所有反斜杠都按字面处理,否则 `\\resources` 里的 `\\r`\n * 会被 JSON 当成回车,路径被悄悄改坏(实测用户样本 #2)。\n * 若字符串里没有非法转义,则只做保守修补(保留 `\\\\`、`\\\"` 等合法转义)。\n */\n/**\n * 修复「**字符串值里出现未转义的双引号**」——实测最高频的坏法,也是「自动停止」的元凶。\n *\n * 实测(2026-09-10 23:27:13,deepseek-reasoner):命令天然写作\n * `Get-ChildItem \"$env:USERPROFILE\\.dsh\" | Select-Object Name`\n * 模型把这串里的引号**原样**塞进 JSON 字符串 → `Expected ',' or '}' after property value`\n * → 整条调用被丢弃 → 那一轮没有工具调用 → agent loop 认为回合正常结束\n * → 用户看到的症状就是「说半句就停了」。\n *\n * 判据(对 JSON 语法是稳的):在字符串内部遇到双引号时,向后跳过空白看一个字符 ——\n * 只有它还是 `,` `}` `]`(或文本结束)时才说明字符串真的结束;否则该引号是内容里的字面引号。\n *\n * ⚠️ 冒号必须**按位置**区别对待:`\"` 后面跟 `:` 只在「键的位置」才是结构符。\n * 若把值里的 `\"` + `:` 也当成结束,那么命令内嵌 JSON 时会误判,例如\n * `node -e \"const o={\"a\":1}\"`\n * 里的 `\"a\"` 会被当成字符串收尾 → 后面全部错位 → 整条调用照样被丢弃(我第一版就踩了这个洞)。\n * 因此这里跟踪「进入字符串时是否处于键位置」(上一结构符是 `{` / `,` / `[`)。\n */\nexport function escapeInnerQuotes(text: string): string {\n let out = ''\n let inString = false\n let keyPosition = false\n let lastStructural = ''\n for (let i = 0; i < text.length; i++) {\n const ch = text[i]\n if (!inString) {\n if (ch === '\"') {\n inString = true\n keyPosition = lastStructural === '{' || lastStructural === ',' || lastStructural === '['\n out += ch\n continue\n }\n if (!' \\t\\n\\r'.includes(ch)) lastStructural = ch\n out += ch\n continue\n }\n if (ch === '\\\\') {\n out += ch + (text[i + 1] ?? '')\n i += 1\n continue\n }\n if (ch === '\"') {\n let j = i + 1\n while (j < text.length && ' \\t\\n\\r'.includes(text[j])) j++\n const next = text[j]\n // 冒号只有在键位置才是结构符;值里的引号 + 冒号属于内容(内嵌 JSON 的常态)\n const isStructural =\n next === ',' || next === '}' || next === ']' || next === undefined || (next === ':' && keyPosition)\n if (isStructural) {\n inString = false\n lastStructural = next === undefined ? '' : next\n out += ch\n } else {\n out += '\\\\\"'\n }\n continue\n }\n out += ch\n }\n return out\n}\n\n/**\n * ⚠️ 刻意**不提供**「更激进的猜测」候选(例如把 `\"` 后跟 `}` 也一律当内容)。\n * 试过,结果是灾难:外层键的收尾引号也会被转义 → 整个载荷被搅坏;\n * 而且即使侥幸解析成功,也可能交出一条**被改坏的命令**并真的执行它。\n * 嵌套引号(`node -e \"console.log({\"k\":\"v\"})\"`)在原理上无法靠单字符前瞻消歧 ——\n * 这种极端用例的正确处置是**拒绝 + 重试**(重试后模型通常会改用更简单的写法),\n * 而不是猜。宁可拒绝,也绝不交出坏命令。\n */\nexport function* jsonRepairCandidates(text: string): Generator<string> {\n const pathTail = (value: string): string => value.replace(/([A-Za-z]:[^\"]*?)\\\\\"(?=[,}\\]\\s])/g, '$1\\\\\\\\\"')\n // 依次尝试:原样 → 补未转义引号 → 结构性补括号;每种再各跑一遍字符串级修复。\n // 所有候选都由 JSON.parse 验证,取先成功的那个。\n for (const base of [text, ...structuralRepairCandidates(text)]) {\n for (const variant of [base, escapeInnerQuotes(base)]) {\n yield repairJsonText(pathTail(variant), { mode: 'smart' })\n yield repairJsonText(variant, { mode: 'smart' })\n yield repairJsonText(pathTail(variant), { mode: 'conservative' })\n yield repairJsonText(variant, { mode: 'conservative' })\n }\n }\n}\n\n/**\n * 结构性修复候选:模型写的 tool_calls JSON 常有**括号结构错误**(漏写闭合、数组/对象闭合顺序错乱)。\n *\n * 已覆盖的实测形态:\n * - 每个调用对象少写一个 `}`(2026-09 事故 #4:批量 3 个调用各少一个)\n * - arguments 写成数组、且 `]`/`}` 顺序错乱(2026-09-11 事故 #5:\n * `{\"tool_calls\":[{\"name\":\"pwsh\",\"arguments\":[{…}}]}` ← args 数组没闭合就写了 `}`)\n * - 外层对象少写收尾 `}`\n *\n * 做法(rebuildToolCallJson):栈引导重排 —— 遇到不匹配的闭合符时,**插入缺失的容器闭合**\n * 使其匹配。只插入括号,绝不改写字符串内容。配合 parseToolCallJson 的\n * 「arguments 数组 → 取唯一元素」解包,这类调用可以完整恢复并执行。\n */\nexport function* structuralRepairCandidates(text: string): Generator<string> {\n const marker = /^\\s*\\{\\s*\"tool_calls?\"\\s*:\\s*\\[/.exec(text)\n if (!marker) return\n const rebuilt = rebuildToolCallJson(text)\n if (rebuilt && rebuilt !== text) yield rebuilt\n}\n\n/**\n * 栈引导的 tool_calls JSON 重排(只在严格解析失败后使用,**只插入括号、绝不改写字符串内容**)。\n *\n * 规则:\n * 1) 正常的开/闭符合配 → 原样输出并弹栈;\n * 2) 闭合符与栈顶不匹配 → 在其前**插入**能使它匹配的闭合序列(有上限保护),再正常闭合;\n * 3) `,` 出现在 tool_calls 数组的元素层级、而栈顶是未闭合的调用对象 → 先补 `}`\n * (实测形态:批量调用每个元素都少写一个 `}`);\n * 4) 收尾按栈补齐剩余闭合。\n *\n * ⚠️ 安全闸门:扫描结束时若**仍在字符串内**(流被服务端 60s 上限截断的典型特征)→ 返回 null。\n * 此时补括号会得到一条**被截断的命令**并真的执行它 —— 宁可拒绝(→ 重试),也不执行半条命令。\n */\nexport function rebuildToolCallJson(text: string): string | null {\n if (!/^\\s*\\{\\s*\"tool_calls?\"\\s*:\\s*\\[/.test(text)) return null\n let out = ''\n const stack: string[] = []\n let inString = false\n let escape = false\n let insertions = 0\n for (let i = 0; i < text.length; i++) {\n const ch = text[i]\n if (inString) {\n out += ch\n if (escape) escape = false\n else if (ch === '\\\\') escape = true\n else if (ch === '\"') inString = false\n continue\n }\n if (ch === '\"') {\n inString = true\n out += ch\n continue\n }\n if (ch === '{' || ch === '[') {\n stack.push(ch)\n out += ch\n continue\n }\n if (ch === '}' || ch === ']') {\n const want = ch === '}' ? '{' : '['\n while (stack.length > 0 && stack[stack.length - 1] !== want) {\n if (insertions >= 8) return null\n out += stack[stack.length - 1] === '{' ? '}' : ']'\n stack.pop()\n insertions += 1\n }\n if (stack.length === 0) return null\n stack.pop()\n out += ch\n continue\n }\n if (ch === ',') {\n // `,` 在 tool_calls 数组的**元素层级**(数组之上只有一层调用对象)而栈顶未闭合\n // → 模型忘了写这个元素的 `}`,补上(实测:批量调用每个都少一个 `}`)。\n // 数组之上超过一层 = 逗号在元素内部的合法位置(args 对象/数组里),不动。\n // ⚠️ 还必须带前瞻:调用对象内部的键分隔逗号(\"name\" 与 \"arguments\" 之间)在这一刻的\n // 深度同样是 1,但后面跟的是 `\"arguments\"` 而不是 `{\"name\"` —— 不带前瞻会把每个调用对象拦腰补坏。\n const bracketIndex = stack.indexOf('[')\n if (\n bracketIndex === 1 &&\n stack.length - bracketIndex - 1 === 1 &&\n stack[stack.length - 1] === '{' &&\n /^\\s*\\{\\s*\"name\"\\s*:/.test(text.slice(i + 1))\n ) {\n out += '}'\n stack.pop()\n insertions += 1\n }\n out += ch\n continue\n }\n out += ch\n }\n if (inString) return null // 安全闸门(见上)\n if (insertions > 8) return null\n while (stack.length > 0) {\n out += stack[stack.length - 1] === '{' ? '}' : ']'\n stack.pop()\n }\n return out\n}\n\n/**\n * 修复常见 JSON 语法问题。\n * @param options.mode - `smart`(默认):字符串内出现非法转义时,把该字符串所有反斜杠按字面\n * 处理(模型原样写路径的常态,避免 `\\r`/`\\n`/`\\t` 被误当转义);\n * `conservative`:只补非法转义,其余原样保留。\n */\nexport function repairJsonText(text: string, options: { mode?: 'smart' | 'conservative' } = {}): string {\n const mode = options.mode ?? 'smart'\n let out = ''\n let inString = false\n let buf = ''\n const flushString = (): void => {\n const raw = buf\n // smart:整串都是「原样写出」的反斜杠 → 全部按字面;否则只补非法转义\n const body =\n mode === 'smart' && hasInvalidEscape(raw)\n ? literalizeBackslashes(raw) // 整串按字面处理(模型原样写路径的常态)\n : escapeInvalidEscapes(raw)\n out += `\"${body}\"`\n buf = ''\n }\n for (let i = 0; i < text.length; i++) {\n const ch = text[i]\n if (!inString) {\n if (ch === '\"') {\n inString = true\n buf = ''\n continue\n }\n out += ch\n continue\n }\n if (ch === '\\\\') {\n const next = text[i + 1]\n if (next === undefined) {\n buf += '\\\\\\\\'\n continue\n }\n buf += ch + next\n i += 1\n continue\n }\n if (ch === '\"') {\n flushString()\n inString = false\n continue\n }\n if (ch === '\\n') {\n buf += '\\\\n'\n continue\n }\n if (ch === '\\r') {\n buf += '\\\\r'\n continue\n }\n if (ch === '\\t') {\n buf += '\\\\t'\n continue\n }\n buf += ch\n }\n if (inString) flushString()\n // 去掉对象/数组结尾的多余逗号\n return out.replace(/,(\\s*[}\\]])/g, '$1')\n}\n\n/** 字符串里是否存在「非法转义」(判断模型是否原样写出了未转义的反斜杠)。 */\nfunction hasInvalidEscape(body: string): boolean {\n for (let i = 0; i < body.length; i++) {\n if (body[i] !== '\\\\') continue\n const next = body[i + 1]\n if (next === undefined) return true\n if (!'\"\\\\/bfnrtu'.includes(next)) return true\n i += 1\n }\n return false\n}\n\n/**\n * 把「模型原样写出的字符串」按字面语义重新转义。\n * 逐字符处理以避免正则的重复加倍:`\\\\` 保留为一个字面反斜杠、`\\\"` 保留为转义引号,\n * 其余单个反斜杠一律补成 `\\\\`(关键:让 `\\resources` 里的 `\\r` 不再变成回车)。\n */\nfunction literalizeBackslashes(raw: string): string {\n let out = ''\n for (let i = 0; i < raw.length; i++) {\n const ch = raw[i]\n if (ch !== '\\\\') {\n out += ch\n continue\n }\n const next = raw[i + 1]\n if (next === '\\\\') {\n out += '\\\\\\\\'\n i += 1\n continue\n }\n if (next === '\"') {\n out += '\\\\\"'\n i += 1\n continue\n }\n out += '\\\\\\\\'\n }\n return out\n}\n\n/** 只把「非法转义」补成字面反斜杠,合法转义原样保留。 */\nfunction escapeInvalidEscapes(body: string): string {\n let out = ''\n for (let i = 0; i < body.length; i++) {\n const ch = body[i]\n if (ch !== '\\\\') {\n out += ch\n continue\n }\n const next = body[i + 1]\n if (next === undefined) {\n out += '\\\\\\\\'\n continue\n }\n if ('\"\\\\/bfnrtu'.includes(next)) {\n out += ch + next\n i += 1\n continue\n }\n out += '\\\\\\\\'\n }\n return out\n}\n\n/** 去掉 CDATA 包装并按 JSON 解析值(解析不出就当字符串)。 */\nfunction parseParameterValue(raw: string): unknown {\n let text = raw.trim()\n const cdata = /^<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>$/.exec(text)\n if (cdata) text = cdata[1]\n if (text === '') return ''\n const parsed = parseJsonLenient(text)\n return parsed === undefined ? text : parsed\n}\n\n/**\n * 解析 XML/DSML 风格的工具调用块(整块文本,可能含多个 invoke)。\n * 支持:`<tool_calls>`/`<function_calls>` 包裹、裸 `<invoke>`、`|DSML|` 前缀、\n * CDATA 值、属性任意顺序、围栏包裹。\n */\n/**\n * XML 家族里**最后一个闭合调用块之后**的正文。\n *\n * 为什么需要它(0.1.82):XML 捕获态是一直吃到轮末的(`flush` 才结算)。收不到包裹收尾时,\n * 整段——包括调用块**之后的正文**——都留在 buffer 里;而 `flush` 只回吐 `calls`,\n * 那段正文既不回吐也不计入 rejected ⇒ 回答\"凭空中断\",且日志里零线索。\n * 这里只负责定位\"最后一个 `</invoke>` 家族收尾之后\"的部分,交给 flush 剥残片后透出。\n * 找不到收尾标签(抢救解析路径)时返回空串 —— 保持旧行为,不猜。\n */\nexport function xmlToolCallTail(block: string): string {\n const text = normalizeDsml(block).replace(FENCE_HEAD_RE, '').replace(/```\\s*$/, '')\n const closeRe = new RegExp(`${TAG_CLOSE_PREFIX}invoke\\\\s*>`, 'gi')\n let end = -1\n let match: RegExpExecArray | null\n while ((match = closeRe.exec(text)) !== null) end = match.index + match[0].length\n return end >= 0 ? text.slice(end) : ''\n}\n\nexport function parseXmlToolCalls(block: string): ToolCallRequest[] | null {\n const text = normalizeDsml(block).replace(FENCE_HEAD_RE, '').replace(/```\\s*$/, '')\n const invokeRe = new RegExp(`${TAG_OPEN_PREFIX}invoke\\\\b([^>]*)>([\\\\s\\\\S]*?)${TAG_CLOSE_PREFIX}invoke\\\\s*>`, 'gi')\n const calls: ToolCallRequest[] = []\n let invoke: RegExpExecArray | null\n while ((invoke = invokeRe.exec(text)) !== null) {\n const name = readAttr(invoke[1], 'name')\n if (!name) continue\n const body = invoke[2]\n const args: Record<string, unknown> = {}\n let sawParam = false\n const paramRe = new RegExp(`${TAG_OPEN_PREFIX}parameter\\\\b([^>]*)>([\\\\s\\\\S]*?)${TAG_CLOSE_PREFIX}parameter\\\\s*>`, 'gi')\n let param: RegExpExecArray | null\n while ((param = paramRe.exec(body)) !== null) {\n const key = readAttr(param[1], 'name')\n if (!key) continue\n sawParam = true\n args[key] = parseParameterValue(param[2])\n }\n if (!sawParam) {\n // 没有 parameter 子元素:尝试把内文当 JSON 参数,否则按 _raw 保留\n const inner = body.trim()\n if (inner) {\n try {\n const parsed = JSON.parse(inner)\n if (parsed && typeof parsed === 'object') Object.assign(args, parsed as Record<string, unknown>)\n else args._raw = parsed\n } catch {\n args._raw = inner\n }\n }\n }\n calls.push({\n id: `call_${randomUUID().replace(/-/g, '').slice(0, 20)}`,\n name,\n arguments: JSON.stringify(args),\n })\n }\n if (calls.length > 0) return calls\n return salvageXmlToolCalls(text)\n}\n\n/**\n * 宽容抢救:模型写出的 XML 调用块**收尾不全**时的最后一道网。\n *\n * 实测泄漏样本(2026-09,正是「一个字符一行」乱码的来源):\n * `<tool_calls><invoke name=\"pwsh\"><parameter name=\"command\">…</parameter>`\n * —— 参数值写完了,但**缺内层 `</invoke>`**(流被服务端上限截断时常见)。此时严格解析\n * 认不出 invoke(它的正则要求 `</invoke>` 收尾),于是整块被当正文吐给用户;\n * 而 Web GUI 把命令行里的 `$…$` 当 KaTeX 渲染 → 用户看到「一个字符一行 + 弯引号」的乱码。\n * (注:只缺最外层 `</tool_calls>` 的情形严格解析本来就能兜住,不是泄漏源。)\n *\n * 做法:不依赖任何闭合标签,只按「`<invoke name=…>` 开标签 → 下一个开标签或块尾」切段取值。\n * ⚠️ 只在**严格解析完全失败**时兜底,因此不会抢占正常路径。\n * 宁可能截断也不要泄漏 —— 截断的调用会在下一轮被模型自己纠正。\n */\nfunction salvageXmlToolCalls(text: string): ToolCallRequest[] | null {\n const invokeStartRe = new RegExp(`${TAG_OPEN_PREFIX}invoke\\\\b([^>]*)>`, 'gi')\n const starts: { index: number; attrs: string }[] = []\n let match: RegExpExecArray | null\n while ((match = invokeStartRe.exec(text)) !== null) starts.push({ index: match.index, attrs: match[1] })\n if (starts.length === 0) return null\n\n const calls: ToolCallRequest[] = []\n for (let i = 0; i < starts.length; i++) {\n const name = readAttr(starts[i].attrs, 'name')\n if (!name) continue\n const bodyStart = starts[i].index + starts[i].attrs.length\n // 段落 = 到下一个 invoke 开标签为止;不能按闭合标签切,因为它们可能整段缺失。\n const nextStart = starts[i + 1]?.index ?? text.length\n const body = text.slice(bodyStart, nextStart)\n calls.push({\n id: `call_${randomUUID().replace(/-/g, '').slice(0, 20)}`,\n name,\n arguments: JSON.stringify(salvageXmlParameters(body)),\n })\n }\n return calls.length > 0 ? calls : null\n}\n\n/** 从残缺的 invoke 内文里取出参数:按开标签切段,值取到下一个开标签或段尾。 */\nfunction salvageXmlParameters(body: string): Record<string, unknown> {\n const args: Record<string, unknown> = {}\n const paramStartRe = new RegExp(`${TAG_OPEN_PREFIX}parameter\\\\b([^>]*)>`, 'gi')\n const found: { start: number; end: number; key: string }[] = []\n let match: RegExpExecArray | null\n while ((match = paramStartRe.exec(body)) !== null) {\n const key = readAttr(match[1], 'name')\n if (key) found.push({ start: match.index, end: paramStartRe.lastIndex, key })\n }\n for (let i = 0; i < found.length; i++) {\n // 值 = 本参数开标签之后 → 下一个参数开标签之前(或段尾)\n // 按**开标签位置**切段而不是按 `</parameter>`:收尾标签可能整段缺失。\n const valueEnd = found[i + 1] ? found[i + 1].start : body.length\n // 值尾部残留的 `</parameter>` / `</invoke>` / `</tool_calls>` 一律剥掉\n args[found[i].key] = parseParameterValue(stripXmlClosers(body.slice(found[i].end, valueEnd)))\n }\n if (found.length === 0) {\n const inner = stripXmlClosers(body).trim()\n if (inner) {\n const parsed = parseJsonLenient(inner)\n if (parsed && typeof parsed === 'object') Object.assign(args, parsed as Record<string, unknown>)\n else args._raw = inner\n }\n }\n return args\n}\n\n/** 剥掉值尾部残留的收尾标签与空白。 */\nfunction stripXmlClosers(value: string): string {\n const re = new RegExp(`(?:\\\\s*${TAG_CLOSE_PREFIX}(?:${XML_CLOSE_NAMES})\\\\s*>)+\\\\s*$`, 'i')\n return value.replace(re, '')\n}\n\n/**\n * 判断捕获到的协议块是否**确实是一次工具调用尝试**(而不是正文里恰好提到了 `<invoke>` 这类词)。\n * 只用于「解析失败时该丢弃还是该透出」的裁决:\n * - 像调用 → 丢弃 + 告警(绝不泄漏成乱码,交给上层重试)\n * - 不像调用 → 当普通正文透出(绝不吞掉模型正文)\n */\nfunction looksLikeToolCallBlock(mode: 'json' | 'xml', raw: string): boolean {\n if (mode === 'json') return MARKER_RE.test(raw)\n const text = normalizeDsml(raw)\n // 带 name 属性的 invoke/parameter 开标签 = 真的在尝试调用。\n // 注意:系统提示词讲协议时只写 `<invoke>` / `<parameter>`(无 name=),因此不会被误判。\n return (\n new RegExp(`${TAG_OPEN_PREFIX}invoke\\\\b[^>]*\\\\bname\\\\s*=`, 'i').test(text) ||\n new RegExp(`${TAG_OPEN_PREFIX}parameter\\\\b[^>]*\\\\bname\\\\s*=`, 'i').test(text)\n )\n}\n\n/**\n * 分类失败形态,用于诊断 —— 日志只保留前 400 字符,看不到后半段的坏点,\n * 所以必须把「没收全」与「收全了但结构不对」分开,否则永远在猜。\n *\n * - `unbalanced`:块没配平/没收全 —— 多半是流被服务端 60s 上限截断,不是模型写错;\n * - `unparsable`:块是完整的,但结构不符(漏括号、引号没转义、形状不对);\n * - `echo` :载荷里裹着转写回声(模型在回放历史,不是在调用)。\n */\nfunction classifyFailure(mode: 'json' | 'xml', raw: string): 'unbalanced' | 'unparsable' | 'echo' {\n // 最先判回声:实测抓到的 8152 字符载荷里有 15 条「调用」,命令串内裹着\n // `[Tool Result for call_…]` / `[Truncated]` —— 那是历史回放,执行它等于重跑旧命令。\n if (/\\[\\s*Tool Result\\b/i.test(raw)) return 'echo'\n if (mode === 'json') return extractBalancedJson(raw.replace(FENCE_HEAD_RE, '')) ? 'unparsable' : 'unbalanced'\n return findXmlToolCallEnd(raw) === -1 ? 'unbalanced' : 'unparsable'\n}\n\n/** 把解析出的 JSON 转成工具调用请求;非协议形状返回 null。 */\nexport function parseToolCallJson(json: string): ToolCallRequest[] | null {\n const parsed: any = parseJsonLenient(json)\n if (!parsed || typeof parsed !== 'object') return null\n const raw = Array.isArray(parsed.tool_calls)\n ? parsed.tool_calls\n : parsed.tool_call && typeof parsed.tool_call === 'object'\n ? [parsed.tool_call]\n : null\n if (!raw) return null\n const calls: ToolCallRequest[] = []\n for (const entry of raw) {\n if (!entry || typeof entry !== 'object') continue\n const name = typeof entry.name === 'string' ? entry.name : typeof entry.tool === 'string' ? entry.tool : ''\n if (!name) continue\n let args = entry.arguments ?? entry.parameters ?? entry.args ?? {}\n // 模型偶尔把 arguments 写成**数组**(实测 2026-09-11:arguments:[{…}],规范是对象)。\n // 只有一个对象元素时取该元素 —— 否则参数会被序列化成 \"[{…}]\",工具拿到的是垃圾。\n if (Array.isArray(args) && args.length === 1 && args[0] && typeof args[0] === 'object' && !Array.isArray(args[0])) {\n args = args[0]\n }\n if (typeof args === 'string') {\n // 已经是字符串:能解析就原样透出(DSH 的 arguments 语义是原始 JSON 串),否则包装\n const reparsed = parseJsonLenient(args)\n if (reparsed === undefined) args = JSON.stringify({ _raw: args })\n } else {\n try {\n args = JSON.stringify(args ?? {})\n } catch {\n args = '{}'\n }\n }\n calls.push({ id: `call_${randomUUID().replace(/-/g, '').slice(0, 20)}`, name, arguments: String(args) })\n }\n return calls.length > 0 ? calls : null\n}\n\n/**\n * 流尾兜底的 JSON 抢救(比 parseToolCallJson 多退一步)。\n *\n * 捕获缓冲里可能带着捕获后残留的多余字符(围栏、正文),此时整段 `JSON.parse` 必然失败,\n * 但**配平的前缀本身是好的调用** —— 取前缀再解析,别把能救的调用整批丢掉。\n * 注意:不配平的截断仍由 structuralRepairCandidates 的安全闸门拒绝(宁可不执行半条命令)。\n */\nfunction parseSalvagedToolCallJson(buffer: string): ToolCallRequest[] | null {\n const text = buffer.replace(FENCE_HEAD_RE, '')\n const direct = parseToolCallJson(text)\n if (direct) return direct\n const balanced = extractBalancedJson(text)\n if (balanced && balanced.end < text.length) return parseToolCallJson(balanced.json)\n return null\n}\n\n/**\n * 在捕获缓冲里找 XML 调用块的结束位置(含结束标签)。\n * - 包裹式(`<tool_calls>` / `<function_calls>`):找对应闭合标签\n * - 裸 `<invoke>`:找到 `</invoke>` 后继续吞并紧随其后的 invoke 块(同一批调用)\n * 返回 -1 表示尚未收全(继续等流)。\n */\nexport function findXmlToolCallEnd(buffer: string): number {\n const text = buffer\n const wrapper = new RegExp(`<\\\\s*${DSML_PREFIX}(?:dsml-)?(${WRAPPER_NAMES})\\\\b`, 'i').exec(text)\n const startsWithWrapper = wrapper !== null && wrapper.index === 0\n const isInvokeStart = (value: string): boolean =>\n new RegExp(`^\\\\s*<\\\\s*${DSML_PREFIX}(?:dsml-)?invoke\\\\b`, 'i').test(value)\n\n if (startsWithWrapper) {\n const tag = wrapper![1].toLowerCase()\n const closeRe = new RegExp(`<\\\\/\\\\s*${DSML_PREFIX}(?:dsml-)?${tag}\\\\s*>`, 'i')\n const match = closeRe.exec(text)\n return match ? match.index + match[0].length : -1\n }\n if (!isInvokeStart(text)) {\n // 可能是 `{\"tool_calls\":…` 之外的 XML 前缀(如仅 `<invoke` 尚未收全)\n return -1\n }\n let cursor = 0\n for (;;) {\n const slice = text.slice(cursor)\n if (!isInvokeStart(slice)) return cursor > 0 ? cursor : -1\n const closeRe = /<\\/\\s*(?:\\|\\s*DSML\\s*\\|\\s*)?(?:dsml-)?invoke\\s*>/i\n const match = closeRe.exec(slice)\n if (!match) return -1\n cursor += match.index + match[0].length\n const rest = text.slice(cursor)\n // 后面的下一个非空白若是新的 invoke,则继续吞并\n if (isInvokeStart(rest)) continue\n // ⚠️ 2026-09-12 实测泄漏:模型常把**包裹开始标签写丢、只留下闭合标签**,\n // 于是正文里冒出 `</|DSML|calls>`(甚至退化成 `</ calls>`)。\n // 孤立的闭合标签属于这一批调用,不该上屏 —— 收全了就一起吞掉。\n // 复现:连续裸 `<|DSML|invoke …>` 之后跟一个 `</|DSML|calls>`,旧代码会把它当正文吐出。\n const strayClose = new RegExp(\n `^\\\\s*<\\\\/\\\\s*${DSML_PREFIX}(?:dsml-)?(?:${WRAPPER_NAMES})\\\\s*>`,\n 'i',\n )\n const stray = strayClose.exec(rest)\n if (stray) return cursor + stray[0].length\n // 看起来\"还在收\"的残片(`<` / `</` / `</|DSM` / `</ calls` …)→ 继续等,\n // 别急着当正文吐。真收不全时 flush() 会走 parseXmlToolCalls,整段不外泄。\n const tail = rest.trim()\n if (tail.startsWith('<') && /^<\\/?\\s*[||]?\\s*[A-Za-z]{0,14}$/.test(tail)) return -1\n return cursor\n }\n}\n\n/**\n * 剥掉「孤立的工具调用标记残片」。\n *\n * ⚠️ 2026-09-12 实测泄漏(用户截图里那句 `voke> </ calls>`):模型把**包裹开始标签写丢**,\n * 只留下闭合标签,或者只留下标签的后半截。这些残片不属于回答,但会绕过捕获逻辑\n * (识别器只认 `<…invoke` / `<…calls>` 这类**开始**形态)落进正文缓冲,最后被当正文吐出去。\n *\n * 触发路径是 `flush()`:残片通常很短(`</|DSML|calls>` 只有 16 字符),\n * 小于 HOLD_BACK_CHARS 就会被一直 hold 住,流结束时无条件吐出。\n *\n * 三道规则,从明确到宽松:\n * 1. 带 DSML 前缀的孤立闭合标签 —— `</|DSML|calls>` / `</ | DSML | invoke>`\n * (DSML 是 DeepSeek 私有的标记名,正文里不可能正常出现,剥掉零风险)\n * 2. 前缀被吃光的退化形态 —— `</ calls>` / `</invoke>`\n * 3. 只剩后半截的 —— 独占一行的 `voke>`(`invoke>` 掉了头)\n * 正文里正常讨论 XML 时通常写在代码围栏或行内代码里,形态与这三条不同。\n */\nexport function stripStrayToolMarkup(text: string): string {\n if (!text) return text\n if (!/voke\\s*>|calls?\\s*>|tool_calls?\\s*>|function_calls?\\s*>|DSML/i.test(text)) return text\n return text\n // 0) DSML 前缀的**裸包裹标签**(开或闭都算)。实测(2026-09-12 用户截图):\n // 模型吐过一个退化的 `<|DSML|calls>` + `</|DSML|invoke>` + `</|DSML|calls>`,\n // 里面没有任何 invoke → 不是调用块 → 被当正文透出。DSML 是私有标记,正文里不会正常出现。\n .replace(\n // `dsml-` 连字符变体(`<dsml-calls>`)也要算 —— 本文件其它地方已声明兼容该变体。\n new RegExp(\n `<\\\\/?\\\\s*(?:(?:[||]+\\\\s*DSML\\\\s*[||]+\\\\s*)|dsml-)(?:dsml-)?(?:${WRAPPER_NAMES}|invoke)\\\\s*>`,\n 'gi',\n ),\n '',\n )\n .replace(\n new RegExp(`<\\\\/\\\\s*(?:[||]+\\\\s*DSML\\\\s*[||]+\\\\s*)?(?:dsml-)?(?:${WRAPPER_NAMES}|invoke)\\\\s*>`, 'gi'),\n '',\n )\n .replace(/<\\/\\s+(?:tool_calls?|function_calls|calls|invoke)\\s*>/gi, '')\n .replace(/(^|\\n)[ \\t]*(?:in)?voke\\s*>\\s*(?=\\n|$)/gi, '$1')\n}\n\n/**\n * 流式工具调用过滤器。\n * - 普通正文:立即透传(仅 hold back 末尾少量字符以观测跨包的调用标记)\n * - 命中调用标记(JSON 或 XML 两套):进入捕获态,收全后转成 tool-call 请求,标记本身不外泄\n * - 解析失败:把捕获内容当普通正文吐出(降级但可见,绝不静默丢内容)\n * - 调用后面的剩余文本继续按普通正文处理(含围栏收尾清理)\n */\nexport class ToolCallStreamFilter {\n private pending = ''\n private capture: { mode: 'json' | 'xml'; buffer: string } | null = null\n private abandoned: { raw: string; mode: 'json' | 'xml'; reason?: 'unbalanced' | 'unparsable' | 'oversize' | 'echo' } | null = null\n private readonly knownTools?: ReadonlySet<string>\n\n constructor(knownTools?: ReadonlySet<string>) {\n this.knownTools = knownTools\n }\n\n push(text: string): FilterOutput {\n const out: FilterOutput = { text: '', calls: [] }\n if (text) {\n if (this.capture) this.capture.buffer += text\n else this.pending += text\n }\n this.drain(out)\n return out\n }\n\n flush(): FilterOutput {\n const out: FilterOutput = { text: '', calls: [] }\n if (this.capture) {\n // 流结束时仍未收全:先尝试宽容解析(转义修复 + 结构性补括号 + 缺闭合标签抢救)。\n const captured = this.capture\n const calls =\n captured.mode === 'xml' ? parseXmlToolCalls(captured.buffer) : parseSalvagedToolCallJson(captured.buffer)\n if (calls) {\n out.calls.push(...calls)\n // 0.1.82:调用块**之后**的正文不能跟着一起消失 —— 见 xmlToolCallTail 的说明。\n if (captured.mode === 'xml') out.text += stripStrayToolMarkup(xmlToolCallTail(captured.buffer))\n } else if (looksLikeToolCallBlock(captured.mode, captured.buffer))\n this.abandoned ??= { raw: captured.buffer, mode: captured.mode, reason: classifyFailure(captured.mode, captured.buffer) }\n // 不像调用(只是正文里提到 `<invoke>` 这类词)→ 照常透出,\n // ⚠️ 但必须先剥残片:实测过 `<|DSML|calls>` + 闭合标签这种退化块会从这里漏到正文(2026-09-12)。\n else out.text += stripStrayToolMarkup(captured.buffer)\n this.capture = null\n }\n // 流结束:把 hold 住的尾巴吐出去之前先剥掉孤立残片 —— 它们会在这里\"逃逸\"成正文\n out.text += stripStrayToolMarkup(this.pending)\n this.pending = ''\n if (this.abandoned) out.rejected = this.abandoned\n return out\n }\n\n private drain(out: FilterOutput): void {\n for (;;) {\n if (this.capture) {\n const captured = this.capture\n if (captured.mode === 'xml') {\n const end = findXmlToolCallEnd(captured.buffer)\n if (end === -1) {\n if (captured.buffer.length > MAX_CAPTURE_CHARS) {\n // 超长仍未收全:是调用就丢弃(不吐乱码),只是正文提及则照常透出\n if (looksLikeToolCallBlock('xml', captured.buffer))\n this.abandoned ??= { raw: captured.buffer, mode: 'xml', reason: 'oversize' }\n else out.text += stripStrayToolMarkup(captured.buffer)\n this.capture = null\n continue\n }\n return\n }\n const block = captured.buffer.slice(0, end)\n const calls = parseXmlToolCalls(block)\n if (calls) out.calls.push(...calls)\n else if (looksLikeToolCallBlock('xml', block)) this.abandoned ??= { raw: block, mode: 'xml', reason: 'unparsable' }\n else out.text += stripStrayToolMarkup(block)\n this.capture = null\n this.pending = captured.buffer.slice(end).replace(FENCE_HEAD_RE, '') + this.pending\n continue\n }\n const balanced = extractBalancedJson(captured.buffer)\n if (!balanced) {\n if (captured.buffer.length > MAX_CAPTURE_CHARS) {\n // 超过上限仍没配平:放弃,但**不吐成正文**(那是乱码,不是回答)\n this.abandoned ??= { raw: captured.buffer, mode: 'json', reason: 'oversize' }\n this.capture = null\n continue\n }\n return\n }\n const calls = parseToolCallJson(balanced.json)\n if (calls) {\n // 未知工具名也照常透出:由运行器给出「未知工具」结果,模型可自行纠正。\n out.calls.push(...calls)\n this.capture = null\n this.pending = captured.buffer.slice(balanced.end).replace(FENCE_HEAD_RE, '') + this.pending\n continue\n }\n // 形状不符:能进到捕获态就说明 MARKER_RE 命中过(`{\"tool_calls\":`),\n // 因此这是**坏掉的调用**而不是正文 → 丢弃 + 告警(泄漏成正文才是真正的乱码来源)。\n const head = captured.buffer.slice(0, balanced.end)\n if (looksLikeToolCallBlock('json', head)) this.abandoned ??= { raw: head, mode: 'json', reason: 'unparsable' }\n else out.text += head\n this.capture = null\n this.pending = captured.buffer.slice(balanced.end) + this.pending\n continue\n }\n\n // 找最早的完整标记(JSON 或 XML)\n const jsonMarker = MARKER_RE.exec(this.pending)\n const xmlMarker = XML_STARTER_RE.exec(this.pending)\n const jsonIndex = jsonMarker?.index ?? -1\n const xmlIndex = xmlMarker?.index ?? -1\n const useXml = xmlIndex !== -1 && (jsonIndex === -1 || xmlIndex < jsonIndex)\n const index = useXml ? xmlIndex : jsonIndex\n if (index !== -1) {\n let head = this.pending.slice(0, index)\n const fence = FENCE_TAIL_RE.exec(head)\n if (fence) head = head.slice(0, fence.index)\n out.text += head\n this.capture = { mode: useXml ? 'xml' : 'json', buffer: this.pending.slice(index) }\n this.pending = ''\n continue\n }\n\n // 未见完整标记:先把夹在正文里的孤立残片剥掉,再保留末尾可能的标记前缀\n this.pending = stripStrayToolMarkup(this.pending)\n if (this.pending.length <= HOLD_BACK_CHARS) return\n const hold = partialMarkerSuffixLength(this.pending)\n if (hold > 0) {\n out.text += this.pending.slice(0, this.pending.length - hold)\n this.pending = this.pending.slice(this.pending.length - hold)\n return\n }\n out.text += this.pending\n this.pending = ''\n return\n }\n }\n}\n\n// ── 转写回声守卫 ──────────────────────────────────────────\n\n/**\n * 剥离模型模仿的「系统标记」(`<ds_system>…</ds_system>` / `<system>…</system>`)。\n *\n * 实测(deepseek-web):模型会在正文里吐出成串的伪系统标记,**模仿它见过的协议格式**。\n * 这与「转写回声」是同一类问题,但形态是 XML 标签而不是 `[Tool Result]` 行,\n * 所以单独一层处理。围栏代码块内不剥(正常回答可能讨论这些标记)。\n *\n * ## 标签清单(只列**有现场证据**的,不做通配)\n *\n * ⚠️ 刻意**不用** `<[a-z_]+>` 这种通配:用户的正常回答可能就是一段讨论这些标签的文档,\n * 通配会把它们一起吃掉。每加一个名字都要有现场 + 穷搜证据。\n *\n * | 标签 | 现场 |\n * | --- | --- |\n * | `ds_system` | 2026-09-11:一条正文里 13 个 `<ds_system>Tool result for call_1a2b3c</ds_system>`,调用 ID 还是字母递增编造的(1a2b3c→4d5e6f→7a8b9c…) |\n * | `system` | 同批现场 |\n * | `ide_result_status` | 2026-09-12(会话 `15ac4c56`):正文里冒出 `<ide_result_status>Tool ran without output or errors</ide_result_status>`。⚠️ 该串在**DSH 的 `app.asar`(0 处)、全部已装插件(0 处)、`~/.dsh` 全树(0 处)**里都搜不到,且会话日志里**只出现在模型的输出字段**(212 条 `tool/result`、用户消息、系统消息里一处都没有)→ 判定是**模型自己编的**,不是 DSH 提供的 |\n *\n * @returns 剥离后的文本;`stripped` = 是否剥掉了至少一个标记(用于日志/告警)。\n */\nconst IMITATED_MARKER_TAGS = ['ds_system', 'system', 'ide_result_status'] as const\n\n/**\n * 需要「正文够长」才剥的**跨行**标记。\n *\n * `tool_result`(2026-09-13 现场,会话 `15ac4c56` 记录 `[1480]`):模型把 SSH 插件一次\n * 读取工具的结果**整段复述**进正文,形如\n * `<tool_result>Path: …` + 换行 + `<path>…</path>` + 换行 + `<type>file</type>` + 换行 + `<content>` …\n * 它比 `ds_system` 那批更「可讨论」——用户正在开发**产出它的那个插件**,正常回答里\n * 可能出现简短示例。所以要求正文 ≥ 120 字才剥:真实回声是整份文件(实测那处上千字),\n * 随口举例不会有那么长。围栏代码块内同样不剥。\n */\nconst LONG_MARKER_TAGS: readonly { tag: string; minBody: number }[] = [{ tag: 'tool_result', minBody: 120 }]\n\nconst LONG_MARKER_ALT = LONG_MARKER_TAGS.map((entry) => entry.tag).join('|')\n/** 跨行闭合形态(`[\\s\\S]` 而不是逐行匹配 —— 真实回声的正文是跨行的)。 */\nconst LONG_CLOSED_MARKER_RE = new RegExp(\n `<(${LONG_MARKER_ALT})\\\\b[^>]*>([\\\\s\\\\S]*?)<\\\\/\\\\1>`,\n 'g',\n)\n/** 跨行未闭合形态(流被截断在标记中间)。 */\nconst LONG_OPEN_MARKER_RE = new RegExp(`<(${LONG_MARKER_ALT})\\\\b[^>]*>([\\\\s\\\\S]*)$`, 'g')\n\nfunction minBodyFor(tag: string): number {\n return LONG_MARKER_TAGS.find((entry) => entry.tag === tag)?.minBody ?? 0\n}\n\n/**\n * 代码围栏区间(成对的 ``` 或 ~~~)。\n * 跨行替换没法像逐行处理那样顺手跟踪 inFence,所以先算出区间再判断命中点是否落在里面。\n */\nfunction fencedRanges(text: string): Array<[number, number]> {\n const marks: number[] = []\n const re = /^[ \\t]*(?:```|~~~)/gm\n let match: RegExpExecArray | null\n while ((match = re.exec(text)) !== null) marks.push(match.index)\n const ranges: Array<[number, number]> = []\n for (let i = 0; i + 1 < marks.length; i += 2) ranges.push([marks[i], marks[i + 1]])\n return ranges\n}\n\nfunction insideFence(index: number, ranges: Array<[number, number]>): boolean {\n return ranges.some(([from, to]) => index >= from && index < to)\n}\n\n/** 任一伪标记的开头是否出现(快速退出用,省掉对每段正文跑逐行循环)。 */\nfunction hasImitatedMarker(text: string): boolean {\n if (IMITATED_MARKER_TAGS.some((tag) => text.includes(`<${tag}`))) return true\n return LONG_MARKER_TAGS.some((entry) => text.includes(`<${entry.tag}`))\n}\n\n/**\n * 闭合形态 `<tag …>…</tag>`:用**反向引用**要求首尾同名,\n * 避免 `<a>…</b>` 这种错配被当成一对连内容一起吃掉。\n */\nconst CLOSED_MARKER_RE = new RegExp(\n `<(${IMITATED_MARKER_TAGS.join('|')})\\\\b[^>]*>[\\\\s\\\\S]*?</\\\\1>`,\n 'g',\n)\n/** 未闭合形态:流在标记中间被截断 —— 半截标记同样是垃圾。 */\nconst OPEN_MARKER_RE = new RegExp(`<(${IMITATED_MARKER_TAGS.join('|')})\\\\b[^>]*>[\\\\s\\\\S]*$`, 'g')\n\nexport class SystemMarkerStreamFilter {\n private pending = ''\n private captured = ''\n private tag = ''\n private fence = ''\n private fenceSize = 0\n private readonly limit = 1024 * 1024\n push(text: string): { text: string; stripped: boolean } {\n this.pending += text\n return this.drain(false)\n }\n flush(): { text: string; stripped: boolean } { return this.drain(true) }\n private drain(final: boolean): { text: string; stripped: boolean } {\n let out = '', stripped = false\n const names = [...IMITATED_MARKER_TAGS, ...LONG_MARKER_TAGS.map(x => x.tag)]\n const opener = new RegExp('<(' + names.join('|') + ')\\\\b[^>]*>')\n while (this.pending.length) {\n const nl = this.pending.indexOf('\\n')\n if (nl < 0 && !final) {\n // ⚠️ 逐行模式在没有换行时会一直缓冲到轮末。安全前缀:所有被识别的标记都以 `<`\n // 开头、围栏也必须在行首,所以「不在围栏内 + 不在捕获中 + 剩余部分既无 `<`\n // 也不是围栏候选开头」时直接吐出去,不可能漏掉标记、也不影响围栏状态判定。\n //\n // 效果边界(2026-09-13 实测,别高估它):真正决定\"上屏节奏\"的是上游的\n // `TranscriptEchoGuard` —— 它同样逐行分类,无换行的回答会被它先扣到轮末,\n // 所以**整段没有换行**时这里再快也收不到东西(adapter 只会在轮末拿到一段)。\n // 这条优化保证的是:本层不再比上游更早地扣住文本(有换行的段落照旧逐行走,\n // 最后一个不完整行的前段也不必等到换行)。\n if (\n this.fence === '' &&\n !this.tag &&\n !this.pending.includes('<') &&\n !/^[ \\t]{0,3}[`~]/.test(this.pending)\n ) {\n out += this.pending\n this.pending = ''\n }\n break\n }\n let line = nl < 0 ? this.pending : this.pending.slice(0, nl + 1)\n this.pending = nl < 0 ? '' : this.pending.slice(nl + 1)\n if (!this.tag) {\n const mark = /^[ \\t]{0,3}(`{3,}|~{3,})/.exec(line)\n if (this.fence) {\n out += line\n if (mark && mark[1][0] === this.fence && mark[1].length >= this.fenceSize &&\n !line.slice(mark[0].length).trim()) this.fence = ''\n continue\n }\n if (mark) { this.fence = mark[1][0]; this.fenceSize = mark[1].length; out += line; continue }\n }\n while (line) {\n if (!this.tag) {\n const match = opener.exec(line)\n if (!match) { out += line; break }\n out += line.slice(0, match.index)\n this.tag = match[1]\n this.captured = match[0]\n line = line.slice(match.index + match[0].length)\n }\n const close = '</' + this.tag + '>'\n const at = line.indexOf(close)\n if (at < 0) { this.captured += line; line = ''; break }\n this.captured += line.slice(0, at + close.length)\n line = line.slice(at + close.length)\n const openEnd = this.captured.indexOf('>') + 1\n const bodySize = this.captured.length - openEnd - close.length\n const long = LONG_MARKER_TAGS.find(x => x.tag === this.tag)\n if (!long || bodySize >= long.minBody) stripped = true\n else out += this.captured\n this.captured = ''; this.tag = ''\n }\n }\n if (this.pending.length + this.captured.length > this.limit) {\n throw new Error('系统标记缓冲超过 1 MiB,拒绝静默截断正文')\n }\n if (final && this.tag) {\n const long = LONG_MARKER_TAGS.find(x => x.tag === this.tag)\n const bodySize = this.captured.length - this.captured.indexOf('>') - 1\n if (!long || bodySize >= long.minBody) stripped = true\n else out += this.captured\n this.captured = ''; this.tag = ''\n }\n return { text: out, stripped }\n }\n}\n\nexport function stripSystemMarkers(text: string): { text: string; stripped: boolean } {\n if (!hasImitatedMarker(text)) return { text, stripped: false }\n\n // 第一遍:**跨行**长标记。逐行循环匹配不到跨行的闭合形式,所以先在全文上做一次\n //(围栏区间先算好,落在围栏里的命中跳过,与逐行那遍的保护策略一致)。\n const ranges = fencedRanges(text)\n let strippedLong = false\n let source = text\n .replace(LONG_CLOSED_MARKER_RE, (match, tag, body, offset) => {\n if (String(body).length < minBodyFor(String(tag))) return match\n if (insideFence(offset, ranges)) return match\n strippedLong = true\n return ''\n })\n .replace(LONG_OPEN_MARKER_RE, (match, tag, body, offset) => {\n if (String(body).length < minBodyFor(String(tag))) return match\n if (insideFence(offset, ranges)) return match\n strippedLong = true\n return ''\n })\n\n text = source\n let out = ''\n let inFence = false\n let stripped = strippedLong\n let i = 0\n while (i < text.length) {\n const lineEnd = text.indexOf('\\n', i)\n const line = lineEnd === -1 ? text.slice(i) : text.slice(i, lineEnd + 1)\n const trimmed = line.trim()\n if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) inFence = !inFence\n if (!inFence) {\n out += line\n .replace(CLOSED_MARKER_RE, () => {\n stripped = true\n return ''\n })\n .replace(OPEN_MARKER_RE, () => {\n // 流在标记中间被截断:半截标记同样是垃圾\n stripped = true\n return ''\n })\n } else {\n out += line\n }\n i = lineEnd === -1 ? text.length : lineEnd + 1\n }\n return { text: out, stripped }\n}\n\n// ── 网页端免责声明剥离 ────────────────────────────────────\n\n/**\n * DeepSeek 网页端在**每一轮回复末尾**自动追加的免责声明(不是模型回答的一部分)。\n *\n * 实测(2026-09-11,27 个 DSH 会话里命中 43 处,形态唯一):\n * `本回答由 AI 生成,内容仅供参考,请仔细甄别`\n * 它会以 SSE 增量形式到达,甚至被拆成「 AI」「 生成」「,」「内容」这样的小包。\n *\n * 为什么必须剥掉:\n * - 它卡在两条回答中间(自动续写的缝就在它后面),用户会以为「模型怎么突然插了这句话」;\n * - 结尾是「甄别」这种汉字 → `looksMidSentence` 恒为真 → **每一轮都被误判成「句中被截」**,\n * 于是无限触发自动续写(续写轮又追加一遍声明,再被判成截断……)。\n */\nconst WEB_DISCLAIMER = '本回答由 AI 生成,内容仅供参考,请仔细甄别'\n\n/**\n * 一次性剥离网页端免责声明(非流式)。\n *\n * 轮末残余必须用它再过一遍:`BoilerplateFilter` 的流式扣留只管它**收到**的文本,\n * 而过滤器扣住的最后 ≤24 个字符还没经过它 —— 声明恰好 23 字,实测就整段从尾巴漏出去\n * (会话 `6c0dbc47` 里它是一个只有单个 delta 的独立 text 块,跟在工具调用后面)。\n */\nexport function stripWebDisclaimer(text: string): { text: string; stripped: boolean } {\n if (!text.includes(WEB_DISCLAIMER)) return { text, stripped: false }\n return { text: text.split(WEB_DISCLAIMER).join(''), stripped: true }\n}\n\n/**\n * 轮末收尾:把三层缓冲扣住的残余按**真实顺序**吐净,并补跑只作用于上屏前的两道清理。\n *\n * ⚠️ 为什么不能只把三层 flush 结果拼起来:\n * - 吐净顺序必须是流水线**反序**(越深的层扣住的文本越早)—— 否则最后几段文字前后颠倒;\n * - 浅层(过滤器)扣住的字符**从没经过**「剥声明」这一层,而声明就爱待在最后几个字符里;\n * - 同理,伪系统标记也可能整段藏在尾巴里。\n * 轮末没有后续输入了,所以这里可以直接做一次性替换,不需要流式扣留。\n *\n * `cleanMarkers`(2026-09-13,审计 N03):是否在**这里**用无状态的 `stripSystemMarkers`\n * 补剥伪系统标记。streamImpl 现在传 `false` —— 因为它已经用有状态的\n * `SystemMarkerStreamFilter` 处理残余了,两遍都跑只会重复劳动;\n * 默认 `true` 保留原语义,供单测与其它调用方使用。\n */\nexport function drainTextPipeline(\n filter: ToolCallStreamFilter,\n boilerplate: BoilerplateFilter,\n guard: TranscriptEchoGuard,\n cleanMarkers = true,\n): {\n text: string\n echoed: boolean\n disclaimers: number\n calls: FilterOutput['calls']\n rejected: FilterOutput['rejected']\n} {\n const tailGuarded = guard.flush()\n const tailBoiled = boilerplate.flush()\n const tail = filter.flush()\n const raw = tailGuarded.text + tailBoiled.text + tail.text\n const dedisclaimered = stripWebDisclaimer(raw)\n const cleaned = cleanMarkers ? stripSystemMarkers(dedisclaimered.text) : {text: dedisclaimered.text, stripped:false}\n return {\n text: cleaned.text,\n echoed: tailGuarded.echoed,\n disclaimers: boilerplate.count + (dedisclaimered.stripped ? 1 : 0),\n calls: tail.calls,\n rejected: tail.rejected,\n }\n}\n\n/**\n * 流式剥离网页端免责声明。\n *\n * 逐包调用:命中即整段丢弃。\n *\n * ⚠️ 扣留策略必须是「**恒定扣住最后 |声明|-1 个字符**」,不能只扣「声明的前缀」:\n * 声明会被 SSE 切成任意小包(实测有「 AI」「 生成」「,」「内容」这种),\n * 一旦切点落在声明中间,前半截已经不是「前缀」了 —— 只扣前缀就会把它放出去,\n * 后半截到齐时再也拼不回来(2026-09-11 实测漏过一次)。\n * 扣 22 个字符的代价是上屏延迟 22 字,肉眼不可见。\n */\nexport class BoilerplateFilter {\n private pending = ''\n private hits = 0\n private stripped = false\n private readonly holdChars = WEB_DISCLAIMER.length - 1\n\n push(text: string): { text: string; stripped: boolean } {\n this.pending += text\n let out = ''\n for (;;) {\n const at = this.pending.indexOf(WEB_DISCLAIMER)\n if (at !== -1) {\n out += this.pending.slice(0, at)\n this.pending = this.pending.slice(at + WEB_DISCLAIMER.length)\n this.hits += 1\n this.stripped = true\n continue\n }\n const hold = Math.min(this.pending.length, this.holdChars)\n out += this.pending.slice(0, this.pending.length - hold)\n this.pending = this.pending.slice(this.pending.length - hold)\n return { text: out, stripped: this.stripped }\n }\n }\n\n flush(): { text: string; stripped: boolean } {\n const rest = this.pending\n this.pending = ''\n return { text: rest, stripped: this.stripped }\n }\n\n /** 本次流剥掉了几处声明(用于留痕)。 */\n get count(): number {\n return this.hits\n }\n}\n\n/**\n * 转写格式标记 —— 也就是 `serializePrompt` 写进 prompt 的那套行首标记。\n *\n * 模型会**照着 prompt 里的转写格式模仿**,把工具结果 / 系统标记当回答吐出来。\n * 这与「工具调用标记泄漏」是**两个独立的泄漏源**:`ToolCallStreamFilter` 只防后者。\n *\n * 实测(2026-09-10,deepseek-web / deepseek-reasoner)可见正文里出现:\n * `[Tool Result for call_xxx]` + 真实工具输出 + `[status: running]`\n * 以及成串的 `User: …` / `Assistant: …` 转写行。\n * 2026-09-11 补:还有一种更隐蔽的形态 —— 给回声行加 `Assistant: ` 前缀\n * (`Assistant: [Tool Result for call_xxx]`),必须按「行内含转写标记」判,见 ECHO_INLINE_SIGNATURES。\n */\nconst ECHO_SIGNATURES: readonly RegExp[] = [\n /^\\[\\s*Tool Result\\b/i,\n /^\\[\\s*status\\s*:\\s*[a-z_]+\\s*\\]$/i,\n /^\\[\\s*(?:System|Assistant)\\s*\\]$/i,\n]\n/** 转写轮次行:单行可能只是正文,成串出现才是回声。 */\nconst ECHO_TURN_RE = /^(?:User|Assistant)\\s*:/\n\n/**\n * 转写特征出现在**行内任意位置**(不要求行首)。\n *\n * 实测(2026-09-11 17:07,install-plugin 工作区):模型输出的回声长这样 ——\n * `Assistant: [Tool Result for call_7b1a7d39a2e54bc0b8f1]`\n * `direct ERR fetch failed`\n * 它给回声加了 `Assistant: ` 前缀,于是行首不再匹配 ECHO_SIGNATURES,\n * 被当成「正文里偶尔出现的 User: 字样」放行(还顺带把后面那行也带了出来)。\n * 所以只要一行里**含有**这些标记,就当回声处理。\n */\n/**\n * 行内转写特征 —— **拆成强弱两档**,因为两者的可信度完全不同。\n *\n * 弱档:模型在**正常回答里引用一次**工具结果当证据是很常见的写法。\n * 实测(2026-09-16 17:35,1ceshi 工作区会话 e17f4ccf):它正是在正文里引 `[Tool Result …]`\n * 来证明 `subagent_fork` 的继承范围与文档不符 —— 旧判据\"行内命中即从该行起砍到结尾\"把\n * 整段回答(问题项 4、5 + 结论)一起吞了,用户只看到\"话说到一半就停了\"。\n * ⇒ 单独出现**不足以判定**是回声:只扣住,等后文再看(见 TranscriptEchoGuard.heldKind)。\n *\n * 强档:`truncated]` / `Assistant truncated` / `[N chars omitted]` 是 **prompt 自己的截断占位符**,\n * 正常回答几乎不会引用它们 ⇒ 仍按回声立即处理\n * (实测 2026-09-11 17:2x:会话超长后模型原样复读这些占位符)。\n */\nconst ECHO_INLINE_WEAK_SIGNATURES: readonly RegExp[] = [\n /\\[\\s*Tool Result\\b/i,\n /\\[\\s*status\\s*:/i,\n /\\[\\s*(?:System|Assistant)\\s*\\]/i,\n]\n\nconst ECHO_INLINE_STRONG_SIGNATURES: readonly RegExp[] = [\n /\\[\\s*truncated\\s*\\]/i,\n /assistant\\s+truncated/i,\n /\\[\\s*\\d+\\s*chars?\\s+omitted\\s*\\]/i,\n]\n\n/** 任一档(用于「该行是否含行内标记」的组合判断,如轮次前缀 + 行内标记)。 */\nconst ECHO_INLINE_SIGNATURES: readonly RegExp[] = [\n ...ECHO_INLINE_WEAK_SIGNATURES,\n ...ECHO_INLINE_STRONG_SIGNATURES,\n]\n\n/**\n * 行内弱特征行被扣住后,还要看到几行**普通内容**才敢判它是正文。\n *\n * 取 2:真回声紧跟着的还是转写内容(行首标记 / 轮次行 / 又一处引用),两行都干净就基本不是回放。\n */\nconst WEAK_HOLD_LINES = 2\n\n/** 扣住的行最多再缓冲几行就强制放行(防止\"只有空行\"时无限期扣住)。 */\nconst MAX_HELD_TAIL = 6\n\n/** 光秃秃的 `Assistant:` / `User:`(冒号后没有内容)—— 模型正在起一行假转写。 */\nconst ECHO_BARE_TURN_RE = /^(?:User|Assistant)\\s*:\\s*$/\n\n/** 回声标记的**半截前缀**(流在行中间被截断时出现)——同样是垃圾,不能上屏。 */\nconst ECHO_PREFIXES = ['[tool result', '[status:', '[system]', '[assistant]']\n\n/** 该行是否是某个回声标记的开头片段。 */\nfunction looksLikeEchoPrefix(line: string): boolean {\n const t = line.trim().toLowerCase()\n return t.length > 0 && ECHO_PREFIXES.some((p) => p.startsWith(t))\n}\n\n/**\n * 逐行守卫:命中回声特征后,**从该行起全部丢弃**。\n *\n * 为什么这样设计:\n * - 回声几乎总出现在末尾(模型在「续写转写」),前面才是真回答 → 截断比整段丢弃更保内容;\n * - 围栏代码块内不判定 —— 正常回答里也可能引用这些标记(比如讨论本插件时);\n * - 逐行缓冲、保留末尾未完成的半行 → 流式下也不会先把垃圾推给用户再吞回去。\n */\nexport class TranscriptEchoGuard {\n private pending = ''\n private inFence = false\n /**\n * 已扣住、尚未判定的一行(等后文决定它是回声还是正文)。\n * 两种来源:转写轮次行(`User:` / `Assistant:` 后有内容)、行内弱特征行(正文里引用了一次 `[Tool Result …]`)。\n */\n private heldLine: string | null = null\n private heldKind: 'turn' | 'weak' | null = null\n /** 弱特征行之后已看到的**非空白**普通行数(够 `WEAK_HOLD_LINES` 行仍无回声 → 判为正文、放行)。 */\n private heldSeen = 0\n /**\n * 扣住期间**后续行也要缓冲**,否则它们会抢在被扣的那行之前上屏(顺序错乱)。\n * 放行时按原顺序一次性吐出;判回声时整段丢弃。\n */\n private heldTail: string[] = []\n private fired = false\n\n /**\n * @returns `text` = 可以安全上屏的部分;`echoed` = 本轮是否出现过回声(那部分已被丢弃)。\n */\n push(text: string): { text: string; echoed: boolean } {\n if (this.fired) return { text: '', echoed: true }\n this.pending += text\n let out = ''\n for (;;) {\n const nl = this.pending.indexOf('\\n')\n if (nl === -1) break\n const line = this.pending.slice(0, nl + 1)\n this.pending = this.pending.slice(nl + 1)\n const verdict = this.classify(line)\n if (verdict === 'echo') {\n this.fired = true\n this.pending = ''\n this.heldLine = null\n this.heldKind = null\n this.heldTail = []\n return { text: out, echoed: true }\n }\n if (verdict === 'turn' || verdict === 'weak') {\n // 单行可能只是正常正文(正文里写 `User: admin`、或引用一次 `[Tool Result …]` 当证据),\n // **先扣住**,等后文判定 —— 否则第一行会先泄漏上屏。\n // 已经持有一行时说明两者**相邻出现** ⇒ 判回声(\"连续两行才算回放\"的判据)。\n if (this.heldLine !== null) {\n this.fired = true\n this.pending = ''\n this.heldLine = null\n this.heldKind = null\n this.heldTail = []\n return { text: out, echoed: true }\n }\n this.heldLine = line\n this.heldKind = verdict\n this.heldSeen = 0\n continue\n }\n // 普通行/围栏行:\n // - 扣住的是转写轮次行 ⇒ 只要这一行**非空白**,就说明那只是正文里的 `User:` 字样 → 放行\n // - 扣住的是行内弱特征行 ⇒ 再看 `WEAK_HOLD_LINES` 行普通内容才敢放行\n // (引用一次工具结果是极常见的写法;而真回声紧跟着的还是转写内容 —— 多等两行能把两者分开)\n if (this.heldLine !== null) {\n // ⚠️ 扣住期间后续行**也必须缓冲**:否则它们会先上屏、被扣的那行后上屏,正文顺序就乱了。\n this.heldTail.push(line)\n const blank = line.trim() === ''\n const release =\n (this.heldKind === 'turn' ? !blank : !blank && ++this.heldSeen >= WEAK_HOLD_LINES) ||\n // 兜底:一直只有空行时别无限扣(最多扣 `MAX_HELD_TAIL` 行就开始放行)\n this.heldTail.length >= MAX_HELD_TAIL\n if (release) {\n out += this.heldLine\n for (const held of this.heldTail) out += held\n this.heldLine = null\n this.heldKind = null\n this.heldTail = []\n }\n continue\n }\n out += line\n }\n return { text: out, echoed: false }\n }\n\n flush(): { text: string; echoed: boolean } {\n if (this.fired) return { text: '', echoed: true }\n let out = ''\n // 只有孤零零一行待判定行 → 判定为正文,放行(连同扣住期间缓冲的后续行)\n if (this.heldLine !== null) {\n out += this.heldLine\n for (const held of this.heldTail) out += held\n this.heldLine = null\n this.heldKind = null\n this.heldTail = []\n }\n const rest = this.pending\n this.pending = ''\n // 流在行中间断掉:完整的回声判不出来,但半截标记前缀同样是垃圾,一律丢弃\n if (rest && (this.classify(rest) === 'echo' || looksLikeEchoPrefix(rest))) {\n this.fired = true\n return { text: out, echoed: true }\n }\n return { text: out + rest, echoed: false }\n }\n\n private classify(line: string): 'echo' | 'turn' | 'weak' | 'fence' | 'plain' {\n const t = line.trim()\n if (t.startsWith('```') || t.startsWith('~~~')) {\n this.inFence = !this.inFence\n return 'fence'\n }\n if (this.inFence) return 'plain'\n for (const re of ECHO_SIGNATURES) if (re.test(t)) return 'echo'\n // 截断占位符被拦腰切开后剩下的残片(如单独一行 `truncated]`)\n if (/^\\]?\\s*truncated\\s*\\]?\\s*$/i.test(t)) return 'echo'\n // 强档行内特征(prompt 的截断占位符):正常回答几乎不会引用 ⇒ 照旧立即判回声\n for (const re of ECHO_INLINE_STRONG_SIGNATURES) if (re.test(t)) return 'echo'\n // 冒号后没内容的 `Assistant:` —— 真回答里几乎不会出现,放过它就等着看回放\n if (ECHO_BARE_TURN_RE.test(t)) return 'echo'\n if (ECHO_TURN_RE.test(t)) {\n // 转写轮次前缀 **+** 行内标记 = 两个证据叠加\n // (2026-09-11 现场形态:`Assistant: [Tool Result for call_…]`)⇒ 判回声\n for (const re of ECHO_INLINE_WEAK_SIGNATURES) if (re.test(t)) return 'echo'\n return 'turn'\n }\n // 只有行内弱特征 ⇒ 可能只是正文里引用了一次工具结果,交给 push 扣住、等后文再判\n for (const re of ECHO_INLINE_WEAK_SIGNATURES) if (re.test(t)) return 'weak'\n return 'plain'\n }\n}\n","/**\n * deepseek-web 适配器:把 DSH 的 LLM 调用翻译成 chat.deepseek.com 网页端对话。\n *\n * 与官方 dsh-llm-deepseek 的差异(受限于网页端能力):\n * - 网页接口只吃单段 `prompt` 字符串 → 由 protocol.ts 序列化整段转写\n * - 无原生 function calling → 提示词 JSON 协议 + 流式解析(protocol.ts)\n * - 无 temperature / stop / max_tokens 字段 → 忽略(不报错)\n * - 每次调用新建 chat_session 并在结束后删除(保持无状态 + 不污染网页端列表)\n */\nimport { appendFileSync, mkdirSync, statSync } from 'node:fs'\nimport { createHash } from 'node:crypto'\nimport { join as joinPath } from 'node:path'\nimport { webLoginDir } from './paths.ts'\nimport { AdapterLlmError, httpErrorCode, maskIdentifier, readAuth, hasUsableAuth, type WebAuth } from './auth.ts'\nimport { staleAuthMessage } from './probe.ts'\nimport { createRequestGate, DEFAULT_MAX_PROMPT_CHARS, DEFAULT_MAX_REF_IMAGES, DEFAULT_MIN_REQUEST_INTERVAL_MS, type RequestGate } from './gate.ts'\nimport { summarizeCookieLife, type CookieLifeSummary } from './cookies.ts'\nimport {\n scheduleDeleteSession,\n streamWebCompletion,\n uploadImageFile,\n type SessionCleaner,\n} from './webapi.ts'\nimport { collectImageRefs, imageUploadName, looksLikeUnexecutedToolProgram, serializePromptParts, stripSystemMarkers, SystemMarkerStreamFilter, BoilerplateFilter, drainTextPipeline, ToolCallStreamFilter, TranscriptEchoGuard, type ToolSchemaLike } from './protocol.ts'\n\n/**\n * 把「被丢弃的完整载荷」落盘,专供事后定位。\n *\n * 为什么必须这么做:日志里只留前 400 字符,而实测的坏点几乎总在后半段\n * (长 PowerShell 命令、批量多调用)。没有完整原文就只能靠猜——\n * 2026-09-10 已经因此多绕了好几轮:先误判成 DSML 双竖线,真实原因却是未转义双引号。\n * 落盘后可以直接把原文喂进解析器复现,从「猜」变成「验」。\n *\n * 失败必须无声(诊断代码绝不能影响主流程)。\n */\n/**\n * 丢弃载荷的**诊断元信息**落盘。\n *\n * ⚠️ 默认**不落原文**(审计 F23):被丢弃的是模型吐坏的工具调用参数,里面完全可能带着\n * 命令里的 token、文件内容、个人信息 —— 以前默认把整段 raw 写进 `~/.dsh/deepseek-web/rejected.jsonl`,\n * 等于给这些内容在磁盘上留了一份没人在看的副本。而且那个路径是**硬编码 homedir** 的,\n * 无视 `DSH_HOME`(我们自己的测试就是被它坑到的:写到了真实用户目录)。\n *\n * 现在只记「什么时候 / 哪种模式 / 什么原因 / 多长 / 摘要」:\n * 足够回答\"是不是同一段坏输出反复出现\",又不需要保存原文。\n * 需要看完整内容时,应该走\"用户显式开启 + 有效期 + 0600 + 清理策略\"的独立诊断,\n * 而不是默认打开(本版没做)。\n *\n * ⚠️ 摘要(sha256)挡不住低熵内容被猜出来,它只是\"不存原文\"而非\"内容不可还原\"。\n */\nexport function dumpRejectedPayload(raw: string, mode: string, reason: string | undefined, logger?: any): void {\n try {\n const dir = joinPath(webLoginDir(), 'diagnostics')\n mkdirSync(dir, { recursive: true, mode: 0o700 })\n const file = joinPath(dir, 'rejected-meta.jsonl')\n const knownMode = ['json', 'xml', 'dsml'].includes(mode) ? mode : 'other'\n const knownReason = ['unbalanced', 'unparsable', 'oversize', 'echo'].includes(reason ?? '') ? reason : 'other'\n const line =\n JSON.stringify({\n at: new Date().toISOString(),\n mode: knownMode,\n reason: knownReason,\n length: raw.length,\n sha256: createHash('sha256').update(raw).digest('hex'),\n }) + '\\n'\n let size = 0\n try {\n size = statSync(file).size\n } catch {\n /* 首次写入 */\n }\n if (size + Buffer.byteLength(line) > 4_000_000) return\n appendFileSync(file, line, { encoding: 'utf8', mode: 0o600 })\n } catch {\n try {\n logger?.debug?.('deepseek-web: 诊断元信息写入失败')\n } catch {}\n }\n}\n\n/**\n * 图片上传缓存:`attachmentId → fileId`。\n *\n * ⚠️ 为什么必须按账号分作用域(审计 F06):`fileId` 是**归属某个账号**的服务端对象。\n * 缓存原先只按 `attachmentId` 记,切到另一个账号后会把上一个账号的 `fileId` 复用出去 ——\n * 服务端是否接受是它的事(不能据此断言\"跨账号能读到图\"),但\"把我们这边两个号的引用串了\"\n * 本身就已经是错的。另外只检查\"被查中的那一项\"的 TTL 不叫淘汰:一直换新图、不换旧 key 时,\n * 旧项会永久滞留在内存里。\n *\n * 三条纪律:切账号(token 变)立刻整批清空;每次使用前清掉**所有**过期项;条数封顶。\n * `token` 只在本进程内存里当作用域,不写日志、不落盘。\n */\nexport class ImageUploadCache {\n private scope = ''\n private readonly entries = new Map<string, { fileId: string; at: number }>()\n // ⚠️ 不要写成「constructor 的参数属性」(`constructor(private readonly ttlMs: number)`):\n // 测试是 `node src/xxx.ts` 直接跑的,而 Node 的类型剥离只支持「擦除型」语法 ——\n // 参数属性需要生成赋值代码,会被判为不支持的语法(实测报 ERR_INVALID_TYPESCRIPT_SYNTAX)。\n private readonly ttlMs: number\n private readonly maxEntries: number\n constructor(ttlMs = 2 * 60 * 60 * 1000, maxEntries = 256) {\n this.ttlMs = ttlMs\n this.maxEntries = maxEntries\n }\n\n /** 绑定当前账号:token 变了就整批清掉(上一个账号的 fileId 不能跨号复用)。 */\n useScope(token: string): void {\n if (this.scope !== token) {\n this.entries.clear()\n this.scope = token\n }\n }\n\n /** 当前作用域(`set` 前校验用:await 期间可能被别的账号切走)。 */\n currentScope(): string {\n return this.scope\n }\n\n /** 清掉**所有**过期项,返回清掉的条数(不只清理\"这次要查的那一个\")。 */\n prune(now: number = Date.now()): number {\n let removed = 0\n for (const [key, value] of this.entries) {\n if (now - value.at >= this.ttlMs) {\n this.entries.delete(key)\n removed += 1\n }\n }\n return removed\n }\n\n /** 命中且未过期才返回;顺手清掉这一条过期项。 */\n get(key: string, now: number = Date.now(), expectScope?: string): string | undefined {\n const hit = this.entries.get(key)\n if (!hit) return undefined\n // ⚠️ 0.1.82:与 `set` 的 `expectScope` 对称 —— 作用域不符一律当未命中。\n // 少了这一步,并发(allowConcurrent)时 B 账号上传的 file_id 会被 A 账号的请求\n // 拿去引用,正是 F06 要防的\"两个号的引用串了\"。\n if (expectScope !== undefined && expectScope !== this.scope) return undefined\n if (now - hit.at >= this.ttlMs) {\n this.entries.delete(key)\n return undefined\n }\n return hit.fileId\n }\n\n /**\n * 写入并封顶(超出时丢最早写入的项)。\n * `expectScope` 用于防\"await 期间账号被切走\":作用域已变则拒绝写入。\n */\n set(key: string, fileId: string, now: number = Date.now(), expectScope?: string): boolean {\n if (expectScope !== undefined && expectScope !== this.scope) return false\n this.entries.delete(key)\n this.entries.set(key, { fileId, at: now })\n while (this.entries.size > this.maxEntries) {\n const oldest = this.entries.keys().next().value\n if (oldest === undefined) break\n this.entries.delete(oldest)\n }\n return true\n }\n\n /** 当前条数(测试用)。 */\n get size(): number {\n return this.entries.size\n }\n\n /**\n * 定点删除指定的若干条(用于「这批 file_id 被服务端拒了」时的清理)。\n *\n * 为什么不做成全清(`reset()`):一次请求可能只用到历史里的一小部分图,\n * 全清会让下一次请求把**所有**历史图重新传一遍 —— 白白多几十次上传请求,\n * 而上传本身也是要被风控看的。知道是哪几条被拒,就只清那几条。\n * 返回真正删掉的条数(便于测试与日志)。\n */\n invalidate(keys: Iterable<string>): number {\n let removed = 0\n for (const key of keys) {\n if (this.entries.delete(key)) removed += 1\n }\n return removed\n }\n\n /** 清空(测试隔离用)。 */\n reset(): void {\n this.entries.clear()\n this.scope = ''\n }\n}\n\n/**\n * 这次失败该不该走「重传 → 禁图」的降级重试。抽成纯函数是为了能直接测 ——\n * 尤其 `yielded` 那一支:用假的流很难构造「内容真的上屏过、然后又出错」的场景\n * (文本会被过滤器 hold 到轮末,而异常发生在轮末之前),只能这样钉住它。\n *\n * 三个条件缺一不可:\n * · 错误是**请求侧的图片引用被拒**(上传侧的 code 9 是另一回事,见 isInvalidRefFileError)\n * · 这次请求**一个字都还没吐给上层** —— generator 已 yield 的内容撤不回来,\n * 重试只会让用户看到重复输出\n * · 还没用完两次机会\n */\nexport function canRetryImageReject(error: any, yielded: boolean, attempt: number): boolean {\n return error?.code === 'INVALID_REF_FILE' && !yielded && attempt < 2\n}\n\n/**\n * 被略过的图片份数至少要再多这么多,才值得再上屏一次截断提示。\n *\n * 为什么需要一个下限:首次提示时被略过的往往只有 1~2 份,若下一档就是\"翻倍\",门槛会落在\n * 2→4 这种量级 ⇒ 每多读一两张图就又提示一次,等于没抑制。\n */\nexport const TRIM_NOTICE_FLOOR = 10\n\n/**\n * 下一条截断提示的触发门槛(纯函数,可单测)。\n *\n * 语义(0.1.81):**首次必说明;之后只有情况明显恶化才再说一次。**\n * · 没提示过,或保留份数变了(用户改了 `maxRefImages`)⇒ 门槛 1(下一次就说明)\n * · 否则 ⇒ `max(FLOOR, 上次的份数 × 2)`,即**翻倍**、且至少再多 `TRIM_NOTICE_FLOOR` 份\n *\n * 为什么不再用「总条数:保留数」那种精确签名(0.1.79 → 0.1.81 的修正):\n * 「总条数」几乎每轮都在涨(模型每 `read_image` 一次就多一条内容)⇒ 签名天天变 ⇒\n * 提示照旧每轮上屏。**判据里只要含一个\"单调增长且变化频繁\"的量,抑制就等于没做。**\n */\nexport function nextTrimNoticeThreshold(\n previous: { kept: number; dropped: number } | undefined,\n keptCount: number,\n): number {\n if (!previous || previous.kept !== keptCount) return 1\n return Math.max(TRIM_NOTICE_FLOOR, previous.dropped * 2)\n}\n\nexport const PROVIDER = 'deepseek-web'\n\nexport interface ModelSpec {\n id: string\n name: string\n description: string\n /** 网页端路由字段:当前实际只有 default 可用(expert/vision 已被服务端停用)。 */\n modelType: 'default' | 'expert' | 'vision'\n /** 是否默认开启思考(thinking_enabled)。 */\n thinking: boolean\n /** 是否允许通过 reasoningEffort 开关思考。 */\n configurableThinking: boolean\n contextWindow: number\n maxOutputTokens: number\n}\n\n/**\n * 网页免费模型目录。\n *\n * 权威依据:`GET /api/v0/client/settings?scope=model` 的 `model_configs`\n * (服务器按账号返回,实测 configVersion 81):\n * default / 快速模式 → enabled=true, switchable=true, is_default=true\n * expert / 专家模式 → enabled=false, switchable=false\n * vision / 识图模式 → enabled=false, switchable=false\n * 即专家/识图已被服务端停用并合并进快速模式。**目录里的两条不是两个模型**,\n * 而是同一个「快速模式」的 `thinking_enabled` 开关两档预设(方便一键选);\n * 也可以通过推理强度(reasoningEffort)在同一个档位上切换。\n * 旧档位选择由 LEGACY_ALIASES 回退承接。\n *\n * 容量(2026-09-11 直接抓服务端 `client/settings` 逐字段核对,configVersion 81):\n * `input_character_limit = 2621440` —— **单请求输入字符数硬上限**(= 2.5 MiB 字符)\n * `file_feature.token_limit = 890880` —— 附件/文件的 token 预算(开不开思考都一样)\n * `file_feature.token_limit_with_thinking = 890880`\n * ⚠️ 曾经的错误:把 `890880` 当成「模型上下文窗口」,还在文档里写成「1M 扣输出预留」。\n * 它是 **file_feature(附件)的 token 预算**,跟上下文窗口不是一回事;而且 890880 = 870×1024,\n * 面板按 ÷1024 显示就成了「870K」,于是看起来像「说好的 1M 变成了 870K」。\n * 服务端并没有给出「总上下文窗口」字段;可核对的硬约束只有上面那条字符上限。\n * 因此 contextWindow 按 DeepSeek 标称的 1M 取 1048576(1 Mi;服务端自己的数字也都是 1024 的整数倍:\n * 2621440 = 2.5×1048576、890880 = 870×1024),真正防越界的是 maxPromptChars(远低于字符硬上限)。\n */\nexport const MODEL_SPECS: ModelSpec[] = [\n {\n id: 'deepseek-chat',\n name: 'DeepSeek 网页 · 快速模式(不思考)',\n description: '同一模型,thinking 关闭:直接作答、最快、最省免费额度。适合工具调用/改写/检索类任务',\n modelType: 'default',\n thinking: false,\n configurableThinking: true,\n contextWindow: 1_048_576,\n maxOutputTokens: 16_384,\n },\n {\n id: 'deepseek-reasoner',\n name: 'DeepSeek 网页 · 快速模式(深度思考)',\n description: '同一模型,thinking 开启:先推理再作答(推理流作为思考块回传)。适合数学/多步调试/规划,更慢也更耗额度',\n modelType: 'default',\n thinking: true,\n configurableThinking: true,\n contextWindow: 1_048_576,\n maxOutputTokens: 32_768,\n },\n]\n\n/**\n * 旧档位兼容:expert/vision 被服务端停用后不再出现在 listModels 里,\n * 但历史会话/预设里若仍指向它们,这里做路由回退而不是直接报错。\n */\nconst LEGACY_ALIASES: Record<string, string> = {\n 'deepseek-pro': 'deepseek-reasoner',\n 'deepseek-expert': 'deepseek-reasoner',\n 'deepseek-vision': 'deepseek-chat',\n}\n\nconst EFFORT_OFF = 'off'\nconst EFFORT_LOW = 'low'\nconst EFFORT_HIGH = 'high'\nconst EFFORT_MAX = 'max'\n\nconst REASONING_EFFORTS = [\n { id: EFFORT_OFF, name: 'Off', description: '关闭思考(网页快速模式)' },\n { id: EFFORT_LOW, name: 'Low', description: '开启思考(网页只区分开/关,等同 High)' },\n { id: EFFORT_HIGH, name: 'High', description: '开启思考(默认)' },\n { id: EFFORT_MAX, name: 'Max', description: '开启思考(网页只区分开/关,等同 High)' },\n]\nconst OFF_ONLY_EFFORTS = [{ id: EFFORT_OFF, name: 'Off', description: '该模型固定为非思考模式' }]\n\n/** 估算 token 数(网页端不返回 usage;CJK/英文混合按 ~3.2 字符/token 粗估)。 */\nfunction estimateTokens(text: string): number {\n if (!text) return 0\n let cjk = 0\n for (const ch of text) {\n const code = ch.codePointAt(0) ?? 0\n if (code >= 0x3000 && code <= 0x9fff) cjk += 1\n }\n const ascii = text.length - cjk\n return Math.ceil(cjk / 1.5 + ascii / 4)\n}\n\n/** 上下文超限的文案识别(网页端返回的是自然语言错误)。 */\nfunction isContextTooLong(message: string): boolean {\n return /(?:content|prompt|context).{0,40}(?:too\\s+long|too\\s+large|length|limit|maximum)|too\\s+many\\s+tokens|内容.{0,12}(?:过长|太长)|上下文.{0,12}(?:过长|超出)|содержани|контекст/i.test(\n message,\n )\n}\n\nexport interface AdapterConfig {\n /** prompt 字符上限(超出走中段截断)。服务端硬上限是 2621440 字符,默认留 ~43% 余量。 */\n maxPromptChars?: number\n /**\n * 一次请求最多带多少张图片(`ref_file_ids` 的长度上限)。\n *\n * 为什么要加(0.1.77,群友实测):图片是**请求级**的,网页端对一批引用的数量有上限 ——\n * 实测最后一次成功 40 张、第一次失败 52 张(真值落在 (40, 52])。超长会话里不同图片数会一路涨\n * (内容寻址去重对\"重新截图/重新渲染\"无效),越过之后 `biz_code 10 / too many ref file`\n * 会让**该会话此后每一轮都失败**。所以按时间只带最近的 N 张。\n * `0` = 不限制(逃生舱,但等于把这个故障放回来,别设)。\n */\n maxRefImages?: number\n /** SSE 空闲超时(毫秒)。 */\n idleTimeoutMs?: number\n /** 是否在调用结束后删除网页端会话(默认 true)。 */\n deleteWebSessions?: boolean\n /**\n * 回答在句中被截时自动发起新请求续写(默认开启)。\n * 续写内容无缝拼进同一条回答;无论续写是否补全,都不会再报 max-tokens\n * (应用户要求移除「已达到输出 token 上限」提示)。\n */\n autoContinue?: boolean\n /** 自动续写的最大轮数(默认 2;每轮是一次新的网页端请求)。 */\n maxContinuations?: number\n /**\n * 是否允许同一账号并发请求(默认 **false = 串行排队**)。\n *\n * 网页端同一账号同时只能生成一条:DSH 的**会话标题生成**(purpose=session-title)\n * 会和主回答撞在一起(实测 272 轮里有 16 对时间重叠),既会被拒、也会推高风控风险\n * (实测:双窗口并发生成不到 6 分钟即被限制 1 天)。只有明确知道自己在做什么时才打开。\n */\n allowConcurrent?: boolean\n /**\n * 最小间隔的**下限**(毫秒,默认 2000)。\n *\n * 实际等待时间在 [minRequestIntervalMs, maxRequestIntervalMs] 之间**随机**取值,\n * 按上一次调用的**结束**时刻计算。随机区间的意义:固定间隔方差≈0,是明显的「定时器特征」。\n */\n minRequestIntervalMs?: number\n /** 最小间隔的**上限**(毫秒,默认 4000);与下限相等即退化为固定间隔。 */\n maxRequestIntervalMs?: number\n /**\n * 临时会话清理策略(默认 `deferred`)。\n *\n * - `immediate`:调用后 1.5s 删掉(老行为,每轮 1 个 DELETE 请求)\n * - `deferred`:攒够 N 个或等满 T 秒再清理,且优先尝试一个请求批量删\n * - `keep`:完全不删(请求最少,但网页端会留下临时会话)\n */\n sessionCleanup?: 'immediate' | 'deferred' | 'keep'\n /**\n * 同一网页端会话复用的轮次上限(默认 20)。\n *\n * 0 = 关闭复用,回到「每请求建一个会话、用完即删」。\n * 复用的安全性由实测判定:每次都发 `parent_message_id: null`,服务端不带会话历史。\n */\n sessionReuseTurns?: number\n /** deferred 模式的等待上限(毫秒,默认 90000)。 */\n sessionCleanupDelayMs?: number\n /** deferred 模式攒够多少个立即清理(默认 8)。 */\n sessionCleanupBatchSize?: number\n /** 日志器(cordis logger;缺省静默)。 */\n logger?: { info?: (msg: string) => void; warn?: (msg: string) => void; debug?: (msg: string) => void }\n}\n\nexport interface AdapterDeps {\n getAuth: () => WebAuth | undefined\n config: AdapterConfig\n /**\n * 读取 DSH 附件服务的图片字节(ctx.attachments.readImage)。\n * 缺省时图片输入不可用(会退化成文本占位提示)。\n */\n readImage?: (ref: any, signal?: AbortSignal) => Promise<{ data: Uint8Array; mediaType?: string; name?: string }>\n /**\n * 每次模型调用的结果上报(成功/失败、耗时、错误码、受限解除时间)。\n *\n * 宿主用它做两件事,所以只留**一个**钩子而不是两个:\n * 1. 把 `mutedUntilMs` 记到账号上 → 设置页显示\"限制还剩多久\";\n * 2. 写本地调用台账 → 看请求密度与失败分类(判断节流是否真的有效)。\n * 上报失败不得影响调用本身(宿主内部自己吞异常)。\n */\n noteCall?: (info: {\n /** 调用用途(chat / session-title / compaction…)。 */\n purpose: string\n ok: boolean\n ms: number\n code?: string\n message?: string\n /** 账号级限制的解除时间(仅 user is muted)。 */\n mutedUntilMs?: number\n /** 是否属于\"限流\"(发太频繁)而非\"账号被限制\"。 */\n throttled?: boolean\n /**\n * **发起这次调用时**的账号 id。\n * ⚠️ 不能由宿主在收到上报时现取 —— 见 F05 的说明。\n */\n accountId?: string\n }) => void\n /**\n * 取\"当前账号 id\"。宿主注入;适配器在**发起请求前**调一次,\n * 结果随 noteCall 回传,避免请求飞行途中切号导致记错账号。\n */\n currentAccountId?: () => string | undefined\n /** 注入自定义流函数(单测用假流验证自动续写);缺省用 streamWebCompletion。 */\n streamCompletion?: (auth: WebAuth, params: any) => AsyncGenerator<any>\n /**\n * 注入自定义图片上传(单测用假上传验证 ref_file_ids 的组装);缺省用 uploadImageFile。\n *\n * 为什么要留这个缝:真实上传要 PoW(sha3 wasm)+ 一次真网络往返,单测跑不动,\n * 于是「同一张图在历史里出现两次时,ref_file_ids 会不会重复」这条**接线级**行为\n * 长期零覆盖(只有纯函数 collectImageRefs 被测过,它压根不管去重)。\n */\n uploadImage?: typeof uploadImageFile\n /**\n * 外部注入的请求闸门(宿主在设置页里要能改它的配置,所以由 index.ts 创建并共享)。\n * 缺省时按 config 自建一个。\n */\n gate?: RequestGate\n /**\n * 外部注入的临时会话清理器(策略由宿主配置:immediate / deferred / keep)。\n * 缺省时退回「调用后 1.5s 删除」的老行为。\n */\n sessionCleaner?: SessionCleaner\n}\n\nfunction modelInfoFor(provider: string, spec: ModelSpec, requestedId?: string) {\n return {\n provider,\n id: requestedId ?? spec.id,\n name: spec.name,\n description: spec.description,\n // 图片输入走「上传成文件 + ref_file_ids」通道(网页端看图的实际机制),\n // 已实测:上传左红右蓝 PNG 后模型准确答出「左红色,右=蓝色」。\n inputModalities: ['text', 'image'] as const,\n }\n}\n\nfunction resolvedModelInfo(provider: string, spec: ModelSpec, requestedId?: string) {\n return {\n ...modelInfoFor(provider, spec, requestedId),\n context: { contextWindow: spec.contextWindow },\n defaultMaxTokens: spec.maxOutputTokens,\n reasoning: spec.configurableThinking\n ? { efforts: REASONING_EFFORTS, defaultEffort: spec.thinking ? EFFORT_HIGH : EFFORT_OFF }\n : { efforts: OFF_ONLY_EFFORTS, defaultEffort: EFFORT_OFF },\n }\n}\n\n/**\n * 解析模型:命中目录直接用;命中旧档位(expert/vision)按别名回退到对应档位,\n * 但**保留请求时的 id** —— 运行时要求 resolveModel 返回的 id 必须与请求一致\n * (INVALID_MODEL_INFO),否则历史会话里的旧档位选择会直接报错。\n */\nfunction resolveSpec(model: string): ModelSpec {\n const requested = String(model ?? '')\n const direct = MODEL_SPECS.find((spec) => spec.id === requested)\n if (direct) return direct\n const alias = LEGACY_ALIASES[requested]\n if (alias) {\n const mapped = MODEL_SPECS.find((spec) => spec.id === alias)\n if (mapped) return mapped\n }\n return MODEL_SPECS[0]\n}\n\n/** 解析本次请求的思考开关。 */\nfunction resolveThinking(options: any, spec: ModelSpec): { thinkingEnabled: boolean } {\n // 辅助调用(会话标题/压缩)永远走非思考,省时省钱\n if (options?.purpose === 'session-title' || options?.purpose === 'compaction') return { thinkingEnabled: false }\n if (!spec.configurableThinking) return { thinkingEnabled: spec.thinking }\n const effort = options?.reasoningEffort\n if (effort === undefined) return { thinkingEnabled: spec.thinking }\n if (effort === EFFORT_OFF) return { thinkingEnabled: false }\n if (effort === EFFORT_LOW || effort === EFFORT_HIGH || effort === EFFORT_MAX) return { thinkingEnabled: true }\n throw new AdapterLlmError(`deepseek-web 不支持 reasoning effort \"${String(effort)}\"`, 'UNSUPPORTED_REASONING_EFFORT')\n}\n\n/**\n * 自动续写的用户指令(流被截后,适配器自动发起新请求让模型接着写——\n * 等价于用户手动说「继续」,但无需用户参与、且文本无缝拼接进同一条回答)。\n */\nconst CONTINUE_INSTRUCTION =\n '继续:请从你上一条回复的结尾处无缝接着往下写——不要重复任何已输出的内容,' +\n '不要加「好的」「以下是」之类的开场白,不要重新组织语言;' +\n '如果上一条回复停在句子中间,就从那个断点直接把句子写完并继续。'\n\n/**\n * 「把工具程序写成了正文」时的纠正指令(比自动续写更强的措辞 —— 续写是\"接着写\",\n * 这个是\"你刚才那一轮等于什么都没做,请重新发一次\")。\n *\n * 为什么需要(2026-09-17 11:00 现场,见 protocol.ts 里 looksLikeUnexecutedToolProgram 的说明):\n * 染神 preset 注入了 PTC 说明(\"所有动作必须通过 run_code 写 TypeScript 程序\"),\n * 模型于是把 run_code 的 code 直接贴进正文;这一轮零工具调用 ⇒ agent loop 判定回合结束\n * ⇒ 界面上看起来\"它停下来了\"。加这一轮纠正后,模型有机会把同一段程序改发成工具调用。\n *\n * 措辞要点:① 点破\"写出来 ≠ 执行了\";② 给出唯一被接受的形态;③ 明确对抗 PTC 措辞 ——\n * 否则模型会继续把系统提示里那句\"写出 TypeScript 程序\"当成\"写进正文\"的许可。\n */\nconst TOOL_CALL_RETRY_INSTRUCTION =\n '你刚才把要执行的程序写进了正文文本。写在正文里的代码不会被执行 —— 这一轮因此没有发生任何工具调用。\\n' +\n '请把同一段程序作为工具调用重新发出:只输出一个 JSON 对象,前后不要有任何其它文字:\\n' +\n '{\"tool_calls\":[{\"name\":\"<工具名>\",\"arguments\":{...}}]}\\n' +\n '即使系统提示要求你写 TypeScript 程序来完成动作,那个程序也必须放进工具调用的 arguments 里,' +\n '不能直接写在正文中 —— 只有作为工具调用发出,它才会真的被执行。'\n\n/**\n * 出现在末尾即「明显还有下文」的标点:列举 / 分句写到一半停了。\n * 正常写完的回答**不可能**以这些收尾(它们是分隔符,不是终止符)。\n *\n * 0.1.66 补:`;`(全角分号)与 `、`(顿号)在旧实现里都落到了「非标点字符」那条兜底规则上,\n * 被判成完整 —— 而这两个恰恰是最强的「没写完」信号(列举到一半、分句列到一半)。\n * 实测(源码函数直接求值)旧行为:`;` → false、`、` → false,而 `,`/`:`/`;` → true,\n * 同一个文件里两套标准。\n */\nconst MID_SENTENCE_TAIL = new Set([',', '、', ';', ':', ',', ';', ':'])\n\n/**\n * 出现在末尾即视为「正常收尾」的标点。\n *\n * ⚠️ `…` 放在这里是**刻意的取舍**:省略号既可能是\"话没说完\",也可能是作者有意的收束语气,\n * 两种都常见。判 true 会让一句正常收尾的话被要求\"接着写\"(模型容易重复一遍),\n * 感知上比偶发漏判更打扰,所以保守放行。真被服务端切断(无 FINISHED)时走 `cutByServer`,\n * 不依赖这条判据。\n */\nconst COMPLETE_TAIL = new Set(['。', '!', '?', '!', '?', '…', ')', ')', '】', '》', '」', '』', '\"', '”', '’'])\n\n/**\n * 启发式:正文是否「在句中被截」。\n * 判据(尾部最后一个非空白字符):\n * - 是 CJK 汉字/字母/数字(没有任何标点收尾)→ 大概率被截;\n * - 是 markdown 强调标记(`**` / `__`)→ 被截在标记中间;\n * - 是逗号/顿号/分号/冒号 → 明显未完。\n * 正常结束的正文几乎总以句号/问号/感叹号/右引号/右括号/代码块收尾/表格行结尾出现。\n *\n * ⚠️ 这是启发式,**只在\"明显没写完\"时才敢返回 true**:误判 true 只是白发一次续写请求,\n * 误判 false 却是用户直接丢内容 —— 两种代价不同,所以判据本身要能读懂「分隔符 vs 终止符」。\n */\nfunction looksMidSentence(text: string): boolean {\n const trimmed = text.trimEnd()\n if (trimmed.length === 0) return false\n // F29:短文本(< 40 字)不判「句中被截」——\"我在\"\"在吗\"这类**完整短答**天然以汉字收尾,\n // 旧判据对它恒真,会白打 1~2 次续写请求。服务端真截断(cutByServer,无 FINISHED)不走\n // 这条判据、仍会续写,所以这里收窄只影响「有 FINISHED 但尾部是汉字」的场景。\n if (trimmed.length < 40) return false\n const last = trimmed[trimmed.length - 1]\n // markdown 标记收尾:`*` `_` `#` `~` `` ` `` 本身可能是「标记被截」,只有成对的一半才算\n if (last === '*' || last === '_' || last === '#' || last === '~' || last === '`') {\n return trimmed.endsWith('**') || trimmed.endsWith('__')\n }\n if (MID_SENTENCE_TAIL.has(last)) return true\n if (COMPLETE_TAIL.has(last)) return false\n // 字母/数字/汉字/其他非标点字符收尾 → 大概率被截\n return /[a-zA-Z0-9\\u4e00-\\u9fff\\u3040-\\u30ff]/.test(last)\n}\n\n/**\n * 自动续写只对**用户可见的回答**(purpose === 'chat')生效。\n *\n * F26(2026-09-14 实测):标题生成 / 上下文压缩这类内部调用天然**不以标点收尾**\n * (标题就是\"以汉字结尾\"),于是 `looksMidSentence` 对它**恒为真** →\n * 每轮都被判成\"句中被截\" → 自动发起续写、要求模型\"接着写\" → 模型把标题重复一遍,\n * 直到续写额度用尽。结果标题变成重复垃圾:实测 13 个会话里 **11 个**中招 ——\n * `\"在吗在吗在吗\"`、`\"AI助手的记忆功能\"×3`、`\"安装 archify skills\"×3`、\n * `\"TCP 三次握手原因解析\"×3` …(只有走 fallback 的两个标题是正常的)。\n *\n * 续写的本意是\"帮用户把被截断的回答写完整\",对内部短文本没有意义,所以按用途白名单收口:\n * 只有 `chat`(或未指定)才续写 —— 将来新增别的内部用途也不会再踩进来。\n */\nfunction allowsAutoContinue(purpose: unknown): boolean {\n return purpose === undefined || purpose === null || purpose === '' || purpose === 'chat'\n}\n\n/** 构造 deepseek-web 适配器(鸭子类型满足 LlmAdapter 契约,无需继承)。 */\nexport function createAdapter(deps: AdapterDeps) {\n const logger = deps.config.logger\n // 流函数可注入(单测用假流验证自动续写);缺省走真实网页端实现\n const runStream = deps.streamCompletion ?? streamWebCompletion\n // 图片上传同理可注入(单测验证 ref_file_ids 组装 / 失败告知);缺省走真实上传\n const uploadImage = deps.uploadImage ?? uploadImageFile\n\n // 请求闸门:串行 + 最小间隔,覆盖**每一次**模型调用(含 DSH 的会话标题/压缩等辅助调用)。\n // 宿主(index.ts)会注入一个共享实例,好让设置页改完立即生效;缺省自建。\n const gate =\n deps.gate ??\n createRequestGate({\n allowConcurrent: deps.config.allowConcurrent === true,\n minIntervalMs: deps.config.minRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS,\n logger,\n })\n\n const adapter = {\n providerInfo(provider: string) {\n return { id: provider, name: 'DeepSeek 网页版(免费)' }\n },\n\n /** 未配置策略 → 走 dsh-llm 默认重试码表(EMPTY_RESPONSE/RATE_LIMIT/SERVER/TIMEOUT/TRANSPORT)。 */\n providerRetryPolicy(_provider: string) {\n return undefined\n },\n\n /**\n * 图片请求计价:本路由不声明 → undefined(消费者回落到自己的中性估算)。\n *\n * ⚠️ 这个方法**必须有**,不是可选装饰:dsh-llm 的适配器注册表在计量/压缩路径上**无条件**转调\n * `adapter.imageRequestPricing(provider, model)`(见 app.asar 内 LlmAdapterRegistry.imageRequestPricing)。\n * 而本适配器是鸭子类型的**普通对象**、不继承 `LlmAdapter` 基类,基类里那个「默认返回 undefined」的实现\n * 我们拿不到 → 缺了它就抛 `... .imageRequestPricing is not a function`,\n * 于是 basic-compaction-engine 每一步压缩都失败(实测 2026-09-10:69 次,压缩**静默失效**,\n * 长会话不再自动压缩,且只留一条 warn)。\n *\n * 契约:必须**同步、无 I/O**(token meter 每次测量都会调)。\n */\n imageRequestPricing(_provider: string, _model: string): undefined {\n return undefined\n },\n\n listModels(provider: string) {\n return Promise.resolve(MODEL_SPECS.map((spec) => modelInfoFor(provider, spec)))\n },\n\n resolveModel(provider: string, model: string) {\n return Promise.resolve(resolvedModelInfo(provider, resolveSpec(model), String(model ?? '')))\n },\n\n /**\n * 运行时契约(dsh-llm 0.1.2-rc.1):dispatch 前先取「精确模型元数据 + 该次调用的 stream」。\n * 返回的 stream 接收运行时补齐后的 options。\n */\n prepareCall(provider: string, model: string, _signal?: AbortSignal) {\n const spec = resolveSpec(model)\n return Promise.resolve({\n model: resolvedModelInfo(provider, spec, String(model ?? '')),\n stream: (options: any) => gatedStream(options),\n })\n },\n\n stream(options: any): AsyncGenerator<any> {\n return gatedStream(options)\n },\n }\n\n /** 图片上传缓存:按账号作用域 + TTL + 条数封顶(见 ImageUploadCache 的说明)。 */\n const uploadCache = new ImageUploadCache()\n\n /**\n * 上一次请求**实际引用**了哪几张图(attachmentId 集合)。\n * 用途只有一个:请求被服务端以 `code 9 / invalid ref file id` 拒掉时,\n * 知道该清掉缓存里哪几条,好让重试时重新上传(见 streamWithImageFallback)。\n */\n let lastImageKeys: Set<string> = new Set()\n\n /**\n * 上一次**已经上屏过**的截断规模(保留数 + 当时被略过的份数)。\n *\n * 为什么要它:只要历史里的图片内容超过上限,`uploadRequestImages` 每轮都会算出同一条提示 ——\n * 实测 19 分钟里连着上屏 **52 次**(2026-09-21 那个 drawio 配图会话),用户直接问\n * \"为啥每次都这么多提示\"。同一件事说一遍就够了,重复的只写日志。\n *\n * 🔴 0.1.81 修正了抑制的粒度。0.1.79 用的是 `总条数:保留数` 这个**精确签名**,\n * 而\"总条数\"几乎每轮都在涨 —— 模型每 `read_image` 一次就多一条内容 ⇒ 签名变了 ⇒\n * 提示照旧每轮上屏(2026-09-22 现场:29 份 → 30 份又来一遍,用户说\"频率还是有点高\")。\n * 改成**阶梯**:首次必说明;之后只有\"被略过的份数\"比上次翻倍(且至少再多 `TRIM_NOTICE_FLOOR`\n * 份)才再说一次。这样一条会话里它是 O(log n) 次,而不是 O(n) 次。\n * 保留数变了(用户改了 `maxRefImages`)⇒ 情况本身变了,重新说明一次。\n */\n let lastTrimNotice: { kept: number; dropped: number } | undefined\n\n /**\n * 把请求里出现的图片全部上传到网页端并返回 file_id 列表。\n * 失败不致命:记日志后跳过该图(prompt 里仍有 [image attached] 标记,模型会知道有图但看不到)。\n * 但**取消**要照常传播 —— 用户点了停止就不该继续传图,也不该把它降级成\"纯文本继续跑\"。\n */\n async function uploadRequestImages(\n auth: WebAuth,\n messages: readonly any[] | undefined,\n signal?: AbortSignal,\n ): Promise<{ ids: string[]; keys: string[]; keptKeys: Set<string>; notice?: string }> {\n signal?.throwIfAborted()\n uploadCache.useScope(auth.token)\n const scope = uploadCache.currentScope()\n const refs = collectImageRefs(messages)\n if (refs.length === 0) return { ids: [], keys: [], keptKeys: new Set() }\n // 去重(0.1.66):同一张图可能在同一份历史里出现多次 —— 用户消息里一次、\n // `read_image` 的工具结果里又一次(工具结果内嵌图片本体),甚至模型对同一个文件\n // 连调两次 read_image。实测本机会话 `install-plugin/session-c1fb8208-*` 里就有\n // 两个 tool/result 各自内嵌同一张 `sha256:23c18a56…`(89962B JPEG)。\n //\n // 为什么必须去重:`attachmentId` 是**内容寻址**(sha256),同一张图必然是同一个 key,\n // 于是缓存命中时会把**同一个 file_id 再推一遍**,请求体变成 `ref_file_ids: [id, id]`。\n // 服务端不接受重复 id(biz_code 9 / invalid ref file id),一旦被拒,\n // 该会话**后续每一轮都会失败**(图留在历史里),只能新开对话。\n //\n // 每个 attachmentId 只产出一个 file id —— 这是最小、也最贴近语义的修法\n // (不同附件即使内容相同,也各有各的 id,不该在这里合并)。\n const seen = new Set<string>()\n const unique: any[] = []\n for (const ref of refs) {\n const key = String(ref?.attachmentId ?? '')\n if (!key || seen.has(key)) continue\n seen.add(key)\n unique.push(ref)\n }\n if (unique.length === 0) return { ids: [], keys: [], keptKeys: new Set() }\n if (!deps.readImage) {\n logger?.warn?.('deepseek-web: 收到图片但附件服务不可用(ctx.attachments),图片被忽略')\n return {\n ids: [],\n keys: [],\n keptKeys: new Set(),\n notice: imageNotice(unique.length, '宿主没有提供附件读取能力(ctx.attachments)'),\n }\n }\n // ── 0.1.77:图片数量阀门 ──────────────────────────────────────────────\n // 图片是**请求级**的(一次请求用一个 `ref_file_ids` 带一批),而网页端对这一批有数量上限。\n // 群友实测报告(2026-09-19):最后一次成功是 40 张、第一次失败是 52 张 ⇒ 真值在 (40, 52]。\n // 超长会话里这个数字会一路涨,且**去重救不了**:`attachmentId` 是内容寻址的,\n // 重新截图 / 重新渲染的预览图每次内容都变 ⇒ 新 id ⇒ 历史里只增不减。\n // 越过上限后 `biz_code 10 / too many ref file` 会让**该会话此后每一轮都失败** ——\n // 图留在历史里,每轮重发都超标,用户只能丢掉整个会话。所以按时间取最近的 N 张。\n //\n // ⚠️ 标记必须一起收敛(见 protocol.ts 的 classifyBlockImages):只截 id 不截标记,\n // 模型会以为它收到了那些图,转而对着没送出去的图瞎猜。\n const maxRefImages = deps.config.maxRefImages ?? DEFAULT_MAX_REF_IMAGES\n const overLimit = maxRefImages > 0 && unique.length > maxRefImages\n const kept = overLimit ? unique.slice(-maxRefImages) : unique\n // ⚠️ 0.1.82:keptKeys **不再**预先按裁剪结果算 —— 它会被当成\"本轮真的带上的图\"\n // 传给 `serializePromptParts`(后者据此写 `[image attached]` / `[earlier image omitted]`),\n // 而**上传失败**的图并没有进 `ref_file_ids` ⇒ 模型以为收到了,转而对着没送出去的图瞎猜。\n // 改成在上传循环里按\"成功(含缓存命中)\"累计,见下面的 sentKeys。\n // 提示按**阶梯**上屏(见 nextTrimNoticeThreshold):首次必说明,之后只有被略过的份数\n // 翻倍(且至少再多 TRIM_NOTICE_FLOOR 份)才再说一次。日志照记不误,每轮的截断都能查。\n const dropped = unique.length - kept.length\n const trimNotice =\n overLimit && dropped >= nextTrimNoticeThreshold(lastTrimNotice, kept.length)\n ? imageTrimNotice(unique.length, dropped, kept.length)\n : undefined\n if (trimNotice) lastTrimNotice = { kept: kept.length, dropped }\n if (overLimit) {\n logger?.info?.(\n `deepseek-web: 本请求的图片共 ${unique.length} 张,超过上限 ${maxRefImages} ⇒ 只发最近的 ` +\n `${kept.length} 张(较早的 ${unique.length - kept.length} 张本轮略过)`,\n )\n }\n uploadCache.prune()\n const ids: string[] = []\n const failures: string[] = []\n // 0.1.80:`attempted` 数\"真的发起过上传的张数\",`skippedByAuth` 记\"因授权失效而放弃的张数\"。\n // 只在授权失败时才有值 —— 那个数字要进用户可见的告知语(否则只报 1 张,看着像小事)。\n let attempted = 0\n let skippedByAuth = 0\n /** 真的进了 `ref_file_ids` 的那些 key —— 提示里的 `[image attached]` 只能写在这些图上。 */\n const sentKeys = new Set<string>()\n /** 与 `ids` **同序平行**的 key 数组 —— webapi 的 sentRefIds 按 key 记账需要这个对应关系。 */\n const sentKeyList: string[] = []\n for (const ref of kept) {\n signal?.throwIfAborted()\n const key = String(ref?.attachmentId ?? '')\n const cached = uploadCache.get(key, Date.now(), scope)\n if (cached) {\n ids.push(cached)\n sentKeys.add(key)\n sentKeyList.push(key)\n continue\n }\n attempted += 1\n try {\n const stored = await deps.readImage(ref, signal)\n const mediaType = stored.mediaType || String(ref.mediaType ?? 'image/png')\n const uploadedFile = await uploadImage(\n auth,\n {\n data: stored.data,\n mediaType,\n // 名字必须声明一个服务端支持的图片类型:宿主给 tool/result 内嵌图片的\n // `name` 是**纯 sha256(无后缀)**,实测会被以 code 9 拒(见 imageUploadName)。\n name: imageUploadName(stored.name ?? ref.name, mediaType),\n },\n signal,\n )\n uploadCache.set(key, uploadedFile.fileId, Date.now(), scope)\n ids.push(uploadedFile.fileId)\n sentKeys.add(key)\n sentKeyList.push(key)\n } catch (error: any) {\n if (signal?.aborted) throw error\n const message = String(error?.message ?? error)\n failures.push(message)\n logger?.warn?.(`deepseek-web: 图片上传失败(已降级为纯文本):${message}`)\n // 0.1.80:授权失效是**全局**的 —— 同一个 token 上传剩下的图必然同样被拒。\n // 继续往下试纯粹是白跑请求:每张图都要先求一次 POW、再发一次上传。\n // 实测(2026-09-21):14 张图 ⇒ 28 次注定失败的请求,而且这些无效请求\n // 同样暴露在风控下。第一张撞 AUTH 就停,把结论交给接下来的 completion ——\n // 它才是判断\"这次凭证到底能不能用\"的地方(万一只是单张图的问题,\n // 剩下的图不该被无辜牺牲,所以是 break 而不是 throw)。\n if (error?.code === 'AUTH') {\n skippedByAuth = kept.length - attempted\n logger?.warn?.(`deepseek-web: 图片上传遭遇授权失败,剩余 ${skippedByAuth} 张不再尝试`)\n break\n }\n }\n }\n // 图丢了必须让**用户**看见,不能只写日志(见 imageNotice 的说明)\n const notices: string[] = []\n if (trimNotice) notices.push(trimNotice)\n if (failures.length > 0) notices.push(imageNotice(failures.length, failures[0], skippedByAuth))\n // 这里回传的是**真的带上了**的集合(不是\"打算带\"的),调用方据此写提示标记\n return { ids, keys: sentKeyList, keptKeys: sentKeys, ...(notices.length > 0 ? { notice: notices.join('') } : {}) }\n }\n\n /**\n * 图片没能送进模型时的用户可见告知。\n *\n * 为什么必须写进回答:旧实现只 `logger.warn` 然后降级成纯文本,**界面上一声不响** ——\n * 用户会以为「模型看不懂图」,而实际上是图根本没发出去(2026-09-15 实测:本机 09-14\n * 有 36 次上传被服务端以 code 9 unsupported file type 拒绝,当轮 completion 正常\n * FINISHED,界面上看不到任何异常)。模型对外的 `inputModalities` 声明了 image,\n * 丢了却不告知,等于让用户对着一个「假装收到了」的输入提问。\n *\n * 与 F28 同一个原则:只要是「本该处理、但被丢弃」的输入,就必须显式说出来。\n *\n * `skipped`(0.1.80):授权失效导致中途放弃时,**必须把\"剩下的根本没试\"说出来**。\n * 否则文案只报 `count`(实际只有第一张),用户会以为\"只丢了一张图\"、继续等结果,\n * 而真实情况是这一轮的图几乎全废了、且凭证已经不能用。\n */\n function imageNotice(count: number, reason: string, skipped = 0): string {\n // 失败原因可能很长(授权失败那条带整段引导语),截断后再放进正文\n const brief = reason.length > 120 ? `${reason.slice(0, 120)}…` : reason\n const tail =\n skipped > 0\n ? `;第一张被拒后即中止,剩余 ${skipped} 张未再尝试(授权失效是全局的,继续重试只是白跑请求)`\n : ''\n return `\\n⚠️ [deepseek-web] 有 ${count} 张图片没能传给模型(${brief})${tail},本轮回答只基于文字内容。\\n`\n }\n\n /**\n * 「本轮只带了最近 N 张图」的告知语。\n *\n * ⚠️ 措辞刻意**不带警告符号、也不用「没能」** —— 这是一次**正常的长度控制**,不是失败。\n * 用报错的口吻会让人以为出了问题,而在弄清原因之前,他很可能就把一个本可以继续用的会话丢掉了\n * (这正是 code 10 最恶劣的地方:会话看起来「坏了」,用户唯一出路是丢掉全部上下文)。\n * 所以这句里明确写了「不是错误」。\n *\n * 与上面那条的分工:那条讲「图发失败」(要警惕),这条讲「图按策略没发」(正常)。\n */\n function imageTrimNotice(total: number, dropped: number, kept: number): string {\n return (\n `\\n[deepseek-web] 本轮只带了最近 ${kept} 份图片内容,更早的 ${dropped} 份未随请求发送` +\n `(历史累计 ${total} 份)。这里的「份」是**图片内容条目**、不是你贴的张数 ——` +\n '模型每读一次图就会多算一条,所以它远多于你亲手贴的张数。网页端对单次请求能引用的图片数有上限,' +\n '超了整轮都会被拒,因此按时间留最近的这些。正常的内容控制,不是错误。\\n'\n )\n }\n\n /**\n * 图片引用被服务端拒绝时的降级重试(0.1.78)。\n *\n * `code 9 / invalid ref file id` 的恶劣之处是「**带上图就失败**」:图留在 DSH 的消息历史里,\n * 之后每一轮都会重新收集、重新引用、再撞一次 —— 用户除了丢掉整个会话没有别的出路。\n * 所以给它两级自救,每级各只做一次:\n * ① 丢掉那几条上传缓存 → 重新上传拿新 file_id → 重发\n * ② 仍被拒 → **不带任何图片**重发,至少让这一轮继续下去(正文里会告知)\n *\n * ⚠️ **只在一次都没吐出内容时才敢重试**:generator 一旦 yield 过,调用方已经收到那部分,\n * 撤回不了(会变成重复输出)。好在服务端的引用校验发生在生成之前,命中这条错误时\n * 一定还没有任何输出 —— `yielded` 守卫是第二道保险,不是判据本身。\n */\n async function* streamWithImageFallback(options: any): AsyncGenerator<any> {\n let attempt = 0\n let current = options\n while (true) {\n let yielded = false\n const inner = streamImpl(current)\n try {\n while (true) {\n const step = await inner.next()\n if (step.done === true) return\n yielded = true\n yield step.value\n }\n } catch (error: any) {\n if (!canRetryImageReject(error, yielded, attempt)) throw error\n attempt += 1\n current = attempt === 1 ? { ...options, __retryImages: true } : { ...options, __skipImages: true }\n logger?.warn?.(\n attempt === 1\n ? 'deepseek-web: 图片引用被服务端拒绝(code 9)—— 丢掉那几张的上传缓存,重新上传后重试一次'\n : 'deepseek-web: 重新上传后图片引用仍被拒(code 9)—— 本轮改为不带图片重发',\n )\n }\n }\n }\n\n /**\n * streamImpl 的闸门外壳:拿到许可后才真正开始请求,流结束(含被中断/抛错)才释放。\n *\n * ⚠️ 许可在 generator 体**内部**获取 —— 只有真正开始迭代(第一次 next())才占位,\n * 消费者拿了 generator 却没迭代时不会泄漏名额;流被 abort 时 finally 一定会释放。\n */\n async function* gatedStream(options: any): AsyncGenerator<any> {\n const purpose = typeof options?.purpose === 'string' && options.purpose ? options.purpose : 'chat'\n // F11:把调用方的取消信号交给闸门 —— 否则「点停止」之后,请求仍会在排队/\n // 等间隔里干等(间隔 2~4s、长休可达 180s),界面停了、闸门还在倒计时。\n const release = await gate.acquire(purpose, options?.signal)\n const startedAt = Date.now()\n // ⚠️ F05(2026-09-12 审计):在**起飞前**就把账号 id 定下来。\n // 旧实现由宿主在上报时现取 `activeAccountId()`,而这次调用可能飞几十秒 ——\n // 期间用户若切了号,失败/限制就会被记到**切换后的账号**上:\n // 被限制的号反而清白,正在用的号却背了别人的处罚。\n const accountIdAtStart = deps.currentAccountId?.()\n let reported = false\n /** 上报一次结果。钩子是宿主给的,它自己负责不抛错;这里再兜一层,别让它影响调用。 */\n const report = (info: { ok: boolean; code?: string; message?: string; mutedUntilMs?: number; throttled?: boolean }): void => {\n if (reported) return\n reported = true\n try {\n deps.noteCall?.({ purpose, ms: Date.now() - startedAt, accountId: accountIdAtStart, ...info })\n } catch {}\n }\n try {\n yield* streamWithImageFallback(options)\n // 跑完没抛错 = 这次调用成功。宿主会用这个信号清理\"已经过期的受限标记\"。\n report({ ok: true })\n } catch (error: any) {\n report({\n ok: false,\n ...(typeof error?.code === 'string' ? { code: error.code } : {}),\n ...(typeof error?.message === 'string' ? { message: error.message.slice(0, 300) } : {}),\n ...(Number.isFinite(error?.mutedUntilMs) ? { mutedUntilMs: error.mutedUntilMs } : {}),\n ...(error?.failure?.rateLimitKind === 'throttled' || error?.rateLimitKind === 'throttled' ? { throttled: true } : {}),\n })\n throw error\n } finally {\n release()\n }\n }\n\n async function* streamImpl(options: any): AsyncGenerator<any> {\n const auth = deps.getAuth()\n if (!hasUsableAuth(auth)) {\n throw new AdapterLlmError(\n '尚未登录 DeepSeek 网页版:请在「设置 → DeepSeek 网页登录」里用浏览器窗口登录,或手动粘贴 userToken。',\n 'MISSING_CREDENTIAL',\n )\n }\n // 0.1.80:已知**授权失效**的账号,请求根本不发。\n //\n // 现场(2026-09-21):探活在 22:42 就判定了 token 失效并在账号上留了标记,\n // 但请求路径没人看那块牌子 —— 22:50 仍拿它去跑,14 张图逐个走一次 POW + 一次上传\n // (28 次注定失败的请求)才撞上 AUTH。更要紧的是:**这些无效请求同样暴露在风控下**。\n // 判据本身早就写好了(probe.ts 的 lastProbeFailed),只是没人在请求前调用。\n //\n // ⚠️ 只拦**授权类**失败(见 auth.ts isAuthFailureMessage):断网 / 超时 / 5xx 不拦,\n // 否则一次网络抖动就会把健康账号锁住。误拦的出口是明确的:重新登录,或点一次\n // 「校验全部」(只读探活,成功即清掉标记)—— 所以错误里要把两句都写上。\n const stale = staleAuthMessage(auth)\n if (stale) {\n logger?.warn?.(`deepseek-web: 跳过请求 —— 该账号登录态已被判定失效(${stale})`)\n throw new AdapterLlmError(\n `这个账号的登录态已失效(${stale}),本次请求没有发出。` +\n '请在「设置 → DeepSeek 网页登录」用浏览器窗口重新登录该账号;' +\n '若确认它其实还能用,点账号行上的「校验全部」重新确认一次即可(只读探活,不消耗额度)。',\n 'AUTH',\n )\n }\n const spec = resolveSpec(String(options?.model ?? ''))\n const { thinkingEnabled } = resolveThinking(options, spec)\n\n // 0.1.78:上一轮的图片引用被服务端拒了 ⇒ 先把缓存里那几条丢掉,本轮重新上传。\n // 缓存里的 file_id 可能因为凭证变化 / 服务端回收而失效,重传是最可能奏效的一步。\n if (options?.__retryImages === true && lastImageKeys.size > 0) {\n const removed = uploadCache.invalidate(lastImageKeys)\n lastImageKeys = new Set()\n logger?.warn?.(`deepseek-web: 已丢弃 ${removed} 条上传缓存(它们对应的 file_id 被服务端拒绝过)`)\n }\n\n // 图片:读取附件 → 上传到网页端 → 用 file_id 随请求引用(网页端看图的实际机制)\n // notice:有图没能送出去时给用户的一句告知(下面会作为正文首段吐出去)\n //\n // `__skipImages`(降级重试的第二级):连着两次都不被认,就**不带图**再发一次。\n // 宁可这一轮没有图,也不能让会话卡在\"每轮都失败\"上 —— 那等于整个会话作废。\n const uploaded =\n options?.__skipImages === true\n ? { ids: [], keys: undefined, keptKeys: undefined, notice: undefined }\n : await uploadRequestImages(auth, options?.messages, options?.signal)\n // ⚠️ 0.1.82:`__skipImages`(不带图重发)**绝不能**更新 lastImageKeys ——\n // 它记的是\"上一轮那批 key 的 file_id 被服务端拒过\",抹成空集就等于下一轮\n // 又从中毒缓存出发,重演\"2 次注定被拒 + 1 次无图\"。\n if (uploaded.keptKeys) lastImageKeys = uploaded.keptKeys\n const refFileIds = uploaded.ids\n\n // 链式投喂需要 prompt 的**结构**(固定头 + 未截断的历史条目)才能算增量,\n // 所以这里取 parts、下面的 params 一起把 entries 传下去(见 context-feed.ts)。\n let promptParts = serializePromptParts({\n system: options?.system,\n messages: options?.messages ?? [],\n tools: (options?.tools ?? []) as ToolSchemaLike[],\n maxChars: deps.config.maxPromptChars ?? DEFAULT_MAX_PROMPT_CHARS,\n // 只对**首轮**传:它才是真正带 ref_file_ids 的那一次(续写轮 refFileIds 传空)。\n // 传了之后,被长度控制略过的图会写成 `[earlier image omitted]` 而不是 `[image attached]`\n // —— 标记与实发必须一致,否则模型会对着没送出去的图瞎猜。\n keptImageKeys: uploaded.keptKeys,\n })\n const prompt = promptParts.full\n\n const knownNames = new Set<string>((options?.tools ?? []).map((tool: any) => String(tool?.name ?? '')))\n // 自动续写的每一轮用全新的过滤器/守卫实例(上一轮的状态在轮次收尾时已吐净),\n // 否则跨请求的行缓冲会让续写内容与 hold 的尾部乱序。\n let filter = new ToolCallStreamFilter(knownNames)\n // 第二道网:模型会模仿 prompt 里的转写格式(`[Tool Result for …]` / `User:` / `Assistant:`…),\n // 把「对话转写」当回答吐出来。这与工具调用标记泄漏是两个独立来源,必须分开防。\n let echoGuard = new TranscriptEchoGuard()\n let systemMarkerFilter = new SystemMarkerStreamFilter()\n // 第四道网:网页端每轮末尾的免责声明(`本回答由 AI 生成…`)不是回答内容,必须剥掉。\n let boilerplate = new BoilerplateFilter()\n\n let nextIndex = 0\n let textBlock: { index: number; text: string } | null = null\n let textStarted = false\n let reasoningBlock: { index: number; text: string } | null = null\n let reasoningStarted = false\n let toolCallCount = 0\n let finishReason: string | undefined\n // 服务端上报的 token 总量(跨续写轮次累加)。>0 时才信它。\n const usageRounds: {prompt:string; outputChars:number; total?:number}[] = []\n let rejectedProtocol = ''\n let rejectedReason: 'unbalanced' | 'unparsable' | 'oversize' | 'echo' | undefined\n let echoedTranscript = false\n /** 被回声守卫砍掉后半段时追加的告知字符数(从 usage 估算里扣掉,别当成模型的输出)。 */\n let echoNoticeChars = 0\n let systemMarkersStripped = false\n /** 本轮是否剥掉了网页端免责声明(`本回答由 AI 生成…`)。 */\n let disclaimerStripped = false\n\n const openText = (): { index: number; text: string } => {\n if (!textBlock) textBlock = { index: nextIndex++, text: '' }\n return textBlock\n }\n const openReasoning = (): { index: number; text: string } => {\n if (!reasoningBlock) reasoningBlock = { index: nextIndex++, text: '' }\n return reasoningBlock\n }\n\n const emitCalls = function* (calls: readonly { id: string; name: string; arguments: string }[]): Generator<any> {\n for (const call of calls) {\n const index = nextIndex++\n toolCallCount += 1\n yield { type: 'block-start', index, blockType: 'tool-call' }\n yield { type: 'tool-call-delta', index, id: call.id, name: call.name, argumentsDelta: call.arguments }\n yield {\n type: 'block-end',\n index,\n block: { type: 'tool-call', id: call.id, name: call.name, arguments: call.arguments },\n }\n }\n }\n\n try {\n // 图片丢失的告知放在最前面:它是**本次调用的输入状态**,不是回答的一部分,\n // 但界面只有\"正文\"这一个通道能保证被用户看到(设置页不会自动弹)。\n if (uploaded.notice) {\n const noticeBlock = openText()\n textStarted = true\n yield { type: 'block-start', index: noticeBlock.index, blockType: 'text' }\n noticeBlock.text += uploaded.notice\n yield { type: 'text-delta', index: noticeBlock.index, text: uploaded.notice }\n }\n // ── 自动续写循环 ──\n // 服务端会在句中截断生成(实测:两个窗口共用同一账号时,后来的请求会抢占在生成中的流,\n // 被抢占的流以 FINISHED 收尾)。截断发生时,这里自动发起新请求让模型「接着写」,\n // 并把续写内容无缝拼进同一条回答 —— 等价于用户手动说「继续」,但无需用户参与。\n let rounds = 0\n /** 本步已因「把工具程序写成正文」纠正过一次 —— 只给一次机会,别把请求密度打上去。 */\n let toolCallRetried = false\n let currentPrompt = prompt\n /** 本轮开始前已累计的正文长度(用来量出「这一轮到底吐了多少字」)。 */\n let textLenAtRoundStart = 0\n /** 本轮开始的时刻(用来量出「这一轮到底跑了多久」)。 */\n let roundStartedAt = Date.now()\n for (;;) {\n let roundError: AdapterLlmError | undefined\n const roundUsage: {prompt:string; outputChars:number; total?:number} = {prompt:currentPrompt,outputChars:0}\n usageRounds.push(roundUsage)\n // 每轮重置:finish 标记只反映**本轮**流,累积值会把上一轮的 FINISHED 带进来。\n finishReason = undefined\n textLenAtRoundStart = textBlock?.text?.length ?? 0\n roundStartedAt = Date.now()\n try {\n for await (const event of runStream(auth as WebAuth, {\n prompt: currentPrompt,\n // 链式投喂用:把结构与 prompt 一起传下去,webapi 才能算出\"这一轮新增了哪几条\"。\n // 漏传 = 链式模式静默退化成全量(有产物断言守着)。\n promptParts: {\n head: promptParts.head,\n entries: promptParts.entries,\n maxChars: deps.config.maxPromptChars ?? DEFAULT_MAX_PROMPT_CHARS,\n },\n // 链式投喂的决策回执(0.1.63)→ 一行日志。webapi 只在「原因变化」时回调,\n // 所以不会每轮刷屏,但\"哪一轮开始不再发增量、为什么\"一定看得见。\n onContextFeed: (report) => {\n logger?.info?.(\n report.reason === 'chained'\n ? `deepseek-web: 上下文投喂=链式:本轮只发增量 ${report.promptChars} 字(历史由服务端维护)`\n : report.reason === 'mode-full'\n ? 'deepseek-web: 上下文投喂=每轮全量:重发完整 prompt'\n : `deepseek-web: 链式投喂退回全量重发(原因=${report.reason})`,\n )\n },\n thinkingEnabled,\n modelType: spec.modelType,\n refFileIds: rounds === 0 ? refFileIds : [],\n refKeys: rounds === 0 ? uploaded.keys : [],\n signal: options?.signal,\n idleTimeoutMs: deps.config.idleTimeoutMs ?? 120_000,\n // 会话复用:同一账号连续多个回合共用一个网页端会话(见 webapi.ts 的实测判定)\n ...(deps.config.sessionReuseTurns !== undefined ? { sessionReuseTurns: deps.config.sessionReuseTurns } : {}),\n onDeleteSession:\n deps.config.deleteWebSessions === false\n ? undefined\n : (sessionId: string) => {\n // 清理策略由宿主注入(攒批 / 立即 / 不删),缺省退回老行为\n if (deps.sessionCleaner) deps.sessionCleaner.schedule(auth as WebAuth, sessionId)\n else scheduleDeleteSession(auth as WebAuth, sessionId)\n },\n })) {\n if (event.kind === 'thinking' || event.kind === 'text') roundUsage.outputChars += event.text.length\n if (event.kind === 'thinking') {\n const block = openReasoning()\n if (!reasoningStarted) {\n reasoningStarted = true\n yield { type: 'block-start', index: block.index, blockType: 'reasoning' }\n }\n block.text += event.text\n yield { type: 'reasoning-delta', index: block.index, text: event.text }\n continue\n }\n if (event.kind === 'text') {\n const out = filter.push(event.text)\n // 第四道网:网页端每轮末尾自动追加的免责声明(不是模型回答)。\n // ⚠️ 必须排在回声守卫**之前**:守卫会扣住「最后一个换行之后」的整行(说明它常在那里),\n // 放到守卫后面就永远看不到被扣住的那行 —— 声明照旧上屏,还会让句中判据每轮误触发。\n const boiled = boilerplate.push(out.text)\n const guarded = echoGuard.push(boiled.text)\n if (guarded.echoed) echoedTranscript = true\n // 第三道网:模型偶尔吐出成串的伪系统标记(<ds_system>…</ds_system> / <system>…</system>),\n // 实测一条消息里出现过 13 个编造调用 ID 的 <ds_system>Tool result…,全是垃圾,必须剥掉\n const cleaned = systemMarkerFilter.push(guarded.text)\n if (cleaned.stripped) {\n systemMarkersStripped = true\n logger?.debug?.('deepseek-web: 已剥离伪系统标记(<ds_system>/<system>)')\n }\n if (cleaned.text) {\n const block = openText()\n if (!textStarted) {\n textStarted = true\n yield { type: 'block-start', index: block.index, blockType: 'text' }\n }\n block.text += cleaned.text\n yield { type: 'text-delta', index: block.index, text: cleaned.text }\n }\n if (out.calls.length > 0) yield* emitCalls(out.calls)\n continue\n }\n if (event.kind === 'status') {\n logger?.debug?.(`deepseek-web: status=${event.value}`)\n continue\n }\n if (event.kind === 'error') {\n if (isContextTooLong(event.message)) {\n throw new AdapterLlmError(`DeepSeek 网页端上下文超限:${event.message}`, 'CONTEXT_WINDOW_EXCEEDED')\n }\n // webapi 层已按语义归类(并发生成 → RATE_LIMIT + retryAfterMs):\n // 这类错误交给 dsh-llm-retry 自动重发,而不是让整轮直接失败。\n if (event.code === 'RATE_LIMIT') {\n // 两种 RATE_LIMIT 的成因完全不同,文案别串台(曾经把「账号节流」说成「另一个窗口正在生成」)\n const throttled = event.rateLimitKind === 'throttled'\n throw new AdapterLlmError(\n throttled\n ? `DeepSeek 网页端对这个账号限流了(发得太频繁)。这不是封号:登录态有效、建会话也正常,只有发消息被拒。这一步会自动退避重试;若一直不过,请等几分钟再继续,或降低自动化步骤密度(每一轮工具调用都是一次网页端请求)。`\n : `DeepSeek 网页端同一账号同时只能生成一条消息(另一个窗口/标签页正在用同一账号生成)。这一步会自动重试;若两个窗口都要用网页模型,建议其中一个换 provider 或换账号。`,\n 'RATE_LIMIT',\n {\n ...(event.retryAfterMs !== undefined ? { providerRetryAfterMs: event.retryAfterMs } : {}),\n ...(throttled ? { rateLimitKind: 'throttled' } : {}),\n },\n )\n }\n throw new AdapterLlmError(`DeepSeek 网页端返回错误:${event.message}`, 'PROVIDER_ERROR')\n }\n if (event.kind === 'finish') {\n finishReason = event.reason\n if (typeof event.totalTokens === 'number' && Number.isSafeInteger(event.totalTokens) && event.totalTokens >= 0) {\n roundUsage.total = event.totalTokens\n }\n }\n }\n } catch (error: any) {\n // 续写轮次失败:保留已上屏的部分并正常收尾(正文已经给用户看到一部分,\n // 此时让整轮失败只会更糟)。仅中止(用户取消)向上抛。\n if (rounds > 0) {\n if (options?.signal?.aborted) throw new AdapterLlmError('deepseek-web 请求被调用方取消', 'ABORTED', { cause: error })\n roundError = error instanceof AdapterLlmError\n ? error\n : new AdapterLlmError(`deepseek-web 自动续写失败:${error?.message ?? error}`, 'TRANSPORT', { cause: error })\n logger?.warn?.(`deepseek-web: 自动续写第 ${rounds} 轮失败,保留已输出部分:${roundError.message}`)\n } else {\n throw error\n }\n }\n // ── 轮次收尾:把三层缓冲里 hold 的残余吐净(每轮都做,续写才有正确的拼接基准)──\n //\n // ⚠️ 只有「按流水线反序 flush」还不够:过滤器扣住的最后 ≤24 个字符**从没经过**声明剥离\n // 那一层,而免责声明恰好 23 字 —— 实测整段从尾巴漏出去(会话 6c0dbc47:它是一个只有单个\n // delta 的独立 text 块,跟在工具调用后面)。所以轮末统一走 drainTextPipeline:\n // 反序吐净 + 对残余做一次性剥声明 / 剥伪系统标记。\n const drained = drainTextPipeline(filter, boilerplate, echoGuard, false)\n if (drained.echoed) echoedTranscript = true\n if (drained.disclaimers > 0) disclaimerStripped = true\n const markerPending = systemMarkerFilter.push(drained.text)\n const markerEnd = systemMarkerFilter.flush()\n const tailText = markerPending.text + markerEnd.text\n if (markerPending.stripped || markerEnd.stripped) systemMarkersStripped = true\n if (tailText) {\n const block = openText()\n if (!textStarted) {\n textStarted = true\n yield { type: 'block-start', index: block.index, blockType: 'text' }\n }\n block.text += tailText\n yield { type: 'text-delta', index: block.index, text: tailText }\n }\n if (drained.calls.length > 0) yield* emitCalls(drained.calls)\n if (drained.rejected) {\n // 协议块解析失败:**绝不**把原始 JSON/标记当正文(Web GUI 会把里面的 `$…$` 渲染成\n // KaTeX 行内公式,用户看到的是「一个字符一行」的乱码,且内容毫无意义)。\n // 只落**元信息**(长度 + 摘要,不落原文,见 dumpRejectedPayload 的说明);\n // 下面这段截断内容只写进宿主日志、不写盘,方便当场看出\"坏在哪一步\"。\n dumpRejectedPayload(drained.rejected.raw, drained.rejected.mode, drained.rejected.reason ?? 'unparsable', logger)\n logger?.warn?.(\n `deepseek-web: 工具调用${drained.rejected.mode === 'xml' ? '(XML)' : ''}解析失败` +\n `[${drained.rejected.reason ?? 'unparsable'}]` +\n `,已丢弃 ${drained.rejected.raw.length} 字符(原文不落盘;以下片段仅写入宿主日志):` +\n drained.rejected.raw.slice(0, 2000),\n )\n // 只有**首轮**的丢弃才触发整步重试;续写轮的丢弃记日志即可\n // (正文已上屏一部分,此时让整轮失败只会更糟)。\n if (rounds === 0) {\n rejectedProtocol = drained.rejected.raw\n rejectedReason = drained.rejected.reason ?? 'unparsable'\n } else {\n // F28:续写轮的丢弃不能全静默 —— 已上屏的正文保留(不整轮重试,原原则不变),\n // 但模型和用户都得知道「这次调用没执行」:否则模型下轮无从重发,\n // 用户只会在网页端看到一段没人处理的乱码(实测 14:29 / 14:52 的 DSML 变体)。\n const notice =\n `\\n[deepseek-web] 本次输出的一个工具调用因格式无法解析(${drained.rejected.reason ?? 'unparsable'})` +\n `被丢弃,该调用未执行;请改用约定的 JSON 格式重发。\\n`\n const noticeBlock = openText()\n if (!textStarted) {\n textStarted = true\n yield { type: 'block-start', index: noticeBlock.index, blockType: 'text' }\n }\n noticeBlock.text += notice\n yield { type: 'text-delta', index: noticeBlock.index, text: notice }\n }\n }\n // ── 一轮流结束:判断是否需要自动续写 ──\n const partial = textBlock?.text ?? ''\n const roundChars = partial.length - textLenAtRoundStart\n const maxRounds = deps.config.maxContinuations ?? 2\n // 没收到 `response/status: FINISHED` = 流被服务端切断(不是模型自己写完)。\n // 旧版靠这个信号报 max-tokens;现在用它触发续写 —— 否则截在标点/反引号处(判据看不出\n // 「没写完」)就会**静默**少一段,用户只看到回答末尾莫名其妙没了。\n const cutByServer = finishReason === undefined\n const midSentence = looksMidSentence(partial)\n logger?.info?.(\n `deepseek-web: 第 ${rounds + 1} 轮流结束:[本轮 ${roundChars} 字 / 累计 ${partial.length} 字 / ` +\n `耗时 ${Date.now() - roundStartedAt}ms] finish=${finishReason ?? '(无 FINISHED → 服务端截断)'}` +\n `${midSentence ? ',尾部是句中' : ''}` +\n // F27 诊断:正文 0 字而本轮确实有输出时,多半是内容全走了思考通道\n // (模型把回答写进思考 / 通道错位)。留一行显式提示,下次一眼可判。\n // 0.1.70:旧文案把\"正文 0 字\"一律说成\"内容可能全在思考通道\",读日志的人(含另一个\n // 模型窗口)据此把**健康的调工具轮**当成了故障规模(实测把它们算成了 127 次/天)。\n // 工具调用是被工具过滤器从正文流里取走的 ⇒「正文 0 字 + 有工具调用」是正常形态,必须分开说。\n `${roundChars === 0 && rounds === 0 ? (toolCallCount > 0 ? '(本轮正文 0 字,但已提取到工具调用 —— 正常形态)' : '(本轮正文 0 字、且无工具调用 —— 内容可能全在思考通道)') : ''}`,\n )\n const eligible =\n // F26:只对用户可见的回答续写(见 allowsAutoContinue 的说明)。\n // 少了这一条,标题生成会被无尽续写、标题变成重复垃圾。\n allowsAutoContinue(options?.purpose) &&\n roundError === undefined &&\n deps.config.autoContinue !== false &&\n rounds < maxRounds &&\n toolCallCount === 0 &&\n !options?.signal?.aborted &&\n partial.length > 0 &&\n roundChars > 0 &&\n (midSentence || cutByServer)\n // 0.1.74:另一种「这一轮等于什么都没做」的形态 —— 模型把要执行的程序写进了正文\n // (判据见 protocol.ts 的 looksLikeUnexecutedToolProgram,现场见其注释)。\n // 它与续写**互斥**:续写是\"话没说完\",这个是\"话说完了、但该发的动作没发出去\",\n // 两者要发的指令完全不同,所以走两条分支。\n // 只给一次机会(toolCallRetried):纠正不成就正常收尾,不把请求密度打上去。\n const unexecutedProgram =\n !eligible &&\n allowsAutoContinue(options?.purpose) &&\n roundError === undefined &&\n deps.config.autoContinue !== false &&\n rounds < maxRounds &&\n toolCallCount === 0 &&\n !toolCallRetried &&\n !options?.signal?.aborted &&\n partial.length > 0 &&\n looksLikeUnexecutedToolProgram(partial)\n if (!eligible && !unexecutedProgram) break\n rounds += 1\n if (unexecutedProgram) toolCallRetried = true\n logger?.info?.(\n unexecutedProgram\n ? `deepseek-web: 本轮把工具程序写进了正文(零工具调用),已要求它改发工具调用(第 ${rounds}/${maxRounds} 轮)……`\n : `deepseek-web: 回答疑似在句中被截,自动续写(第 ${rounds}/${maxRounds} 轮)……`,\n )\n // 续写/纠正 prompt = 原对话 + 已输出的那一轮(作为 assistant 消息)+ 指令\n promptParts = serializePromptParts({\n system: options?.system,\n messages: [\n ...(options?.messages ?? []),\n { role: 'assistant', content: [{ type: 'text', text: partial }] },\n {\n role: 'user',\n content: [{ type: 'text', text: unexecutedProgram ? TOOL_CALL_RETRY_INSTRUCTION : CONTINUE_INSTRUCTION }],\n },\n ],\n tools: (options?.tools ?? []) as ToolSchemaLike[],\n maxChars: deps.config.maxPromptChars ?? DEFAULT_MAX_PROMPT_CHARS,\n })\n currentPrompt = promptParts.full\n // 上一轮的过滤器/守卫状态已在上面收尾时吐净;续写用全新实例\n filter = new ToolCallStreamFilter(knownNames)\n echoGuard = new TranscriptEchoGuard()\n systemMarkerFilter = new SystemMarkerStreamFilter()\n boilerplate = new BoilerplateFilter()\n }\n } catch (error: any) {\n if (error instanceof AdapterLlmError) throw error\n if (options?.signal?.aborted) throw new AdapterLlmError('deepseek-web 请求被调用方取消', 'ABORTED', { cause: error })\n throw new AdapterLlmError(`deepseek-web 流失败:${error?.message ?? error}`, 'TRANSPORT', { cause: error })\n }\n\n // 0.1.71:回声守卫是「从命中行起砍到结尾」—— 所以「有正文 + 本轮有回声」意味着**后半段很可能被砍掉了**。\n // 不能静默:实测 2026-09-16 17:35(1ceshi 会话 e17f4ccf)两次都因此断在「问题项 4」处,\n // 丢的正是问题项 4、5 与结论,而 turn/end 却是 completed、界面上一个字都没有 ——\n // 用户只能以为\"它没说完就停了\"。这里补一句告知,让他知道是被过滤而不是模型罢工。\n // **不重试也不续写**:正文已有价值;回声意味着模型正在复读历史,续写大概率又是回声。\n if (echoedTranscript && toolCallCount === 0 && textBlock && textBlock.text.length > 0) {\n const echoNotice =\n '\\n\\n[deepseek-web] 本轮有一部分「历史回放格式」的内容被过滤(未上屏),回答可能因此不完整。\\n'\n textBlock.text += echoNotice\n echoNoticeChars = echoNotice.length\n yield { type: 'text-delta', index: textBlock.index, text: echoNotice }\n }\n\n // 关闭未闭合的块\n if (reasoningBlock) {\n yield { type: 'block-end', index: reasoningBlock.index, block: { type: 'reasoning', text: reasoningBlock.text } }\n }\n if (textBlock) {\n yield { type: 'block-end', index: textBlock.index, block: { type: 'text', text: textBlock.text } }\n }\n\n const outputChars =\n (textBlock?.text?.length ?? 0) + (reasoningBlock?.text?.length ?? 0) - echoNoticeChars\n let inputTokens = 0, outputTokens = 0\n for (const round of usageRounds) {\n const estimateOutput = Math.ceil(round.outputChars / 3.2)\n if (round.total !== undefined) {\n const output = Math.min(round.total, estimateOutput)\n outputTokens += output\n inputTokens += round.total - output\n } else {\n outputTokens += estimateOutput\n inputTokens += estimateTokens(round.prompt)\n }\n }\n yield { type: 'usage', usage: { inputTokens, outputTokens,\n ...(reasoningBlock ? { reasoningTokens: Math.min(outputTokens, estimateTokens(reasoningBlock.text)) } : {}) } }\n\n if (toolCallCount > 0) {\n yield { type: 'finish', reason: { kind: 'tool-calls' } }\n return\n } const hasVisibleText = (textBlock?.text?.length ?? 0) > 0\n if (echoedTranscript) {\n // 已把回声段从可见正文里剔除;这里只留痕,方便事后定位。\n logger?.warn?.('deepseek-web: 模型回声了「对话转写格式」([Tool Result for …] / User: / Assistant: 等),该段已丢弃、不上屏')\n }\n if (disclaimerStripped) {\n // 网页端在每轮末尾追加的免责声明已经剥掉(它本来会卡在两条回答中间,\n // 而且结尾的「甄别」是汉字 → 会让「句中截断」判据每轮误触发)。\n logger?.info?.('deepseek-web: 已剥离网页端免责声明(本回答由 AI 生成,内容仅供参考,请仔细甄别)')\n }\n if (echoedTranscript && !hasVisibleText && toolCallCount === 0) {\n // 整轮输出就是一段转写回声、没有任何真内容 → 当作空响应重试(与坏掉的调用同一处理)。\n yield {\n type: 'finish',\n reason: {\n kind: 'error',\n failure: {\n message: 'DeepSeek 网页端把「对话转写格式」当成回答输出了(已丢弃,未上屏),本次没有产生有效内容。',\n code: 'EMPTY_RESPONSE',\n },\n },\n }\n return\n }\n if (rejectedProtocol) {\n // 调用被丢弃 → **必须报可重试错误**,不管有没有正文。\n //\n // ⚠️ 这里曾经分两种处理:无正文 → 报错重试;有正文 → 只补一句提示、然后**当作正常完成**。\n // 结果是一个很隐蔽的故障:模型先写了半句话、再吐出坏掉的调用 JSON,调用被丢弃后\n // 这一轮**没有任何工具调用** → agent loop 判定回合结束 → 用户看到「说半句就停了」,\n // 而且 turn/end 的 reason 是 `completed`,连报错都看不到(实测 2026-09-10 23:27:13,\n // 症状复现多次,用户反复重启也无法恢复)。\n //\n // EMPTY_RESPONSE 在 dsh-llm-retry 默认可重试集合里 → 自动重发这一步;\n // 重试仍失败时用户看到的是下面这句人话,而不是一段渲染成乱码的 JSON 或一次静默停止。\n //\n // 文案按失败原因分档:实测「回声」是最常见的一种,而它其实是**我们主动拒绝**了\n // 一段历史回放,不是故障——用「格式无法解析」来描述会让用户以为程序坏了。\n const reasonText =\n rejectedReason === 'echo'\n ? '网页端本次输出的是一段历史内容回放(不是真要执行调用),已丢弃并自动重试;无需处理。'\n : rejectedReason === 'unbalanced'\n ? '网页端本次输出被截断,调用没收全,已丢弃并自动重试;无需处理。'\n : '网页端本次的调用格式无法解析,已丢弃并自动重试;无需处理。'\n yield {\n type: 'finish',\n reason: {\n kind: 'error',\n failure: { message: reasonText, code: 'EMPTY_RESPONSE' },\n },\n }\n return\n }\n // 0.1.70:判据只看**正文**(`hasVisibleText` 在上面已声明)。\n // 旧写法用 `outputChars > 0`,而 `outputChars` 含思考通道 ⇒「思考写了一堆、正文与工具调用\n // 都没有」的轮次会跳过这里、一路落到下面的 stop(=正常完成);agent loop 收到\"回合干完了、\n // 零个工具调用\"就结束回合 —— 用户看到的是\"模型莫名停住,只能手动说『继续』\"。\n // 实测(2026-09-16):该形态全局 3/885,但长思考的会话里 2/15;共同成因是**思考不收敛**\n // (实测那一轮思考 11 万字)→ 服务端始终没进入正文阶段 → 流被截断。\n // 报 EMPTY_RESPONSE 会被 dsh-llm-retry 自动重发,比静默停住强。\n // 注:走到这里 toolCallCount 必为 0(上面已对 >0 提前 return)⇒ 不会误伤健康的调工具轮。\n if (!hasVisibleText) {\n const onlyThinking = outputChars > 0\n yield {\n type: 'finish',\n reason: {\n kind: 'error',\n failure: {\n message: onlyThinking\n ? 'DeepSeek 网页端本轮没有正文、也没有工具调用(内容可能全落在思考通道 —— 通常是思考不收敛后被截断),已按可重试错误上报'\n : 'DeepSeek 网页端返回了空响应(可能触发频控或长上下文截断)',\n code: 'EMPTY_RESPONSE',\n },\n },\n }\n return\n }\n // 0.1.12:不再报 max-tokens(应用户要求移除「已达到输出 token 上限」提示)。\n // 截断由「自动续写」兜底(绝大多数被无声补全);续写额度用尽仍被截时,\n // 按正常完成(stop)上报并留痕 —— 用户可手动说「继续」。\n if (textBlock?.text && looksMidSentence(textBlock.text)) {\n if (!allowsAutoContinue(options?.purpose)) {\n // F29:内部用途(标题 / 压缩)的回答天然以汉字收尾 —— 旧版在这里打\n // 「额度已用尽」会让人误判成\"标题还在被无尽续写\"(实际白名单早就拦住了,\n // 日志里根本没有续写动作行)。按事实分档打日志。\n logger?.info?.(\n `deepseek-web: 内部用途(purpose=${String(options?.purpose)})的回答以非标点收尾,按约定不自动续写,正常收尾(尾部:${JSON.stringify(textBlock.text.slice(-40))})`,\n )\n } else {\n logger?.warn?.(\n `deepseek-web: 回答在句中被截且自动续写额度已用尽,按正常完成上报(尾部:${JSON.stringify(textBlock.text.slice(-60))})`,\n )\n }\n }\n yield { type: 'finish', reason: { kind: 'stop' } }\n }\n\n return adapter\n}\n\n/** 供 UI 展示的账号摘要。 */\nexport function describeAuth(auth: WebAuth | undefined): {\n loggedIn: boolean\n display?: string\n capturedAt?: string\n hasCookie: boolean\n hasFingerprint: boolean\n wasmHost?: string\n unverified?: boolean\n tokenLength?: number\n /**\n * cookie 的过期构成(捕获时记下的)。没有记录时为 `undefined`。\n *\n * ⚠️ 展示时务必说清它**不是登录态寿命**:实测真正鉴权用的是 `token`\n * (只发 token 不带 cookie 能通过,只发 cookie 不带 token 直接被拒),\n * 所以这里最晚的到期时间只是\"浏览器侧的上界\"。\n */\n cookieLife?: CookieLifeSummary\n} {\n if (!hasUsableAuth(auth)) return { loggedIn: false, hasCookie: false, hasFingerprint: false }\n let wasmHost: string | undefined\n try {\n wasmHost = auth.wasmUrl ? new URL(auth.wasmUrl).host : undefined\n } catch {\n wasmHost = undefined\n }\n return {\n loggedIn: true,\n ...(auth.user?.display ? { display: maskIdentifier(auth.user.display) } : auth.user?.id ? { display: `id:${maskIdentifier(auth.user.id)}` } : {}),\n ...(auth.capturedAt ? { capturedAt: auth.capturedAt } : {}),\n hasCookie: !!auth.cookie,\n hasFingerprint: !!(auth.hifDliq || auth.hifLeim),\n ...(wasmHost ? { wasmHost } : {}),\n ...(auth.unverified ? { unverified: true } : {}),\n tokenLength: auth.token.length,\n ...(() => {\n const life = summarizeCookieLife(auth.cookieMeta)\n return life ? { cookieLife: life } : {}\n })(),\n }\n}\n","/**\n * 浏览器登录(CDP 版)—— 用**系统里真实的 Edge/Chrome** 当登录窗口。\n *\n * 为什么需要它(2026-09-11 DSH 更新后的架构变化):\n * DSH 把插件宿主从 Electron **主进程**挪到了 **utility 进程**(实测 `process.type === 'utility'`)。\n * utility 进程里 `require('electron')` 拿不到 `BrowserWindow` / `session`(那是主进程专属 API),\n * 于是原来「插件自己开一个 BrowserWindow 登录」的做法直接炸在 `session.fromPartition` 上。\n * 而且新架构也没有给插件暴露任何「开窗口 / 开外部 URL」的通用服务\n * (`desktopRuntime` 只有 openTerminal / pickDirectory / openProfileCreateWindow 这类专用接口)。\n *\n * 做法:拉起一个**可见的真实浏览器**(独立 profile 目录 + 远程调试端口),用 CDP 读:\n * - `localStorage.userToken`(网页端的真实 token,AppKit 包装 `{\"value\":\"…\"}` → 解包)\n * - `Storage.getCookies`(cookie 串,免去 DPAPI 解密)\n * - `Network.*` 事件里的 `/api/*` 真实请求头(authorization / x-hif-* / x-client-*)\n * - `navigator.userAgent`(后续 API 请求要用同一个 UA)\n * 优点:真实浏览器不会被网页端判「使用环境异常」;也不依赖任何 Electron API。\n *\n * 踩坑记录(都已在代码里规避):\n * 1) **必须用 `--remote-debugging-port=0`**:Windows 保留了大量端口区间\n * (实测 8792-9897、10001-10100… 全被排除),硬编码端口会 `bind()` 失败(WSAEACCES 10013),\n * Chromium 报 \"Cannot start http server for devtools\"。端口 0 由系统分配,然后把真实端口\n * 写进 `<profile>/DevToolsActivePort`(第一行端口、第二行 ws 路径)。\n * 2) 未登录时 `localStorage.userToken` 是 `{\"value\":null,\"__version\":\"0\"}` ——\n * 解包必须把 null 当空值,不能把字符串 \"null\" 当 token。\n * 3) 用**独立 profile**(`<DSH_HOME>/web-login/browser-profile`):既避免和用户正在用的浏览器\n * 抢单实例(同 user-data-dir 会转发给已有实例、调试端口根本不起来),也让登录态可复用。\n */\nimport { pickCookieMeta, type CookieMeta } from './cookies.ts'\nimport { spawn, type ChildProcess } from 'node:child_process'\nimport { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport { unwrapStoredToken, type WebAuth } from './auth.ts'\nimport { DS_BASE } from './webapi.ts'\n\nexport interface BrowserCandidate {\n name: string\n path: string\n}\n\n/** 找系统里可用的 Chromium 系浏览器(Edge 优先:Windows 必装)。 */\nexport function findSystemBrowser(): BrowserCandidate | null {\n const pf = process.env.ProgramFiles || 'C:\\\\Program Files'\n const pf86 = process.env['ProgramFiles(x86)'] || 'C:\\\\Program Files (x86)'\n const local = process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local')\n const candidates: BrowserCandidate[] =\n process.platform === 'win32'\n ? [\n { name: 'Microsoft Edge', path: join(pf86, 'Microsoft', 'Edge', 'Application', 'msedge.exe') },\n { name: 'Microsoft Edge', path: join(pf, 'Microsoft', 'Edge', 'Application', 'msedge.exe') },\n { name: 'Google Chrome', path: join(pf, 'Google', 'Chrome', 'Application', 'chrome.exe') },\n { name: 'Google Chrome', path: join(pf86, 'Google', 'Chrome', 'Application', 'chrome.exe') },\n { name: 'Google Chrome', path: join(local, 'Google', 'Chrome', 'Application', 'chrome.exe') },\n ]\n : process.platform === 'darwin'\n ? [\n { name: 'Google Chrome', path: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' },\n { name: 'Microsoft Edge', path: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge' },\n ]\n : [\n { name: 'Google Chrome', path: '/usr/bin/google-chrome' },\n { name: 'Chromium', path: '/usr/bin/chromium' },\n { name: 'Chromium', path: '/usr/bin/chromium-browser' },\n { name: 'Microsoft Edge', path: '/usr/bin/microsoft-edge' },\n ]\n for (const candidate of candidates) {\n try {\n if (existsSync(candidate.path)) return candidate\n } catch {}\n }\n return null\n}\n\n/** 启动参数(纯函数,便于单测):**必须**带 `--remote-debugging-port=0`。 */\nexport function buildBrowserArgs(profileDir: string, url: string): string[] {\n return [\n '--remote-debugging-port=0',\n '--remote-debugging-address=127.0.0.1',\n `--user-data-dir=${profileDir}`,\n '--no-first-run',\n '--no-default-browser-check',\n '--disable-sync',\n // 别把用户的默认浏览器设置/会话搅进来\n '--no-service-autorun',\n '--disable-background-mode',\n url,\n ]\n}\n\n/** 解析 `<profile>/DevToolsActivePort`:第一行是端口;容忍 CRLF 与附带内容。 */\nexport function parseDevToolsActivePort(text: string): number | undefined {\n const first = String(text ?? '').split(/\\r?\\n/)[0]?.trim()\n if (!first) return undefined\n const port = Number(first)\n return Number.isInteger(port) && port > 0 && port < 65536 ? port : undefined\n}\n\n/** 从 CDP 的 cookie 列表拼出请求用的 cookie 串(只取 deepseek 域)。纯函数。 */\nexport function buildCookieHeader(cookies: readonly any[]): string {\n return (cookies ?? [])\n .filter((cookie) => cookie && typeof cookie.name === 'string' && String(cookie.domain ?? '').includes('deepseek'))\n .map((cookie) => `${cookie.name}=${cookie.value}`)\n .join('; ')\n}\n\n/**\n * 从 `/api/*` 请求头里挑出我们需要的指纹/版本头(与旧 webRequest 钩子同款规则)。\n * 纯函数,便于单测。\n */\nexport function pickExtraHeaders(headers: Record<string, any> | undefined): Record<string, string> {\n const out: Record<string, string> = {}\n for (const [key, value] of Object.entries(headers ?? {})) {\n const lower = key.toLowerCase()\n if (!/^x-/.test(lower)) continue\n if (lower === 'x-ds-pow-response' || lower === 'x-hif-dliq' || lower === 'x-hif-leim') continue\n out[lower] = String(value)\n }\n const acceptLanguage = Object.entries(headers ?? {}).find(([key]) => key.toLowerCase() === 'accept-language')\n if (acceptLanguage) out['accept-language'] = String(acceptLanguage[1])\n return out\n}\n\n/** 极简 CDP 客户端:只需 send + 事件监听。 */\nexport class CdpClient {\n private socket: any\n private nextId = 0\n private readonly pending = new Map<\n number,\n { resolve: (value: any) => void; reject: (error: Error) => void; timer: ReturnType<typeof setTimeout> }\n >()\n private listeners: ((method: string, params: any) => void)[] = []\n private opened = false\n private readonly url: string\n\n // ⚠️ 不用 TS 的「参数属性」写法(constructor(private readonly url: string)):\n // 那是需要**转换**的语法,Node 的 strip-only 类型剥离不支持(测试直接跑源码会报\n // \"TypeScript parameter property is not supported in strip-only mode\")。\n constructor(url: string) {\n this.url = url\n }\n\n /**\n * 建连(审计 F15)。\n *\n * 三处旧问题:① 只监听 open/error,**close 不结算** → 建连时对端关闭会一直等到超时;\n * ② 超时后 socket 仍可能在之后 open —— 成为一条**没人持有的孤立连接**;\n * ③ error/超时后没有主动释放 socket。现在统一走 `finish()`:只结算一次、清掉定时器与监听,\n * 失败时顺便 `this.close()` 把 socket 收掉。\n */\n async connect(timeoutMs = 10_000): Promise<void> {\n const WebSocketCtor = (globalThis as any).WebSocket\n if (typeof WebSocketCtor !== 'function') throw new Error('当前 Node 没有全局 WebSocket,无法使用 CDP')\n const socket = new WebSocketCtor(this.url)\n this.socket = socket\n await new Promise<void>((resolve, reject) => {\n let settled = false\n const finish = (error?: Error) => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n socket.removeEventListener('open', onOpen)\n socket.removeEventListener('error', onFail)\n socket.removeEventListener('close', onClosed)\n if (error) {\n this.close()\n reject(error)\n } else {\n this.opened = true\n resolve()\n }\n }\n const onOpen = () => finish()\n const onFail = () => finish(new Error('CDP 连接失败'))\n const onClosed = () => finish(new Error('CDP 建连时连接被关闭'))\n const timer = setTimeout(() => finish(new Error('CDP 连接超时')), timeoutMs)\n socket.addEventListener('open', onOpen)\n socket.addEventListener('error', onFail)\n socket.addEventListener('close', onClosed)\n })\n // 连接建立后的任何关闭/错误 → 立刻结算所有在途命令(不再等到各自超时)\n socket.addEventListener('close', () => this.close())\n socket.addEventListener('error', () => this.close())\n socket.addEventListener('message', (event: any) => {\n let message: any\n try {\n message = JSON.parse(String(event.data))\n } catch {\n return\n }\n if (typeof message.id === 'number') {\n const item = this.pending.get(message.id)\n if (!item) return\n this.pending.delete(message.id)\n clearTimeout(item.timer)\n // ⚠️ 协议层错误(`{id, error}`)必须 reject(审计 F15):旧实现把它当 result=undefined 的成功,\n // 调用方会拿着 undefined 继续跑,报错信息完全丢失。\n // 注意与 Runtime.evaluate 的 exceptionDetails 区分:后者是**执行结果**,由调用方自己检查。\n if (message.error) {\n item.reject(new Error(`CDP 错误 ${message.error.code ?? ''}: ${message.error.message ?? ''}`))\n } else {\n item.resolve(message.result)\n }\n return\n }\n if (message.method) {\n for (const listener of this.listeners) {\n try {\n listener(message.method, message.params)\n } catch {}\n }\n }\n })\n }\n\n onEvent(listener: (method: string, params: any) => void): void {\n this.listeners.push(listener)\n }\n\n send(method: string, params: Record<string, unknown> = {}, timeoutMs = 15_000): Promise<any> {\n if (!this.opened) return Promise.reject(new Error('CDP 未连接'))\n const id = ++this.nextId\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n this.pending.delete(id)\n reject(new Error(`CDP ${method} 超时`))\n }, timeoutMs)\n this.pending.set(id, { resolve, reject, timer })\n try {\n this.socket.send(JSON.stringify({ id, method, params }))\n } catch (error) {\n // send 同步抛错时不能留一个永远不结算的 pending(旧实现会挂到超时)\n clearTimeout(timer)\n this.pending.delete(id)\n reject(error instanceof Error ? error : new Error(String(error)))\n }\n })\n }\n\n close(): void {\n this.opened = false\n for (const item of this.pending.values()) {\n clearTimeout(item.timer)\n item.reject(new Error('CDP 已关闭'))\n }\n this.pending.clear()\n this.listeners = []\n const socket = this.socket\n this.socket = undefined\n try {\n if (socket && socket.readyState < 2) socket.close()\n } catch {}\n }\n}\n\nexport interface BrowserLoginOutcome {\n ok: boolean\n auth?: WebAuth\n message: string\n /** 失败分类,便于面板给出针对性提示。 */\n reason?:\n | 'no-browser'\n | 'spawn-failed'\n | 'no-debug-port'\n | 'no-page'\n | 'cdp-failed'\n | 'timeout'\n | 'aborted'\n | 'no-token'\n | 'browser-closed'\n /** 浏览器窗口是否仍然开着(超时时保留,用户可继续登录后重试)。 */\n browserLeftOpen?: boolean\n}\n\nexport interface BrowserLoginOptions {\n /** 总的等待时限(含用户手动登录的时间)。 */\n timeoutMs?: number\n /** 持久化 profile 目录(默认 `<DSH_HOME>/web-login/browser-profile`)。 */\n profileDir?: string\n /** 进度回调(给面板看)。 */\n onProgress?: (message: string) => void\n /** 用于取消(卸载插件/用户取消)。 */\n signal?: AbortSignal\n /** 轮询间隔(测试用)。 */\n pollIntervalMs?: number\n}\n\nconst DEFAULT_TIMEOUT_MS = 5 * 60_000\nconst DEFAULT_PROFILE_DIR = join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'web-login', 'browser-profile')\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))\n\n/**\n * 探一次 CDP 的 HTTP 端点。\n *\n * ⚠️ 每次请求都要有**自己的**超时(审计 F15):旧写法只有外层循环的 deadline,\n * 一次请求挂住就再也回不到循环条件上,\"deadline\"形同虚设。\n */\nasync function cdpJson(port: number, endpoint: '/json/version' | '/json/list', signal?: AbortSignal): Promise<any> {\n const bounded = signal ? AbortSignal.any([signal, AbortSignal.timeout(2_000)]) : AbortSignal.timeout(2_000)\n const res = await fetch(`http://127.0.0.1:${port}${endpoint}`, { signal: bounded, redirect: 'error' })\n if (!res.ok) {\n await res.body?.cancel().catch(() => {})\n throw new Error('CDP HTTP 请求失败')\n }\n return await res.json()\n}\n\n/**\n * 这个 target 是不是 chat.deepseek.com 的页面。\n *\n * 用 `origin` **严格相等**,不用 `includes`(审计 F15):`includes('deepseek.com')` 会命中\n * `chat.deepseek.com.evil.example` 这类域名,也会命中深链页/其它子域,可能选错 target。\n */\n/** R7:本插件最近一次拉起的浏览器子进程(clearBrowserLoginProfile 清理失败时杀它重试)。 */\nlet lastSpawnedChild: ChildProcess | undefined\n\nexport function isDeepSeekPage(target: any): boolean {\n try {\n return target?.type === 'page' && new URL(String(target.url)).origin === DS_BASE\n } catch {\n return false\n }\n}\n\n/** 等 CDP 的 HTTP 端点可用,返回调试端口。 */\nasync function waitForDebugPort(\n profileDir: string,\n child: ChildProcess,\n timeoutMs: number,\n signal?: AbortSignal,\n): Promise<number | undefined> {\n const portFile = join(profileDir, 'DevToolsActivePort')\n const deadline = Date.now() + timeoutMs\n while (Date.now() < deadline) {\n if (signal?.aborted) return undefined\n if (child.exitCode !== null) return undefined\n try {\n const port = parseDevToolsActivePort(readFileSync(portFile, 'utf8'))\n if (port) {\n // 端口文件出现不代表 HTTP 已就绪,探一下(自身带 2s 超时)\n try {\n await cdpJson(port, '/json/version', signal)\n return port\n } catch {}\n }\n } catch {}\n await sleep(300)\n }\n return undefined\n}\n\n/** 找到 chat.deepseek.com 的页面 target(等 SPA 起来)。 */\nasync function findPageTarget(port: number, timeoutMs: number, signal?: AbortSignal): Promise<any | null> {\n const deadline = Date.now() + timeoutMs\n while (Date.now() < deadline) {\n if (signal?.aborted) return null\n try {\n const targets = (await cdpJson(port, '/json/list', signal)) as any[]\n const page = Array.isArray(targets) ? targets.find((t) => isDeepSeekPage(t)) : undefined\n if (page?.webSocketDebuggerUrl) return page\n } catch {}\n await sleep(400)\n }\n return null\n}\n\n/**\n * 完整流程:拉起真实浏览器 → CDP 抓凭证。\n * 成功时返回可直接落盘的 WebAuth;失败时给出分类原因。\n */\nexport async function browserLogin(options: BrowserLoginOptions = {}): Promise<BrowserLoginOutcome> {\n const browser = findSystemBrowser()\n if (!browser) {\n return {\n ok: false,\n reason: 'no-browser',\n message: '没有找到 Edge/Chrome。请改用「用我的默认浏览器登录」+ 手动粘贴 Token。',\n }\n }\n\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS\n const profileDir = options.profileDir ?? DEFAULT_PROFILE_DIR\n const pollIntervalMs = options.pollIntervalMs ?? 1_200\n const progress = options.onProgress ?? (() => {})\n\n try {\n mkdirSync(profileDir, { recursive: true })\n } catch {}\n\n progress(`正在启动 ${browser.name}(独立 profile,不会影响你日常浏览器的登录态)……`)\n let child: ChildProcess\n try {\n child = spawn(browser.path, buildBrowserArgs(profileDir, `${DS_BASE}/`), {\n stdio: 'ignore',\n detached: false,\n })\n // R7:模块级留一份引用 —— clearBrowserLoginProfile(模块级函数)清理失败时要能杀掉它重试。\n lastSpawnedChild = child\n } catch (error: any) {\n return { ok: false, reason: 'spawn-failed', message: `启动 ${browser.name} 失败:${error?.message ?? error}` }\n }\n\n // ⚠️ F14(2026-09-12 审计):spawn 的失败有**一大部分是异步**的\n // (ENOENT / EACCES / 被安全软件拦截,都以 'error' 事件异步到达)。\n // 上面那个 try/catch 只接得住**同步**抛错;不监听 'error' 的话,\n // 异步失败会变成未捕获异常 —— 后果是把**整个 DSH 宿主进程**带崩,\n // 而这里真实语义只是\"登录窗口没起来\",完全不该有这个量级的破坏力。\n let spawnError: Error | undefined\n child.on('error', (error: Error) => {\n spawnError = error\n progress(`${browser.name} 启动失败(异步错误):${error?.message ?? error}`)\n })\n // 给异步错误一个冒头的机会:ENOENT/EACCES 都是立刻触发,300ms 足够,\n // 不必让用户干等 waitForDebugPort 的 25 秒才看到原因。\n await new Promise((resolve) => setTimeout(resolve, 300))\n if (spawnError) {\n try {\n child.kill()\n } catch {}\n return {\n ok: false,\n reason: 'spawn-failed',\n message: `启动 ${browser.name} 失败:${spawnError.message}`,\n }\n }\n\n const cleanupBrowser = (): void => {\n try {\n child.kill()\n } catch {}\n }\n\n const port = await waitForDebugPort(profileDir, child, 25_000, options.signal)\n if (!port) {\n cleanupBrowser()\n return {\n ok: false,\n reason: 'no-debug-port',\n message: `${browser.name} 起来了但调试端口不可用(可能被安全软件拦截)。请改用「用我的默认浏览器登录」+ 手动粘贴 Token。`,\n }\n }\n\n const page = await findPageTarget(port, 20_000, options.signal)\n if (!page) {\n cleanupBrowser()\n return { ok: false, reason: 'no-page', message: `${browser.name} 里没找到 chat.deepseek.com 页面。` }\n }\n\n const cdp = new CdpClient(page.webSocketDebuggerUrl)\n try {\n await cdp.connect()\n } catch (error: any) {\n cleanupBrowser()\n return { ok: false, reason: 'cdp-failed', message: `连接浏览器调试接口失败:${error?.message ?? error}` }\n }\n\n // 从 /api/* 的真实请求里捡指纹头(x-hif-* / x-client-*)与 UA\n let extraHeaders: Record<string, string> = {}\n let apiUserAgent = ''\n cdp.onEvent((method, params) => {\n if (method !== 'Network.requestWillBeSent' && method !== 'Network.requestWillBeSentExtraInfo') return\n const url = String(params?.request?.url ?? '')\n const headers = (method === 'Network.requestWillBeSentExtraInfo' ? params?.headers : params?.request?.headers) ?? {}\n if (!url.includes('/api/')) return\n const lower: Record<string, string> = {}\n for (const [key, value] of Object.entries(headers)) lower[key.toLowerCase()] = String(value)\n if (lower['user-agent']) apiUserAgent = lower['user-agent']\n if (Object.keys(extraHeaders).length === 0) extraHeaders = pickExtraHeaders(lower)\n })\n await cdp.send('Runtime.enable').catch(() => {})\n await cdp.send('Network.enable').catch(() => {})\n\n progress('浏览器已打开:请在其中登录 DeepSeek(手机号/邮箱/扫码均可)。登录成功后会自动捕获,无需复制粘贴。')\n\n const deadline = Date.now() + timeoutMs\n let lastNotice = 0\n try {\n while (Date.now() < deadline) {\n if (options.signal?.aborted) {\n cdp.close()\n cleanupBrowser()\n return { ok: false, reason: 'aborted', message: '已取消登录。' }\n }\n // R7(0.1.84):用户关掉浏览器后,CDP 调用会一直抛错被内层 catch 吞掉,\n // 循环要空转到 deadline 才退。看子进程退出码,提前结束。\n if (child.exitCode !== null || child.killed) {\n cleanupBrowser()\n return { ok: false, reason: 'browser-closed', message: '浏览器已关闭,登录已取消。需要的话再点一次「浏览器窗口登录」即可。' }\n }\n let token = ''\n let pageUserAgent = ''\n try {\n const value = await cdp.send('Runtime.evaluate', {\n expression: \"String(localStorage.getItem('userToken') || '')\",\n returnByValue: true,\n })\n token = unwrapStoredToken(String(value?.result?.value ?? ''))\n const ua = await cdp.send('Runtime.evaluate', { expression: 'navigator.userAgent', returnByValue: true })\n pageUserAgent = String(ua?.result?.value ?? '')\n } catch {\n // 页面可能在跳转,下一轮再试\n }\n\n if (token) {\n progress('已捕获 token,正在读取 cookie 与指纹头……')\n let cookie = ''\n let cookieMeta: CookieMeta[] = []\n try {\n const cookies = await cdp.send('Storage.getCookies', {})\n const raw = cookies?.cookies ?? []\n cookie = buildCookieHeader(raw)\n // 过滤条件与 buildCookieHeader 内**逐字一致**(都是 `includes('deepseek')`)——\n // 元信息要描述的正是请求头上实际带的那批 cookie,不能多也不能少。\n cookieMeta = pickCookieMeta(raw, (domain) => String(domain ?? '').includes('deepseek'))\n } catch {}\n const auth: WebAuth = {\n token,\n cookie,\n hifDliq: String((extraHeaders as any)['x-hif-dliq'] ?? ''),\n hifLeim: String((extraHeaders as any)['x-hif-leim'] ?? ''),\n wasmUrl: '',\n userAgent: apiUserAgent || pageUserAgent,\n ...(Object.keys(extraHeaders).length > 0 ? { extraHeaders } : {}),\n ...(cookieMeta.length > 0 ? { cookieMeta } : {}),\n capturedAt: new Date().toISOString(),\n unverified: true,\n }\n cdp.close()\n cleanupBrowser()\n return { ok: true, auth, message: `已从 ${browser.name} 捕获登录态(token + ${cookie ? 'cookie + ' : ''}指纹头)` }\n }\n\n if (Date.now() - lastNotice > 30_000) {\n lastNotice = Date.now()\n const left = Math.ceil((deadline - Date.now()) / 60_000)\n progress(`等待登录中……(还剩约 ${left} 分钟;已在浏览器里登录的话下一步就会自动读取)`)\n }\n await sleep(pollIntervalMs)\n }\n // 超时:浏览器留着不关,用户下次点按钮可以直接继续\n cdp.close()\n return {\n ok: false,\n reason: 'timeout',\n browserLeftOpen: true,\n message: `等了 ${Math.round(timeoutMs / 60_000)} 分钟没读到登录态。浏览器窗口保留着,登录完成后可以再点一次「浏览器窗口登录」(profile 复用,不用重新登录)。`,\n }\n } finally {\n cdp.close()\n }\n}\n\n/** 清掉浏览器登录用的独立 profile(退出账号时调用:连浏览器端的登录态一起清)。 */\nexport function clearBrowserLoginProfile(profileDir = DEFAULT_PROFILE_DIR): boolean {\n try {\n rmSync(profileDir, { recursive: true, force: true })\n return true\n } catch {\n // R7(0.1.84):Windows 上浏览器进程还占着 profile 目录的文件锁 ⇒ rmSync 必 EBUSY。\n // 旧实现直接吞掉返回 false,调用方再用 || 掩盖 ⇒ 「先清登录态」静默失效。\n // 先杀掉本插件拉起的残留浏览器再试一次;还不行就返回 false 让调用方如实上报。\n try {\n lastSpawnedChild?.kill()\n } catch {\n /* 没有可杀的进程就算了 */\n }\n try {\n rmSync(profileDir, { recursive: true, force: true })\n return true\n } catch {\n return false\n }\n }\n}\n","/**\n * 「登录新账号(添加)」的**添加模式** + 「重新登录某个账号」的**原地更新意图**。\n *\n * ── 为什么需要\"添加模式\" ──\n * 所有捕获路径最后都会走 `writeAuth()`,而它的语义是\"写入并**设为当前**\"。但\"往账号库里\n * 再加一个账号\"的语义是**入库、但不打扰当前正在用的那个号** —— 这两件事必须分开,\n * 否则用户每加一个账号,正在用的号就被顶掉了(添加完还得手动切回来)。\n *\n * 所以:点「登录新账号」→ `beginAddAccount()` → 下一次捕获走 `commitCapturedAuth()`,\n * 它只 `upsertAccount()`(入库),不 `setActiveAccount()`(不切换)。\n *\n * 两个安全约束:\n * 1. **一次有效**:无论成功失败,`commitCapturedAuth()` 都会消费掉这个标志,\n * 绝不让它泄漏到后面某个无关的捕获上去。\n * 2. **有期限**:登录窗口可能被用户丢在那儿不管,所以超过 TTL 自动失效\n * (否则十分钟后你随手粘个 token,会莫名其妙变成\"只入库不切换\")。\n *\n * ── 为什么又加了\"重新登录意图\"(2026-09-15,用户实测反馈)──\n * 用户点「重新登录这个账号」后重新登录成功,结果**库里多出一条同名账号**,旧那条还挂着\n * 「需要重新登录」。根因有两层:\n * ① 旧实现里 `/login/relogin` **完全没用到传进来的 id**,只调了 `beginAddAccount()`\n * —— 那只是\"别切换当前账号\",并不保证更新**同一条记录**;\n * ② 捕获时只有 token/cookie、**没有身份信息**,`upsertAccount` 的 serverId 去重无从下手,\n * 而重新登录必然换新 token(旧的 token 匹配不上)⇒ 只能新增一条;\n * 身份是**捕获之后**校验才拿到的,那时已经晚了一步。\n * 所以现在:明确记住\"这次捕获要原地更新哪条记录\",并按 id 落库(`patch.id` 优先于一切去重),\n * 顺带清掉旧的失败标记 —— 否则那条记录会一直显示「需要重新登录」(\"重登了还报错\"就是这么来的)。\n *\n * ⚠️ 注意:`/status` 里那次\"补全账号展示信息\"的写回**不走这里**(见 index.ts 的注释)——\n * 那是对**当前账号**的元数据刷新,不是新捕获,让它消费掉这些意图会是个 bug。\n */\nimport { activeAccountId, listAccounts, readAccount, setActiveAccount, updateAccount, upsertAccount } from './accounts.ts'\nimport type { AccountRecord } from './accounts.ts'\nimport { writeAuth, type WebAuth } from './auth.ts'\n\n/** 添加模式的有效期:够用户走完一次登录窗口,又不至于久到误伤后续操作。 */\nexport const ADD_MODE_TTL_MS = 15 * 60_000\n\nlet startedAt: number | null = null\n\n/** 进入添加模式(点「登录新账号」时调用)。 */\nexport function beginAddAccount(now: number = Date.now()): void {\n startedAt = now\n}\n\n/** 当前是否处于添加模式(超时即失效并自行清除)。 */\nexport function addModeActive(now: number = Date.now()): boolean {\n if (startedAt === null) return false\n if (now - startedAt > ADD_MODE_TTL_MS) {\n startedAt = null\n return false\n }\n return true\n}\n\n/** 退出添加模式(捕获完成 / 用户又点了退出或切换 —— 那些是明确的\"改当前账号\"动作)。 */\nexport function endAddAccount(): void {\n startedAt = null\n}\n\n// ── 「重新登录某个账号」的原地更新意图 ──────────────────────────────────────\n\n/**\n * 重新登录意图的有效期。比添加模式长得多:用户可能开着登录窗口去吃个饭再回来\n * (用户实测就是拖了 34 分钟 —— 按 15 分钟算的话意图早就失效了,\n * 于是捕获走了\"写入并设为当前\",既多出一条记录又顶掉了当前账号)。\n */\nexport const RELOGIN_TTL_MS = 60 * 60_000\n\nlet reloginTarget: { id: string; at: number } | null = null\n\n/** 进入\"重新登录\"意图(点某个账号那行的「重新登录」时调用)。 */\nexport function beginRelogin(id: string, now: number = Date.now()): void {\n reloginTarget = { id, at: now }\n}\n\n/** 退出意图(捕获完成时消费;用户又去点别的动作时也可以显式清掉)。 */\nexport function endRelogin(): void {\n reloginTarget = null\n}\n\n/** 取当前待生效的重新登录目标(超时即失效并自行清除)。 */\nexport function pendingReloginTarget(now: number = Date.now()): string | undefined {\n if (!reloginTarget) return undefined\n if (now - reloginTarget.at > RELOGIN_TTL_MS) {\n reloginTarget = null\n return undefined\n }\n return reloginTarget.id\n}\n\n/** 只给测试用:清空两个意图。 */\nexport function resetAccountIntents(): void {\n startedAt = null\n reloginTarget = null\n}\n\n/**\n * 这次捕获的凭证是不是\"就是那条记录对应的账号\"。\n *\n * 捕获本身只拿到 token/cookie,**没有身份**;认不出身份时按\"就是它\"处理 ——\n * 因为意图是用户刚刚在本机点出来的(且只有 TTL 内有效),\n * 而\"认得出、但明显是另一个号\"时才必须放行成新增,免得把别人的记录覆盖掉。\n */\nfunction sameAccount(existing: AccountRecord, auth: WebAuth): boolean {\n const incoming = auth as Partial<AccountRecord>\n const incomingId = incoming.serverId ?? incoming.user?.id\n const knownId = existing.serverId ?? existing.user?.id\n if (!incomingId || !knownId) return true\n return incomingId === knownId\n}\n\nexport interface CommitResult {\n /**\n * `add` = 只入库不切换;`relogin` = **原地更新指定那条记录**(也不切换);\n * `switch` = 写入并设为当前(默认语义)。\n */\n mode: 'add' | 'switch' | 'relogin'\n /** 仅 add 模式:这次捕获的账号是不是**新**的(false = 库里本来就有)。 */\n created?: boolean\n /**\n * 这次凭证落到了哪条记录上。调用方拿到它就能把\"校验回来的身份信息\"补写回去 ——\n * 否则新加的账号在列表里只能显示内部 id(`acc_xxxxxxxx`),要等下一次探活才有名字。\n */\n recordId?: string\n}\n\n/**\n * 捕获到凭证后的统一落库动作。\n *\n * 默认(非添加模式):`writeAuth()` —— 写入并设为当前,行为与以前完全一致。\n * 添加模式:`upsertAccount()` —— 只入库;当前账号**原样不动**。\n */\nexport function commitCapturedAuth(auth: WebAuth, now: number = Date.now()): CommitResult {\n // ① 重新登录意图优先:把新凭证写回**那一条**记录(按 id 落库,不走 serverId/token 去重 ——\n // 重登必然换 token,靠 token 匹配只会新增一条)。不切换当前账号。\n const target = pendingReloginTarget(now)\n if (target) {\n try {\n const existing = readAccount(target)\n if (existing && sameAccount(existing, auth)) {\n const record = upsertAccount(auth, { id: target })\n // ⚠️ 必须显式清掉旧的失败标记:旧凭证的失败结论对新凭证不成立,\n // 留着它那条记录会一直显示「需要重新登录」(用户实测:\"重登了怎么还报错\")。\n // 这里用 updateAccount 而不是 upsertAccount —— 后者的 carried 逻辑用 `??`,传 undefined 清不掉。\n // 真有问题的话,30 分钟内的探活会重新把它标红。\n updateAccount(target, { lastVerifyError: undefined })\n return { mode: 'relogin', created: false, recordId: record.id }\n }\n // 认得出\"这是另一个号\"(或那条记录已被移除)→ 放行成普通捕获,别覆盖别人的记录\n } finally {\n // 0.1.84:endAddAccount 必须在 finally 里 —— 旧写法放在 upsert/update 之后,\n // 落库一旦抛错,添加模式会泄漏到下一次无关捕获(F08 同款,relogin 分支漏了保护)。\n endAddAccount()\n endRelogin()\n }\n }\n if (!addModeActive(now)) {\n writeAuth(auth)\n const active = activeAccountId()\n return { mode: 'switch', ...(active ? { recordId: active } : {}) }\n }\n const before = new Set(listAccounts().map((item) => item.id))\n const hadActive = activeAccountId() !== undefined\n // F08:添加模式必须**总是**被消费掉。旧写法把 endAddAccount() 放在 upsertAccount 之后,\n // 一旦落库/切号抛错,模式就一直挂着 —— 于是下一次提交(哪怕是登录另一个号)\n // 会被当成\"添加\"处理,与模块注释承诺的\"只消费一次\"相反。\n try {\n const record = upsertAccount(auth)\n // 边界:库里本来一个当前账号都没有(比如刚才退出过又直接点「登录新账号」)——\n // 这时没人会被\"打扰\",不设当前反而会留下\"库里有账号却没选中\"的状态。\n // 这不违反\"添加不自动切换\":那条规则针对的是**别顶掉正在用的号**。\n if (!hadActive) setActiveAccount(record.id)\n return { mode: 'add', created: !before.has(record.id), recordId: record.id }\n } finally {\n endAddAccount()\n }\n}\n","/**\n * 网页登录:Electron 独立分区窗口(persist:dsh-deepseek-web-login)。\n *\n * 为什么用 Electron 窗口而不是外部浏览器 + 扩展/CDP:\n * DSH Desktop 本身就是 Electron 主进程,插件直接开窗口即可 ——\n * 窗口内用户正常完成手机号/密码/验证码登录,插件旁路捕获:\n * 1) webRequest.onBeforeSendHeaders 抓 /api/* 的真实 Authorization(权威 token)、\n * Cookie、x-hif-* 指纹头、x-client-* 版本头\n * 2) 读 localStorage —— ⚠️ 实测(2026-09)新版网页端的 userToken 是 AppKit 包装的\n * JSON:`{\"value\":\"<token>\",...}`,必须解包;早期版本才是裸字符串。\n * 把包装 JSON 原文当 token 会被服务端判 40003 Authorization Failed。\n * 3) 校验通过即落盘;**校验不通过也先落盘(fail-open)**,避免出现\n * 「用户已登录成功、但校验端点不配合 → 凭证永远拿不到」的死局。\n * 非 Electron 环境(纯 web profile)自动降级为「手动粘贴 token」。\n */\nimport { pickCookieMeta, type CookieMeta } from './cookies.ts'\nimport { createRequire } from 'node:module'\nimport { captureDefect, clearAuth, maskIdentifier, readAuth, unwrapStoredToken, withVerifiedIdentity, type WebAuth } from './auth.ts'\nimport { commitCapturedAuth } from './account-add.ts'\nimport { clearBrowserLoginProfile } from './browser-login.ts'\nimport { DS_BASE, DEFAULT_WASM_URL, FALLBACK_UA, validateAuth } from './webapi.ts'\n\nconst PARTITION = 'persist:dsh-deepseek-web-login'\nconst LOGIN_URL = `${DS_BASE}/`\n\n// ── 浏览器指纹伪装 ────────────────────────────────────────\n// 事故(2026-09-11 用户实测):点「浏览器窗口登录」后网页端直接显示\n// 「使用环境异常 —— 当前页面的使用环境可能存在数据和隐私泄露风险,为保障安全,\n// 建议您使用我们的官方产品。」\n// 原因:Electron 的默认 UA 里带应用名与 `Electron/<版本>` 字样,网页端一眼识别出\n// 「这不是普通浏览器」就拒绝服务。所以登录窗口必须报**干净的 Chrome UA**。\n// 顺带一个好处:捕获到的 UA 会用于后续 API 请求(auth.userAgent),干净的 UA 与网页端一致。\n\n/** 当前运行环境对应的平台串(与 Chromium 的取值一致)。 */\nfunction platformToken(): string {\n if (process.platform === 'win32') return 'Windows NT 10.0; Win64; x64'\n if (process.platform === 'darwin') return 'Macintosh; Intel Mac OS X 10_15_7'\n return 'X11; Linux x86_64'\n}\n\n/**\n * 构造干净的 Chrome UA(剔除 Electron/应用名)。\n * Chromium 大版本取当前运行时真实版本,避免出现「UA 版本与能力不符」这类更明显的矛盾。\n */\nexport function buildLoginUserAgent(chromiumVersion = process.versions.chrome): string {\n const major = String(chromiumVersion ?? '').split('.')[0] || '131'\n return `Mozilla/5.0 (${platformToken()}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${major}.0.0.0 Safari/537.36`\n}\n\n/**\n * 清掉 UA-CH(Sec-CH-UA*)里的 Electron/应用品牌 —— 只改 UA 字符串是不够的:\n * Chromium 还会通过 client hints 把品牌列表发出去,里面同样带着非浏览器品牌。\n * 纯函数,便于单测。\n */\nexport function sanitizeClientHints(headers: Record<string, string>): Record<string, string> {\n const out: Record<string, string> = { ...headers }\n for (const key of Object.keys(out)) {\n const lower = key.toLowerCase()\n if (lower !== 'sec-ch-ua' && lower !== 'sec-ch-ua-full-version-list' && lower !== 'user-agent') continue\n const value = String(out[key])\n if (lower === 'user-agent') {\n // UA 里出现 Electron / 应用名 → 换成干净 Chrome UA\n if (/electron/i.test(value)) out[key] = buildLoginUserAgent()\n continue\n }\n // 品牌列表:只保留 Chromium(丢弃 Electron 等非浏览器品牌)\n const brands = value\n .split(',')\n .map((part) => part.trim())\n .filter((part) => part && !/electron/i.test(part))\n out[key] = brands.length > 0 ? brands.join(', ') : '\"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"'\n }\n return out\n}\n\n/** 把干净指纹应用到分区与窗口(session 管网络,webContents 管页面里的 navigator.userAgent)。 */\nfunction applyBrowserFingerprint(ses: any, win: any): void {\n const ua = buildLoginUserAgent()\n try {\n ses.setUserAgent(ua)\n } catch {}\n try {\n win.webContents.setUserAgent(ua)\n } catch {}\n}\n\n/**\n * 页面内归一化 + 回读「网页端实际看到的指纹」。\n *\n * 为什么要回读:服务端对两种 UA 返回的 HTML 完全一样(实测 2026-09-11 探针),\n * 说明「使用环境异常」是**页面内 JS**判定的。既然如此,就必须能看到**页面到底看到了什么**,\n * 否则永远只能猜(UA 改没改对、品牌列表脏不脏、webdriver 是不是 true)。\n *\n * 归一化只动「非浏览器品牌」与 webdriver 这两个明确属于自动化痕迹的字段;\n * 没有 Electron 痕迹时不做任何改写。\n */\nfunction observePageFingerprint(win: any, report: { pageUa?: string; pageBrands?: string[]; pageWebdriver?: boolean }): void {\n const script = `(() => {\n const bad = /electron|dsh|deepseek-harness/i\n let patchedBrands = null\n try {\n const data = navigator.userAgentData\n if (data && Array.isArray(data.brands)) {\n const dirty = data.brands.filter((b) => bad.test(String(b.brand)))\n if (dirty.length > 0) {\n const clean = data.brands.filter((b) => !bad.test(String(b.brand)))\n try {\n Object.defineProperty(Object.getPrototypeOf(data), 'brands', { get: () => clean, configurable: true })\n } catch {}\n }\n }\n } catch {}\n try {\n if (navigator.webdriver) {\n Object.defineProperty(Object.getPrototypeOf(navigator), 'webdriver', { get: () => false, configurable: true })\n }\n } catch {}\n try {\n const data = navigator.userAgentData\n patchedBrands = data && Array.isArray(data.brands) ? data.brands.map((b) => b.brand + '/' + b.version) : null\n } catch {}\n return JSON.stringify({\n ua: navigator.userAgent,\n brands: patchedBrands,\n webdriver: !!navigator.webdriver,\n })\n })()`\n const read = (): void => {\n try {\n const promise = win.webContents.executeJavaScript(script, true)\n void Promise.resolve(promise)\n .then((raw: any) => {\n try {\n const info = JSON.parse(String(raw))\n report.pageUa = String(info.ua ?? '')\n report.pageBrands = Array.isArray(info.brands) ? info.brands.map(String) : undefined\n report.pageWebdriver = !!info.webdriver\n fingerprintReport = { ...(fingerprintReport ?? { at: new Date().toISOString(), url: LOGIN_URL, stripped: [] }), ...report }\n } catch {}\n })\n .catch(() => {})\n } catch {}\n }\n try {\n win.webContents.on('dom-ready', read)\n win.webContents.on('did-finish-load', read)\n } catch {}\n}\n\nlet loginWindow: any = null\n/**\n * 停止当前登录轮询:清定时器 **并且**取消进行中的校验。\n * F08 之前存的是定时器句柄,`clearInterval` 只能阻止\"下一轮\",\n * 拦不住已经在 await 里的那一轮 —— 窗口关闭后它照样会把凭证写回来。\n */\nlet stopPolling: (() => void) | null = null\n\nexport interface LoginProgress {\n open: boolean\n startedAt?: string\n /** 已捕获的中间态(用于 UI 提示),不含完整 token。 */\n captured?: { token: boolean; cookie: boolean; fingerprint: boolean; wasm: boolean }\n lastError?: string\n finished?: boolean\n}\n\nlet progress: LoginProgress = { open: false }\nlet lastResult: { ok: boolean; message: string; at: string } | undefined\n\n/**\n * 最近一次「剔除 Electron 指纹」的记录。\n * 留这个是为了让面板能如实展示「到底改了哪些头、页面实际看到了什么」——\n * 否则用户没法判断环境异常是已经被处理掉了,还是压根没生效。\n */\nexport interface FingerprintReport {\n at: string\n url: string\n /** 被改写的请求头名(空数组 = 本次没发现 Electron 痕迹) */\n stripped: string[]\n /** 页面里回读到的 UA —— 网页端实际看到的就是它 */\n pageUa?: string\n /** 页面里回读到的品牌列表(navigator.userAgentData.brands) */\n pageBrands?: string[]\n /** 页面里的 navigator.webdriver(自动化痕迹,正常浏览器为 false) */\n pageWebdriver?: boolean\n}\n\nlet fingerprintReport: FingerprintReport | undefined\n\nexport function getFingerprintReport(): FingerprintReport | undefined {\n return fingerprintReport\n}\n\n/**\n * 用**系统默认浏览器**打开 chat.deepseek.com(兜底路径)。\n *\n * 适用场景:网页端连干净指纹的窗口也拦、或者用户就是想用自己的日常浏览器。\n * 注意:外部浏览器里的登录态插件抓不到(没有 webRequest 钩子),\n * 所以这条路径要和「手动粘贴 token」配合 —— 面板里给了现成的控制台命令。\n *\n * ⚠️ 2026-09-11:插件宿主在 **utility 进程**里没有 `shell`(主进程专属),所以加了\n * 纯 Node 的打开方式(Windows `start` / macOS `open` / Linux `xdg-open`),保证任何宿主都能用。\n */\n/** 单测注入:替换 spawn(默认用 node:child_process 的真身)。 */\nlet spawnImpl: typeof import('node:child_process').spawn | undefined\nexport function setSpawnImpl(impl?: typeof import('node:child_process').spawn): void {\n spawnImpl = impl\n}\n\n/**\n * 用系统浏览器打开登录页。\n *\n * ⚠️ 2026-09-13 第二轮审计 N10:旧实现在 `spawn(...)` 之后立刻 `return {ok:true}`,\n * 外面那层 try/catch **只能接同步异常**;`error` 是 EventEmitter 在下一个事件循环异步发出的,\n * 没有监听就意味着**未处理错误 → 宿主进程直接退出**(同一类问题在 browser-login.ts 修过,\n * 这条路径漏了)。现在等 `spawn`/`error` 之一落地再返回。\n */\nexport async function openExternalLogin(): Promise<{ ok: boolean; url: string; message?: string; via?: string }> {\n // 主进程:用 Electron 的 shell(最干净)\n if (canOpenElectronWindow()) {\n try {\n const electron = createRequire(import.meta.url)('electron')\n await electron.shell.openExternal(LOGIN_URL)\n return { ok: true, url: LOGIN_URL, via: 'electron-shell' }\n } catch {\n // 继续走下面的纯 Node 兜底\n }\n }\n const failed = (error: unknown) => ({\n ok: false,\n url: LOGIN_URL,\n message: `${error instanceof Error ? error.message : String(error)} —— 请手动在浏览器打开 ${LOGIN_URL}`,\n })\n try {\n const command = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open'\n const args = process.platform === 'win32' ? ['/c', 'start', '', LOGIN_URL] : [LOGIN_URL]\n return await new Promise((resolve) => {\n const child = (spawnImpl ?? createRequire(import.meta.url)('node:child_process').spawn)(command, args, {\n stdio: 'ignore',\n detached: true,\n })\n // 必须监听:不监听的话这里会在下一个事件循环把宿主整个带走\n child.once('error', (error: Error) => resolve(failed(error)))\n child.once('spawn', () => {\n child.unref?.()\n resolve({ ok: true, url: LOGIN_URL, via: command })\n })\n })\n } catch (error) {\n return failed(error)\n }\n}\n\nexport function getLoginProgress(): LoginProgress {\n return progress\n}\n\nexport function getLastLoginResult(): { ok: boolean; message: string; at: string } | undefined {\n return lastResult\n}\n\n/**\n * 本进程能否**真的开 Electron 窗口**(= Electron 主进程,且 electron 模块带 session/BrowserWindow)。\n *\n * ⚠️ 旧实现只检查 `process.versions.electron`,在 DSH 把插件宿主挪到 **utility 进程**之后成了**假阳性**:\n * `process.versions.electron` 依然有值,但 utility 进程里 `require('electron')` 拿不到\n * `BrowserWindow` / `session`(它们是主进程专属 API)→ 检查通过、随后炸在 `session.fromPartition`\n * (实测:`Cannot read properties of undefined (reading 'fromPartition')`,面板表现是「窗口登录打不开」)。\n */\nexport function canOpenElectronWindow(): boolean {\n return canOpenElectronWindowWith({\n // @types/node 的 ProcessVersions **不含** electron 字段(Electron 运行时注入)——\n // 进程边界上做一次断言,类型上承认\"这里有个未知的 electron 版本位\"。\n versions: process.versions as unknown as { electron?: string },\n processType: (process as any).type,\n loadElectron: () => createRequire(import.meta.url)('electron'),\n })\n}\n\n/**\n * canOpenElectronWindow 的纯函数内核(便于用真实事故参数做单测)。\n * 判定条件三条缺一不可:① 是 Electron 运行时;② 进程类型是主进程;③ electron 模块真的带窗口 API。\n */\nexport function canOpenElectronWindowWith(deps: {\n versions?: { electron?: string } | undefined\n processType?: string | undefined\n loadElectron: () => any\n}): boolean {\n if (!deps.versions?.electron) return false\n // 'browser' = Electron 主进程;utility / renderer 都没有窗口 API\n if (deps.processType && deps.processType !== 'browser') return false\n try {\n const electron = deps.loadElectron()\n if (!electron || typeof electron === 'string') return false\n return !!(electron.session && electron.BrowserWindow)\n } catch {\n return false\n }\n}\n\n/** 兼容旧调用点:语义即「能否使用 Electron 能力」,因此等同于 canOpenElectronWindow。 */\nexport function electronAvailable(): boolean {\n return canOpenElectronWindow()\n}\n\nexport function isLoginWindowOpen(): boolean {\n return !!loginWindow\n}\n\nexport function hasStoredAuth(): boolean {\n return !!readAuth()?.token\n}\n\nfunction cleanup(): void {\n if (stopPolling) {\n try {\n stopPolling()\n } catch {}\n stopPolling = null\n }\n loginWindow = null\n progress = { ...progress, open: false }\n}\n\nexport interface CapturePollOptions {\n /** 一轮采集:读页面 / cookie,返回可用的 token 候选(空数组 = 还没登录上)。 */\n capture: () => Promise<string[]>\n /** 校验单个候选(调用方负责加超时与取消信号)。 */\n verify: (token: string) => Promise<{ ok: boolean; error?: string }>\n /** 校验通过(或 fail-open)时提交。**至多被调用一次**。 */\n commit: (token: string, verified: boolean) => Promise<void>\n /** 采集到中间态(用于 UI 提示)。 */\n onCaptured?: () => void\n /** 一轮里所有候选都没通过 / 出错时的提示文案。 */\n onError?: (message: string) => void\n intervalMs?: number\n maxAttempts?: number\n logger?: { warn?: (message: string) => void }\n}\n\n/**\n * 登录轮询(F08)。三条不变量都在这里守住,抽成独立函数是为了**能离线验证**——\n * 原来它写在 Electron 的 `setInterval` 回调里,一条都测不了。\n *\n * ① **串行**:一轮跑完才排下一轮(旧写法是 `setInterval` + 异步体,\n * 校验耗时超过 2 秒时会有多轮同时在跑);\n * ② **至多提交一次**:`committed` 先占位再提交,成功与 fail-open 两条路都走它\n * (旧写法下第二轮会把\"添加新账号\"再提交一次,而添加模式已被消费 → 变成普通切换);\n * ③ **停止后不写回**:`stop()` 之后,已经在 `await` 里的那一轮结果一律丢弃。\n *\n * 返回的 `stop()` 可重复调用。\n */\nexport function startCapturePoll(options: CapturePollOptions): () => void {\n const intervalMs =\n Number.isFinite(options.intervalMs) && (options.intervalMs as number) > 0 ? (options.intervalMs as number) : 2000\n const maxAttempts =\n Number.isFinite(options.maxAttempts) && (options.maxAttempts as number) > 0 ? (options.maxAttempts as number) : 3\n const abort = new AbortController()\n let timer: ReturnType<typeof setTimeout> | null = null\n let stopped = false\n let committed = false\n let running = false\n let attempts = 0\n\n const stop = (): void => {\n stopped = true\n abort.abort()\n if (timer) {\n clearTimeout(timer)\n timer = null\n }\n }\n\n const schedule = (): void => {\n if (stopped || committed) return\n timer = setTimeout(() => {\n void round()\n }, intervalMs)\n ;(timer as any).unref?.()\n }\n\n const round = async (): Promise<void> => {\n timer = null\n // 串行:上一轮还在跑就什么也不做(定时器只在上一轮的 finally 里排,所以这是兜底)\n if (stopped || committed || running) return\n running = true\n try {\n const candidates = await options.capture()\n // 采集期间被关闭 / 已经提交过 → 丢弃这一轮\n if (stopped || committed) return\n options.onCaptured?.()\n if (!candidates.length) return\n attempts += 1\n\n let lastError = ''\n for (const token of candidates) {\n const check = await options.verify(token)\n // ⚠️ 关键:校验期间窗口可能已经关了(或已提交过)。\n // 迟到的结果必须丢掉 —— 否则就是\"关了窗口还会把凭证写回来\"。\n if (stopped || committed) return\n if (check.ok) {\n committed = true // 先占位:保证至多一次(即便 commit 自己抛错也不重试)\n stop()\n await options.commit(token, true)\n return\n }\n lastError = check.error ?? 'validation failed'\n }\n options.onError?.(lastError)\n if (attempts >= maxAttempts) {\n committed = true\n stop()\n await options.commit(candidates[0], false)\n }\n } catch (error: any) {\n // 旧写法这里是裸 async IIFE,没有 catch —— 写盘失败会形成未处理拒绝。\n // 已提交过的失败也要报出来:提交只尝试一次,用户需要知道并重新发起登录。\n if (!stopped || committed) {\n options.onError?.('登录捕获或保存失败,请重新发起登录')\n options.logger?.warn?.(`deepseek-web: 登录捕获或保存失败: ${error?.message ?? error}`)\n }\n } finally {\n running = false\n schedule()\n }\n }\n\n timer = setTimeout(() => {\n void round()\n }, intervalMs)\n ;(timer as any).unref?.()\n return stop\n}\n\n/** 页面内取值脚本:处理 AppKit 包装({\"value\":...})与裸值两种形态。 */\nconst PAGE_READ_SCRIPT = `JSON.stringify({\n userToken: (function () {\n try {\n var raw = localStorage.getItem('userToken')\n if (!raw) return ''\n if (raw.charAt(0) === '{') {\n var parsed = JSON.parse(raw)\n return typeof parsed.value === 'string' ? parsed.value : ''\n }\n return raw\n } catch (e) { return '' }\n })(),\n userInfo: (function () {\n try {\n var raw = localStorage.getItem('__appKit_userInfo')\n if (!raw) return ''\n var parsed = JSON.parse(raw)\n var value = parsed && parsed.value ? parsed.value : parsed\n return JSON.stringify({ id: value && value.id, name: value && (value.name || value.nickname) })\n } catch (e) { return '' }\n })(),\n hifLeim: (function () {\n try {\n var raw = localStorage.getItem('hif_leim_cached')\n if (!raw) return ''\n if (raw.charAt(0) === '\"') return JSON.parse(raw)\n return raw\n } catch (e) { return '' }\n })(),\n wasm: (function () {\n try {\n return performance.getEntriesByType('resource').map(function (r) { return r.name })\n .find(function (n) { return /sha3[^\\\\s]*\\\\.wasm/.test(n) }) || ''\n } catch (e) { return '' }\n })()\n})`\n\n// unwrapStoredToken 已移到 auth.ts(浏览器登录与 Electron 登录都要用它,\n// 放在 auth.ts 可避免 browser-login ↔ login 的循环 import)。\nexport { unwrapStoredToken } from './auth.ts'\n\nfunction successPage(message: string): string {\n const html = `<!doctype html><meta charset=\"utf-8\"><title>DSH · 登录成功</title>\n<style>\n body{margin:0;height:100vh;display:flex;align-items:center;justify-content:center;\n background:#0f1115;color:#e6e6e6;font:15px/1.6 -apple-system,\"Segoe UI\",\"Microsoft YaHei\",sans-serif}\n .card{text-align:center;padding:36px 48px;border:1px solid #2a2f3a;border-radius:14px;background:#151922}\n .ok{font-size:44px;margin-bottom:10px}\n .sub{color:#8b93a3;font-size:13px;margin-top:8px}\n</style>\n<div class=\"card\"><div class=\"ok\">✅</div><div>${message}</div>\n<div class=\"sub\">此窗口将自动关闭,可回到 DSH 继续使用</div></div>`\n return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`\n}\n\ninterface CaptureBuffer {\n /** 来自 /api/* 请求头的 Bearer(权威) */\n headerToken: string\n /** 来自 localStorage.userToken(解包后) */\n localToken: string\n cookie: string\n cookieMeta: CookieMeta[]\n hifDliq: string\n hifLeim: string\n wasmUrl: string\n userAgent: string\n extraHeaders: Record<string, string>\n user: { id?: string; display?: string }\n}\n\nfunction newBuffer(): CaptureBuffer {\n return {\n headerToken: '',\n localToken: '',\n cookie: '',\n cookieMeta: [],\n hifDliq: '',\n hifLeim: '',\n wasmUrl: '',\n userAgent: '',\n extraHeaders: {},\n user: {},\n }\n}\n\n/** 候选 token:请求头里的(服务端实际在用,权威)优先,其次 localStorage。 */\nfunction tokenCandidates(buffer: CaptureBuffer): string[] {\n return [...new Set([buffer.headerToken, buffer.localToken].filter((token) => !!token && token.length > 8))]\n}\n\nfunction buildAuth(buffer: CaptureBuffer, token: string, unverified: boolean): WebAuth {\n const extraHeaders = Object.keys(buffer.extraHeaders).length > 0 ? buffer.extraHeaders : undefined\n // 0.1.78:捕获当场检查「信息偏少」并留痕。**只记录、不拦截** —— 见 captureDefect 的说明:\n // 鉴权只用 token,缺 cookie 是合法的,拿它拦人会把手工粘 token 的正常路径一起误伤。\n const defect = captureDefect({ token, cookie: buffer.cookie, extraHeaders })\n return {\n token,\n cookie: buffer.cookie,\n cookieMeta: buffer.cookieMeta,\n hifDliq: buffer.hifDliq,\n hifLeim: buffer.hifLeim,\n wasmUrl: buffer.wasmUrl || DEFAULT_WASM_URL,\n userAgent: buffer.userAgent || FALLBACK_UA,\n ...(extraHeaders ? { extraHeaders } : {}),\n capturedAt: new Date().toISOString(),\n ...(unverified ? { unverified: true } : {}),\n ...(defect ? { captureWarning: defect } : {}),\n ...(Object.keys(buffer.user).length > 0 ? { user: buffer.user } : {}),\n }\n}\n\nfunction progressFrom(buffer: CaptureBuffer): LoginProgress['captured'] {\n return {\n token: tokenCandidates(buffer).length > 0,\n cookie: !!buffer.cookie,\n fingerprint: !!(buffer.hifDliq || buffer.hifLeim),\n wasm: !!buffer.wasmUrl,\n }\n}\n\nasync function readCookies(ses: any, buffer: CaptureBuffer): Promise<void> {\n try {\n const cookies: any[] = await ses.cookies.get({})\n const relevant = cookies.filter((cookie) => String(cookie?.domain ?? '').includes('deepseek.com'))\n if (relevant.length > 0) {\n buffer.cookie = relevant.map((cookie) => `${cookie.name}=${cookie.value}`).join('; ')\n // 过滤条件与上面拼 cookie 头时**逐字一致**(同一批 relevant)。\n // Electron 的 cookie 对象用 `expirationDate`(秒),会话级不出现该字段。\n buffer.cookieMeta = pickCookieMeta(relevant, (domain) =>\n String(domain ?? '').includes('deepseek.com'),\n )\n }\n } catch {}\n}\n\nasync function readPage(win: any, buffer: CaptureBuffer): Promise<void> {\n try {\n const raw = await win.webContents.executeJavaScript(PAGE_READ_SCRIPT, true)\n const info = typeof raw === 'string' ? JSON.parse(raw) : raw\n const token = unwrapStoredToken(info?.userToken)\n if (token) buffer.localToken = token\n if (info?.hifLeim) buffer.hifLeim = String(info.hifLeim)\n if (info?.wasm) buffer.wasmUrl = String(info.wasm)\n if (info?.userInfo) {\n try {\n const parsed = JSON.parse(String(info.userInfo))\n if (parsed?.id) buffer.user.id = String(parsed.id)\n if (parsed?.name) buffer.user.display = String(parsed.name)\n } catch {}\n }\n } catch {\n // 页面导航中 executeJavaScript 可能失败:下一轮重试\n }\n}\n\n/**\n * 在 session 上挂请求头捕获钩子(同时负责剔除 Electron 指纹)。\n * ⚠️ 一个 session 只能注册一个 onBeforeSendHeaders 处理器(后注册会覆盖先注册),\n * 所以「清理指纹」必须合并在同一个回调里,不能另开一个。\n */\nfunction hookHeaders(ses: any, buffer: CaptureBuffer, onRewrite?: (info: { url: string; stripped: string[] }) => void): void {\n ses.webRequest.onBeforeSendHeaders({ urls: ['https://chat.deepseek.com/*', 'https://*.deepseek.com/*'] }, (details: any, callback: any) => {\n const headers = { ...(details?.requestHeaders ?? {}) }\n const lower: Record<string, string> = {}\n for (const [key, value] of Object.entries(headers)) lower[key.toLowerCase()] = String(value)\n // 先剔除 Electron 品牌(UA 字符串 + UA-CH);否则网页端会判定「使用环境异常」\n const sanitized = sanitizeClientHints(headers)\n const stripped: string[] = []\n for (const key of Object.keys(headers)) {\n if (String(headers[key]) !== String(sanitized[key])) stripped.push(key.toLowerCase())\n }\n for (const key of Object.keys(headers)) delete headers[key]\n Object.assign(headers, sanitized)\n if (stripped.length > 0) onRewrite?.({ url: String(details?.url ?? ''), stripped })\n // 捕获用**清理后**的头(捕获到的 UA 之后会用于 API 请求,必须是干净的那个)\n const cleanLower: Record<string, string> = {}\n for (const [key, value] of Object.entries(headers)) cleanLower[key.toLowerCase()] = String(value)\n if (String(details?.url ?? '').includes('/api/')) {\n if (!buffer.userAgent && cleanLower['user-agent']) buffer.userAgent = cleanLower['user-agent']\n const authHeader = cleanLower['authorization']\n if (authHeader?.toLowerCase().startsWith('bearer ')) buffer.headerToken = authHeader.slice(7).trim()\n if (cleanLower['cookie']) buffer.cookie = cleanLower['cookie']\n if (cleanLower['x-hif-dliq']) buffer.hifDliq = cleanLower['x-hif-dliq']\n if (cleanLower['x-hif-leim']) buffer.hifLeim = cleanLower['x-hif-leim']\n if (!buffer.extraHeaders['x-client-version']) {\n const snapshot: Record<string, string> = {}\n for (const [key, value] of Object.entries(cleanLower)) {\n if (!/^x-/.test(key)) continue\n if (key === 'x-ds-pow-response' || key === 'x-hif-dliq' || key === 'x-hif-leim') continue\n snapshot[key] = value\n }\n if (cleanLower['accept-language']) snapshot['accept-language'] = cleanLower['accept-language']\n buffer.extraHeaders = snapshot\n }\n }\n callback({ requestHeaders: headers })\n })\n}\n\n/**\n * 打开登录窗口并开始捕获(分区已登录时几乎是瞬间完成)。\n */\nexport async function openLoginWindow(logger?: { info?: (m: string) => void; warn?: (m: string) => void }): Promise<{ started: boolean; reason?: string }> {\n if (!electronAvailable()) return { started: false, reason: 'not-electron' }\n if (loginWindow) {\n try {\n loginWindow.focus()\n } catch {}\n return { started: true, reason: 'already-open' }\n }\n\n const electron = createRequire(import.meta.url)('electron')\n const { BrowserWindow, session } = electron\n\n const buffer = newBuffer()\n fingerprintReport = undefined\n progress = { open: true, startedAt: new Date().toISOString(), captured: progressFrom(buffer) }\n const ses = session.fromPartition(PARTITION)\n\n try {\n hookHeaders(ses, buffer, (info) => {\n // 只记第一次命中(避免刷屏),但要保留后面回读到的页面指纹\n if (!fingerprintReport) {\n fingerprintReport = { at: new Date().toISOString(), url: info.url, stripped: info.stripped }\n logger?.info?.(`deepseek-web login: 已剔除 Electron 指纹头 [${info.stripped.join(', ')}]`)\n } else {\n fingerprintReport = { ...fingerprintReport, stripped: info.stripped }\n }\n })\n } catch (error: any) {\n logger?.warn?.(`deepseek-web login: header capture unavailable: ${error?.message ?? error}`)\n }\n\n // F08:窗口关闭时用来取消\"已经在 await 里\"的校验(stopPolling 里会 abort 它)\n const captureAbort = new AbortController()\n\n const win = new BrowserWindow({\n width: 1180,\n height: 840,\n title: 'DSH · 登录 DeepSeek 网页版(登录后自动捕获)',\n autoHideMenuBar: true,\n webPreferences: { session: ses, nodeIntegration: false, contextIsolation: true },\n })\n loginWindow = win\n win.on('closed', () => cleanup())\n // ⚠️ 必须在 loadURL 之前应用:网页端第一次请求就带 UA,晚一步就来不及了\n applyBrowserFingerprint(ses, win)\n // 页面加载后回读「网页端实际看到的指纹」,并顺手抹掉 JS 侧的品牌/webdriver 痕迹\n observePageFingerprint(win, {})\n\n try {\n await win.loadURL(LOGIN_URL)\n } catch (error: any) {\n logger?.warn?.(`deepseek-web login: load failed: ${error?.message ?? error}`)\n }\n\n const finish = async (auth: WebAuth, verified: boolean): Promise<void> => {\n // 走统一的落库动作:添加模式(点了「登录新账号」)下只入库、不切换当前账号,\n // 否则每加一个号就把正在用的号顶掉了。见 account-add.ts。\n // F08:提交动作的\"至多一次\"由 startCapturePoll 的 committed 占位保证,\n // 这里只负责落库与 UI(不再需要额外的完成标记)。\n const commit = commitCapturedAuth(auth)\n const tail = commit.mode === 'add' ? '(已加入账号库,当前账号未改动)' : ''\n lastResult = {\n ok: true,\n message:\n (verified\n ? `登录成功${auth.user?.display ? `(${maskIdentifier(auth.user.display)})` : ''},凭证已保存并校验通过`\n : '已捕获并保存凭证,但服务端校验未通过(可用「发送测试」做真实判定)') + tail,\n at: new Date().toISOString(),\n }\n logger?.info?.(`deepseek-web login: credentials saved (verified=${verified}, mode=${commit.mode})`)\n progress = { ...progress, finished: true }\n if (!verified) {\n try {\n win.setTitle('DSH · 已捕获凭证(未通过服务端校验,可直接关闭此窗口)')\n } catch {}\n return\n }\n try {\n await win.loadURL(successPage('已捕获 DeepSeek 网页端登录状态'))\n } catch {}\n setTimeout(() => {\n try {\n win.close()\n } catch {}\n }, 3500)\n }\n\n // F08:串行轮询 + 至多提交一次 + 停止后不写回(详见 startCapturePoll 的注释)。\n // 校验用的 signal 同时受「窗口关闭」和「15 秒单次超时」约束:\n // 旧写法没有超时,一次挂住的校验会一直占着这一轮。\n let verifiedAuth: WebAuth | undefined\n stopPolling = startCapturePoll({\n capture: async () => {\n await readPage(win, buffer)\n await readCookies(ses, buffer)\n return tokenCandidates(buffer)\n },\n verify: async (token) => {\n const auth = buildAuth(buffer, token, false)\n const signal = AbortSignal.any([captureAbort.signal, AbortSignal.timeout(15_000)])\n const check = await validateAuth(auth, signal)\n if (check.ok) verifiedAuth = withVerifiedIdentity(auth, check.user)\n return { ok: !!check.ok, ...(check.error ? { error: check.error } : {}) }\n },\n commit: async (token, verified) => {\n const auth = verified && verifiedAuth ? verifiedAuth : buildAuth(buffer, token, true)\n await finish(auth, verified)\n },\n onCaptured: () => {\n progress.captured = progressFrom(buffer)\n },\n onError: (message) => {\n progress = { ...progress, lastError: message }\n },\n logger,\n })\n\n return { started: true }\n}\n\n/**\n * 从已登录的持久化分区恢复凭证(免重新登录)。\n * 用于凭证文件被删/未落盘、或重启后快速恢复。\n */\nexport async function captureFromPartition(logger?: { info?: (m: string) => void; warn?: (m: string) => void }): Promise<{ ok: boolean; verified: boolean; message: string }> {\n if (!electronAvailable()) return { ok: false, verified: false, message: '当前环境不是 Electron 桌面端' }\n const electron = createRequire(import.meta.url)('electron')\n const { BrowserWindow, session } = electron\n const ses = session.fromPartition(PARTITION)\n const buffer = newBuffer()\n let win: any\n try {\n win = new BrowserWindow({ show: false, width: 1000, height: 720, webPreferences: { session: ses, nodeIntegration: false, contextIsolation: true } })\n try {\n hookHeaders(ses, buffer)\n } catch {}\n await win.loadURL(LOGIN_URL)\n for (let i = 0; i < 10; i++) {\n await new Promise((resolve) => setTimeout(resolve, 1000))\n await readPage(win, buffer)\n if (buffer.headerToken || buffer.localToken) break\n }\n await readCookies(ses, buffer)\n } catch (error: any) {\n try {\n if (win && !win.isDestroyed()) win.close()\n } catch {}\n return { ok: false, verified: false, message: `打开分区失败:${error?.message ?? error}` }\n }\n try {\n if (win && !win.isDestroyed()) win.close()\n } catch {}\n\n const candidates = tokenCandidates(buffer)\n if (candidates.length === 0) {\n return { ok: false, verified: false, message: '分区里没有登录态:请先用「浏览器窗口登录」登录一次' }\n }\n for (const token of candidates) {\n const auth = buildAuth(buffer, token, false)\n const check = await validateAuth(auth)\n if (check.ok) {\n const commit = commitCapturedAuth(withVerifiedIdentity(auth, check.user))\n const tail = commit.mode === 'add' ? '(已加入账号库,当前账号未改动)' : ''\n lastResult = { ok: true, message: '已从已登录窗口恢复凭证(校验通过)' + tail, at: new Date().toISOString() }\n logger?.info?.(`deepseek-web login: recovered credentials from partition (verified, mode=${commit.mode})`)\n return { ok: true, verified: true, message: '已从已登录窗口恢复凭证(校验通过)' + tail }\n }\n }\n const fallback = commitCapturedAuth(buildAuth(buffer, candidates[0], true))\n const tail = fallback.mode === 'add' ? '(已加入账号库,当前账号未改动)' : ''\n lastResult = { ok: true, message: '已从已登录窗口恢复凭证(未通过服务端校验)' + tail, at: new Date().toISOString() }\n logger?.info?.(`deepseek-web login: recovered credentials from partition (unverified, mode=${fallback.mode})`)\n return { ok: true, verified: false, message: '已恢复凭证,但服务端校验未通过(可用「发送测试」验证)' + tail }\n}\n\n/** 手动粘贴 token 登录(非 Electron 环境 / 用户偏好)。 */\nexport async function loginWithToken(\n token: string,\n cookie?: string,\n logger?: { info?: (m: string) => void },\n): Promise<{ ok: boolean; error?: string; display?: string }> {\n // R8(0.1.84)fail-closed:输入是包装 JSON(localStorage 原文)但 unwrap 解出空 ——\n // value 为 null / 坏 JSON。绝不能把整段 JSON 当 token 落盘(旧写法 `unwrap(...) || 原始串` 会 fail-open,\n // 一个 >8 字符的 JSON 文本就过了长度检查,垃圾凭证入库)。\n const raw = String(token ?? '').trim()\n if (raw.startsWith('{') && !unwrapStoredToken(token)) {\n return {\n ok: false,\n error: '粘贴的内容像 localStorage 包装 JSON,但解不出 token(value 为空或已损坏)——请只复制其中的字符串值,或直接粘贴裸 token',\n }\n }\n const trimmed = unwrapStoredToken(token) || raw\n if (trimmed.length < 8) return { ok: false, error: 'token 太短,请确认复制的是 chat.deepseek.com 的登录 token' }\n const auth: WebAuth = {\n token: trimmed,\n cookie: String(cookie ?? '').trim(),\n hifDliq: '',\n hifLeim: '',\n wasmUrl: DEFAULT_WASM_URL,\n userAgent: FALLBACK_UA,\n capturedAt: new Date().toISOString(),\n }\n const check = await validateAuth(auth)\n if (!check.ok) {\n // fail-open:手工粘贴的凭证也落盘(校验端点可能不配合),真实判定交给「发送测试」\n // 手动粘 token 也尊重添加模式:用户点的是「登录新账号」,只是换了条路\n commitCapturedAuth({ ...auth, unverified: true })\n lastResult = { ok: true, message: `凭证已保存,但服务端校验未通过:${check.error ?? ''}`, at: new Date().toISOString() }\n logger?.info?.('deepseek-web login: token saved (unverified)')\n return { ok: true, error: `已保存(未通过校验:${check.error ?? 'unknown'})` }\n }\n commitCapturedAuth(withVerifiedIdentity(auth, check.user))\n lastResult = {\n ok: true,\n message: `token 校验通过,凭证已保存${check.user?.display ? `(${maskIdentifier(check.user.display)})` : ''}`,\n at: new Date().toISOString(),\n }\n logger?.info?.('deepseek-web login: token saved')\n return { ok: true, ...(check.user?.display ? { display: maskIdentifier(check.user.display) } : {}) }\n}\n\n/**\n * 清掉登录窗口所在 Electron 分区里的 **chat.deepseek.com 站点数据**(cookie / localStorage)。\n *\n * 为什么必须做:只删本地凭证文件的话,浏览器分区里仍是同一个账号的登录态 ——\n * 于是「退出当前账号」之后:\n * 1) 再点「从已登录窗口恢复」会把**同一个账号**原样抓回来(用户以为退不掉);\n * 2) 点「浏览器窗口登录」打开的是已登录页面,根本没法换号。\n * 只清 deepseek 域,不动分区里的其它数据;失败静默(退出登录本身必须成功)。\n */\n/**\n * 登录前清掉「登录态存放处」—— 独立 profile 目录 + Electron 登录分区。\n *\n * 只有**明确需要从零开始**的入口才该调它:\n * - 「登录新账号(添加)」:不清的话新窗口一打开就是旧账号,抓回来还是它(等于没加);\n * - 「用浏览器登录」主按钮:登录前按 `fresh: true` 请求清理;\n * - 「退出并登录其它账号」:走 `logout()`,那边已清。\n *\n * ⚠️ **「重登」有意不清** —— 留着登录态才可能一打开就复用、一个密码都不用敲\n * (见 `beginRelogin` 那条路由的日志)。所以这里是\"按需清理\",不是路由的默认行为。\n *\n * 拆成函数是为了能单测:删目录这种事,值得有一条用例盯着它真的删掉了。\n */\nexport async function clearLoginState(\n options: { profileDir?: string } = {},\n): Promise<{ profileCleared: boolean; partitionCleared: boolean }> {\n const profileCleared = clearBrowserLoginProfile(options.profileDir)\n const partitionCleared = await clearLoginPartition().catch(() => false)\n return { profileCleared, partitionCleared }\n}\n\nexport async function clearLoginPartition(): Promise<boolean> {\n if (!electronAvailable()) return false\n try {\n const electron = createRequire(import.meta.url)('electron')\n const ses = electron.session.fromPartition(PARTITION)\n await ses.clearStorageData({\n origin: 'https://chat.deepseek.com',\n storages: ['cookies', 'localstorage', 'indexdb', 'cachestorage', 'serviceworkers', 'websql'],\n })\n return true\n } catch {\n return false\n }\n}\n\n/**\n * 退出登录:关闭登录窗口 → 清除本地凭证 → **清除浏览器分区里的站点登录态**。\n *\n * 最后一步是 2026-09-11 补的:此前只有前两步,导致「退出」在网页端看来根本没退出\n * (同一个账号随时能被恢复回来,也无法切换到另一个账号)。\n */\n/**\n * 只关闭登录窗口(不动凭证)。\n * 卸载/热重载插件时用它 —— 卸载插件不应该把用户登出(这是旧实现的一个隐患:\n * 卸载时它调用的是 logout(),会把凭证一起删掉)。\n */\nexport function closeLoginWindow(): void {\n if (loginWindow) {\n try {\n loginWindow.close()\n } catch {}\n }\n cleanup()\n}\n\nexport async function logout(): Promise<boolean> {\n closeLoginWindow()\n clearAuth()\n // 浏览器登录用的独立 profile 也是一个「登录态存放处」,退出要一起清 ——\n // 否则再点登录会直接复用里面的登录态,等于没退出。\n const browserProfileCleared = clearBrowserLoginProfile()\n // ⚠️ 必须 await:调用方(面板的「退出并登录其它账号」)紧接着就会打开登录窗口,\n // 分区没清完的话新窗口会带着旧账号的 cookie 打开 → 又登录回同一个账号。\n const partitionCleared = await clearLoginPartition().catch(() => false)\n // 0.1.84(R7):不能用 || 掩盖 —— profile 没清 = 浏览器里还留着登录态,下次「浏览器窗口登录」\n // 会直接复用旧登录态(等于没退出,上面注释自己写了这个后果)。\n // 分区维度只在「环境真有 Electron」时才考核:桌面外环境没有分区可清,不该拖后腿。\n const partitionOK = partitionCleared || !electronAvailable()\n const cleared = browserProfileCleared && partitionOK\n lastResult = {\n ok: true,\n message: cleared\n ? '已退出登录:本地凭证与浏览器登录态都已清除'\n : !browserProfileCleared\n ? '已退出登录:本地凭证已清除,但**浏览器登录态未清掉**(浏览器窗口还开着或进程残留)——下次「浏览器窗口登录」会直接复用旧登录态,相当于没退出;请关掉浏览器后重试'\n : '已退出登录:本地凭证已清除(浏览器分区清理失败,登录窗口可能仍是旧账号,请手动退出网页端)',\n at: new Date().toISOString(),\n }\n return cleared\n}\n","/**\n * 传输层选择 —— 网页端请求到底是「从 Node 网络栈出去」还是「从 Chromium 网络栈出去」。\n *\n * 为什么在意(2026-09-12 实测,同一台机器同一天):\n *\n * | | JA4 | cipher 列表哈希 | ALPN |\n * |---|---|---|---|\n * | Node fetch(undici) | `t13d5212h1_…` | — | **h1** |\n * | Chrome(本机 152) | `t13d1517h2_8daaf6152771_cb7bf5808d99` | `8daaf6152771` | h2 |\n * | net.fetch(Electron 43) | `t13d1516h2_8daaf6152771_806a8c22fdea` | **`8daaf6152771`** | h2 |\n *\n * 也就是说 Node 的请求在 **TLS 层**就能被判定为「非浏览器客户端」(cipher 数量差 3 倍多、\n * 不走 HTTP/2、完全不带 GREASE),而这几项**调参修不了**。\n * 改用 Electron 的 `net.fetch` 后,走的是 Chromium 内置网络栈:\n * cipher 列表哈希与 Chrome **逐字节一致**,ALPN 与 cipher 数量也都对上,\n * 唯一差异是扩展数 16 vs 17(Electron 43 内置 Chromium 150,本机 Chrome 是 152,版本差异,属正常)。\n *\n * 集成方式:宿主(index.ts)在启动时按设置**一次性注入**,而不是让 webapi 去 require electron ——\n * webapi 保持与环境无关,单测里天然不会碰到 electron。\n *\n * ⚠️ 行为变更(必须知道):Chromium 网络栈会走**系统代理**,而 Node fetch 完全无视代理。\n * 若系统代理指向未开启的梯子,切到 chromium 后请求会失败 —— 所以这项做成设置页可切换,\n * 并在失败时给出明确指引。\n */\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { join } from 'node:path'\nimport { resolveDshHome } from './auth.ts'\nimport { setFetchImpl } from './webapi.ts'\n\nexport type TransportKind = 'chromium' | 'node'\n\n/** 默认走 Chromium 网络栈 —— 这才是本插件想要的\"看起来像浏览器\"。 */\nexport const DEFAULT_TRANSPORT: TransportKind = 'chromium'\n\nexport const TRANSPORT_HINT =\n 'Chromium 网络栈会跟随「系统代理」(Node 则完全无视代理)。若梯子关闭时系统代理仍指向 ' +\n '127.0.0.1:7897,切到 Chromium 后请求会失败 —— 这时切回 Node 即可。'\n\n/** 取 Electron 的 `net.fetch`;不可用(非 Electron 环境 / 未暴露 net)返回 undefined。 */\nexport function electronNetFetch(): typeof fetch | undefined {\n try {\n const electron: any = createRequire(import.meta.url)('electron')\n const impl = electron?.net?.fetch\n return typeof impl === 'function' ? impl : undefined\n } catch {\n return undefined\n }\n}\n\nexport function transportSettingsPath(): string {\n return join(resolveDshHome(), 'web-login', 'transport.json')\n}\n\nexport function readTransportSetting(): TransportKind | undefined {\n try {\n const file = transportSettingsPath()\n if (!existsSync(file)) return undefined\n const parsed = JSON.parse(readFileSync(file, 'utf8'))\n return parsed?.transport === 'node' || parsed?.transport === 'chromium' ? parsed.transport : undefined\n } catch {\n return undefined\n }\n}\n\nexport function writeTransportSetting(kind: TransportKind): void {\n const file = transportSettingsPath()\n mkdirSync(join(file, '..'), { recursive: true })\n writeFileSync(file, JSON.stringify({ transport: kind }, null, 2) + '\\n', 'utf8')\n}\n\nexport interface TransportState {\n /** 用户/配置要求的那一个 */\n requested: TransportKind\n /** 实际生效的那一个 */\n effective: TransportKind\n /** 要求 chromium 但环境不支持,已降级到 Node */\n degraded: boolean\n /** 本环境是否具备 Chromium 网络栈(Electron utility 进程里才有) */\n chromiumAvailable: boolean\n}\n\n/**\n * 决定实际用哪个。\n *\n * 降级只在**启动时**按能力判定(拿不到 `electron.net.fetch` 就用 Node),\n * **不做「请求失败后自动换一条重试」** —— 完成请求一旦重发可能就是一次重复生成,\n * 代价比\"切错了手动改回来\"大得多。\n */\nexport function resolveTransportState(requested: TransportKind): TransportState {\n const chromiumAvailable = electronNetFetch() !== undefined\n if (requested === 'chromium' && chromiumAvailable) {\n return { requested, effective: 'chromium', degraded: false, chromiumAvailable }\n }\n if (requested === 'chromium') {\n return { requested, effective: 'node', degraded: true, chromiumAvailable }\n }\n return { requested, effective: 'node', degraded: false, chromiumAvailable }\n}\n\n/** 把状态落到 webapi 的注入层(`undefined` 即还原为 Node 全局 fetch)。 */\nexport function applyTransportState(state: TransportState): void {\n setFetchImpl(state.effective === 'chromium' ? electronNetFetch() : undefined)\n}\n\n/** 读设置 → 解析 → 应用,一步到位。 */\nexport function applyTransport(requested: TransportKind): TransportState {\n const state = resolveTransportState(requested)\n applyTransportState(state)\n return state\n}\n","/**\n * net.fetch 诊断 —— 验证「把网页端请求从 Node 网络栈切到 Chromium 网络栈」是否可行。\n *\n * 背景(2026-09-12 实测):\n * - 用 Node 的 fetch(undici)与 Chrome 分别请求 tls.peet.ws,指纹差异是**结构性**的:\n * JA4 的 h1 vs h2、Node 无 GREASE、cipher 55 个 vs 15 个、扩展集合完全不同。\n * 也就是说请求在 TLS 层就能被判定为「非浏览器客户端」。\n * - 参考项目 cuckoo-code / deepseek-pp 都**不让 Node 发请求**(前者内嵌浏览器、后者 hook 用户浏览器),\n * 从未被风控 —— 印证了\"请求从哪出去\"才是关键差异。\n * - DSH 本体是 Electron,插件宿主是 **utility 进程**。官方文档:`net` 模块适用 Main + Utility,\n * 且 utility 的网络请求默认走 Chromium 的 system network context。\n * - 实测能力探测(2026-09-12)确认:utility 进程里 `require('electron')` 只暴露\n * `net` 与 `systemPreferences`,其中 **`net.fetch` 是 function** ✅\n *\n * 所以理论上不必引入 uTLS / curl-impersonate,换一处传输层就能拿到浏览器指纹。\n * 但动主路径之前必须先坐实三件事,本模块就是干这个的:\n * ① TLS/HTTP2 指纹是否真的变成浏览器(请求第三方检测站,零额度)\n * ② **能否读流式响应**(`response.body` + AbortSignal)—— 这是成败点:\n * 拿不到 body 就没法读 SSE,整条改造路线直接作废。用**本地分块服务**测,\n * 确定性、零外部依赖、零额度。\n * ③ 鉴权是否照常(header / cookie 原样透传:请求 DeepSeek 的 users/current,只读不生成)\n *\n * 另有一个可选档 `stream`:再跑一次迷你 completion(**会消耗一点额度**),\n * 端到端验证 DeepSeek 的 SSE 流。默认不跑。\n *\n * 触发方式(两种共用同一份实现):\n * - 接口:`POST /deepseek-web-login/api/diagnostics/net-fetch`,body `{\"mode\":\"probe\"|\"stream\"}`\n * - 启动时:往 `<DSH_HOME>/web-login/probe-request.json` 写 `{\"mode\":\"probe\"}` 后重启 DSH\n * (宿主进程的 HTTP 端点只有 DSH 自己的同源页面打得通,从外部 curl 会撞同源守卫;\n * 读完会把文件改名为 `*.done-<时间>`,不删文件)\n */\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { createServer } from 'node:http'\nimport { dirname, join } from 'node:path'\nimport { resolveDshHome, type WebAuth } from './auth.ts'\nimport { buildDsHeaders, fetchImplKind, scheduleDeleteSession, setFetchImpl, streamWebCompletion } from './webapi.ts'\nimport { electronNetFetch } from './transport.ts'\n\nexport type NetFetchMode = 'probe' | 'stream'\n\n/**\n * 用一次本地分块响应,验证指定 fetch 能否**增量读流**并支持 AbortSignal。\n * 导出是为了单测 —— 这条判据本身必须可被正/反向验证(见 tests/check-net-diagnostics.mjs)。\n *\n * 为什么不直接打远程:这一步只关心传输实现的流式能力,本地服务是确定性的 ——\n * 不受外网抖动/代理影响,也不会产生任何真实请求。服务器写满 50 个分片才算完,\n * 我们读到 3 个就 abort,同时观察服务端是否看到连接被断开。\n */\nexport async function probeStreamingSupport(fetchImpl: typeof fetch) {\n const evidence: any = { chunks: 0, abortedEarly: false }\n let server: ReturnType<typeof createServer> | undefined\n let reader: ReadableStreamDefaultReader<Uint8Array> | undefined\n let timer: ReturnType<typeof setInterval> | undefined\n let aborted = false\n\n try {\n server = createServer((req, res) => {\n res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' })\n let n = 0\n timer = setInterval(() => {\n n += 1\n res.write(`data: chunk-${n}\\n\\n`)\n if (n >= 50) {\n if (timer) clearInterval(timer)\n res.end()\n }\n }, 20)\n req.on('close', () => {\n aborted = true\n if (timer) clearInterval(timer)\n })\n })\n await new Promise<void>((resolve, reject) => {\n server!.once('error', reject)\n server!.listen(0, '127.0.0.1', () => resolve())\n })\n // 别让这个探测用的监听端口把宿主进程吊住\n server.unref()\n\n const port = (server.address() as any)?.port\n evidence.url = `http://127.0.0.1:${port}/`\n const controller = new AbortController()\n const response = await fetchImpl(evidence.url, { signal: controller.signal })\n evidence.status = response.status\n evidence.hasBody = !!response.body\n if (!response.body) {\n evidence.ok = false\n evidence.error = 'response.body 为空 —— 无法读 SSE,改造路线不成立'\n return evidence\n }\n\n reader = response.body.getReader()\n const decoder = new TextDecoder()\n let text = ''\n while (evidence.chunks < 3) {\n const { value, done } = await reader.read()\n if (done) break\n evidence.chunks += 1\n text += decoder.decode(value, { stream: true })\n }\n evidence.sample = text.slice(0, 60)\n\n // 验证 AbortSignal:abort 之后服务端应当看到连接断开\n controller.abort()\n await new Promise((resolve) => setTimeout(resolve, 150))\n evidence.abortedEarly = aborted\n\n evidence.ok = evidence.chunks >= 3 && evidence.abortedEarly\n if (evidence.chunks < 3) evidence.error = `只读到 ${evidence.chunks} 个分片,疑似被整体缓冲`\n else if (!evidence.abortedEarly) evidence.error = 'abort 之后服务端仍认为连接开着,AbortSignal 可能未生效'\n return evidence\n } catch (error: any) {\n evidence.ok = false\n evidence.error = error?.message ?? String(error)\n return evidence\n } finally {\n try {\n await reader?.cancel()\n } catch {}\n if (timer) clearInterval(timer)\n try {\n ;(server as any)?.closeAllConnections?.()\n server?.close()\n } catch {}\n }\n}\n\n/**\n * 跑一轮诊断。\n *\n * `probe`(默认,**零额度**):指纹 + 流式能力 + 鉴权,三步都不产生生成请求。\n * `stream`:在 probe 之上再跑一次迷你 completion(**会消耗一点额度**),端到端验证 DeepSeek 的 SSE。\n *\n * 整个过程**不改变主请求路径** —— 注入的传输层用完即还原(finally 保证)。\n */\nexport async function runNetFetchDiagnostics(auth: WebAuth | undefined, mode: NetFetchMode = 'probe') {\n const netFetch = electronNetFetch()\n if (!netFetch) {\n return { ok: false, error: 'electron.net.fetch 不可用(宿主未暴露 net)' } as const\n }\n\n const transportBefore = fetchImplKind()\n const results: any[] = []\n\n // ① TLS / HTTP2 指纹(零额度,直击\"指纹是否变成浏览器\"这个核心问题)\n try {\n const response = await netFetch('https://tls.peet.ws/api/all')\n const payload: any = await response.json()\n results.push({\n step: '① netFetch → tls.peet.ws(指纹)',\n ok: true,\n status: response.status,\n ja3_hash: payload?.tls?.ja3_hash,\n ja4: payload?.tls?.ja4,\n http2_hash: payload?.http2?.akamai_fingerprint_hash,\n http_version: payload?.http_version,\n ua: String(payload?.user_agent ?? '').slice(0, 70),\n })\n } catch (error: any) {\n results.push({ step: '① netFetch → tls.peet.ws(指纹)', ok: false, error: error?.message ?? String(error) })\n }\n\n // ② 流式能力(零额度、零外部依赖):拿不到 response.body 就没法读 SSE,成败点\n results.push({ step: '② netFetch 本地分块流(response.body + AbortSignal)', ...(await probeStreamingSupport(netFetch)) })\n\n // ③ 鉴权(只读 users/current,不生成)\n if (!auth) {\n results.push({ step: '③ netFetch → users/current(鉴权)', ok: false, error: '尚未登录' })\n } else {\n try {\n const started = Date.now()\n const response = await netFetch('https://chat.deepseek.com/api/v0/users/current', {\n headers: buildDsHeaders(auth),\n signal: AbortSignal.timeout(20_000),\n })\n const text = await response.text()\n results.push({\n step: '③ netFetch → users/current(鉴权)',\n ok: response.ok,\n status: response.status,\n ms: Date.now() - started,\n body: text.slice(0, 240),\n })\n } catch (error: any) {\n results.push({ step: '③ netFetch → users/current(鉴权)', ok: false, error: error?.message ?? String(error) })\n }\n }\n\n // ④ 端到端 SSE —— 只有显式要求才跑,会消耗一点额度\n if (mode === 'stream' && auth) {\n setFetchImpl(netFetch)\n try {\n const started = Date.now()\n let chunks = 0\n let sample = ''\n for await (const event of streamWebCompletion(auth, {\n // 参数形态必须与适配器真实调用一致:thinkingEnabled / modelType 是必填,\n // 缺了会发出残缺请求体(JSON.stringify 丢掉 undefined 字段)而被服务端拒。\n prompt: '只回复两个字:好的',\n thinkingEnabled: false,\n modelType: 'default',\n refFileIds: [],\n idleTimeoutMs: 30_000,\n onDeleteSession: (sessionId: string) => scheduleDeleteSession(auth, sessionId),\n })) {\n if (event?.kind === 'text') {\n chunks += 1\n sample += String(event.text ?? '')\n if (chunks >= 6) break\n }\n }\n results.push({\n step: '④ netFetch → DeepSeek 流式 completion(端到端)',\n ok: true,\n text_chunks: chunks,\n ms: Date.now() - started,\n sample: sample.slice(0, 80),\n })\n } catch (error: any) {\n results.push({\n step: '④ netFetch → DeepSeek 流式 completion(端到端)',\n ok: false,\n code: error?.code,\n error: error?.message ?? String(error),\n })\n } finally {\n // ⚠️ 0.1.82:必须还原成**进来时那一个**,不能无参调用 ——\n // `setFetchImpl()` 的语义是\"还原为 Node 全局 fetch\",而默认配置是 Chromium 网络栈,\n // 于是跑一次诊断就把指纹能力悄悄关掉(设置页仍显示 chromium),直到重启才恢复。\n if (transportBefore === 'injected') setFetchImpl(netFetch)\n else setFetchImpl()\n const now = fetchImplKind()\n results.push({\n step: '传输层已还原',\n ok: now === transportBefore,\n was: transportBefore,\n now,\n ...(now === transportBefore ? {} : { error: '还原后与诊断前不一致,请重启 DSH' }),\n })\n }\n }\n\n return { ok: true, mode, transportBefore, transportAfter: fetchImplKind(), results } as const\n}\n\n/** 启动探测的标记文件路径(与 gate.json 同目录,沿用 DSH_HOME 约定)。 */\nexport function probeRequestPath(): string {\n return join(resolveDshHome(), 'web-login', 'probe-request.json')\n}\n\n/** 写一个启动探测请求(写完后重启 DSH 即会执行)。 */\nexport function writeProbeRequest(mode: NetFetchMode = 'probe'): string {\n const file = probeRequestPath()\n mkdirSync(dirname(file), { recursive: true })\n writeFileSync(file, JSON.stringify({ mode, requestedAt: new Date().toISOString() }, null, 2), 'utf8')\n return file\n}\n\n/**\n * 读并\"消费\"启动探测请求。读到就返回 mode,并把文件改名为 `*.done-<时间戳>`\n * (本机禁止删除文件,用改名表示已执行;也留作历史记录)。\n */\nexport function consumeProbeRequest(): NetFetchMode | undefined {\n const file = probeRequestPath()\n try {\n if (!existsSync(file)) return undefined\n let mode: NetFetchMode = 'probe'\n try {\n const raw = JSON.parse(readFileSync(file, 'utf8'))\n if (raw?.mode === 'stream') mode = 'stream'\n } catch {\n // 内容损坏也照跑一次 probe —— 比静默跳过有用(多半是手写这个文件时格式写错)\n }\n const stamp = new Date().toISOString().replace(/[:.]/g, '-')\n renameSync(file, `${file}.done-${stamp}`)\n return mode\n } catch {\n return undefined\n }\n}\n","/**\n * 本地调用台账 —— 记「每次调用发生了什么」,用来看**请求密度**与**失败分类**。\n *\n * 为什么要它(2026-09-12,借鉴 workbuddy-switch 的用量统计):\n * 插件里已经有\"请求节流\"(串行 + 随机间隔),但**没有任何东西能证明它是否有效**。\n * 判断账号会不会被限流,靠感觉不行 —— 要看两个可量化的东西:\n * 1. **相邻调用的间隔分布**(p50 / p90 / 最短):固定间隔的方差≈0 是\"定时器特征\",\n * 真实节奏应该有抖动;最短间隔尤其说明问题 —— 它直接对应\"有没有连环请求\"。\n * 2. **失败分类**:限流 / 账号被限制 / 鉴权失效 / 网络错误,各自占比多少。\n * 只看到\"失败了\"没用,要知道是哪一类才谈得上对策。\n *\n * 存储:`<DSH_HOME>/web-login/ledger/YYYY-MM-DD.jsonl`(按天分文件,只保留近 N 天)。\n * 刻意用 JSONL 追加:崩溃最多丢最后一行,不需要读-改-写整个文件。\n * 每条都很小,且**只记元信息**(时间/账号 id/用途/耗时/错误码),不含任何对话内容与凭证。\n */\nimport { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { webLoginDir } from './paths.ts'\n\n/** 台账保留天数。 */\nexport const LEDGER_KEEP_DAYS = 7\n\n/** 单日文件软上限:超过就不再追加(避免异常情况下把磁盘写爆)。 */\nconst DAY_FILE_SOFT_LIMIT_BYTES = 2 * 1024 * 1024\n\nexport interface LedgerEntry {\n /** 时间戳(毫秒)。 */\n at: number\n /** 发生时的账号 id(`acc_xxx`,不是账号本身)。 */\n accountId?: string\n /** 调用用途:chat / session-title / compaction… */\n purpose: string\n ok: boolean\n /** 耗时(毫秒)。 */\n ms: number\n /** 失败时的语义错误码(RATE_LIMIT / AUTH / TRANSPORT…)。 */\n code?: string\n /** 是否属于「发得太频繁被限流」。 */\n throttled?: boolean\n /** 是否属于「账号被临时限制」。 */\n muted?: boolean\n}\n\n/** 汇总结果。 */\nexport interface LedgerSummary {\n hours: number\n /** 窗口内的调用总数。 */\n calls: number\n succeeded: number\n failed: number\n /** 失败分类计数(键是给人看的名字)。 */\n failures: Record<string, number>\n /** 相邻「对话类」调用的间隔(毫秒)分布;样本不足时为 null。 */\n gaps: { samples: number; min: number; p50: number; p90: number; max: number } | null\n /** 按小时分桶的调用数(长度 = hours,最后一个桶是最近一小时)。 */\n hourly: number[]\n /** 按小时分桶的**失败**数(与 `hourly` 同长同序),给界面把失败时段标出来。 */\n hourlyFailed: number[]\n /** F3(0.2.0):按账号聚合的用量(仅 chat 类调用;账号名由界面用 /accounts 数据映射)。 */\n byAccount: { accountId: string; calls: number; succeeded: number; failed: number }[]\n /** 台账目录占用(便于发现异常膨胀)。 */\n footprint: { files: number; bytes: number }\n}\n\nexport function ledgerDir(): string {\n return join(webLoginDir(), 'ledger')\n}\n\nfunction dayFile(at: number): string {\n const date = new Date(at)\n const stamp = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`\n return join(ledgerDir(), `${stamp}.jsonl`)\n}\n\nfunction listDayFiles(): string[] {\n try {\n return readdirSync(ledgerDir())\n .filter((name) => /^\\d{4}-\\d{2}-\\d{2}\\.jsonl$/.test(name))\n .sort()\n } catch {\n return []\n }\n}\n\n/**\n * 追加一条。**永不抛错** —— 台账是旁路设施,绝不能因为它写失败而影响调用。\n */\nexport function noteCall(entry: LedgerEntry): void {\n try {\n const file = dayFile(entry.at)\n mkdirSync(join(file, '..'), { recursive: true })\n try {\n if (statSync(file).size > DAY_FILE_SOFT_LIMIT_BYTES) return\n } catch {}\n appendFileSync(file, `${JSON.stringify(entry)}\\n`, 'utf8')\n } catch {}\n}\n\n/** 删除超过保留期的台账文件(插件自己的数据,按天滚动清理)。 */\nexport function pruneLedger(keepDays = LEDGER_KEEP_DAYS): number {\n const cutoff = Date.now() - keepDays * 86_400_000\n let removed = 0\n for (const name of listDayFiles()) {\n const stamp = name.replace(/\\.jsonl$/, '')\n const at = Date.parse(`${stamp}T00:00:00`)\n if (!Number.isFinite(at) || at >= cutoff) continue\n try {\n rmSync(join(ledgerDir(), name), { force: true })\n removed += 1\n } catch {}\n }\n return removed\n}\n\nfunction readEntries(sinceMs: number): LedgerEntry[] {\n const out: LedgerEntry[] = []\n for (const name of listDayFiles()) {\n // 文件名前缀就是日期,能先跳过明显过期的文件\n const stamp = name.replace(/\\.jsonl$/, '')\n const dayEnd = Date.parse(`${stamp}T23:59:59.999`)\n if (Number.isFinite(dayEnd) && dayEnd < sinceMs) continue\n let text = ''\n try {\n text = readFileSync(join(ledgerDir(), name), 'utf8')\n } catch {\n continue\n }\n for (const line of text.split('\\n')) {\n if (!line) continue\n try {\n const parsed = JSON.parse(line) as LedgerEntry\n if (Number.isFinite(parsed?.at) && parsed.at >= sinceMs) out.push(parsed)\n } catch {}\n }\n }\n out.sort((a, b) => a.at - b.at)\n return out\n}\n\n/** 失败分类:把语义错误码翻成\"人话\",同类合并。 */\nfunction failureBucket(entry: LedgerEntry): string {\n if (entry.muted) return '账号被限制'\n if (entry.throttled) return '限流(发太频繁)'\n if (entry.code === 'RATE_LIMIT') return '限流(未分类)'\n if (entry.code === 'AUTH' || entry.code === 'MISSING_CREDENTIAL') return '登录态问题'\n if (entry.code === 'TRANSPORT' || entry.code === 'TIMEOUT') return '网络 / 超时'\n if (entry.code === 'CONTEXT_WINDOW_EXCEEDED') return '上下文超限'\n if (entry.code === 'ABORTED') return '被取消'\n return entry.code ? `其它(${entry.code})` : '其它'\n}\n\nfunction percentile(sorted: number[], ratio: number): number {\n if (sorted.length === 0) return 0\n const index = Math.min(sorted.length - 1, Math.max(0, Math.round((sorted.length - 1) * ratio)))\n return sorted[index]\n}\n\n/**\n * 汇总最近 `hours` 小时。\n *\n * `hours` 会先**取整并夹到 1–72**:以前直接把入参喂给 `new Array(hours)`,而路由只 clamp\n * 没取整 → `?hours=1.5` 直接 `RangeError`(已复现)。\n *\n * 间隔只统计**对话类**调用(`purpose === 'chat'`),并且**按账号分组**:\n * - 跨账号算间隔没有意义(两个号的节奏互不相干,混在一起只会把分布拉平);\n * - `at` 是**结束**时刻,所以\"这次等了多久\" = `本次开始 − 上次结束`(`at - ms` 即本次开始)。\n * 直接拿相邻 `at` 相减,会把上一轮的生成耗时算进\"等待\"里;\n * - **失败的调用同样计入**:失败也占用了等待窗口,跳过它们只会把间隔拉大。\n *\n * 负数保留(表示可能与上一轮重叠,例如被抢占后重发)。要毫秒级精确就得新增显式\n * `startedAt` 字段,而不是从 `ms` 反推 —— 旧台账只能按现在的算法近似。\n */\nexport function summarizeLedger(input = 24): LedgerSummary {\n const hours = Number.isFinite(input) ? Math.min(72, Math.max(1, Math.floor(input))) : 24\n const now = Date.now()\n const entries = readEntries(now - hours * 3_600_000).filter(\n (entry) => entry.at <= now && Number.isFinite(entry.ms) && entry.ms >= 0,\n )\n\n const failures: Record<string, number> = {}\n const hourly = new Array<number>(hours).fill(0)\n const hourlyFailed = new Array<number>(hours).fill(0)\n let succeeded = 0\n const groups = new Map<string, LedgerEntry[]>()\n\n for (const entry of entries) {\n const bucket = Math.floor((now - entry.at) / 3_600_000)\n if (bucket >= 0 && bucket < hours) {\n hourly[hours - 1 - bucket] += 1\n if (!entry.ok) hourlyFailed[hours - 1 - bucket] += 1\n }\n if (entry.ok) succeeded += 1\n else {\n const key = failureBucket(entry)\n failures[key] = (failures[key] ?? 0) + 1\n }\n if (entry.purpose === 'chat') {\n const key = entry.accountId ?? 'unknown'\n const list = groups.get(key) ?? []\n list.push(entry)\n groups.set(key, list)\n }\n }\n\n const gaps: number[] = []\n for (const list of groups.values()) {\n list.sort((a, b) => a.at - a.ms - (b.at - b.ms))\n let lastEnd: number | undefined\n for (const entry of list) {\n if (lastEnd !== undefined) gaps.push(entry.at - entry.ms - lastEnd)\n lastEnd = Math.max(lastEnd ?? entry.at, entry.at)\n }\n }\n gaps.sort((a, b) => a - b)\n\n let files = 0\n let bytes = 0\n for (const name of listDayFiles()) {\n files += 1\n try {\n bytes += statSync(join(ledgerDir(), name)).size\n } catch {}\n }\n\n return {\n hours,\n calls: entries.length,\n succeeded,\n failed: entries.length - succeeded,\n failures,\n hourlyFailed,\n gaps:\n gaps.length > 0\n ? {\n samples: gaps.length,\n min: gaps[0],\n p50: percentile(gaps, 0.5),\n p90: percentile(gaps, 0.9),\n max: gaps[gaps.length - 1],\n }\n : null,\n hourly,\n footprint: { files, bytes },\n // F3(0.2.0):按账号聚合(groups 本来就为 gap 计算而建,顺手返回 ——\n // 多账号场景\"哪个号在烧钱/在被限\"一目了然)。\n byAccount: Array.from(groups.entries()).map(([accountId, list]) => ({\n accountId,\n calls: list.length,\n succeeded: list.filter((entry) => entry.ok).length,\n failed: list.filter((entry) => !entry.ok).length,\n })),\n }\n}\n\n\n/** 台账文件是否存在(界面用来区分\"还没跑过\"与\"跑了但没数据\")。 */\nexport function ledgerExists(): boolean {\n try {\n return existsSync(ledgerDir())\n } catch {\n return false\n }\n}\n","/**\n * 检查更新 —— 比对自身版本与 GitHub Releases 的 latest。\n *\n * 借鉴 workbuddy-switch 的「自动更新」。它是 Tauri 桌面 App,能签名校验后**整包升级**;\n * 我们是 DSH 插件,**装不了包**,所以只做\"检查 + 告诉你 + 给链接\"这一段。\n *\n * ⚠️ 三个必须守住的点:\n * 1. **短超时 + 优雅失败**:GitHub API 在国内常常连不上(或要走代理)。\n * 查更新失败绝不能把设置页卡住或让它报错 —— 失败就如实说\"没查到\"。\n * 2. **走当前传输层**:用插件自己的 fetch(默认 Chromium 网络栈),\n * 这样\"系统代理开着\"时也能跟着走,和网页端请求的环境一致。\n * 3. **只读**:只 GET 一个 releases 接口,不带任何凭证。\n */\n\n/** 仓库地址(发布源)。 */\nexport const RELEASE_REPO = 'cv-superding/dsh-deepseek-web-login'\n\nexport interface UpdateCheckResult {\n ok: boolean\n /** 当前版本(调用方传入)。 */\n current: string\n /** 远端最新 tag(去掉前缀 v),取不到时为空。 */\n latest?: string\n /** 是否有新版本(能取到 latest 且严格大于 current 时为 true)。 */\n hasUpdate: boolean\n /** Release 页面链接。 */\n url?: string\n publishedAt?: string\n /** Release 说明的前几行(够用户判断要不要升级)。 */\n notes?: string\n /** 失败原因(网络不通 / 超时 / 限流…)。 */\n error?: string\n checkedAt: string\n}\n\n/** 把 `v1.2.3` / `1.2.3-beta.1` 解析成可比较的数字数组(只取前三段数字)。 */\nexport function parseVersion(input: string): number[] | undefined {\n const match = /v?(\\d+)\\.(\\d+)\\.(\\d+)/.exec(String(input ?? '').trim())\n if (!match) return undefined\n return [Number(match[1]), Number(match[2]), Number(match[3])]\n}\n\n/** a 是否严格大于 b(无法解析时返回 false —— 宁可说\"没有更新\")。 */\nexport function isNewer(a: string, b: string): boolean {\n const left = parseVersion(a)\n const right = parseVersion(b)\n if (!left || !right) return false\n for (let i = 0; i < 3; i += 1) {\n if (left[i] !== right[i]) return left[i] > right[i]\n }\n return false\n}\n\n/**\n * 查一次最新版本。\n *\n * @param current 当前版本\n * @param fetchImpl 注入的 fetch(默认由宿主传入当前传输层)\n */\nexport async function checkForUpdate(current: string, fetchImpl: typeof fetch): Promise<UpdateCheckResult> {\n const checkedAt = new Date().toISOString()\n const base: UpdateCheckResult = { ok: false, current, hasUpdate: false, checkedAt }\n try {\n const response = await fetchImpl(`https://api.github.com/repos/${RELEASE_REPO}/releases/latest`, {\n headers: { accept: 'application/vnd.github+json', 'user-agent': `${RELEASE_REPO}-plugin` },\n // 8 秒:国内连 GitHub 往往卡住而不是立刻失败,等太久会让界面以为点了没反应\n signal: AbortSignal.timeout(8_000),\n })\n if (!response.ok) {\n return { ...base, error: `GitHub 返回 HTTP ${response.status}${response.status === 403 ? '(可能是接口限流,稍后再试)' : ''}` }\n }\n const payload: any = await response.json()\n const tag = String(payload?.tag_name ?? '')\n const latest = tag.replace(/^v/, '')\n if (!latest) return { ...base, error: 'Release 数据里没有版本号' }\n return {\n ok: true,\n current,\n latest,\n hasUpdate: isNewer(latest, current),\n ...(typeof payload?.html_url === 'string' ? { url: payload.html_url } : {}),\n ...(typeof payload?.published_at === 'string' ? { publishedAt: payload.published_at } : {}),\n ...(typeof payload?.body === 'string' ? { notes: payload.body.split('\\n').slice(0, 6).join('\\n') } : {}),\n checkedAt,\n }\n } catch (error: any) {\n const message = error?.name === 'TimeoutError' || /timeout|aborted/i.test(String(error?.message))\n ? '连接 GitHub 超时(国内常见;梯子开着的话它会跟随系统代理)'\n : `连接 GitHub 失败:${error?.message ?? error}`\n return { ...base, error: message }\n }\n}\n","/**\n * 本插件版本号 —— 单一事实来源是 `package.json`,这里做一层运行时读取 + 兜底常量。\n *\n * 为什么要兜底常量:`package.json` 相对 `lib/index.js` 在上一级目录,\n * 正常安装(作为 npm 包)一定在;但 bunder/注入式装配不一定。\n * 兜底常量与 package.json 的一致性由 `tests/check-smoke.mjs` 守着,不会漂。\n */\nimport { readFileSync } from 'node:fs'\n\n/** 与 package.json 保持一致的兜底版本(由测试保证不会漂)。 */\nexport const FALLBACK_VERSION = '0.2.0'\n\nlet cached: string | undefined\n\n/** 本插件版本(如 `0.1.26`)。 */\nexport function pluginVersion(): string {\n if (cached) return cached\n try {\n const raw = readFileSync(new URL('../package.json', import.meta.url), 'utf8')\n const parsed = JSON.parse(raw)\n if (typeof parsed?.version === 'string' && parsed.version) {\n cached = parsed.version\n return cached\n }\n } catch {}\n cached = FALLBACK_VERSION\n return cached\n}\n","/**\n * 「欠删除的会话」日志 —— 让插件在**进程退出之后**仍能认出并回收网页端会话。\n *\n * ## 背景(2026-09-14 排查,有实测数据)\n *\n * 复用槽(`webapi.ts` 的 `reuseSlot`)与「待删队列」都只活在进程内存里。宿主一退出\n * —— 尤其是被强杀 —— 两者静默消失,于是**正在复用的那个会话永远不会被删**,\n * 网页端侧栏就堆出一批标题 = DSH 会话主题的对话。\n *\n * 实测:09-12 起累计启动 DSH 58 次(40/33/20/5),残留会话量级与启动次数相符;\n * 且残留标题与 `~/.dsh/sessions/<id>/` 里的 `session/title` 记录**一一对应**,\n * 证明这些会话确实是本插件建的(不是用户自己的聊天)。\n *\n * ## 做法\n *\n * 把「**还欠一次删除**」的会话按账号落盘:进入复用槽时记一条、进入待删队列时记一条,\n * **确认删掉之后才移除**。于是文件始终等于\"服务端可能还留着、我们打算删\"的集合:\n *\n * - 正常退出:teardown 退役槽 + flush,删成功的会各自把记录摘掉;文件自然变空;\n * - 被强杀:记录原样留着 → **下次启动扫尾**,按账号排进清理器补删;\n * - 删失败:记录留着,下次启动再试(不会\"记录没了、会话还在\")。\n *\n * 为什么记账号:网页端删除必须用**会话所属账号**的凭证 —— 拿 A 的凭证去删 B 的会话,\n * 轻则被服务端拒绝,重则 resp.ok 时被当成全部成功(见 CHANGELOG 的 F07)。\n *\n * 为什么不会越攒越多:一次模型调用最多让池子里多一个会话(20 轮复用),\n * 而这里只记\"还没删掉的\",稳态就是 0~1 条。\n */\nimport { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { webLoginDir } from './paths.ts'\n\n/** 文件格式版本。以后改结构时用它决定要不要兼容读。 */\nexport const JOURNAL_VERSION = 1\n\n/** 一条「还欠一次删除」的网页端会话。 */\nexport interface JournalEntry {\n /** 会话所属账号 —— 删除时必须用它的凭证。 */\n accountId: string\n sessionId: string\n /** 记录时的进程号:用来判断\"写这条的进程是不是还活着\"。 */\n pid: number\n /** 记录时间(毫秒),仅用于诊断输出。 */\n at: number\n /** `slot` = 正在复用(还没排队);`queued` = 已排进待删队列。纯诊断用。 */\n state: 'slot' | 'queued'\n}\n\n/** `~/.dsh/web-login/sessions-in-use.json`(与 gate.json / 账号库同目录)。 */\nexport function sessionJournalPath(): string {\n return join(webLoginDir(), 'sessions-in-use.json')\n}\n\nfunction sanitizeEntry(raw: any): JournalEntry | undefined {\n const accountId = typeof raw?.accountId === 'string' ? raw.accountId.trim() : ''\n const sessionId = typeof raw?.sessionId === 'string' ? raw.sessionId.trim() : ''\n if (!accountId || !sessionId) return undefined\n return {\n accountId,\n sessionId,\n pid: Number.isFinite(raw?.pid) ? Math.floor(raw.pid) : 0,\n at: Number.isFinite(raw?.at) ? Math.floor(raw.at) : 0,\n state: raw?.state === 'queued' ? 'queued' : 'slot',\n }\n}\n\n/**\n * 读日志。文件不存在 / 损坏 / 结构不对一律当作**空**(绝不因此让插件启动失败);\n * 单条坏记录被丢掉,其余照常可用。\n */\nexport function readJournal(file: string = sessionJournalPath()): JournalEntry[] {\n try {\n if (!existsSync(file)) return []\n const parsed = JSON.parse(readFileSync(file, 'utf8'))\n const raw = Array.isArray(parsed) ? parsed : parsed?.entries\n if (!Array.isArray(raw)) return []\n const out: JournalEntry[] = []\n for (const item of raw) {\n const entry = sanitizeEntry(item)\n if (entry) out.push(entry)\n }\n return out\n } catch {\n return []\n }\n}\n\n/** 原子写:先写同目录临时文件再 rename,避免中途被杀留下半截 JSON。 */\nexport function writeJournal(entries: readonly JournalEntry[], file: string = sessionJournalPath()): void {\n const dir = dirname(file)\n try {\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true })\n } catch {}\n const payload = `${JSON.stringify({ version: JOURNAL_VERSION, entries }, null, 2)}\\n`\n const tmp = `${file}.tmp-${process.pid}`\n try {\n writeFileSync(tmp, payload, { encoding: 'utf8', mode: 0o600 })\n try {\n renameSync(tmp, file)\n } catch {\n // rename 失败:退一步直接写目标文件\n writeFileSync(file, payload, { encoding: 'utf8', mode: 0o600 })\n try {\n unlinkSync(tmp)\n } catch {}\n }\n } catch {\n /* 写不进去不影响主流程 */\n }\n}\n\n/** 记一条(同一 sessionId 覆盖)。 */\nexport function upsertJournalEntry(\n entry: Omit<JournalEntry, 'pid' | 'at' | 'state'> & Partial<Pick<JournalEntry, 'pid' | 'at' | 'state'>>,\n file: string = sessionJournalPath(),\n): void {\n const next: JournalEntry = {\n accountId: entry.accountId,\n sessionId: entry.sessionId,\n pid: entry.pid ?? process.pid,\n at: entry.at ?? Date.now(),\n state: entry.state ?? 'slot',\n }\n const entries = readJournal(file).filter((item) => item.sessionId !== next.sessionId)\n entries.push(next)\n writeJournal(entries, file)\n}\n\n/** 确认删掉 → 摘掉这条。 */\nexport function removeJournalEntry(sessionId: string, file: string = sessionJournalPath()): void {\n const entries = readJournal(file)\n const kept = entries.filter((item) => item.sessionId !== sessionId)\n if (kept.length === entries.length) return // 没有这条就别白写一次盘\n writeJournal(kept, file)\n}\n\n/** 进程是否还活着(判断\"写记录的那个进程走了没有\")。 */\nexport function isProcessAlive(pid: number): boolean {\n if (!Number.isFinite(pid) || pid <= 0) return false\n try {\n process.kill(pid, 0)\n return true\n } catch (error: any) {\n // EPERM = 进程存在但没权限发信号 ⇒ 仍然算活着\n return error?.code === 'EPERM'\n }\n}\n\nexport interface SweepOptions {\n /** 当前进程号(自己写的记录当然不动)。 */\n ownPid: number\n /** 进程存活判断,默认 `isProcessAlive`。 */\n isAlive?: (pid: number) => boolean\n /** 这个账号还在不在账号库里(不在就没凭证可删)。 */\n accountExists: (accountId: string) => boolean\n /** 用户是否允许删网页端会话(`deleteWebSessions !== false`)。 */\n deleteEnabled: boolean\n /** 清理模式;`keep` = 用户明确要求不删。 */\n mode: 'immediate' | 'deferred' | 'keep'\n}\n\nexport interface SweepPlan {\n /** 该补删的(进程已不在 + 账号还在 + 用户允许删)。 */\n toDelete: JournalEntry[]\n /** 删不掉的(账号已从账号库移除)——只能放弃,否则会永远躺在文件里。 */\n dropped: JournalEntry[]\n /** 保留的(进程还活着,或用户不允许删)。 */\n kept: JournalEntry[]\n}\n\n/**\n * 纯决策:每一条记录该怎么办。**不碰磁盘、不发请求**,可离线测。\n *\n * 判定顺序有意如此:\n * ① 进程还活着 → 保留(多实例共用同一个 DSH_HOME 时,别删另一个实例正在用的会话);\n * ② 用户不允许删(`keep` / `deleteWebSessions: false`)→ 保留,等以后允许了再扫;\n * ③ 账号已被移除 → 放弃(没凭证,留着也删不掉);\n * ④ 其余 → 补删。\n */\nexport function planStartupSweep(entries: readonly JournalEntry[], options: SweepOptions): SweepPlan {\n const isAlive = options.isAlive ?? isProcessAlive\n const plan: SweepPlan = { toDelete: [], dropped: [], kept: [] }\n for (const entry of entries) {\n if (entry.pid === options.ownPid || isAlive(entry.pid)) {\n plan.kept.push(entry)\n continue\n }\n if (!options.deleteEnabled || options.mode === 'keep') {\n plan.kept.push(entry)\n continue\n }\n if (!options.accountExists(entry.accountId)) {\n plan.dropped.push(entry)\n continue\n }\n plan.toDelete.push(entry)\n }\n return plan\n}\n\nexport interface SweepResult {\n scheduled: number\n dropped: number\n kept: number\n}\n\n/**\n * 启动扫尾:读日志 → 决策 → 对每条该补删的调用 `onSweep`(由调用方排进清理器,\n * 那里才有账号凭证与节流)。\n *\n * **不在这里清记录**:记录要留到\"确认删掉\"(`removeJournalEntry` 由删除回执触发)。\n * 这样即使本次删除失败、或者进程又在中途被杀,下次启动还能再补。\n * 唯一会立刻移除的是 `dropped`(账号没了,永远删不掉)。\n */\nexport function runStartupSweep(\n options: SweepOptions & {\n onSweep: (entry: JournalEntry) => void\n file?: string\n log?: (message: string) => void\n },\n): SweepResult {\n const file = options.file ?? sessionJournalPath()\n const entries = readJournal(file)\n if (entries.length === 0) return { scheduled: 0, dropped: 0, kept: 0 }\n\n const plan = planStartupSweep(entries, options)\n let scheduled = 0\n for (const entry of plan.toDelete) {\n try {\n options.onSweep(entry)\n scheduled += 1\n } catch {\n plan.kept.push(entry) // 排不进去 → 留到下次启动再试\n }\n }\n if (plan.dropped.length > 0) writeJournal(plan.kept, file)\n\n if (scheduled > 0) {\n options.log?.(`deepseek-web: 上次退出遗留了 ${scheduled} 个临时会话,已交给清理器补删`)\n }\n if (plan.dropped.length > 0) {\n options.log?.(\n `deepseek-web: ${plan.dropped.length} 个遗留会话的账号已不在账号库里,无法回收(已从记录移除)`,\n )\n }\n return { scheduled, dropped: plan.dropped.length, kept: plan.kept.length }\n}\n","/**\n * 账号分组:组定义落盘 + 列表按组分区。\n *\n * ── 为什么要单独一个文件,而不是把组名写进每条账号记录 ──\n *\n * 1. 组要能**改名** —— 写进账号记录的话,改一次要遍历所有账号文件;\n * 2. 组要能**排序** —— 否则\"组之间\"照样乱(这次要解决的正是\"顺序不受控\");\n * 3. 账号记录里只留一个 `groupId` 指针 ⇒ 删组不会留下散落的组名,也不会误删账号。\n *\n * 因此分工是:`groups.json` 存**组的定义**(id / name / order),\n * 账号记录里只存 `groupId`。**组不存在(被删了)或没设组的账号 ⇒ 一律归入「未分组」**,\n * 一个都不会从列表里消失。\n *\n * 纯函数(normalizeGroupList / createGroup / renameGroup / removeGroup / partitionByGroup)\n * 不碰磁盘,便于单测;读写只有 readGroups / writeGroups 两个口子。\n *\n * ⚠️ 分组的定位是**只影响显示**:切号、会话复用槽、会话清理一律不感知组。\n * 让分组参与调度会把\"插件什么时候建会话/删会话\"变得难以预测,收益却很小。\n */\nimport { randomBytes } from 'node:crypto'\nimport { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { webLoginDir } from './paths.ts'\n\nexport interface AccountGroup {\n id: string\n name: string\n /** 组之间的显示顺序(越小越靠前)。当前账号所在的组会被提到最前,见 partitionByGroup。 */\n order: number\n}\n\n/** 分区结果。`key` 可直接当 DOM 的 key 用(`__ungrouped__` 表示未分组)。 */\nexport interface GroupSection<T> {\n key: string\n name: string\n /** null = 未分组(DSH 面板据此决定要不要画\"移出组\"之类的入口)。 */\n groupId: string | null\n accounts: T[]\n}\n\n/** 未分组的伪组 key(不会与真实组 id 冲突:真实 id 一律以 `g_` 开头)。 */\nexport const UNGROUPED_KEY = '__ungrouped__'\nexport const UNGROUPED_NAME = '未分组'\n\n/** 组名上限(够写「工作号-主力」这种),也顺带挡住把整段话粘进来。 */\nexport const MAX_GROUP_NAME = 20\n/** 组数量上限:分组是给人看的,超过这个数只会更难找。 */\nexport const MAX_GROUPS = 12\n\nexport function groupsFilePath(): string {\n return join(webLoginDir(), 'groups.json')\n}\n\n/**\n * 规整组名:去首尾空白、把连续空白折成一个空格、去掉换行与控制字符、截断到上限。\n *\n * 为什么要去掉换行:组名会进 `title` / `option` 这类文本节点,带换行的名字在界面上会撑破一行,\n * 而且用户从别处粘贴时很容易把换行一起带进来。\n */\nexport function normalizeGroupName(raw: unknown): string {\n if (typeof raw !== 'string') return ''\n return raw\n .replace(/[\\u0000-\\u001f\\u007f\\u200b-\\u200f\\ufeff]/g, '')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, MAX_GROUP_NAME)\n}\n\n/**\n * 读盘容错:只收形状正确的条目,丢掉的坏数据**不会**让整个分组表读不出来。\n *\n * 与账号记录的姿态一致(那边也是\"形状不对的条目丢掉、不让整条记录失败\"):\n * 分组只是展示辅助,宁可少显示一个组,也不要因为一条脏数据让面板报错。\n * 读回后按 order 重排(顺序值可能是手改过的、重复的、非整数的)。\n */\nexport function normalizeGroupList(raw: unknown): AccountGroup[] {\n if (!Array.isArray(raw)) return []\n const seen = new Set<string>()\n const out: AccountGroup[] = []\n for (const item of raw) {\n if (!item || typeof item !== 'object') continue\n const id = typeof (item as any).id === 'string' ? (item as any).id.trim() : ''\n const name = normalizeGroupName((item as any).name)\n if (!id || !name || seen.has(id)) continue\n const orderRaw = (item as any).order\n const order = Number.isFinite(orderRaw) ? Number(orderRaw) : out.length\n seen.add(id)\n out.push({ id, name, order })\n }\n out.sort((a, b) => a.order - b.order || a.name.localeCompare(b.name))\n return out.slice(0, MAX_GROUPS).map((group, index) => ({ ...group, order: index }))\n}\n\nexport function readGroups(): AccountGroup[] {\n try {\n const file = groupsFilePath()\n if (!existsSync(file)) return []\n return normalizeGroupList(JSON.parse(readFileSync(file, 'utf8')))\n } catch {\n return []\n }\n}\n\nexport function writeGroups(list: AccountGroup[]): void {\n const file = groupsFilePath()\n mkdirSync(join(file, '..'), { recursive: true })\n const tmp = `${file}.tmp-${process.pid}`\n writeFileSync(tmp, JSON.stringify(normalizeGroupList(list), null, 2), { encoding: 'utf8', mode: 0o600 })\n try {\n renameSync(tmp, file)\n } catch (error) {\n // rename 失败时清掉临时文件,**原文件保持完好**(不先删目标 —— 那是丢数据的风险点)\n try {\n rmSync(tmp, { force: true })\n } catch {\n /* 清不掉就算了,下次写会覆盖 */\n }\n throw error\n }\n}\n\nexport function newGroupId(taken: Iterable<string>): string {\n const used = new Set(taken)\n for (let i = 0; i < 32; i += 1) {\n const id = `g_${randomBytes(4).toString('hex')}`\n if (!used.has(id)) return id\n }\n return `g_${randomBytes(6).toString('hex')}`\n}\n\n/**\n * 新建组。名字为空 / 重名 / 超过组数上限时返回 `error`(不改动 `list`)。\n *\n * 重名判定用**规整后的名字**:`\" 工作 \"` 与 `\"工作\"` 算同一个,免得界面上出现两个看起来一样的组。\n */\nexport function createGroup(list: AccountGroup[], rawName: unknown): { list: AccountGroup[]; group?: AccountGroup; error?: string } {\n const current = normalizeGroupList(list)\n const name = normalizeGroupName(rawName)\n if (!name) return { list: current, error: '组名不能为空' }\n if (current.length >= MAX_GROUPS) return { list: current, error: `最多 ${MAX_GROUPS} 个组` }\n if (current.some((group) => group.name === name)) return { list: current, error: `已经有一个叫「${name}」的组了` }\n const group: AccountGroup = { id: newGroupId(current.map((item) => item.id)), name, order: current.length }\n return { list: [...current, group], group }\n}\n\nexport function renameGroup(list: AccountGroup[], id: string, rawName: unknown): { list: AccountGroup[]; error?: string } {\n const current = normalizeGroupList(list)\n const name = normalizeGroupName(rawName)\n if (!name) return { list: current, error: '组名不能为空' }\n if (!current.some((group) => group.id === id)) return { list: current, error: '组不存在' }\n if (current.some((group) => group.id !== id && group.name === name)) {\n return { list: current, error: `已经有一个叫「${name}」的组了` }\n }\n return { list: current.map((group) => (group.id === id ? { ...group, name } : group)) }\n}\n\n/**\n * 删除组定义。\n *\n * ⚠️ **只删组、不删账号**:账号记录里的 `groupId` 会变成一个\"指向不存在组\"的悬挂指针,\n * 而 `partitionByGroup` 对这种情况的处理就是把它归入「未分组」——\n * 所以调用方不需要(也不应该)去逐个改账号文件。\n */\nexport function removeGroup(list: AccountGroup[], id: string): AccountGroup[] {\n const current = normalizeGroupList(list)\n const kept = current.filter((group) => group.id !== id)\n return kept.map((group, index) => ({ ...group, order: index }))\n}\n\nexport interface GroupableAccount {\n id: string\n groupId?: string\n}\n\n/**\n * 把账号切成「按组分区」的若干段,供面板直接渲染。\n *\n * 排序规则(用户 2026-09-16 明确要的):\n * 1. **当前账号所在的组置顶** —— 常用号不因为新加账号而沉下去;\n * 2. 其余组按 `order`;\n * 3. **未分组垫底**(含 `groupId` 指向已删除组的账号)。\n *\n * 组内顺序**保持传入顺序**(`listAccounts()` 已按捕获时间倒序),不再排序 ——\n * 组的意义是\"分类\",组内再按捕获时间排就是用户熟悉的既有行为。\n *\n * 空组**照样返回**:新建完组要能立刻看见它,否则\"建了组却没反应\"。\n * 未分组为空时**不返回**(没意义的一行)。\n */\nexport function partitionByGroup<T extends GroupableAccount>(\n accounts: T[],\n groups: AccountGroup[],\n activeId: string | null | undefined,\n): GroupSection<T>[] {\n const normalized = normalizeGroupList(groups)\n const known = new Set(normalized.map((group) => group.id))\n const buckets = new Map<string, T[]>()\n const ungrouped: T[] = []\n for (const account of accounts) {\n const gid = account.groupId && known.has(account.groupId) ? account.groupId : ''\n if (!gid) {\n ungrouped.push(account)\n continue\n }\n const bucket = buckets.get(gid)\n if (bucket) bucket.push(account)\n else buckets.set(gid, [account])\n }\n\n const activeGroupId = accounts.find((account) => account.id === activeId)?.groupId ?? ''\n const pinned = activeGroupId && known.has(activeGroupId) ? activeGroupId : ''\n\n const sections: GroupSection<T>[] = []\n const ordered = pinned\n ? [...normalized.filter((group) => group.id === pinned), ...normalized.filter((group) => group.id !== pinned)]\n : normalized\n for (const group of ordered) {\n sections.push({ key: group.id, name: group.name, groupId: group.id, accounts: buckets.get(group.id) ?? [] })\n }\n if (ungrouped.length) {\n sections.push({ key: UNGROUPED_KEY, name: UNGROUPED_NAME, groupId: null, accounts: ungrouped })\n }\n return sections\n}\n","/**\n * dsh-deepseek-web-login — 插件入口(host)。\n *\n * 做三件事:\n * 1) 把 deepseek-web 适配器注册进 ctx.llm(注册即绑定本插件 fiber,热重载即净)\n * 2) 挂 webServer 前缀 API /deepseek-web-login/api/*(client 面板消费)\n * 3) 提供设置页(client 侧 slots: settings.section)所需的状态/登录/测试接口\n *\n * 设计约束:宿主自包含打包(除 node: 与 electron 外全部 bundle),\n * 不依赖 DSH 内部包的可解析性 —— 任何装配路径(注入 / bundle / patch)都能加载。\n */\nimport { maskIdentifier, readAuth, refreshVerifiedIdentity, withVerifiedIdentity, writeAuth, type WebAuth } from './auth.ts'\nimport { createRequire } from 'node:module'\nimport { readFileSync, statSync } from 'node:fs'\nimport { PROVIDER, createAdapter, describeAuth, MODEL_SPECS, type AdapterConfig } from './adapter.ts'\nimport {\n createRequestGate,\n readGateSettings,\n writeGateSettings,\n DEFAULT_MIN_REQUEST_INTERVAL_MS,\n DEFAULT_MAX_REQUEST_INTERVAL_MS,\n DEFAULT_LONG_RUN_THRESHOLD,\n MAX_PROMPT_CHARS_BOUNDS,\n DEFAULT_MAX_PROMPT_CHARS,\n MAX_REF_IMAGES_BOUNDS,\n DEFAULT_MAX_REF_IMAGES,\n clampMaxPromptChars,\n INTERVAL_PRESETS,\n MAX_INTERVAL_MS,\n CLEANUP_BATCH_BOUNDS,\n CLEANUP_DELAY_BOUNDS_MS,\n CLEANUP_GAP_BOUNDS_MS,\n DEFAULT_CLEANUP_BATCH,\n DEFAULT_CLEANUP_DELAY_MS,\n DEFAULT_CLEANUP_GAP_MS,\n normalizeCleanupRange,\n clampMaxRefImages,\n type GateSettings,\n} from './gate.ts'\nimport { browserLogin, clearBrowserLoginProfile, findSystemBrowser } from './browser-login.ts'\nimport { canOpenElectronWindow, clearLoginPartition, clearLoginState, closeLoginWindow, captureFromPartition, getFingerprintReport, getLastLoginResult, getLoginProgress, isLoginWindowOpen, loginWithToken, logout, openExternalLogin, openLoginWindow } from './login.ts'\nimport { beginAddAccount, beginRelogin, commitCapturedAuth, endAddAccount, endRelogin } from './account-add.ts'\nimport {\n validateAuth,\n createSessionCleaner,\n currentFetch,\n contextChainInfo,\n resetContextChain,\n DEFAULT_SESSION_CLEANUP,\n DEFAULT_SESSION_REUSE_TURNS,\n disposeSessionReuse,\n setSessionLifecycleHook,\n type SessionCleanupMode,\n} from './webapi.ts'\nimport { consumeProbeRequest, runNetFetchDiagnostics, type NetFetchMode } from './net-diagnostics.ts'\nimport { noteCall as writeLedgerEntry, pruneLedger, summarizeLedger, ledgerDir, LEDGER_KEEP_DAYS } from './ledger.ts'\nimport { probeOnce, startProbeLoop } from './probe.ts'\nimport { checkForUpdate, RELEASE_REPO } from './update-check.ts'\nimport { pluginVersion } from './version.ts'\nimport { webLoginDir } from './paths.ts'\nimport { removeJournalEntry, runStartupSweep, upsertJournalEntry } from './session-journal.ts'\nimport {\n accountsDir,\n accountsFootprint,\n accountTitle,\n activeAccountId,\n exportAccounts,\n exportAccountsToFile,\n importAccounts,\n legacyMigrationError,\n listAccounts,\n migrateLegacyAuthIfNeeded,\n readAccount,\n removeAccount,\n setActiveAccount,\n updateAccount,\n} from './accounts.ts'\nimport {\n createGroup,\n partitionByGroup,\n readGroups,\n removeGroup,\n renameGroup,\n writeGroups,\n} from './account-groups.ts'\nimport {\n applyTransport,\n readTransportSetting,\n transportSettingsPath,\n writeTransportSetting,\n DEFAULT_TRANSPORT,\n TRANSPORT_HINT,\n type TransportKind,\n} from './transport.ts'\nimport {\n applyContextMode,\n contextModeSettingsPath,\n readContextModeSetting,\n writeContextModeSetting,\n CONTEXT_MODE_HINT,\n DEFAULT_CONTEXT_MODE,\n type ContextMode,\n} from './context-feed.ts'\n\nexport const name = 'dsh-deepseek-web-login'\nexport const inject = ['llm', 'webServer']\n\n/** 账号备份导入的大小上限 —— 与前端 `IMPORT_FILE_LIMIT_BYTES` 保持一致(实测单账号\n * ~1.7 KB、账号数上限 500 → ~850 KB,取 2 MiB 留 2 倍余量)。 */\nconst IMPORT_FILE_LIMIT_BYTES = 2 * 1024 * 1024\n\nconst API_PREFIX = '/deepseek-web-login/api'\n\nexport interface Config extends AdapterConfig {\n /**\n * 传输层:网页端请求从哪个网络栈出去。\n * `chromium`(默认)= Electron 的 `net.fetch`,指纹与真实浏览器一致;`node` = 原来的 undici。\n * 设置页保存的值优先于这里;环境不支持 chromium 时自动降级为 node。详见 transport.ts。\n */\n transport?: TransportKind\n /**\n * 上下文投喂方式:`full`(默认)=每轮重发全量 prompt;`chained`=只发增量、把上一条回答\n * 当父消息链接上去,让服务端维护上下文。设置页保存的值优先于这里,改动即时生效。\n * 取舍与回退条件见 context-feed.ts 的模块注释。\n */\n contextMode?: ContextMode\n /**\n * 登录态主动探活的间隔(毫秒),默认 30 分钟;设 0 关闭。\n *\n * 探活走**只读**的 `users/current`(零额度),目的是在任务跑到一半之前发现登录态失效。\n * 关掉它的代价就是\"过期只能靠一次失败的调用才发现\"。\n */\n probeIntervalMs?: number\n}\n\ninterface Logger {\n info?: (message: string) => void\n warn?: (message: string) => void\n debug?: (message: string) => void\n}\n\nfunction normalizeLogger(logger: any): Logger {\n if (!logger) return {}\n return {\n info: typeof logger.info === 'function' ? (message: string) => logger.info(message) : undefined,\n warn: typeof logger.warn === 'function' ? (message: string) => logger.warn(message) : undefined,\n debug: typeof logger.debug === 'function' ? (message: string) => logger.debug(message) : undefined,\n }\n}\n\n/**\n * 读取并解析 JSON 请求体。\n *\n * ⚠️ 审计 F16 修的三件事:\n * - **生命周期**:客户端只触发 `close`/`aborted`(不发 `end`/`error`)时,旧实现会**永不结算**,\n * 那个 Promise 连同它的监听器一起挂着;现在把 close/aborted 也当\"结束\",并且**每种结局都清监听**。\n * - **应用层超时**:慢速上传没有 deadline,一个连接可以永远占着;现在 10 秒截止。\n * - **结构化错误**:超限旧实现是 `destroy()` 后 resolve(undefined),调用方只能报\"缺少 payload\",\n * 客户端更可能只看到断连。现在抛带状态码的 BodyError,由 handler 统一回 413/400/408。\n */\nexport class BodyError extends Error {\n status: number\n constructor(status: number, message: string) {\n super(message)\n this.status = status\n }\n}\n\nexport async function readJsonBody(req: any, limitBytes = 256 * 1024): Promise<any> {\n return await new Promise((resolve, reject) => {\n let size = 0\n let settled = false\n const chunks: Buffer[] = []\n const cleanup = () => {\n clearTimeout(timer)\n req.off('data', onData)\n req.off('end', onEnd)\n req.off('error', onError)\n req.off('aborted', onAborted)\n req.off('close', onClose)\n }\n const done = (error?: Error, value?: unknown) => {\n if (settled) return\n settled = true\n cleanup()\n if (error) {\n // 主动把剩余请求体丢掉,避免连接卡在\"客户端还在写、我们已不读\"的状态\n try {\n req.resume?.()\n } catch {}\n reject(error)\n } else {\n resolve(value)\n }\n }\n const onData = (chunk: Buffer | string) => {\n const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n size += bytes.byteLength\n if (size > limitBytes) {\n done(new BodyError(413, `请求体过大(上限 ${Math.floor(limitBytes / 1024)} KiB)`))\n return\n }\n chunks.push(bytes)\n }\n const onEnd = () => {\n if (chunks.length === 0) {\n done(undefined, {})\n return\n }\n try {\n done(undefined, JSON.parse(Buffer.concat(chunks).toString('utf8')))\n } catch {\n done(new BodyError(400, '请求体不是合法 JSON'))\n }\n }\n const onError = () => done(new BodyError(400, '读取请求体失败'))\n const onAborted = () => done(new BodyError(400, '请求已中止'))\n const onClose = () => {\n // `close` 在正常结束之后也会触发 —— 只有\"还没收完就关了\"才算中止\n if (!req.complete) onAborted()\n }\n const timer = setTimeout(() => done(new BodyError(408, '读取请求体超时')), 10_000)\n req.on('data', onData)\n req.on('end', onEnd)\n req.on('error', onError)\n req.on('aborted', onAborted)\n req.on('close', onClose)\n })\n}\n\nfunction sendJson(res: any, status: number, payload: unknown): void {\n const text = JSON.stringify(payload)\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })\n res.end(text)\n}\n\n/** `/accounts/refresh` 的互斥:手动刷新是串行探活,狂点按钮不该叠起来打。 */\nlet accountsRefreshInFlight = false\n\nexport function apply(ctx: any, config: Config = {}): void {\n const logger = normalizeLogger(ctx.logger)\n\n // ── 运行时能力探测(每次启动打印一次)──────────────────────────────\n // 为什么需要:插件宿主是 Electron 的 **utility 进程**,能拿到的 API 与主进程不同\n // (已知 shell 可用;session / BrowserWindow 属主进程专属,不可用)。\n // 最关心 `electron.net`:官方文档写明 net 模块适用于 Main + Utility 两个进程,\n // 且 utility 的网络请求默认走 Chromium 的 system network context —— 若 net.fetch 可用,\n // 就能把网页端请求从 Node 网络栈换成 **Chromium 网络栈**,从而获得与真实浏览器一致的\n // TLS / HTTP2 指纹(实测 Node fetch 的 JA3/JA4 与 Chrome 是结构性差异:h1 vs h2、无 GREASE 等)。\n try {\n const electron: any = createRequire(import.meta.url)('electron')\n const net = electron?.net\n const keys = electron && typeof electron === 'object' ? Object.keys(electron).sort() : []\n logger.info?.(\n `deepseek-web: [能力探测] process.type=${(process as any).type ?? '-'}` +\n ` electron=${process.versions?.electron ?? '-'}` +\n ` | electron:${typeof electron} keys=[${keys.join(',')}]` +\n ` | net=${typeof net} net.fetch=${typeof net?.fetch} net.request=${typeof net?.request}` +\n ` | shell=${typeof electron?.shell} session=${typeof electron?.session}` +\n ` BrowserWindow=${typeof electron?.BrowserWindow}`,\n )\n } catch (error: any) {\n logger.info?.(`deepseek-web: [能力探测] require('electron') 失败:${error?.message ?? error}`)\n }\n\n // 启动时的一次性 net.fetch 诊断:往 <DSH_HOME>/web-login/probe-request.json 写\n // {\"mode\":\"probe\"} 或 {\"mode\":\"stream\"} 后重启 DSH 即会执行(宿主进程不接受 HTTP 时走这条路)。\n // 平时这个文件不存在 → 零开销;执行完会改名为 *.done-<时间戳>,不删文件。\n const startupProbe = consumeProbeRequest()\n if (startupProbe) {\n void (async () => {\n try {\n const result = await runNetFetchDiagnostics(readAuth(), startupProbe)\n logger.info?.(`deepseek-web: [net-fetch 探测] ${JSON.stringify(result)}`)\n } catch (error: any) {\n logger.info?.(`deepseek-web: [net-fetch 探测] 失败:${error?.message ?? error}`)\n }\n })()\n }\n\n // 节流设置:设置页保存过的值(gate.json)优先于 cordis config —— 设置页是用户的显式操作,\n // 不该被配置文件里的旧值盖回去。闸门在这里创建并共享给适配器,设置页改完即时生效。\n const savedGate = readGateSettings()\n\n // 临时会话清理策略:默认「攒批 + 延迟」,减少「每轮建一个立刻删一个」的机器特征。\n // immediate 用老参数(1.5s / 每次一个);deferred 用可配的延迟与批量阈值。\n // 在闸门之前算:闸门不执行它,但要**存下来**(否则保存设置页时会把它冲掉,见下面那几行)。\n const cleanupMode: SessionCleanupMode =\n savedGate?.sessionCleanup ?? config.sessionCleanup ?? DEFAULT_SESSION_CLEANUP.mode\n // 三个\"区间\"参数:设置页保存过就用保存的,否则用内置默认(均值都落在原来的固定值上)。\n // 只在 deferred 模式生效 —— immediate 是\"老行为\",固定 1.5s / 每次一个,不掺随机。\n const cleanupBatchRange = savedGate?.cleanupBatch ?? DEFAULT_CLEANUP_BATCH\n const cleanupDelayRange = savedGate?.cleanupDelayMs ?? DEFAULT_CLEANUP_DELAY_MS\n const cleanupGapRange = savedGate?.cleanupGapMs ?? DEFAULT_CLEANUP_GAP_MS\n\n const gate = createRequestGate({\n allowConcurrent: savedGate?.allowConcurrent ?? config.allowConcurrent === true,\n // ⚠️ min / max 必须**成对**传:createRequestGate 在只收到 `minIntervalMs` 时,\n // 会把 `maxIntervalMs` 兜底成 min(见 gate.ts),也就是**随机区间退化成固定间隔**。\n // 2026-09-12 实测:设置页保存的 2000~4000,重启 DSH 后就变成固定 2000ms;\n // 而固定间隔恰恰是最典型的机器特征,用户完全不知情(日志里只会显示\"区间 2000~2000\")。\n minIntervalMs: savedGate?.minRequestIntervalMs ?? config.minRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS,\n maxIntervalMs: savedGate?.maxRequestIntervalMs ?? config.maxRequestIntervalMs ?? DEFAULT_MAX_REQUEST_INTERVAL_MS,\n // 长任务保护:连续 N 次请求后强制长休一次(0 = 关闭)。\n longRunThreshold: savedGate?.longRunThreshold ?? DEFAULT_LONG_RUN_THRESHOLD,\n maxPromptChars: savedGate?.maxPromptChars ?? config.maxPromptChars ?? DEFAULT_MAX_PROMPT_CHARS,\n maxRefImages: savedGate?.maxRefImages ?? config.maxRefImages ?? DEFAULT_MAX_REF_IMAGES,\n longRunBreakMs: savedGate?.longRunBreakMs,\n // ⚠️ 会话清理这几个字段必须**一起传**(2026-09-14 修):设置页保存时写的是\n // `gate.settings()` 的返回值 —— 没存进闸门的字段会被**静默抹掉**,\n // 于是用户只调了下请求间隔,清理设置(模式 + 三个区间)就在下次重启时回到内置默认。\n sessionCleanup: cleanupMode,\n cleanupBatch: cleanupBatchRange,\n cleanupDelayMs: cleanupDelayRange,\n cleanupGapMs: cleanupGapRange,\n logger,\n })\n // 旧版(≤0.1.25)只有一份 deepseek-auth.json;首次启动时迁进账号库。\n // 只在「库为空 且 旧文件在」时跑一次;成功迁移后**删掉旧文件**(审计 F21:\n // 留 `.migrated-*` 明文副本会让\"退出清凭证\"变成谎话)。\n const migratedAccount = migrateLegacyAuthIfNeeded()\n if (migratedAccount) {\n logger.info?.(`deepseek-web: 已把旧的单账号凭证迁移进账号库(${migratedAccount.id})`)\n } else {\n // ⚠️ 迁移失败不能静默:旧凭证可能还明文躺在磁盘上、照样能登录,用户有权知道\n const migrationError = legacyMigrationError()\n if (migrationError) logger.warn?.(`deepseek-web: ${migrationError}`)\n }\n\n // 传输层:默认走 Chromium 网络栈(TLS/HTTP2 指纹与真实浏览器一致,见 transport.ts 的模块注释)。\n // 优先级:设置页保存的值 > cordis config > 内置默认;环境拿不到 electron.net.fetch 时降级为 Node。\n let transportState = applyTransport(\n readTransportSetting() ?? (config.transport === 'node' ? 'node' : DEFAULT_TRANSPORT),\n )\n logger.info?.(\n `deepseek-web: 传输层=${transportState.effective}` +\n (transportState.degraded\n ? '(配置要求 Chromium,但本环境没有 electron.net.fetch,已降级为 Node)'\n : ''),\n )\n\n // 上下文投喂方式(2026-09-14):设置页保存的值优先于 cordis config,即时生效无需重启。\n // 默认 full(每轮重发全量)—— 与 0.1.61 及以前的行为完全一致。\n let contextMode = applyContextMode(\n readContextModeSetting() ?? (config.contextMode === 'chained' ? 'chained' : DEFAULT_CONTEXT_MODE),\n )\n logger.info?.(\n `deepseek-web: 上下文投喂=${contextMode}` +\n (contextMode === 'chained' ? '(只发增量 + parent 指向上一条回答)' : '(每轮重发全量 prompt)'),\n )\n\n const adapterConfig: AdapterConfig = {\n maxPromptChars: savedGate?.maxPromptChars ?? config.maxPromptChars ?? DEFAULT_MAX_PROMPT_CHARS,\n maxRefImages: savedGate?.maxRefImages ?? config.maxRefImages ?? DEFAULT_MAX_REF_IMAGES,\n idleTimeoutMs: config.idleTimeoutMs ?? 120_000,\n deleteWebSessions: config.deleteWebSessions !== false,\n // 会话复用:默认 20 轮共用一个网页端会话。0 = 关闭(回到每请求一个会话)\n sessionReuseTurns: config.sessionReuseTurns ?? DEFAULT_SESSION_REUSE_TURNS,\n autoContinue: config.autoContinue !== false,\n maxContinuations: config.maxContinuations ?? 2,\n // 防风控:默认串行 + 每次调用之间至少 3 秒(见 README「配置」)。\n // 取闸门的实际生效值(可能来自设置页保存的 gate.json)。\n allowConcurrent: gate.settings().allowConcurrent,\n minRequestIntervalMs: gate.settings().minRequestIntervalMs,\n // 同上:必须与 min 成对传,否则这里也退化成固定间隔\n maxRequestIntervalMs: gate.settings().maxRequestIntervalMs,\n logger,\n }\n\n // 清理参数在上面(闸门之前)已经算好并传进闸门了,这里直接用。\n const sessionCleaner = createSessionCleaner({\n policy: {\n mode: cleanupMode,\n delayMs: cleanupMode === 'immediate' ? 1_500 : (config.sessionCleanupDelayMs ?? DEFAULT_SESSION_CLEANUP.delayMs),\n batchSize: cleanupMode === 'immediate' ? 1 : (config.sessionCleanupBatchSize ?? DEFAULT_SESSION_CLEANUP.batchSize),\n ...(cleanupMode === 'immediate'\n ? {}\n : { batchRange: cleanupBatchRange, delayRange: cleanupDelayRange, gapRange: cleanupGapRange }),\n },\n logger,\n })\n\n // ── 「欠删除的会话」落盘 + 启动补删(2026-09-14)────────────────────────────\n //\n // 复用槽与待删队列都只活在进程内存里,宿主一退出(尤其被强杀)就静默丢失,\n // 正在复用的那个会话于是**永远留在网页端**。实测:09-12 起启动 58 次 DSH,\n // 网页端侧栏就堆出同等量级、标题 = DSH 会话主题的对话(与 sessions 里的\n // session/title 一一对应)。\n //\n // 做法:webapi 每发生一次「进槽 / 进队列 / 确认删除」就通知这里,\n // 这里把\"还欠一次删除\"的会话按账号落盘;**确认删掉才销账**。\n // 于是强杀、删失败都能在下次启动补删(见 session-journal.ts)。\n const journalEnabled = adapterConfig.deleteWebSessions !== false && cleanupMode !== 'keep'\n /** 会话所属账号 —— 删除必须用它自己的凭证(拿 A 的凭证删 B 的会话会被服务端拒,见 F07)。 */\n const accountIdOfAuth = (auth?: WebAuth): string | undefined => {\n const token = auth?.token\n if (!token) return activeAccountId()\n return listAccounts().find((account) => account.token === token)?.id\n }\n setSessionLifecycleHook((event) => {\n if (!journalEnabled) return\n if (event.kind === 'deleted') {\n removeJournalEntry(event.sessionId)\n return\n }\n const accountId = accountIdOfAuth(event.auth)\n if (!accountId) return\n upsertJournalEntry({\n accountId,\n sessionId: event.sessionId,\n state: event.kind === 'queued' ? 'queued' : 'slot',\n })\n })\n\n // 启动补删:上次退出遗留的(含被强杀的)会话,按账号排进清理器。\n if (journalEnabled) {\n try {\n runStartupSweep({\n ownPid: process.pid,\n accountExists: (id) => readAccount(id) !== undefined,\n deleteEnabled: adapterConfig.deleteWebSessions !== false,\n mode: cleanupMode,\n onSweep: (entry) => {\n const account = readAccount(entry.accountId)\n if (!account) throw new Error('账号已不在账号库里')\n sessionCleaner.schedule(account, entry.sessionId)\n },\n log: (message) => logger.info?.(message),\n })\n } catch (error: any) {\n logger.warn?.(`deepseek-web: 启动补删失败(不影响使用):${error?.message ?? error}`)\n }\n }\n\n const getAuth = (): WebAuth | undefined => readAuth()\n\n // 台账按天滚动清理(保留 LEDGER_KEEP_DAYS 天),启动时做一次就够。\n try {\n const pruned = pruneLedger(LEDGER_KEEP_DAYS)\n if (pruned > 0) logger.info?.(`deepseek-web: 已清理 ${pruned} 个过期台账文件`)\n } catch {}\n\n /**\n * 每次模型调用的结果上报(adapter 的 noteCall 钩子)。做两件事:\n *\n * 1. **把\"账号级限制\"学到账号上**。这个状态只能在生成请求被拒时学到\n * (受限期间 `users/current` 依然 200),所以必须在这里记;成功一次且已过解除时间就清掉。\n * 2. 写本地台账,供设置页看请求密度与失败分类。\n *\n * 全程 try/catch:旁路设施绝不能影响调用本身。\n */\n const recordCallOutcome = (info: {\n purpose: string\n ok: boolean\n ms: number\n code?: string\n message?: string\n mutedUntilMs?: number\n throttled?: boolean\n /** 发起调用那一刻的账号 id(由适配器在起飞前捕获)。 */\n accountId?: string\n }): void => {\n try {\n // 优先用**发起时**捕获的 id;只在拿不到时才回退到\"此刻\"的当前账号。\n const accountId = info.accountId ?? activeAccountId()\n const muted = Number.isFinite(info.mutedUntilMs)\n if (!info.ok && muted && accountId) {\n updateAccount(accountId, {\n limit: { untilMs: Number(info.mutedUntilMs), observedAt: new Date().toISOString() },\n })\n logger.warn?.(\n `deepseek-web: 账号被临时限制,已记录解除时间 ${new Date(Number(info.mutedUntilMs)).toLocaleString()}`,\n )\n }\n // 0.1.61:AUTH 失败(服务端判 token 无效 / HTTP 401·403)立刻回写账号状态 ——\n // 之前 `lastVerifyError` 只由 30 分钟一次的探活写入,刚切到死号时界面完全静默:\n // 用户只看到一条报错,不知道是哪个账号的登录态死了(实测 2026-09-14 切号后的空窗)。\n // 回写之后账号库会立刻出现红标「需要重新登录」。\n if (!info.ok && info.code === 'AUTH' && accountId) {\n // F2(0.2.0):AUTH 不再一票制标记 —— 0.1.80 的请求前拦截只看这个标记,\n // 一次端点级误判就会把健康账号锁死(实测 2026-09-22:94ms 被拒,而同 token\n // 2 秒前刚成功跑完一轮)。先用**同一份凭证**做只读探活复核:\n // 复核也失败 ⇒ 确认失效,照旧标记;\n // 复核通过 ⇒ 不标记(账号保持可用,本轮仍按失败计、用户重发即可),日志留痕;\n // 复核自身失败(网络)⇒ 无法定论,不标记 —— 误标(功能锁死)代价远大于漏标(白跑一轮)。\n const record = listAccounts().find((item) => item.id === accountId)\n if (record) {\n validateAuth(record, AbortSignal.timeout(20_000))\n .then((verdict) => {\n if (verdict.ok) {\n // 复核通过 ⇒ 顺手清掉上一次的失败标记:端点级误判之后,如果更早之前\n // 被标过失效,这次复核证明账号活着,应当解锁(与 probeOnce 成功清标记同一语义)。\n updateAccount(accountId, { lastVerifyError: undefined })\n logger.info?.(\n `deepseek-web: 账号 ${accountId} 请求被判 AUTH,但只读探活通过 —— 按端点级误判处理,不标记失效(本轮仍按失败计,重发即可)`,\n )\n return\n }\n updateAccount(accountId, {\n lastVerifyError: {\n at: new Date().toISOString(),\n message: String(info.message ?? '登录态无效,请重新登录'),\n },\n })\n logger.warn?.(\n `deepseek-web: 账号 ${accountId} AUTH 复核确认失效(${verdict.error ?? '探活未通过'}),已标记为「需要重新登录」`,\n )\n })\n .catch((error) => {\n logger.warn?.(\n `deepseek-web: 账号 ${accountId} AUTH 复核未完成(${String((error as Error)?.message ?? error)}),暂不标记`,\n )\n })\n }\n }\n if (info.ok && accountId) {\n const record = listAccounts().find((item) => item.id === accountId)\n // 限制时间已过 + 这次生成成功 → 确实解除了,清掉标记(不靠猜)\n if (record?.limit && Date.now() >= record.limit.untilMs) {\n updateAccount(accountId, { limit: undefined })\n logger.info?.('deepseek-web: 账号级限制已解除,已清除本地的限制标记')\n }\n }\n writeLedgerEntry({\n at: Date.now(),\n ...(accountId ? { accountId } : {}),\n purpose: info.purpose,\n ok: info.ok,\n ms: info.ms,\n ...(info.code ? { code: info.code } : {}),\n ...(muted ? { muted: true } : {}),\n ...(info.throttled ? { throttled: true } : {}),\n })\n } catch {}\n }\n\n // 登录态主动探活:启动后 20 秒探一次,之后每 probeIntervalMs 一次(0 = 关闭)。\n const probeIntervalMs = config.probeIntervalMs ?? 30 * 60_000\n ctx.effect(() =>\n startProbeLoop({\n intervalMs: probeIntervalMs,\n getAuth: () => readAuth(),\n logger,\n }),\n )\n logger.info?.(\n probeIntervalMs > 0\n ? `deepseek-web: 登录态探活已开启,每 ${Math.round(probeIntervalMs / 60_000)} 分钟一次(只读、零额度)`\n : 'deepseek-web: 登录态探活已关闭(probeIntervalMs=0)',\n )\n // 附件服务(ctx.attachments):图片输入所需。用 ctx.get 取(可选依赖,缺省则图片降级为文本)\n const adapter = createAdapter({\n getAuth,\n noteCall: recordCallOutcome,\n currentAccountId: activeAccountId,\n gate,\n sessionCleaner,\n config: adapterConfig,\n readImage: async (ref: any, signal?: AbortSignal) => {\n const attachments = ctx.get?.('attachments')\n if (!attachments || typeof attachments.readImage !== 'function') {\n throw new Error('attachment service unavailable (ctx.attachments)')\n }\n const stored = await attachments.readImage(ref, signal)\n return {\n data: stored.data,\n ...(stored.ref?.mediaType ? { mediaType: String(stored.ref.mediaType) } : {}),\n ...(stored.ref?.name ? { name: String(stored.ref.name) } : {}),\n }\n },\n })\n\n // 1) LLM 适配器:registerAdapter 内部走 ctx.effect(traceable 代理把副作用绑到本 fiber)\n ctx.llm.registerAdapter([PROVIDER], adapter)\n logger.info?.(`deepseek-web: 已注册 provider \"${PROVIDER}\"(模型:${MODEL_SPECS.map((spec) => spec.id).join(', ')})`)\n\n // 2) host API\n ctx.effect(\n () =>\n ctx.webServer.register({\n kind: 'prefix',\n path: API_PREFIX,\n handler: async (req: any, res: any) => {\n const url = new URL(String(req.url ?? '/'), 'http://127.0.0.1')\n const route = url.pathname.slice(API_PREFIX.length) || '/'\n\n try {\n // 请求节流设置(设置页的开关与滑块)——读写都即时生效,并持久化到 gate.json\n if (req.method === 'GET' && route === '/gate') {\n sendJson(res, 200, {\n ...gate.settings(),\n presets: INTERVAL_PRESETS.map(([lo, hi]) => ({ min: lo, max: hi })),\n maxIntervalMs: MAX_INTERVAL_MS,\n defaultMinIntervalMs: DEFAULT_MIN_REQUEST_INTERVAL_MS,\n defaultMaxIntervalMs: DEFAULT_MAX_REQUEST_INTERVAL_MS,\n maxPromptCharsBounds: MAX_PROMPT_CHARS_BOUNDS,\n maxPromptCharsDefault: DEFAULT_MAX_PROMPT_CHARS,\n maxRefImagesBounds: MAX_REF_IMAGES_BOUNDS,\n maxRefImagesDefault: DEFAULT_MAX_REF_IMAGES,\n cleanup: sessionCleaner.policy(),\n // 界面的滑块边界/默认值由后端给 —— 免得两边各写一套数字、改了一边忘另一边\n cleanupBounds: {\n batch: CLEANUP_BATCH_BOUNDS,\n delayMs: CLEANUP_DELAY_BOUNDS_MS,\n gapMs: CLEANUP_GAP_BOUNDS_MS,\n },\n cleanupDefaults: {\n batch: DEFAULT_CLEANUP_BATCH,\n delayMs: DEFAULT_CLEANUP_DELAY_MS,\n gapMs: DEFAULT_CLEANUP_GAP_MS,\n },\n })\n return\n }\n if (req.method === 'POST' && route === '/gate') {\n const body = await readJsonBody(req)\n if (!body || typeof body !== 'object') {\n sendJson(res, 400, { ok: false, error: '请求体不是合法 JSON' })\n return\n }\n const patch: Partial<GateSettings> = {}\n if (typeof body.allowConcurrent === 'boolean') patch.allowConcurrent = body.allowConcurrent\n for (const field of ['minRequestIntervalMs', 'maxRequestIntervalMs']) {\n if (body[field] === undefined) continue\n const ms = Number(body[field])\n if (!Number.isFinite(ms)) {\n sendJson(res, 400, { ok: false, error: `${field} 必须是数字` })\n return\n }\n patch[field] = ms\n }\n if (body.maxPromptChars !== undefined) {\n const chars = Number(body.maxPromptChars)\n if (!Number.isFinite(chars)) {\n sendJson(res, 400, { ok: false, error: 'maxPromptChars 必须是数字' })\n return\n }\n const clamped = clampMaxPromptChars(chars)\n if (clamped !== chars) {\n // 越界不静默:直接告诉用户被夹到了哪里,否则\"拖到底没反应\"很难查\n logger.warn?.(`deepseek-web: maxPromptChars ${chars} 越界,夹到 ${clamped}`)\n }\n patch.maxPromptChars = clamped\n }\n if (body.sessionCleanup !== undefined) {\n if (!['immediate', 'deferred', 'keep'].includes(body.sessionCleanup)) {\n sendJson(res, 400, { ok: false, error: 'sessionCleanup 只能是 immediate / deferred / keep' })\n return\n }\n patch.sessionCleanup = body.sessionCleanup\n }\n // 会话清理的三个区间(上下限)。非法输入直接报错,不静默吞掉 ——\n // 用户拖了滑块却\"没生效\",是最难查的一类问题。\n for (const [field, bounds] of [\n ['cleanupBatch', CLEANUP_BATCH_BOUNDS],\n ['cleanupDelayMs', CLEANUP_DELAY_BOUNDS_MS],\n ['cleanupGapMs', CLEANUP_GAP_BOUNDS_MS],\n ] as const) {\n if (body[field] === undefined) continue\n const range = normalizeCleanupRange(body[field], bounds)\n if (!range) {\n sendJson(res, 400, { ok: false, error: `${field} 需要 { min, max } 两个数字` })\n return\n }\n patch[field] = range\n }\n // 0.1.82:图片上限也是「界面 → 路由 → 落盘」的一段,此前只接了显示层 ——\n // 前端确实 POST 这个字段,而白名单里没有它 ⇒ patch 为空 ⇒ 直接 400「没有可更新的字段」,\n // 滑块拖完弹回默认值(0.1.77 起一直如此,测试断的是字符串出现与否,守不住接线)。\n if (body.maxRefImages !== undefined) {\n const count = Number(body.maxRefImages)\n if (!Number.isFinite(count)) {\n sendJson(res, 400, { ok: false, error: 'maxRefImages 必须是数字' })\n return\n }\n patch.maxRefImages = clampMaxRefImages(count)\n }\n if (Object.keys(patch).length === 0) {\n sendJson(res, 400, { ok: false, error: '没有可更新的字段' })\n return\n }\n const applied = gate.configure(patch)\n // prompt 上限是 adapter 每次调用现读的 → 改这里即时生效,不必重启\n if (applied.maxPromptChars !== undefined) adapterConfig.maxPromptChars = applied.maxPromptChars\n // 图片上限同样是 adapter 每轮现读的 ⇒ 必须一起同步,否则保存成功但要等重启\n if (applied.maxRefImages !== undefined) adapterConfig.maxRefImages = applied.maxRefImages\n // 清理策略由 cleaner 执行 → 同步生效\n if (patch.sessionCleanup) sessionCleaner.configure({ mode: patch.sessionCleanup })\n // 三个区间即时作用到清理器(它会用新区间重新随机取值)\n const rangePatch: Record<string, { min: number; max: number }> = {}\n if (patch.cleanupBatch) rangePatch.batchRange = patch.cleanupBatch\n if (patch.cleanupDelayMs) rangePatch.delayRange = patch.cleanupDelayMs\n if (patch.cleanupGapMs) rangePatch.gapRange = patch.cleanupGapMs\n if (Object.keys(rangePatch).length > 0) sessionCleaner.configure(rangePatch)\n try {\n writeGateSettings(applied)\n } catch (error: any) {\n // 落盘失败不影响本次生效,但要说清楚(重启后会回到旧值)\n logger.warn?.(`deepseek-web: 节流设置落盘失败:${error?.message ?? error}`)\n sendJson(res, 200, { ok: true, ...applied, persisted: false, warning: '已即时生效,但写入 gate.json 失败,重启后会回到旧值' })\n return\n }\n // ⚠️ `cleanup` 必须一起返回:前端保存后立刻重画界面,读不到就回落成默认的\n // 「延迟」档(GET /status 里是有的,两边形状必须一致)。\n sendJson(res, 200, { ok: true, ...applied, persisted: true, cleanup: sessionCleaner.policy() })\n return\n }\n // 本地调用台账:请求密度 + 失败分类(用来判断节流到底有没有效)\n if (req.method === 'GET' && route === '/ledger') {\n const hours = Math.min(72, Math.max(1, Number(url.searchParams.get('hours')) || 24))\n sendJson(res, 200, summarizeLedger(hours))\n return\n }\n // 检查更新:读自身版本 → 比对 GitHub Releases latest。\n // 走当前传输层;8 秒超时;失败如实返回原因(国内连不上 GitHub 很正常)。\n if (req.method === 'POST' && route === '/update-check') {\n const result = await checkForUpdate(pluginVersion(), currentFetch)\n sendJson(res, 200, { ...result, repo: RELEASE_REPO })\n return\n }\n\n // ── 账号库:多账号并存 + 一键切换 ─────────────────────────────\n // ⚠️ 返回给界面的**只有元信息**(掩码账号、时间、限制状态等),**永不回传凭证**:\n // 设置页跑在渲染层,把它能触及的敏感面收窄没有坏处。\n if (req.method === 'GET' && route === '/accounts') {\n const activeId = activeAccountId()\n const list = listAccounts()\n const groups = readGroups()\n // ⚠️ 先把记录加工成「可直接渲染的视图」,**再**拿去分区。\n // partitionByGroup 只按 id / groupId 归组,但它返回的 accounts 会**原样**\n // 交给客户端渲染 —— 喂原始记录的话,title / display / isActive 这些\n // 「响应加工字段」就不在数组里:标题退化成 acc_xxxxxxx、显示名与「当前」\n // 徽章一起消失(0.1.71 的真实回归,由端到端用例 + 产物断言守)。\n const accounts = list.map((record) => ({\n id: record.id,\n title: accountTitle(record, maskIdentifier),\n display: record.user?.display ? maskIdentifier(record.user.display) : '',\n label: record.label ?? '',\n groupId: record.groupId ?? '',\n unverified: record.unverified === true,\n capturedAt: record.capturedAt,\n lastVerifiedAt: record.lastVerifiedAt ?? null,\n lastVerifyError: record.lastVerifyError ?? null,\n limit: record.limit ?? null,\n // cookie 的过期构成(捕获时记下)。老记录 / 手动粘 token 的账号是空数组,\n // 界面据此区分\"没记录\"和\"记录到全是会话级\"—— 这两种含义完全不同。\n cookieMeta: record.cookieMeta ?? [],\n isActive: record.id === activeId,\n }))\n sendJson(res, 200, {\n activeId: activeId ?? null,\n // 分组定义单独存 groups.json;账号记录里只有 groupId 指针。\n // ⚠️ 客户端的重建签名按 `{activeId, accounts, groups}` 整包算 ——\n // 顶层字段不列进签名,新建/改名组后面板就不会自己刷新(0.1.63/0.1.67 那类坑)。\n groups,\n // 「按组分区」在宿主侧算好:客户端只负责画。\n // 这样分区规则(当前账号所在组置顶 → 其余按 order → 未分组垫底)只有一份实现,\n // 也就只有一处要测 —— 放进客户端会因为 node 依赖而不得不复制一份。\n sections: partitionByGroup(accounts, groups, activeId),\n // 顶层 `accounts` 必须继续返回:客户端有\"没有 sections 就平铺\"的兜底路径,\n // 而且重建签名是 `{activeId, accounts, groups, sections}` 整包算的。\n accounts,\n footprint: accountsFootprint(),\n })\n return\n }\n if (req.method === 'POST' && route === '/accounts/switch') {\n const body = await readJsonBody(req)\n const id = String(body?.id ?? '')\n endAddAccount()\n endRelogin()\n // 0.1.61:切号前先对目标账号做一次零额度探活(只读 users/current)。\n // 死号当场拦下 —— 否则用户切过去、发消息、看到 AUTH 才知道,白折腾一轮\n // (实测 2026-09-14:切到一个一天多没用过的号,凭证早已过期)。\n const target = id ? readAccount(id) : undefined\n if (!target) {\n sendJson(res, 404, { ok: false, error: '账号不存在(可能已被移除)' })\n return\n }\n const probed = await probeOnce(target, {\n info: (message) => logger.info?.(message),\n warn: (message) => logger.warn?.(message),\n })\n if (probed && !probed.ok) {\n sendJson(res, 200, {\n ok: false,\n needsRelogin: true,\n error: `该账号登录态校验未通过(${probed.error ?? '未知原因'}),请重新登录后再切换`,\n })\n return\n }\n if (!setActiveAccount(id)) {\n sendJson(res, 404, { ok: false, error: '账号不存在(可能已被移除)' })\n return\n }\n logger.info?.(`deepseek-web: 当前账号已切换为 ${id}`)\n sendJson(res, 200, { ok: true, activeId: id })\n return\n }\n if (req.method === 'POST' && route === '/accounts/rename') {\n const body = await readJsonBody(req)\n const id = String(body?.id ?? '')\n const label = String(body?.label ?? '').slice(0, 40)\n if (!updateAccount(id, { label })) {\n sendJson(res, 404, { ok: false, error: '账号不存在' })\n return\n }\n sendJson(res, 200, { ok: true, id, label })\n return\n }\n if (req.method === 'POST' && route === '/accounts/remove') {\n const body = await readJsonBody(req)\n const id = String(body?.id ?? '')\n if (!removeAccount(id)) {\n sendJson(res, 404, { ok: false, error: '账号不存在' })\n return\n }\n logger.info?.(`deepseek-web: 已从账号库移除 ${id}`)\n sendJson(res, 200, { ok: true, removed: id, activeId: activeAccountId() ?? null })\n return\n }\n // ── 分组:组定义存 groups.json,账号记录里只留 groupId 指针 ──\n // 组**只影响显示**(分区 / 折叠),不参与切号、会话复用与清理 —— 让调度逻辑保持可预期。\n if (req.method === 'POST' && route === '/accounts/group/create') {\n const body = await readJsonBody(req)\n const result = createGroup(readGroups(), body?.name)\n if (result.error || !result.group) {\n sendJson(res, 400, { ok: false, error: result.error ?? '创建失败' })\n return\n }\n writeGroups(result.list)\n sendJson(res, 200, { ok: true, group: result.group, groups: result.list })\n return\n }\n if (req.method === 'POST' && route === '/accounts/group/rename') {\n const body = await readJsonBody(req)\n const result = renameGroup(readGroups(), String(body?.id ?? ''), body?.name)\n if (result.error) {\n sendJson(res, 400, { ok: false, error: result.error })\n return\n }\n writeGroups(result.list)\n sendJson(res, 200, { ok: true, groups: result.list })\n return\n }\n if (req.method === 'POST' && route === '/accounts/group/delete') {\n const body = await readJsonBody(req)\n const id = String(body?.id ?? '')\n const next = removeGroup(readGroups(), id)\n writeGroups(next)\n // 组内账号的 groupId 会变成悬挂指针 ⇒ 界面上按「未分组」显示。\n // 所以这里**故意不去改账号文件**:删一个组不该逐个重写凭证文件。\n logger.info?.(`deepseek-web: 已删除分组 ${id}(组内账号回到「未分组」,账号本身未动)`)\n sendJson(res, 200, { ok: true, groups: next })\n return\n }\n if (req.method === 'POST' && route === '/accounts/group/assign') {\n const body = await readJsonBody(req)\n const id = String(body?.id ?? '')\n const groupId = String(body?.groupId ?? '')\n // 只接受已知组 id(空串 = 移出组);传了不存在的组就当场拒绝,\n // 免得界面看起来\"归组成功\"而列表里并没有它\n if (groupId && !readGroups().some((group) => group.id === groupId)) {\n sendJson(res, 400, { ok: false, error: '分组不存在' })\n return\n }\n if (!updateAccount(id, { groupId })) {\n sendJson(res, 404, { ok: false, error: '账号不存在' })\n return\n }\n sendJson(res, 200, { ok: true, id, groupId })\n return\n }\n /**\n * 手动刷新账号状态:对库里每个账号做一次**只读探活**(`users/current`,零额度),\n * 顺带把补上的显示名、清掉的失败标记写回记录(那是 probeOnce 自己干的)。\n *\n * 为什么需要它:自动探活 30 分钟才一次,而\"我刚在浏览器里动过这个号,它现在到底还行不行\"\n * 是随时会冒出来的问题 —— 以前只能等,或者切过去试(那要发一次生成请求,烧额度)。\n * 串行 + 互斥:单次探活虽轻,一口气并发 7 个也会像脚本;狂点按钮更不该叠起来打。\n */\n if (req.method === 'POST' && route === '/accounts/refresh') {\n if (accountsRefreshInFlight) {\n sendJson(res, 200, { ok: false, error: '正在刷新,请稍候' })\n return\n }\n accountsRefreshInFlight = true\n try {\n let passed = 0\n let failed = 0\n for (const account of listAccounts()) {\n const outcome = await probeOnce(account, {\n info: (message) => logger.info?.(message),\n warn: (message) => logger.warn?.(message),\n })\n if (outcome?.ok) passed += 1\n else if (outcome) failed += 1\n }\n logger.info?.(`deepseek-web: 手动刷新账号状态完成 —— 通过 ${passed}、失败 ${failed}`)\n sendJson(res, 200, { ok: true, checked: passed + failed, passed, failed })\n } finally {\n accountsRefreshInFlight = false\n }\n return\n }\n // 回退路径:宿主自己写到插件目录,只回传**路径**(明文 token 不进 HTTP)。\n if (req.method === 'POST' && route === '/accounts/export') {\n try {\n const result = exportAccountsToFile()\n sendJson(res, 200, { ok: true, ...result, warning: '导出文件含可完整登录的凭证,请妥善保管、勿分享' })\n } catch (error: any) {\n sendJson(res, 500, { ok: false, error: `导出失败:${error?.message ?? error}` })\n }\n return\n }\n // 首选路径:界面弹系统「另存为」让用户自己选位置与文件名,然后把内容写进去。\n //\n // ⚠️ 这一条会把**明文凭证**交给渲染进程(本机回环 + DSH 同源守卫)。\n // 为什么躲不开:要\"让用户选保存位置\",只有渲染进程能弹系统对话框;\n // 而写盘必须由拿到那份数据的一方做。反过来\"宿主只收一个路径\"做不到 ——\n // File System Access 只给 FileSystemFileHandle、不暴露路径,宿主无从代写。\n // 权衡后接受:能读到这个响应的,本来就以本机同用户进程为主,\n // 而插件宿主自身有完整 fs 权限、直接读账号文件更省事,边际风险接近于零。\n // 不接受的场景也有出口:/accounts/export(上面那条)始终保留,凭证不出宿主。\n if (req.method === 'POST' && route === '/accounts/export-json') {\n try {\n sendJson(res, 200, { ok: true, ...exportAccounts() })\n } catch (error: any) {\n sendJson(res, 500, { ok: false, error: `导出失败:${error?.message ?? error}` })\n }\n return\n }\n if (req.method === 'POST' && route === '/accounts/import') {\n const body = await readJsonBody(req, IMPORT_FILE_LIMIT_BYTES)\n if (!body || typeof body !== 'object') {\n sendJson(res, 400, { ok: false, error: '请提供备份内容(payload)或文件路径' })\n return\n }\n let payload: unknown = (body as any).payload\n const path = typeof (body as any).path === 'string' ? (body as any).path.trim() : ''\n if (payload === undefined && path) {\n // 路径导入:**由宿主自己读**,明文凭证不进 HTTP(见 client 侧 readImportSource 的注释)。\n // ⚠️ 但路径来自渲染进程,不能因为\"前端有文件选择框\"就当它可信(审计 F16):\n // 只接受**普通文件**(挡掉目录 / FIFO / 设备这类会阻塞或异常的东西),并限定大小。\n // 更进一步的做法是\"宿主批准的一次性句柄/令牌\",需要新的宿主 API,本版没做。\n try {\n const info = statSync(path)\n if (!info.isFile()) throw new Error('不是普通文件')\n if (info.size > IMPORT_FILE_LIMIT_BYTES) {\n throw new Error(`文件 ${Math.ceil(info.size / 1024)} KiB,超过上限 ${Math.floor(IMPORT_FILE_LIMIT_BYTES / 1024 / 1024)} MiB`)\n }\n payload = JSON.parse(readFileSync(path, 'utf8'))\n } catch (error: any) {\n sendJson(res, 400, { ok: false, error: `读取导入文件失败:${error?.message ?? error}` })\n return\n }\n }\n if (payload === undefined || payload === null) {\n sendJson(res, 400, { ok: false, error: '请提供要导入的内容或文件路径' })\n return\n }\n try {\n const result = importAccounts(payload)\n logger.info?.(`deepseek-web: 账号库导入完成(新增 ${result.imported} / 更新 ${result.updated} / 跳过 ${result.skipped})`)\n sendJson(res, 200, { ok: true, ...result, activeId: activeAccountId() ?? null })\n } catch (error: any) {\n // 内容不合格(形状/数量/类型)算客户端错误,别报 500 让人以为是插件坏了\n if (error instanceof TypeError || error instanceof RangeError) {\n sendJson(res, 400, { ok: false, error: error?.message ?? String(error) })\n return\n }\n throw error\n }\n return\n }\n\n // 传输层:网页端请求从哪个网络栈出去(Chromium / Node)。见 transport.ts。\n if (req.method === 'GET' && route === '/transport') {\n sendJson(res, 200, { ...transportState, hint: TRANSPORT_HINT, settingsPath: transportSettingsPath() })\n return\n }\n if (req.method === 'POST' && route === '/transport') {\n const body = await readJsonBody(req)\n const wanted = body?.transport\n if (wanted !== 'chromium' && wanted !== 'node') {\n sendJson(res, 400, { ok: false, error: \"transport 必须是 'chromium' 或 'node'\" })\n return\n }\n // 先即时生效(无需重启),再落盘;落盘失败如实回报,不假装成功\n transportState = applyTransport(wanted)\n let persisted = true\n try {\n writeTransportSetting(wanted)\n } catch {\n persisted = false\n }\n logger.info?.(\n `deepseek-web: 传输层切换为 ${transportState.effective}` +\n (transportState.degraded ? '(要求 Chromium 但本环境不可用,已降级 Node)' : ''),\n )\n sendJson(res, 200, {\n ok: true,\n ...transportState,\n persisted,\n hint: TRANSPORT_HINT,\n settingsPath: transportSettingsPath(),\n })\n return\n }\n\n // 上下文投喂方式:每轮重发全量 prompt,还是只发增量 + parent 链(见 context-feed.ts)。\n if (req.method === 'GET' && route === '/context-mode') {\n sendJson(res, 200, {\n mode: contextMode,\n hint: CONTEXT_MODE_HINT,\n settingsPath: contextModeSettingsPath(),\n // 只在链式模式下回链状态(0.1.63):全量模式下链已经作废,\n // 回它会让界面出现「每轮全量 + 链式投喂正在跑」这种自相矛盾的组合。\n chain: contextMode === 'chained' ? (contextChainInfo() ?? null) : null,\n })\n return\n }\n if (req.method === 'POST' && route === '/context-mode') {\n const body = await readJsonBody(req)\n const wanted = body?.mode\n if (wanted !== 'full' && wanted !== 'chained') {\n sendJson(res, 400, { ok: false, error: \"mode 必须是 'full' 或 'chained'\" })\n return\n }\n // 同 /transport:先即时生效(无需重启),再落盘;落盘失败如实回报,不假装成功\n contextMode = applyContextMode(wanted)\n // 切到全量 ⇒ 链立刻作废(0.1.63):全量下每轮都是根消息,链的父消息早就不是最新的了,\n // 留着它既会让界面自相矛盾,也会在以后切回链式时从一个过期节点续链、让上下文错位。\n if (contextMode === 'full') resetContextChain()\n let persisted = true\n try {\n writeContextModeSetting(wanted)\n } catch {\n persisted = false\n }\n logger.info?.(`deepseek-web: 上下文投喂切换为 ${contextMode}`)\n sendJson(res, 200, {\n ok: true,\n mode: contextMode,\n persisted,\n hint: CONTEXT_MODE_HINT,\n settingsPath: contextModeSettingsPath(),\n chain: contextMode === 'chained' ? (contextChainInfo() ?? null) : null,\n })\n return\n }\n\n // 诊断:用 Electron 的 net.fetch(Chromium 网络栈)对比指纹与连通性。\n // 实现与取舍见 src/net-diagnostics.ts 的模块注释。\n if (req.method === 'POST' && route === '/diagnostics/net-fetch') {\n const body = await readJsonBody(req)\n const mode: NetFetchMode = body?.mode === 'stream' ? 'stream' : 'probe'\n const result = await runNetFetchDiagnostics(getAuth(), mode)\n sendJson(res, result.ok ? 200 : 500, result)\n return\n }\n if (req.method === 'GET' && (route === '/status' || route === '/')) {\n const light = url.searchParams.get('light') === '1'\n const auth = getAuth()\n const summary = describeAuth(auth)\n let validation: { ok: boolean; error?: string } | undefined\n if (summary.loggedIn && !light) {\n const check = await validateAuth(auth as WebAuth, AbortSignal.timeout(15_000))\n validation = { ok: check.ok, ...(check.error ? { error: check.error } : {}) }\n if (check.ok && check.user && auth && (!auth.user || auth.user.display !== check.user.display)) {\n // 补全**当前账号**的展示信息。\n // ⚠️ 这里刻意不走 commitCapturedAuth:它不是\"新捕获\",而是对当前账号的\n // 元数据刷新。若让它消费掉添加模式,用户点了「登录新账号」后一刷新面板,\n // 添加模式就没了 —— 那会是个很难查的 bug。\n //\n // ⚠️ 也不能走 writeAuth(审计 N02):它的语义是「写入并**设为当前账号**」。\n // `/status` 的校验请求是异步的,等待期间用户可能已经切到别的账号、甚至把\n // 这个账号删掉了 —— 迟到的结果一旦走 writeAuth,就会把账号**切回去**、\n // 或者把已删除的凭证**复活**。刷新元信息不该有这两个副作用。\n // 所以只更新「仍存在、且 token 匹配」的那条记录。\n const target = listAccounts().find((item) => item.token === auth.token)\n if (target) refreshVerifiedIdentity(target.id, target.token, check.user)\n }\n }\n let registeredProviders: string[] = []\n try {\n registeredProviders = (ctx.llm.listProviders() ?? []).map((provider: any) => String(provider?.id ?? provider))\n } catch {}\n sendJson(res, 200, {\n provider: PROVIDER,\n registeredProviders,\n electron: canOpenElectronWindow(),\n loginWindowOpen: isLoginWindowOpen(),\n loginProgress: getLoginProgress(),\n fingerprint: getFingerprintReport(),\n lastLoginResult: getLastLoginResult(),\n // 登录能力自检:宿主进程类型 + 能否开 Electron 窗口 + 有没有真实浏览器可用。\n // 这三项是「窗口登录打不开」这类问题的第一现场证据(2026-09-11 就栽在这里)。\n loginCapability: {\n processType: (process as any).type ?? 'node',\n canOpenWindow: canOpenElectronWindow(),\n browser: findSystemBrowser()?.name ?? null,\n },\n // 账号元信息(限制解除时间 / 探活结果)跟着 auth 一起给界面。\n // 注意 limit 目前仍只从\"生成被拒\"里学到 —— 虽然 users/current 的响应体里\n // 也带着 chat.mute_until(2026-09-12 实测),但还没接上,见 accounts.ts 的说明。\n // 本地状态位置(「关于」页展示)\n paths: {\n webLogin: webLoginDir(),\n accounts: accountsDir(),\n ledger: ledgerDir(),\n },\n auth: {\n ...summary,\n limitUntilMs: Number.isFinite((auth as any)?.limit?.untilMs) ? (auth as any).limit.untilMs : null,\n limitObservedAt: (auth as any)?.limit?.observedAt ?? null,\n lastVerifiedAt: (auth as any)?.lastVerifiedAt ?? null,\n lastVerifyError: (auth as any)?.lastVerifyError ?? null,\n },\n validation,\n models: MODEL_SPECS.map((spec) => ({\n id: spec.id,\n name: spec.name,\n description: spec.description,\n modelType: spec.modelType,\n thinking: spec.thinking,\n contextWindow: spec.contextWindow,\n })),\n config: {\n maxPromptChars: adapterConfig.maxPromptChars,\n idleTimeoutMs: adapterConfig.idleTimeoutMs,\n deleteWebSessions: adapterConfig.deleteWebSessions !== false,\n allowConcurrent: adapterConfig.allowConcurrent === true,\n minRequestIntervalMs: adapterConfig.minRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS,\n maxRequestIntervalMs: adapterConfig.maxRequestIntervalMs ?? DEFAULT_MAX_REQUEST_INTERVAL_MS,\n sessionCleanup: cleanupMode,\n sessionCleanupPending: sessionCleaner.pendingCount(),\n transport: transportState.effective,\n contextMode,\n contextChain: contextChainInfo() ?? null,\n version: pluginVersion(),\n probeIntervalMs,\n },\n })\n return\n }\n\n // 「登录新账号(添加)」:往账号库里**再加一个号**,但不顶掉当前正在用的。\n // 为什么必须先清\"登录态存放处\":登录窗口/独立浏览器 profile 里还留着当前账号\n // 的会话,不清的话新窗口一打开就是旧账号,抓回来还是它(等于没加)。\n // 注意这里**不动账号库里的任何账号** —— 与 /logout 的区别就在这。\n if (req.method === 'POST' && route === '/login/add') {\n // ⚠️ 必须同时清掉「重登意图」:它在 commitCapturedAuth 里**优先于**添加模式,\n // 残留期 60 分钟 ⇒ 这段时间内登另一个号会被写进上次那条旧记录(0.1.82 修)。\n endRelogin()\n beginAddAccount()\n const { profileCleared, partitionCleared } = await clearLoginState()\n logger.info?.(\n `deepseek-web: 准备添加新账号(profile=${profileCleared} partition=${partitionCleared})—— 接下来捕获到的凭证只入库、不切换`,\n )\n sendJson(res, 200, {\n ok: true,\n profileCleared,\n partitionCleared,\n hint: '登录窗口里登录另一个账号;它会加入账号库,但不会自动切换',\n })\n return\n }\n\n // 「重新登录这个账号」:给**凭证失效**(探活失败)的账号用。\n //\n // 与 /login/add 的唯一区别:**不清浏览器登录态**。\n // 为什么:add 的目的是\"加一个**别的**号\",所以要先把登录态清干净,否则新窗口\n // 一打开就是旧账号、抓回来还是它;而这里是\"**修好同一个**号\" —— 正是要复用\n // 浏览器里可能还在的登录态(还在的话一打开就能捕获,用户一个密码都不用敲)。\n // 真掉了也没关系:窗口里重新登录一次即可,那条路和 add 完全相同。\n //\n // 落库仍然走「添加模式」(commitCapturedAuth → 只入库、不切换):\n // 修好它,但不顶掉你正在用的账号。若它本来就是当前账号,当前账号不会变、\n // 只是凭证被换成新的 —— 这正是期望行为。\n if (req.method === 'POST' && route === '/login/relogin') {\n // 必须记住**要更新哪一条记录**:旧实现只调 beginAddAccount()、把传进来的 id 扔了,\n // 而 beginAddAccount 只保证\"别切换当前账号\",不保证更新同一条 —— 于是重登成功后\n // 库里新增一条同名记录、旧那条还挂着「需要重新登录」(用户实测,0.1.65 修)。\n const body = await readJsonBody(req)\n const id = String(body?.id ?? '')\n const target = id ? readAccount(id) : undefined\n if (!target) {\n sendJson(res, 404, { ok: false, error: '账号不存在(可能已被移除),请刷新后重试' })\n return\n }\n // 0.1.75:账号**已经被标记失效**时,复用浏览器登录态是无解的 ——\n // 那份登录态正是让它失效的那一份,复用只会把同一个坏 token 再抓一遍\n // (2026-09-17 实测:连点两次重登,两次都在 1 秒内\"已捕获 token\"、又在 5 秒内\n // 校验失败 invalid token;用户看到的是一个点不出来的死循环)。\n // 这种情况直接按\"全新登录\"走:清掉 profile + 登录分区,让用户在浏览器里真正登一次。\n // 账号健康时的复用行为**完全不变** —— 那才是\"一个密码都不用敲\"的便利所在。\n const stale = !!target.lastVerifyError\n if (stale) {\n const cleared = await clearLoginState()\n logger.info?.(\n `deepseek-web: 账号「${target.label || target.id}」已被标记失效,重登不再复用登录态,` +\n `先清掉(profile=${cleared.profileCleared} partition=${cleared.partitionCleared})`,\n )\n }\n beginRelogin(id)\n logger.info?.(\n `deepseek-web: 准备重新登录「${target.label || target.id}」` +\n `(${stale ? '登录态已失效,本次不复用' : '不清理浏览器登录态,能复用就直接复用'})` +\n '—— 捕获后原地更新这条记录,不新增、也不切换当前账号',\n )\n sendJson(res, 200, {\n ok: true,\n targetId: id,\n keptBrowserSession: !stale,\n hint: stale\n ? '这条账号已被标记失效,浏览器里剩下的登录态也已经不可用 —— 已帮你清掉,请在打开的窗口里重新登录一次'\n : '登录窗口会打开:如果浏览器里还留着这个账号的登录态会立刻复用,否则在里面重新登录一次',\n })\n return\n }\n\n if (req.method === 'POST' && route === '/login/browser') {\n // 主按钮(「用 Microsoft Edge 登录」)带 `fresh: true` 过来:**登录前先清登录态**。\n // 它此前是唯一**没有任何前置清理**的登录入口 —— 独立 profile 里若还留着上次那个\n // 账号,窗口一打开就是已登录,用户以为在登录、抓回来的却是旧号。\n // (「重登」那条路有意不清,所以这里做成按需,而不是路由的默认行为。)\n const freshBody = await readJsonBody(req).catch(() => undefined)\n if (freshBody?.fresh === true) {\n const { profileCleared, partitionCleared } = await clearLoginState()\n logger.info?.(\n `deepseek-web: 登录前清理登录态(profile=${profileCleared} partition=${partitionCleared})`,\n )\n }\n // 两条路:\n // 1) 宿主在主进程(旧架构)→ 插件自己开 Electron 窗口(带指纹伪装,见 login.ts)\n // 2) 宿主在 utility 进程(2026-09-11 起的架构)→ 没有窗口 API,\n // 改为拉起**真实 Edge/Chrome**(独立 profile + CDP)读取登录态\n if (canOpenElectronWindow()) {\n const result = await openLoginWindow(logger)\n sendJson(res, 200, { ...result, mode: 'window' })\n return\n }\n const outcome = await browserLogin({\n onProgress: (message) => logger?.info?.(`deepseek-web login(browser): ${message}`),\n signal: undefined,\n })\n if (outcome.ok && outcome.auth) {\n // 0.1.82:**先校验拿到身份,再落库**。此前是先 commit 再 validate ——\n // 而捕获出来的凭证不带身份(unverified),只能按 token 去重、而重登必然换 token\n // ⇒ 每次都新增一条账号记录,`serverId` 这条去重键也永远补不上。\n const check = await validateAuth(outcome.auth).catch(() => undefined)\n const verified = !!check?.ok\n // 校验通过才敢把 unverified 摘掉(没通过时 withVerifiedIdentity 会误清该标记)\n const commitAuth = verified ? withVerifiedIdentity(outcome.auth, check?.user) : outcome.auth\n // 添加模式下只入库(见 account-add.ts);默认仍是\"写入并设为当前\"\n const commit = commitCapturedAuth(commitAuth)\n // 把这个账号的身份写回记录。\n // 为什么必须在这里补:捕获本身只拿到 token/cookie,**不含账号名**;\n // 不补的话列表只能显示内部 id(`acc_xxxxxxxx`),要等下一次探活(最长 30 分钟)\n // 才有名字 —— 实测用户加完账号一刷新就看到了那串 hex,会以为是 bug。\n // 身份来自刚才这次零额度的只读校验,顺手就拿到了。\n if (verified && check?.user && commit.recordId) {\n const record = listAccounts().find((item) => item.id === commit.recordId)\n const verifiedId = typeof (check.user as { id?: unknown }).id === 'string' ? String((check.user as { id?: unknown }).id) : ''\n updateAccount(commit.recordId, {\n user: { ...(record?.user ?? {}), ...check.user },\n // ⚠️ `serverId` 是**去重键**:只回写 user 而不写它,下次同一个号还会被当成新账号\n ...(verifiedId ? { serverId: verifiedId } : {}),\n lastVerifiedAt: new Date().toISOString(),\n lastVerifyError: undefined,\n } as any)\n }\n sendJson(res, 200, {\n started: true,\n mode: 'browser',\n added: commit.mode === 'add',\n relogin: commit.mode === 'relogin',\n created: commit.created === true,\n activeId: activeAccountId() ?? null,\n ok: true,\n verified,\n message: verified\n ? `${outcome.message},服务端校验通过`\n : `${outcome.message};服务端校验未通过(${check?.error ?? '未知原因'})——可用「发送测试」再确认`,\n display: check?.user?.display ? maskIdentifier(check.user.display) : undefined,\n })\n return\n }\n logger?.warn?.(`deepseek-web api /login/browser(browser) failed: ${outcome.reason} ${outcome.message}`)\n sendJson(res, 200, {\n started: false,\n mode: 'browser',\n ok: false,\n reason: outcome.reason ?? 'unknown',\n browserLeftOpen: !!outcome.browserLeftOpen,\n message: outcome.message,\n })\n return\n }\n\n if (req.method === 'POST' && route === '/login/external') {\n // 兜底:网页端连干净指纹的 Electron 窗口也拦时,用系统默认浏览器打开\n const result = await openExternalLogin()\n sendJson(res, 200, result)\n return\n }\n\n if (req.method === 'POST' && route === '/login/token') {\n const body = await readJsonBody(req)\n if (!body || typeof body.token !== 'string') {\n sendJson(res, 400, { ok: false, error: '请求体需要 { token: string, cookie?: string }' })\n return\n }\n const result = await loginWithToken(body.token, typeof body.cookie === 'string' ? body.cookie : undefined, logger)\n sendJson(res, 200, result)\n return\n }\n\n // 从已登录的持久化分区恢复凭证(免重新登录;凭证丢失/未落盘时的救急通道)\n if (req.method === 'POST' && route === '/login/recover') {\n const result = await captureFromPartition(logger)\n sendJson(res, 200, result)\n return\n }\n\n if (req.method === 'POST' && route === '/logout') {\n // 退出/换号是明确的\"改当前账号\"动作 —— 顺手清掉添加模式,\n // 免得它一直挂着、影响后面某次无关的捕获。\n endAddAccount()\n endRelogin()\n // await:面板会在退出后立刻打开登录窗口(换号),必须等分区清理完成\n const cleared = await logout()\n sendJson(res, 200, { ok: true, partitionCleared: cleared })\n return\n }\n\n if (req.method === 'POST' && route === '/test') {\n const body = await readJsonBody(req)\n const model = typeof body?.model === 'string' ? body.model : 'deepseek-chat'\n const prompt = typeof body?.prompt === 'string' && body.prompt.trim() ? body.prompt : '请用一句话确认你已连通。'\n const started = Date.now()\n const text: string[] = []\n const reasoning: string[] = []\n const toolCalls: string[] = []\n let finish: any\n try {\n const options = {\n provider: PROVIDER,\n model,\n system: '你是连通性测试探针,回答保持简短。',\n messages: [\n {\n id: 'dsw-test-1',\n role: 'user',\n content: [{ type: 'text', text: prompt }],\n source: { kind: 'user' },\n },\n ],\n signal: AbortSignal.timeout(90_000),\n }\n for await (const chunk of adapter.stream(options)) {\n if (chunk?.type === 'text-delta') text.push(chunk.text)\n else if (chunk?.type === 'reasoning-delta') reasoning.push(chunk.text)\n else if (chunk?.type === 'tool-call-delta') toolCalls.push(`${chunk.name ?? '?'}(${chunk.argumentsDelta ?? ''})`)\n else if (chunk?.type === 'finish') finish = chunk.reason\n }\n } catch (error: any) {\n sendJson(res, 200, {\n ok: false,\n ms: Date.now() - started,\n error: error?.message ?? String(error),\n code: error?.code ?? error?.failure?.code,\n })\n return\n }\n sendJson(res, 200, {\n ok: finish?.kind !== 'error',\n ms: Date.now() - started,\n model,\n text: text.join(''),\n ...(reasoning.length > 0 ? { reasoning: reasoning.join('').slice(0, 800) } : {}),\n ...(toolCalls.length > 0 ? { toolCalls } : {}),\n finish,\n })\n return\n }\n\n if (req.method === 'POST' && route === '/models') {\n sendJson(res, 200, { models: await adapter.listModels(PROVIDER) })\n return\n }\n\n sendJson(res, 404, { error: `unknown route ${route}` })\n } catch (error: any) {\n if (error instanceof BodyError) {\n // 请求体问题不是\"插件坏了\":给出结构化状态码,并关掉 keep-alive\n // —— 否则超大的请求会继续占着这条连接(审计 F16)。\n logger.warn?.(`deepseek-web api ${route} 请求体被拒:${error.message}`)\n try {\n res.shouldKeepAlive = false\n } catch {}\n if (!res.destroyed && !res.headersSent) sendJson(res, error.status, { ok: false, error: error.message })\n return\n }\n logger.warn?.(`deepseek-web api ${route} failed: ${error?.message ?? error}`)\n sendJson(res, 500, { error: error?.message ?? String(error) })\n }\n },\n }),\n 'dsh-deepseek-web-login: api',\n )\n\n // 3) 卸载即净:关登录窗口与定时器;**退出时把欠删的会话交出去**。不清理凭证 ——\n // 卸载/热重载插件不等于登出。\n ctx.effect(() => () => {\n try {\n if (isLoginWindowOpen()) closeLoginWindow()\n } catch {}\n // 2026-09-14:退出收尾。顺序很重要 ——\n // ① `disposeSessionReuse()` 把复用槽里的会话**交回它自己的清理回调**(排队待删)。\n // 只清槽不排队 = 每次退出白丢一个会话(实测就是这么堆起来的)。\n // ② `flush()` 尽力把队列里的删掉(同步返回,不等网络完成 —— 宿主的卸载流程\n // 不会为一个网络请求停留)。\n // 于是删不掉的仍会留在 `session-journal` 的记录里(记录只在**确认删掉**时摘除),\n // 下次启动的补删会接上。被强杀时同理。\n try {\n disposeSessionReuse()\n } catch {}\n try {\n void sessionCleaner.flush()\n } catch {}\n }, 'dsh-deepseek-web-login: teardown')\n}\n"],"mappings":"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"}
|