中文 · English
<!-- badges -->
把 chat.deepseek.com 网页版接进 DSH:用浏览器登录态(不是 API Key)驱动 agent 的工具调用、思考流与图片理解。
这是什么
DSH(DeepSeek Harness)通过 ctx.llm 的 provider 适配器接入模型。本项目实现了 deepseek-web
这个 provider —— 它不调用官方 API,而是复用你已登录的网页版:PoW 挑战、会话、SSE 流式、
文件上传,全部走网页端私有接口。
于是:在模型选择器里选 DeepSeek 网页 · 快速模式,就能用网页版免费额度跑 DSH 的 agent。
DSH agent loop ──▶ ctx.llm ──▶ [deepseek-web 适配器] ──▶ chat.deepseek.com
│ ├─ PoW(SHA3 WASM,**在浏览器页面里**求解)
│ ├─ chat_session(**按 DSH 会话复用,跨重启续上**)
│ ├─ chat/completion(SSE patch 流,链式只发增量)
│ └─ file/upload_file(图片输入)
└─ 提示词工具协议 ⇄ tool-call 块
> ⚠️ 0.7.0 起:必须能启动 Edge / Chrome。
> PoW 挑战改为在真实页面上下文里求解(原因见下方「为什么这样更不容易被限流」)。
> 找不到浏览器时会明确报错,不会退回 Node 侧求解 —— 这是刻意的:
> Node 侧解 PoW 是被公开列为的封号触发条件,退回去等于这个改动没做。
> 已经用 DSH_NO_BROWSER_TRANSPORT=1 显式关掉浏览器传输的环境需要撤销该设置。
它是怎么工作的(以及为什么不是「反代」)
被问得最多的两个问题:你们是操作 DOM,还是截获页面的请求?是不是个反代?
答案都是不是 —— 它自己构造请求、直接发给网页端私有接口。准确的说法是
「网页端私有接口的非官方客户端」。逐步拆开:
| 阶段 | 做什么 | 用什么 |
|---|---|---|
| ① 登录捕获(只做一次) | 开一个独立 profile 的浏览器窗口让你登录,把页面自己发出的请求头读一份存下来:Authorization: Bearer、域 cookie、反爬头 x-hif-dliq / x-hif-leim、一批 x-client-* |
Electron 的 webRequest.onBeforeSendHeaders |
| ② 请求构造(每次调用) | 自己拼 POST /api/v0/chat/completion;PoW 自己解:先要 create_pow_challenge,再用 SHA3 WASM 算出答案塞进 x-ds-pow-response |
插件自己的 HTTP 客户端 |
| ③ 发送 | 默认交给系统 Edge / Chrome 的网络栈发出(CDP 驱动真实页面),TLS / HTTP2 指纹与真实浏览器一致;也可切回 Node | 见下方「传输层」 |
| ④ 解析与对接 | 自己解 response/fragments 的 SSE 帧、分思考与正文通道;工具调用走提示词协议(网页端没有原生 function calling) |
自研解析器 |
唯一沾到"截获"的只有第 ① 步,而且那一步是「读一份 + 清掉自己的痕迹」:同一个回调顺手把请求头里的
Electron 品牌(UA 与 UA-CH)删掉,否则网页端会判「使用环境异常」。它不改写业务请求、也不转发任何东西,
而且只在登录那一次生效 —— 登录完之后页面关掉,插件照样跑。
为什么不是「反代」
反代(反向代理)的关键是中间人:客户端以为自己在跟原服务器说话,实际请求被转发了一层。
这里没有中间人 —— 插件自己就是那个客户端,以网页端的身份直连 DeepSeek。
中间没有转发层,也不需要额外跑一个本地服务。
和另外两条常见路线的区别
| DOM 自动化(如 cuckoo-code) | 请求 hook(浏览器扩展,如 deepseek-pp) | 本插件 | |
|---|---|---|---|
| 请求由谁发出 | 页面 | 页面 | 插件自己 |
| 登录态来源 | 人在窗口里手动登 | 你日常浏览器里的登录态 | 开窗捕获一次、存在本机 |
| 网页改版会怎样 | 选择器一改就坏 | 只依赖接口路径 | 只依赖接口,完全不碰 DOM |
| 浏览器要一直开着吗 | 是 | 是 | 否 |
前两条路线的共同点是「请求是浏览器发的」,只有本插件是发送方。代价是接口变了要跟着改;
换来的是不依赖 DOM、可以后台无人值守地跑。
为什么这样更不容易被限流
网页端会拦自动化调用,而且公开的同类项目文档里已经列出了触发条件。0.7.0 按其中最可识别的那几条
做了调整:
| 原本的做法 | 问题 | 现在 |
|---|---|---|
在 Node 里 WebAssembly.instantiate 求PoW 挑战 |
被明确列为「自动解挑战」的封号触发条件 | 在真实页面里用官方自己的 wasm 求 |
| 每次调用临时建会话、用完即删 | 真人不会这样建删对话(实测曾一天建 182 个会话) | 按 DSH 会话复用,跨重启续上(resume-state.json) |
| 每轮全量重发 | 请求体量与节奏都不像真人 | 链式投喂:只发增量,parent_message_id 挂在上一条回答下面 |
| 固定请求间隔 | 真人打字不是等距的 | 见设置页的请求间隔(可配随机区间) |
代价(也是 0.7.0 的破坏性变更):必须有可用的 Edge / Chrome。
找不到浏览器时会明确报错—— 不会静默退回 Node 侧解PoW,因为那样等于这个改动没做,
只是给了「已经改好了」的错觉。
⚠️ 需要说清的边界:这些调整降低被识别为自动化的概率,不构成"不会被封"的保证。
逆向网页端本身违反服务条款,风险是"判罚多严"而不是"会不会被抓到"。
稳定性优先请用官方 API,或用已有的付费订阅走 OAuth。
界面预览
设置页按用途分成 7 个标签(一次只显示一页)—— 账号(登录状态 / 当前账号 / 账号库 / 手动 Token)·
模型(可用模型 / 连通性测试)· 防风控(请求节流 / 会话清理及其三个区间 / 调用台账)·
传输层(指纹 + 一键测试)· 浏览器(内置浏览器:状态 / 启动 / 关闭 / 打开页签时自动启动)·
上下文(每轮全量 / 链式投喂)· 关于(版本与更新 / 数据位置 / 风险说明)。
操作反馈条常驻在标签栏之上,切到哪一页都看得见。
主区页签栏里多了一个 「网页版」(与 对话 / 轨迹 / LDW 并排)—— 点开就是 DSH 自带 Chromium 里的
chat.deepseek.com 本体,由插件保活并自动装回登录态(见「快速开始 → 4.」)。
设置页(真实截图,拍于拆页之前):当前账号 / 登录状态(适配器注册、凭证来源、PoW WASM、服务端校验)/ 三种登录方式(Microsoft Edge · 我的默认浏览器 · 从已登录窗口恢复)/ 手动粘贴 token。
DSH「使用统计」里看到的调用量 —— 免费网页通道,当日 1042.8 万 tokens / 164 次调用:
设置页示意图(标注各区块用途):登录状态 / 当前账号(退出当前账号 · 退出并登录其它账号) / 浏览器窗口登录 / 从已登录窗口恢复 / 手动 token / 连通性测试 / 模型列表。
核心能力
| 能力 | 说明 |
|---|---|
| 🔐 网页登录(无 API Key) | Electron 独立分区窗口里正常登录,插件旁路捕获真实 Authorization、cookie、x-hif-* 指纹头与客户端版本头 |
| 🧩 PoW 求解 | create_pow_challenge + DeepSeek 自家 sha3_wasm_bg.*.wasm 的 wasm_solve,在浏览器页面上下文里执行(0.7.0 起,见下方「为什么这样更不容易被限流」);WASM 地址自动发现(哈希随部署变化),失败自动回退 |
| 🌊 流式 | 同时兼容 response/fragments(THINK/RESPONSE 片段)与直连 thinking_content/content 两套 SSE 格式,含 {o:"APPEND"} 与裸 {v} 续段;按逻辑流去重,快照重放不重复吐字 |
| 🛠 工具调用 | 网页端没有原生 function calling → 提示词 JSON 协议 + 流式过滤器(跨包标记、围栏、多调用、假阳性回退)→ 合成 tool-call 块并给出 finish: tool-calls;工具定义按 5.6 万字符预算整段下发,超预算时列出被省略的工具名并要求模型别猜参数 |
| 🛡 格式漂移双保险 | ① 指令层显式禁止 XML/DSML 标记并说明后果(实测模型会主动拒绝该格式);② 解析层同时容忍 JSON 与 XML/DSML 两族(\|DSML\| 前缀、dsml- 连字符、裸 ``、CDATA) |
| 🩹 非法 JSON 宽容修复 | 模型常把 Windows 路径写成单反斜杠:\A 是非法转义,而 \r 合法却会把 \resources 静默变成回车。多候选修复链逐字还原路径,解析不出才降级为正文(绝不静默丢内容) |
| 🖼 图片输入 | 走网页端文件上传通道(/api/v0/file/upload_file → ref_file_ids),同一张图在多处出现时自动去重(服务端拒绝重复 id)。实测:上传自造的「左红右蓝」PNG,模型答出「左=红色,右=蓝色」 |
| 🧹 会话卫生 | 每次调用新建临时会话并在结束后删除 —— 实测调用前后网页端会话列表完全一致,不污染你的聊天记录 |
| 🎛 设置面板 | 状态展示、退出当前账号 / 退出并登录其它账号(连带清除浏览器分区登录态)、浏览器登录、从已登录窗口恢复、手动粘贴 token、连通性测试(host HTTP API:/deepseek-web-login/api/*) |
| 🖥 内置浏览器(「网页版」页签) | 主区页签里挂 DSH 自带 Chromium 加载的 chat.deepseek.com 本体;登一次即可——登录态(localStorage + cookie)落盘、重启自动装回;在里面登录会自动同步进账号库并切号;崩溃自动重挂(≤3 次);不影响 DSH 启动(等 UI 空闲再挂) |
| 💓 传输层保活心跳 | 盯"那个页面还活着吗、还登着吗",坏了就修(只做本地探测,不给站点发定时 ping);传输层浏览器 10 分钟没人用自动退场(transport.json 可调) |
登录与凭证捕获流程(点击展开)
快速开始
1. 安装
# 方式 A:从 Release 的 tgz 装配(推荐,免构建;版本号以最新 Release 为准)
dsh plugin --profile desktop add ./dsh-deepseek-web-login-0.1.3.tgz
# 方式 B:git 装配(本机需可访问 github.com)
dsh plugin --profile desktop add github:cv-superding/dsh-deepseek-web-login
# 方式 C:从 npm 装(免 git、免构建,国内镜像也会同步)
# 先确认已发布:npm view dsh-deepseek-web-login version ← 查不到就先用方式 A/B
dsh plugin --profile desktop add dsh-deepseek-web-login
> ⚠️ 在插件市场里点「更新」失败时(报 update failed and restoration of the previous build could
> not be verified,括号里写着"当前宿主无法安装更新前的精确 GitHub 提交")—— 那句括号只是
> "失败后无法自动回滚" 的说明,不是失败原因。本插件长期只提供 git 安装来源,所以更新要
> clone GitHub、失败后市场也救不回来。绕过办法(挑一条,都不用等市场修好):
# 1) 按 tag 精确装(比装 HEAD 稳,不会被中间提交绊住)
dsh plugin --profile desktop add github:cv-superding/dsh-deepseek-web-login#v0.2.0
# 2) 用 Release 的 tgz(完全不走 git;先从 Releases 页下载对应 tgz)
dsh plugin --profile desktop add ./dsh-deepseek-web-login-0.2.0.tgz
# 3) 从 npm 装(免 git、免构建;先 `npm view dsh-deepseek-web-login version` 确认已发布)
dsh plugin --profile desktop add dsh-deepseek-web-login
> 市场目录里的这个条目当前标记为 npm: null(未发布到 npm)⇒ 只能走 git。
> 包侧已就绪(private 已去掉、repository 已补 —— 后者是目录识别 npm 包的硬条件),
> 发布之后目录每天重探一次(未发布的判定 1 天后过期),探到即自动切到 npm 路径
> (版本比较 + tarball,不再依赖本机 git)。仍显示 git 时等一天再看,或直接用上面第 3 条。
2. 登录一次
> --profile desktop 是 DSH Desktop(Electron 应用)使用的 profile;若你跑的是 web profile,换成 --profile web。
> 重启 DSH 后生效(本插件是 bundle 装配,重启即自动加载)。
设置 → DeepSeek 网页登录 → 浏览器窗口登录,在弹出的窗口里正常登录(手机号 / 邮箱 / 验证码均可)。
登录窗口报的是干净 Chrome UA(不含 Electron 字样,UA-CH 品牌也清过)—— 否则网页端会判定
「使用环境异常」直接拒绝服务;若仍被拦,用同一行的 用我的默认浏览器登录,按提示用 F12 取 token 粘贴。
捕获成功后窗口自动关闭,面板显示已登录。
> 💡 这个按钮每次都会先清掉上次的登录状态 —— 登录用的是独立 profile
> (~/.dsh/web-login/browser-profile,不碰你日常 Edge 的 cookie 与历史),
> 所以窗口一打开就是干净的登录页,不会带着上一个账号。
> 想省一次输入(凭证明明还有效、只是要重新捕获)就用账号行上的「重登」——
> 那条路默认不清,浏览器里若还留着登录态会立刻复用、一个密码都不用敲。
> ⚠️ 但如果这条账号已经被标记失效(红标「需要重新登录」),它就会先清掉再让你重新登录 ——
> 因为此时浏览器里那份登录态也已经不可用,复用只会把同一个坏凭证再抓一遍(会陷入点多少遍都失败的死循环)。
- 凭证只存在本机
~/.dsh/web-login/deepseek-auth.json,不在仓库里;面板「当前账号 → 退出当前账号」一键清除(同时清掉浏览器分区里的登录态,保证真退出、可换号) - 凭证丢了或校验不通过:点 从已登录窗口恢复(复用上次登录的持久化分区,无需重新登录)
- 非 Electron 环境(纯 web profile):用面板里的手动粘贴 Token通道
3. 选模型开跑
模型选择器 → provider DeepSeek 网页版(免费) → DeepSeek 网页 · 快速模式,
然后照常用 agent(工具调用、思考流、贴图都可用)。
> ⚠️ 一个账号同时只开一个聊天窗口:多开并发会触发网页端临时封禁(1 天)——要开多个窗口就换账号,或把多余的窗口换到别的 provider。详见已知限制。
4.(可选)开着「网页版」页签:让一个真页面一直活着
主区页签栏里(与 对话 / 轨迹 / LDW 并排)有一个 「网页版」 页签:里面是 DSH 自带的 Chromium
加载的 chat.deepseek.com 本体——真页面、真 JS、真指纹。它的作用不是"回显聊天"(发送仍走原有通路,
网页端的回答不会显示在这里),而是保持一个真人网页端会话活着,并把它的登录态交给插件使用。
怎么用(一次就够):
- 打开「网页版」页签 —— 它会自动向 DSH 申请内置浏览器(默认开,设置页「浏览器」页可关)。
首次会看到转圈 + 「正在恢复内置浏览器…」,几秒后页面就上来了; - 在里面登录一次(手机号 / 扫码都行)。登录完成后约 10 秒内:
- 插件把页面的登录态(localStorage+ cookie)存到本机(~/.dsh/web-login/guest-storage.json);
- 同时把token与身份同步进账号库:新号会自动入库并切为当前账号(「账号」页能看到
捕获时间 / 指纹头 / token 长度); - 以后重启 DSH 不用再登 —— 分区是内存的(DSH 原生设计),但插件会在挂载时把快照装回页面并刷新,
页面直接回到登录态; - 想看手动入口:设置页 → DeepSeek 网页登录 → 浏览器 页(状态 / 启动 / 关闭 /
「打开『网页版』页签时自动启动」开关)。
它崩了会怎样? 自动重挂(20 秒后,最多 3 次;你手动点过「关闭」就不再自动拉起)。
崩溃原因(crashed / destroyed / render-process-gone)会写进诊断文件,不再只有一句"探测不到"。
页面关着会丢东西吗? 不会丢存量:
| 东西 | 存在哪 | 页面关着时 |
|---|---|---|
页面指纹(UA 洗白 / webdriver=false / UA-CH 品牌) |
代码级补丁,每次挂载重新打 | 重开自动带上 |
| 登录态(localStorage + cookie 快照) | ~/.dsh/web-login/guest-storage.json |
重开自动装回 |
| 账号 / API token | 账号库(与页面无关) | 不受影响,照常发请求 |
| 页面自身的"活着"状态 | —— | 只有这一项抓不到(这正是"保活"的意义) |
它为什么这么设计(都是实测踩出来的,写在这里省得你踩):
- DSH 给内置浏览器的分区不是持久化的(
dsh-sidebar-browser-,没有persist:)⇒
登录态只能由插件落盘再装回; - webview 的
useragent只在挂载/导航前生效 —— 晚一步,页面就带着Electron/出去,
网页端直接回「使用环境异常」; - 页面必须在目标域上装载登录态(
about:blank是 opaque origin,storage 按源隔离,写了也白写); - 自动挂不能在 DSH 启动阶段做(冷启 Chromium + 加载整个网页端会和 UI 抢资源,实测让启动多花 ~15 秒),
所以它等 UI 空闲后再挂。
诊断文件(都在 ~/.dsh/web-login/diagnostics/):
tail -1 ~/.dsh/web-login/diagnostics/guest-fingerprint.jsonl # 页面眼里的环境:UA/webdriver/品牌/WebGL/视口
tail -5 ~/.dsh/web-login/diagnostics/guest-errors.jsonl # 非正常状态与原因(含崩溃原因)
cat ~/.dsh/web-login/guest-storage.json | head -20 # 登录态快照(含 cookie 与 localStorage)
模型档位
权威依据:GET /api/v0/client/settings?scope=model 的 model_configs(按账号返回,实测 configVersion 81)
| model_type | 名称 | enabled | switchable |
|---|---|---|---|
default |
快速模式 | ✅ | ✅ |
expert |
专家模式 | ❌ 已停用 | ❌ |
vision |
识图模式 | ❌ 已停用 | ❌ |
即专家 / 识图已被服务端停用并合并进「快速模式」。因此本插件只暴露两条 —— 它们
不是两个模型,而是同一个「快速模式」的 thinking_enabled 开关两档预设:
| 模型 id | thinking | 适合 |
|---|---|---|
deepseek-chat |
关 | 工具调用、改写、检索:直接作答,最快、最省额度 |
deepseek-reasoner |
开 | 数学、多步调试、规划:先推理再作答(推理流作为思考块回传) |
同一档位内也可用推理强度(Off/High)切换;历史里的 deepseek-pro / deepseek-expert / deepseek-vision
会按别名回退到快速模式,不报错。
容量(2026-09-11 逐字段核对 GET /api/v0/client/settings?scope=model,configVersion 81):
- 单请求输入硬上限:
input_character_limit = 2621440字符(≈2.5 MiB 字符) - 附件(file_feature)token 预算:
token_limit = 890880(开不开思考都一样)
—— ⚠️ 这个数字不是上下文窗口。曾经把它当上下文窗口写进代码与文档(还写成「1M 扣输出预留」),
而 890880 = 870×1024,面板一按 ÷1024 显示就成了「870K」,看起来像「说好的 1M 缩水成 870K」。 - 上下文窗口:服务端没有这个字段,按 DeepSeek 标称的 1M 取
1048576(服务端自己的数字都是 1024 的整数倍)
本插件 contextWindow 声明 1048576,
prompt 字符上限默认 400,000(可配)—— 这个数字同时是风控阀门,见「配置」一节的说明。
配置
插件 entry config(cordis.patch.yml / profile bundles 装配时的 config):
| 字段 | 默认 | 说明 |
|---|---|---|
maxPromptChars |
400000 |
送出 prompt 的字符上限(超出走中段截断:保系统提示+工具协议与最近回合)。⚠️ 调高会明显提高被限流的风险,可调区间 [120000, 1500000],默认刻意不顶格 |
maxRefImages |
24 |
一次请求最多带多少张历史图片(ref_file_ids 的长度)。网页端对这一批有上限(实测 40~52),超了整轮都会被拒、且该会话此后每轮都失败,所以按时间只带最近的 N 张;被略过的图在 prompt 里标成 [earlier image omitted]。0 = 不限制(不推荐)。⚠️ 这里说的「张」其实是图片内容条目:模型每 read_image 读一次、或同一张图被重新渲染/裁剪出新内容,都会多算一条 —— 所以它往往远多于你亲手贴的张数。0.1.79 起提示语改说「份图片内容」;0.1.81 起提示按阶梯上屏 —— 首次必说明,之后要「被略过的份数翻倍、且至少再多 10 份」才再说一次(此前每轮都刷,实测 19 分钟 52 次;0.1.79 的「同一规模只提示一次」没修干净:那个签名里含「总条数」,而它每轮都在涨) |
idleTimeoutMs |
120000 |
SSE 空闲超时 |
deleteWebSessions |
true |
调用后删除临时网页端会话 |
autoContinue |
true |
回答在句中被截时自动发起新请求续写(无缝拼进同一条回答);截断提示从此不出现。另外,模型把工具程序写进正文(没有作为工具调用发出)时,也由它管那一轮纠正请求 |
maxContinuations |
2 |
自动续写的最大轮数(每轮是一次新的网页端请求,调高消耗更多免费额度) |
minRequestIntervalMs |
2000 |
请求间隔区间下限(毫秒),从上次调用结束时刻算起 |
maxRequestIntervalMs |
4000 |
间隔区间上限;实际等待在 [下限, 上限] 内随机取值(上下限相等=固定间隔) |
allowConcurrent |
false |
是否允许同一账号并发请求。默认串行,多个调用排队(FIFO) |
sessionCleanup |
deferred |
临时会话清理:immediate=结束后 1.5s 删 / deferred=攒批集中清理(默认)/ keep=不删 |
sessionCleanupDelayMs |
90000 |
deferred:从第一个会话入队起最多等多久就清理(区间未设时的兜底标量) |
sessionCleanupBatchSize |
8 |
deferred:攒够多少个立即清理(区间未设时的兜底标量) |
cleanupBatch |
6~10(随机) |
deferred:攒批阈值的区间(个)。这一轮具体攒几个 = 每次清理时在区间内随机抽 |
cleanupDelayMs |
60000~120000(随机) |
deferred:最长等待的区间(毫秒)。每轮清理重抽 |
cleanupGapMs |
800~2500(随机) |
deferred:相邻两个删除请求之间的间隔区间(毫秒)。每删一个重抽 |
transport |
chromium |
传输层:chromium=系统浏览器进程代理(拉起 Edge/Chrome,CDP 驱动页面发请求)/ node=Node 原生 fetch |
contextMode |
full |
上下文投喂:full=每轮重发全量 prompt / chained=只发增量 + 把上一条回答当父消息(见下节) |
probeIntervalMs |
1800000 |
登录态主动探活间隔(毫秒),0=关闭。只读 users/current,零额度 |
> ⚠️ maxPromptChars 是风控阀门,不是「越大越好」。
> 网页端是无状态的:每一轮请求都要把整段对话转写重新发一遍,所以这个上限直接决定单次请求的体量。
> 实测同一个会话里单次输入从 9.7k token 一路涨到 293k;把它默认顶到 150 万字符
> (纯中文 ≈100 万 token)等于默认就允许「一次顶满 1M 上下文」。我们自己的四个账号在两天内陆续被限制,
> 体量是主要嫌疑。所以 0.1.76 起默认值降到 40 万(≈27 万 token,长任务够用)。
>
> 确实需要更长的转写可以自己往上调(上限仍是 150 万),但请把它理解为「拿账号稳定换更长的记忆」:
> 真要长上下文,更划算的是把「上下文投喂」切成 chained(只发增量、历史交给服务端维护),
> 而不是抬高这个天花板 —— 后者是每一轮都要多付的成本。
上下文投喂:每轮全量 vs 链式增量
一次 completion 请求的 prompt 是整份对话转写(系统提示 + 工具目录 + 全部历史)。为什么必须这么发?
因为插件一直把 parent_message_id 写成 null —— 每条消息都是网页端会话里的根消息、
没有父链,服务端按消息树回溯上下文时回溯到空。这是 2026-09-12 实测判定过的行为
(同一会话内先发「记住编号 ZC-7391-KX」得 OK,再问编号答「不知道」)。
浏览器不是这么干的。参考实现里 nextParentMessageId = history?.parentMessageId ?? finalAssistantMessageId、
isFirstMessage = parent_message_id === null —— 只有会话第一条的 parent 是 null,
之后每轮都把上一条消息 id 当 parent 发上去,历史由服务端维护。
设置页「上下文」标签可以切到 链式投喂:后续轮只发新增内容,parent_message_id 指向上一条回答的
message_id(取自 SSE 首帧 event: ready 的 response_message_id)。收益是请求体小得多、
更像真人连续对话;代价是工具协议只存在于链首那条消息里,一旦服务端把早期上下文丢掉,
模型可能不按约定格式发工具调用。
所以默认仍是 full(与 0.1.61 及以前完全一致),而 chained 采用「能省则省、一有不确定就退回全量」
的策略 —— 出现下面任何一条就重新起链(发全量 + parent=null,只是多花点 token,不会错位):
| 退回全量的情形 | 为什么 |
|---|---|
| 本轮是新会话 / 会话轮换 / 切号 | 链属于某个具体会话,换了就不能续 |
| 固定头(系统提示 + 工具目录)变了 | 链首那份已经过期,续上去模型会照旧定义干活 |
| 历史不是严格追加(被压缩、改写、回退) | 增量算不出来 |
| 本轮新增内容为空 / 增量本身超预算 | 没有值得省的东西,或风险大于收益 |
上一轮流失败、被取消、或没拿到 message_id |
父消息可能不存在或已作废 |
判定逻辑是纯函数(src/context-feed.ts),测试在 tests/check-context-feed.mjs(判据)
与 tests/check-context-chain.mjs(接线与生命周期,假 transport + 假 SSE)。
请求节流:为什么需要,值该给多少
网页端对同一账号同时只能生成一条,并发生成会被直接拒绝;实测更严重的是账号级限制:
两个窗口并发生成,不到 6 分钟就触发 1 天的临时限制(登录态没坏,但期间该账号所有请求被拒)。
而 DSH 本身会并发调用同一个账号 —— 从插件日志反推 272 轮调用的起止时间,发现 16 对真重叠:
重叠的一方是主回答,另一方只有 8~17 字、耗时 1~3 秒,那是 DSH 的会话标题生成
(options.purpose === 'session-title')。也就是说你还在等回答时,另一个请求已经发往同一账号了。
所以插件默认做了两件事:串行(一次只放行一条,含标题这类辅助调用)+ 两次调用之间至少间隔 3 秒。
| 场景 | 间隔区间(min~max) | allowConcurrent |
|---|---|---|
| 推荐(默认) | 2000~4000 |
false |
| 追速度、只跑短任务 | 1500~2500 |
false |
| 已被限流过 / 高密度自动化(多步骤工具调用) | 5000~9000 |
false |
| 完全关闭节流(不建议,会恢复并发重叠) | 0~0 |
false |
| 实验:还原 DSH 原生并发 | 任意 | true ⚠️ |
为什么是区间而不是固定值:固定间隔的方差≈0,统计上就是明显的「定时器特征」;
同一场景的开源项目 cuckoo-code(从未被风控)用的正是 2000~4000ms 随机区间。
还有两道"像人"的闸门(1.0.2 起)
间隔只保证"两次不挨太近",管不了"连着几十分钟不停发"。所以还有:
- 长任务保护:连续 N 次(
longRunThreshold,默认 15)之后强制长休 60~180 秒;
"连续"的判定是距上次请求超过 10 分钟才算人歇过(1.0.2 从 2 分钟提到 10 分钟——
实测那种"十几发一波、隔一两分钟再来一波"会把 2 分钟门槛洗掉,长休从此永不触发)。 - 滚动窗口限速(
rateWindowMs/rateMaxInWindow,默认 30 分钟最多 20 次):
超过就长休 60~180 秒。这个计数小间隙洗不掉,因为它只看"这段时间一共发了多少次"。
> 这两道闸门的由来是一次真实的会话吊销:调用台账显示那一晚 22:31→22:53 共 49 次请求、
> 间隔 8~12 秒(真人网页端做不到),而长休一次都没触发。三个参数都在
> ~/.dsh/web-login/gate.json 里,设 rateMaxInWindow: 0 可关掉窗口限速(不建议)。
> 触发时日志会写明原因:近 30 分钟已发 20 次(上限 20)—— 长休 97s 再继续。
会话清理(为什么默认不是「立刻删」)
一次模型调用要发 4 个请求:建会话 → 取 PoW → completion → 删会话。其中
「每轮新建一个临时会话、用完立刻删掉」是最强的机器行为特征之一 —— 真人绝不会每 30 秒建删一次对话。
为什么不能干脆复用会话:DSH 每次把全量历史交给适配器,而网页端会话是有状态的,
复用会让服务端同时看到「会话自身的历史」和「我们重发的全量 prompt」两份上下文,很快撑爆窗口。
所以只能优化删除侧:deferred(默认)攒够 6~10 个(每轮随机)或最多等 60~120 秒
(每轮随机)后集中清理,并且优先用一个请求批量删(服务端支持的话 N 个会话只花 1 个请求;
不支持则自动回退为逐个删,此后不再尝试)。想要彻底不留记录就选 keep(请求最少,但网页端会留下临时会话)。
为什么这三个参数也是"区间 + 随机":它们原来全是死值 —— 正好攒到第 8 个动手、正好等 90 秒、
逐个删除时请求连发。固定值的方差≈0,本身就是统计上最明显的机器特征(真人不会这么精确)。
所以三个参数各给一对上下限,实际取值在区间内随机抽:
| 参数 | 谁在重抽 |
|---|---|
cleanupBatch(攒够几个) |
每轮清理重抽一次 |
cleanupDelayMs(最长等多久) |
每轮清理重抽一次 |
cleanupGapMs(删除间隔) |
每删一个重抽一次 |
「删除间隔」是为"别一下子连发几十个删除请求":批量删是首选(1 个请求搞定),
但服务端一旦不接受批量删就会退化为逐个删 —— 那时如果连发,几十个删除请求会瞬间打过去,
这比"攒批"本身更像脚本。所以相邻两个删除请求之间会按 cleanupGapMs 停一下;
上限设成 0 就等于"不等待"(回到老行为)。
批量删与逐个删都串行化了:上一轮没删完时,下一次 flush 只会排在后面,不会插进来并发发请求。
> 间隔按「上一次调用结束」起算,所以长回答(几十秒)之后不会额外白等 —— 它只在
> 「结束 → 下一个开始」这段真正密集的空隙里起作用。
> 两种改法:① 设置页的「防风控」页里直接调 —— 并发开关 + 间隔滑块 + 三个快捷档位 +
> 会话清理模式与三个清理区间滑块,改完即时生效并自动落盘;② 写在插件 entry config 里(改完需重启 DSH)。
> 优先级:设置页保存的值 > entry config > 内置默认(设置页是显式操作,不会被配置文件里的旧值盖回去),
> 落盘位置 ${DSH_HOME:-~/.dsh}/web-login/gate.json。「登录状态」卡里也会显示当前生效值。
传输层(指纹):默认走 Chromium 网络栈
网页端请求的网络栈决定了「在服务端眼里你是浏览器还是一个脚本」。实测三方对比(同机同日):
| JA4 | cipher 列表哈希 | ALPN | |
|---|---|---|---|
| Node fetch(undici) | t13d5212h1_… |
— | h1 |
| Chrome(本机 152) | t13d1517h2_8daaf6152771_cb7bf5808d99 |
8daaf6152771 |
h2 |
| 默认:net.fetch(Electron 43) | t13d1516h2_8daaf6152771_806a8c22fdea |
8daaf6152771 |
h2 |
Node 的请求在 TLS 层就能被判定为非浏览器(不走 HTTP/2、cipher 数量差 3 倍多、不带 GREASE),
而且这几项调参修不了。
> ⚠️ 0.7.0 更新:实际走的是「系统浏览器进程代理」,不是 Electron 的 net.fetch。
> 官方端是 ELECTRON_RUN_AS_NODE=1 的子进程,取不到 electron.net.fetch,
> 所以浏览器传输层改为拉起系统的 Edge / Chrome、用 CDP(Runtime.addBinding)把请求
> 交给真实页面发出,响应分块回传。上表的 net.fetch 一行是当时的对比数据,
> 结论仍然成立(Node 的 TLS 指纹确实区分得出来),但现在不是靠 Electron 实现的。
> 代价就是上面那条:必须有可用的 Edge / Chrome。
设置页「传输层(指纹)」卡可以直接切换,并带一个零额度的一键测试
(回显指纹 / 流式 / 鉴权三项结论)。
⚠️ 切到 Chromium 后请求会跟随系统代理(Node 则完全无视代理)。
如果梯子关闭时系统代理仍指向 127.0.0.1:7897,请求会失败 —— 这时切回 node 即可。
那个页面是真的「已登录」的(0.9.1 起)。只装 cookie 是不够的:网页端的登录态读的是
localStorage.userToken。不装的话页面会渲染登录页、而我们的请求却带着合法 Authorization ——
这是真人身上不会出现的组合,而且站点自己的 JS 从不运行(没有会话轮询、没有长连接),
页面自然也没有心跳。所以首次请求会把登录态一并写进页面 localStorage、读回来核对、
然后重载一次,让站点应用自己跑起来。
- 只写有来源的键(
userToken← 请求头、hif_leim_cached←x-hif-leim),不编造形态; - 换号后签名变了会重装(否则会出现「页面是 A、请求带 B」);
- 装成功才记账 ⇒ 偶发失败下次会重试(页面没停在站点 origin 时
localStorage是 opaque origin,
写不进去;这条路径只 warn,不影响请求本身)。
账号库:多账号并存与一键切换
凭证失效时不用自己找路:探活失败的账号会直接标成「需要重新登录」,那一行上就有
「重新登录这个账号」按钮。它与「登录新账号」的区别只有一点 —— 不清浏览器登录态:
「登录新账号」必须先清干净(否则新窗口还是旧账号,抓回来还是它),而修同一个号正相反,
留着才可能一打开就复用上,一个密码都不用敲。修完原地更新那条记录,不改变当前正在用的账号。
已经判定失效的账号,请求不会再发出去。 探活失败时会往账号记录里写下原因,0.1.80 起
发起请求前会先看一眼:如果那份失败是授权类的(Authorization Failed / invalid token /
HTTP 401·403 / 中文「登录态已失效」),就直接返回一句「这个账号的登录态已失效(…),本次请求
没有发出」,不再白跑一整轮。
在此之前探活的判定只用于展示(红标 + 日志)—— 22:42 就判死的 token,22:50 还能被拿去
逐个重试 14 张图的上传。现在这两条路用的是同一个判据。
⚠️ 断网、超时、5xx、429 不拦 —— 那些情况下凭证是好的,拦下来会把健康账号锁住。
所以如果你确定某个被拦的账号其实还能用,点一次「校验全部」(只读探活、零额度)就能把它解锁。
cookie 的过期构成会记下来:捕获时顺手存下每个 cookie 是会话级还是持久级、最晚什么时候到期,
界面上写成 5 项 · 1 会话级 · 4 持久级 · smidV2 还剩 399 天。
⚠️ 它不是登录态寿命 —— 实测真正鉴权用的是 token(只发 token 不带 cookie 能通过,
只发 cookie 不带 token 直接被拒 40002 Missing Token),所以这只是浏览器侧的上界。
老记录 / 手动粘 token 的账号没有这份信息,界面会写「未记录(重新登录后会补上)」。
保存过的账号都在本机 ~/.dsh/web-login/accounts/,设置页「账号」标签里可以一键切换、
改备注、移除、导出/导入备份(都弹系统对话框,自己选位置和文件)。以前换号的代价是「退出 → 清浏览器分区 → 重新登录 → 等捕获」,
现在切换即时生效(下一次请求就用新账号)。
账号多了可以分组:工具条上有「新建分组」,每行一个下拉把账号归到某个组,组标题可点开/收起
(折叠状态只记在本地)。列表按组分区显示,当前账号所在的组自动置顶 —— 正在用的号不会沉下去;
组内仍是捕获时间倒序。「未分组」是兜底区,不给改名/删除。
删组不会删账号:组定义存在 ~/.dsh/web-login/groups.json,账号记录里只有一个指针;
组没了,那些账号就落回「未分组」,一个都不会从列表里消失。分组只影响显示 ——
切号、会话复用、清理策略都不看它。
「校验全部」 会对库里每个账号跑一次只读探活(users/current,零额度,串行执行),
用来刷新登录态、补上账号名、清掉已经恢复的失败标记。它和「刷新状态」(刷当前账号那张卡)不是一回事。
换账号不会丢对话、也不会让模型失忆 —— 这是个常见的担心,但在这套架构下不成立:
对话记录存在 DSH 本地、每一轮请求都把整段历史完整重发、网页端不留任何会话。
账号只是"通行证 + 额度归属",换它不影响对话内容。(真正会"失忆"的是在 DSH 里开一个新会话。)
> ⚠️ 关于「自动换号」(默认关闭,滑块在「设置 → DeepSeek 网页登录 → 防风控」):
> 它会按你设的间隔(分钟,0 = 关闭)换到下一个可用账号,0.6.8 起还有一条"失效即换":
> 某个账号的凭证被服务端判为失效(AUTH / 40003)时,只要库里还有别的可用账号,
> 就会自动换过去并把这一轮接着跑完 —— 不需要你再手动点「切换」。
>
> 但请想清楚再打开它:真人不会在几分钟内换一个账号接着发消息 —— 那是极强的机器行为特征,
> 与本文档里传输层指纹、随机间隔、会话清理这些"降低机器可识别性"的努力直接冲突。
> 另外同一服务商会把多账号关联起来(同设备 / 同 IP / 同指纹 / 相近行为),
> 一旦被判定为"同一人的多开小号",处置通常比单账号超频更重。
> 账号库的目标是"在自己的多个正常账号之间切换更省事",不是"靠轮换把限流绕过去" ——
> 所以它默认是关的,间隔也别给太短(半小时起步比较稳)。
>
> ⚠️ 这个设置存在 ~/.dsh/web-login/gate.json。换到新环境/重装 DSH 时它不会跟着迁过去
> (账号库也不会 —— 用「导出备份 / 导入备份」搬),重启后会回到"关闭",
> 表现为"以前会自己换号,现在不换了"。
>
> 导出的备份文件里是可完整登录的凭证,等同于账号本身 —— 别分享、别提交到仓库。
「登录新账号」和「退出」的区别(很容易搞混,但后果完全不同):
- 登录新账号(添加) —— 只清掉浏览器里的登录态,然后把新账号加入账号库但不切换。
你正在用的号不受影响;加完在列表里点「切换」才会用它。(多账号并存靠它。) - 退出 —— 等于把该账号从账号库移除:本地凭证与浏览器登录态一起清掉,不是"只登出"。
想留住它就先「导出备份」。
另外「账号」页分成两个子页(登录状态 / 账号库),一次只看一半,不用滚很久。
> ℹ️ 账号显示名用的是"网页端返回的屏蔽值":接口只给 192******27、
> lidi*********+mn1@gmail.com 这种形式,原始邮箱/手机号插件拿不到,
> 所以做不到"显示全"。0.1.69 起不再对这个值做二次屏蔽 —— 之前两个 Gmail 账号
> 都会被显示成 lid***@gmail.com,看上去像同一个号。
导出/导入怎么选文件:点「导出备份…」弹系统另存为,位置与文件名自己定;
点「导入备份…」弹系统打开框选文件 —— 都不用再手打路径(导入也不再要求你先把路径抄出来)。
两条路都有回退:万一当前环境拿不到系统对话框,导出会写进插件目录并在界面回显完整路径,
导入会改为由界面读文件内容后交给宿主 —— 功能不会因此失效,只是路径由插件决定。
导入优先走"只把文件路径告诉宿主、宿主自己去读",所以凭证明文通常并不经过 HTTP。
调用台账:证明节流真的在起作用
「防风控」标签底部有一张台账卡,按天记录每次调用的结果(只留近 7 天、只记元信息,
不含任何对话内容与凭证),看两个数:
- 相邻对话间隔(中位 / p90 / 最短):固定间隔方差≈0 是"定时器特征";最短间隔尤其说明问题
—— 它直接对应"有没有连环请求"。间隔只统计purpose === 'chat',会话标题生成是 DSH 自己发的
旁路请求,算进去会让分布失真。 - 失败分类:限流 / 账号被限制 / 登录态问题 / 网络,各占多少。只看"失败了"没用,
要知道是哪一类才谈得上对策。
登录态探活与受限倒计时
- 探活:启动后 20 秒 + 之后每 30 分钟(
probeIntervalMs)用只读的users/current确认登录态
还有效,目的是在任务跑到一半之前发现过期。零额度、可关、失败只提示不阻断。 - 受限倒计时:账号被临时限制时(
user is muted),设置页显示「还剩 X 小时 Y 分」。
注意这个状态只能从生成请求被拒里学到 —— 受限期间只读接口依然返回 200,探活探不出来。
已知限制
- 同账号同时只能开一个聊天窗口:网页端按账号限制并发生成,多开会触发服务端的临时封禁(1 天)——登录态没坏,但期间该账号所有请求都会被拒。要多窗口就换账号,或把多余的窗口换到别的 provider
- 原生 tools 不存在:工具调用靠提示词协议;模型偶发格式漂移已被解析器与指令双重兜住,但本质是模型行为,无法 100% 保证
- 工具目录有预算上限:DSH 下发的工具定义会尽量全部写进 prompt(0.1.33 前只有 2.4 万字符预算,实测 61 个工具时静默砍掉了 26 个)。工具特别多或描述特别长时仍可能装不下,此时会把没描述到的工具名列出来,让模型向用户确认参数,而不是默默砍掉
- 单次请求 60s 上限(
completion_request_timeout_ms):网页端靠sse_auto_resume续接,本插件不实现续接;流在没有FINISHED标记的情况下结束时报max-tokens,而不是假装正常完成 - 思考模式的推理过程不进上下文:历史序列化只回放正文与工具调用/结果,以省 token
- 图片:走上传通道(
/api/v0/file/upload_file→ref_file_ids)。上传失败时降级为[image attached]文本标记,并在回答开头明确告知「有 N 张图片没能传给模型(原因)」 —— 图丢了不会再无声无息(0.1.66 前只写日志,界面上看不出来)。同一张图在历史里出现多次(用户消息 +read_image工具结果内嵌)时ref_file_ids自动去重:服务端不接受重复 id(biz_code 9 / invalid ref file id),被拒后整条会话后续每轮都会失败。上传时文件名必须带受支持的图片后缀(png / jpg / jpeg / webp / gif):服务端是按文件名后缀判类型的,multipart 里的content-type说了不算 —— 而宿主给read_image这类工具结果的name是纯 sha256、没有后缀。0.1.68 起由imageUploadName()统一归一回image.(0.1.67 及以前:凡是经工具返回的图,一律传不上去)。0.1.83 起链式投喂的后续轮不再重复引用历史图片:服务端按parentMessageId链回溯时,历史消息的附件本来就在它的上下文里(真机实测:两张不同布局的图、第二轮都不带ref_file_ids,仍都答对四个角),所以旧行为「每轮把最近 24 张重挂一遍」纯属冗余 —— 那会让网页端每条新消息下都挂一批同样的图、会话内的图片引用越积越多。现在只有全量重发(新会话 / 换号 / 系统提示变了)或这一轮确实新贴了图时才发送引用 - DSH 渲染层把单个
$当行内公式(不是本插件的行为):DSH 前端的 markdown 默认开singleDollarTextMath,所以含$的文本会被渲染成公式 —— 现象是$消失、-变成−(U+2212)、|变成∣(U+2223),字母被逐个拆行而数字串(如256)仍连在一起。PowerShell / bash 命令首当其冲,看起来极像「模型输出了乱码」。判据:原文能完整复原 ⇒ 不是模型退化(退化会丢信息,编码/渲染错只是把信息换了个样子)。规避:讨论命令时套围栏代码块或行内反引号 —— 代码构造里不跑数学扩展。 temperature/stop/max_tokens网页端无对应字段,会被忽略;usage 为估算值(网页端不返回 token 计数)- 免费额度有频控;
429会带上providerRetryAfterMs交给 DSH 的重试策略 describe_image是 DSH 侧另一个独立工具(调用外部视觉模型),与本插件无关;本插件的图片能力不依赖它- 必须能启动 Edge / Chrome(0.7.0 起):PoW 改为在真实页面里求解,官方端是
ELECTRON_RUN_AS_NODE=1子进程拿不到electron.net.fetch,只能拉起系统浏览器、用 CDP 驱动页面发请求。
找不到浏览器会明确报错(不会静默退回 Node 侧解PoW —— 那是刻意的,见「为什么这样更不容易被限流」) - 默认走系统浏览器的网络栈(CDP 驱动真实页面),TLS/HTTP2 指纹与真实浏览器一致;
但它会跟随系统代理,梯子关着而系统代理仍指向它时会连不上 —— 设置页「传输层(指纹)」切回 Node 即可
测试与验证
node tests/logic-test.mjs # 111 项断言(8 个测试文件)(序列化 / 工具过滤 JSON+XML / JSON 修复 / SSE / token 解包 / 掩码)
node tests/probe-live.mjs # 线上直连探针:原始 SSE 事件流 + 时长(--big=N 验证长 prompt)
node tests/probe-xml-live.mjs # 线上验证 XML 标记场景(指令劝阻 + 解析兜底)
node tests/probe-vision.mjs # 线上验证图片通道(自造左红右蓝 PNG → 上传 → 提问)
node tests/probe-batch-live.mjs # 线上复现事故 #4 的触发条件(深度思考 + 批量 3 条带 $env:/Windows 路径的命令)
node tools/changelog-section.mjs # 从 CHANGELOG 取某版本段落(发布流程复用)
node tests/check-bundle.mjs # 产物核对(关键修复是否都进了 lib)
node tests/check-injector-guards.mjs # 复核注入器注入前校验的正则
node tests/check-fetch-injection.mjs # 传输层注入必须"每次现取"(防单测静默打到线上)
node tests/check-net-diagnostics.mjs # net.fetch 诊断通道(标记文件生命周期 + 流式探针正反向)
node tests/check-transport.mjs # 传输层选择(降级判定 + 注入层真的跟着变)
node tests/check-context-feed.mjs # 上下文投喂判据(增量/回退的五种情形)
node tests/check-context-chain.mjs # 链式投喂接线与生命周期(假 transport + 假 SSE)
node tests/check-image-refs.mjs # 图片引用组装(同一张图去重 + 图丢了要写进回答)
node tests/check-image-ref-reject.mjs # 图片引用被拒(code 9)→ 识别 + 两级降级重试;授权失败则中止剩余上传
node tests/check-image-chain-send.mjs # 链式投喂只发「服务端还没见过的」图(新贴的图不能丢)
node tests/check-bugfix-0182.mjs # 全库审查确认的缺陷(图片上限落盘 / 重登保元数据 / 重登意图等)
node tests/check-stale-auth.mjs # 已知授权失效的账号不许发请求(含行为侧:请求到底发没发)
node tests/check-login-token.mjs # 手动粘贴 token:包装 JSON 解出空 ⇒ fail-closed(不把 JSON 当 token)
node tests/check-auth-recheck.mjs # F1 探活带回限流状态 + F2 AUTH 先复核再标记(apply 级行为) # 手动粘贴 token:包装 JSON 解出空 ⇒ fail-closed(不把 JSON 当 token)
node tests/count-session-images.mjs # 某个会话里累积了多少「图片内容条目」(回答"我没发这么多图"的质疑)
node tests/probe-upload-name.mjs # 真机 A/B:文件名后缀如何影响上传(需要已登录凭证)
node tests/check-account-sync.mjs # 账号库自动同步(重读节拍 + 内容签名:变了才重建列表)
node tests/check-account-groups.mjs # 账号库分组(读盘容错 / 建改删重名 / 分区排序含当前组置顶)
node tests/check-accounts-view.mjs # /accounts 的 sections 必须是「可直接渲染的视图」(标题不能退化成 id)
node tests/check-login-fresh.mjs # 登录前按需清理登录态(清 profile + 清分区,幂等语义)
node tests/check-unexecuted-program.mjs # 判「模型把要执行的程序写进了正文」(正反两侧都钉)
node tests/check-relogin-integrity.mjs # 重登不能损坏记录(保住显示名 / 失效账号先清登录态)
node tests/check-accounts.mjs # 账号库(去重/切换/移除/导入导出/旧文件迁移)
node tests/check-smoke.mjs # 新模块能否被独立加载(循环依赖 / 版本号漂移)
CI(.github/workflows/ci.yml)在每次推送到 main 与每个 PR 上跑上面两条命令;
打 v* tag 由 .github/workflows/release.yml 自动创建 Release 并附上 tgz(用仓库自带的 token,维护者无需持有个人令牌)。
多条断言直接固化自真实事故现场:
- 工具调用里含未转义 Windows 路径,曾导致解析失败、标记泄漏成正文 → 现在必须解析成功且路径逐字还原
- SSE 去重模型错误,曾把完整回答丢成「,」「不上」「了一圈」这类 1~3 字碎片(并触发 EMPTY_RESPONSE 重试)
→ 现在「缩水快照」与「分歧快照」都必须被忽略,回答一字不丢(见 CHANGELOG.md 0.1.1) - 跨包工具调用标记的 hold-back 判断失效,曾让合法 JSON 泄漏成正文(分块把
{"tool与_calls":…切开时)
→ 现在用真实会话日志的分块序列回归(见CHANGELOG.md0.1.2) - 模型漏写调用对象的闭合括号(批量调用时每个少一个
}),曾让整段调用 JSON 泄漏成正文
→ 现在结构性补括号(仅当数组已闭合,被截断的流绝不补)+ 解析失败不再吐成正文(见CHANGELOG.md0.1.3) - 会话被自己提前删除(建会话后立刻排定 1.5s 后删除 → PoW+建连超时就删掉了正在用的会话)
→ 现在删除只发生在流结束之后;会话失效还会换新会话透明重试(见CHANGELOG.md0.1.4) envelopeError只看外层code,把data.biz_code里的真实错误吞掉(invalid chat session id/user is muted都被降级成看不懂的「非流式响应」)
→ 现在识别biz_code,并区分「可恢复」「需等待」「重试没用」三类(见CHANGELOG.md0.1.4)
故障排查
| 现象 | 处理 |
|---|---|
| 面板「未登录」但登录窗口里已登录 | 点「从已登录窗口恢复」;或看 loginProgress.lastError |
AUTH / 40003 Authorization Failed |
这个账号的凭证被服务端判失效了。0.6.8 起:自动换号开着、且库里还有别的可用账号时,会自动换过去把这一轮接着跑完(不再需要你手动切号;日志里会写「将自动换到另一个可用账号重试」)。否则请重新登录这个号,或点它那一行的「切换」换一个能用的号 |
| 以前会自己换号,现在不换了(尤其是刚换过 DSH 环境 / 重装过之后) | 自动换号的设置存在 ~/.dsh/web-login/gate.json,换环境不会跟着迁过去 ⇒ 静默回到默认的「关闭」,表现为"以前会自己换号,现在不换了"。去「设置 → DeepSeek 网页登录 → 防风控」把「自动换号」重新拉起来即可(拉动一次就会写回该文件) |
MISSING_CREDENTIAL |
凭证文件不存在(~/.dsh/web-login/) |
EMPTY_RESPONSE |
可能触发频控或长上下文截断,属于默认可重试码 |
RATE_LIMIT |
免费额度频控,稍后重试 |
报错里带「临时限制」/ user is muted |
账号被网页端临时限制(不是插件问题):登录态有效、建会话也正常,只有发消息被拒。消息里会给出解除时间;等解除或改用其它账号/官方 API key |
「浏览器窗口登录」点了没反应,host 日志有 fromPartition 报错 |
DSH 把插件宿主挪到了 utility 进程(没有窗口 API)→ 0.1.7 起改用真实 Edge/Chrome + CDP 登录:面板会显示「宿主进程」,按钮变成「用 Microsoft Edge 登录」。升级插件 + 重启 DSH 即可 |
| 面板显示「Cookie / 指纹头 未捕获」 | 说明你走的是手动粘贴 token 那条路(该路径本来就没有这两项)。实测仅凭 Bearer token 即可完成校验、PoW 求解与真实生成;若日后频繁遇到 AUTH / 40003,改用「浏览器登录」获取更完整的凭证(token + cookie + 指纹头) |
报错 A message is being generated, please try again later. |
这个报错本身不是封号:同一账号同时只能生成一条消息(另一个窗口/标签页正在生成),0.1.9 起自动重试。但别把多开当常态 —— 多窗口并发会触发服务端的临时封禁(1 天),见已知限制;建议一个账号只保留一个窗口,多开请换账号或换 provider |
正文里出现 … / … / … |
模型在模仿系统消息格式(与转写回声同类)。这些标签 DSH 从来不产生(app.asar、已装插件、~/.dsh 原始字节搜索 0 处;会话日志里只出现在模型的输出字段),所以一律当垃圾剥离、不上屏。清单只收有现场证据的名字,不做通配 —— 否则会吃掉正常回答里讨论这些标签的段落 |
| 回复在句中截断但没报错 | 服务端在句中截断、却仍然发了 FINISHED(实测 12s 内即断,不是 60s 上限)。这类截断没有可靠信号,只能靠启发式判断尾部字符:以「,」「、」「;」「:」(中英文逗号/顿号/分号/冒号)收尾 = 明显未写完 → 自动续写;以「。」「!」「?」「)」「」」等句末标点收尾 = 认为正常结束(… 也按正常结束处理,因为省略号也可能是有意的收束语气)。判定为未完时会自动发起新请求接着写并拼进同一条回答,不再出现「可能被截断」提示。真被切断(没有 FINISHED)时走另一条判据,不受上面的尾部规则影响 |
回复里出现 {"tool_calls":…} 或 `` 标记 |
模型格式漂移。解析器已两族兼容 + 修复兜底;若仍出现请把原文贴进 issue(解析不出时不再把 JSON 吐进正文:本轮无其他正文则自动重试,已有正文则给一句提示) |
报错 DeepSeek 网页端错误(code 10):too many ref file |
这一次请求要引用的图片太多了。图片是请求级的(一次请求用 ref_file_ids 带一批),网页端对这一批有数量上限(实测 40 张通过 / 52 张被拒)。0.1.77 起默认只带最近的 24 张,被略过的会在 prompt 里标成 [earlier image omitted] 并给一句说明,所以正常情况下不会再撞上。⚠️ 如果是在旧版本上撞到的:那几轮会连续失败,且**该会话此后 |