网页登录模型接入 cv-superding/dsh-deepseek-web-login
DeepSeek Harnessv1.0.3把 chat.deepseek.com 网页版接进 DSH:用浏览器登录态不是 API Key驱动 agent…
66.2/100
综合分
285
GitHub 星标
1,532
npm 周下载
—/5
用户评分
数据截至 2026/10/18 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dsh-deepseek-web-loginnpm 源 · dsh-deepseek-web-login 的发布者账号已校验与本仓库一致
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:已验证
是什么
dsh 原生插件 · browser
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 16 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✓npm 包dsh-deepseek-web-login @ 1.0.3
✓Node 引擎要求 ^22.18.0 || >=24.11.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/29 15:33:47
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 132 处,展示前 41 条)
- 写入本地文件lib/index.js:211
mkdirSync(join(file, ".."), { recursive: true }); - 写入本地文件lib/index.js:213
writeFileSync(tmp, JSON.stringify(value, null, 2), { - 写入本地文件lib/index.js:218
renameSync(tmp, file); - 删除本地文件lib/index.js:221
rmSync(tmp, { force: true }); - 写入本地文件lib/index.js:226
chmodSync(file, 384); - 读取本地文件lib/index.js:231
if (!existsSync(file)) return void 0; - 读取本地文件lib/index.js:232
return JSON.parse(readFileSync(file, "utf8")); - 读取本地文件lib/index.js:308
names = readdirSync(accountsDir()).filter((name) => name.endsWith(".json") && !name.includes(".tmp-")); - 读取本地文件lib/index.js:337
return existsSync(accountFilePath(activeId)) ? activeId : void 0; - 读取本地文件lib/index.js:348
if (!existsSync(accountFilePath(id))) return false; - 读取本地文件lib/index.js:377
if (!existsSync(file)) return false; - 删除本地文件lib/index.js:379
rmSync(file, { force: true }); - 删除本地文件lib/index.js:566
rmSync(legacy); - 写入本地文件lib/index.js:1150
mkdirSync(join(file, ".."), { recursive: true }); - 写入本地文件lib/index.js:1151
writeFileSync(file, JSON.stringify(settings, null, 2) + "\n", "utf8"); - 发起网络请求lib/index.js:4375
const res = await fetch(`http://127.0.0.1:${port}${endpoint}`, { - 执行外部命令来自配置/变量lib/index.js:4539
child = spawn(browser.path, buildBrowserArgs(profileDir, `${DS_BASE}/`), { - 删除本地文件lib/index.js:4712
rmSync(profileDir, { - 删除本地文件lib/index.js:4722
rmSync(profileDir, { - 执行外部命令固定命令lib/index.js:5076
execFile("taskkill", [ - 删除本地文件lib/index.js:5112
rmSync(retired, { - 发起网络请求lib/index.js:5141
if ((await fetch(`http://127.0.0.1:${port}/json/version`, { - 发起网络请求lib/index.js:5155
const page = (await (await fetch(`http://127.0.0.1:${port}/json/list`, { signal: AbortSignal.timeout(2e3) })).json()).find((t) => t.type === "page" && - 执行外部命令来自配置/变量lib/index.js:5383
const child = spawn(browser.path, transportBrowserArgs(PROFILE_DIR, transportPageUserAgent()), { - 发起网络请求lib/client.js:953
return await (await fetch(API + path, { - 读取本地文件scripts/prepare.mjs:15
if (existsSync(join(ROOT, 'lib', 'index.js')) && existsSync(join(ROOT, 'lib', 'client.js'))) { - 执行外部命令来自配置/变量scripts/prepare.mjs:20
const result = spawnSync(process.execPath, [join(ROOT, 'scripts', 'build.mjs')], { - 读取本地文件scripts/build.mjs:26
const pkg = JSON.parse(readFileSync(manifest, 'utf8')) - 执行外部命令来自配置/变量scripts/build.mjs:35
const result = spawnSync(process.execPath, [tsdown.path, '--config', 'tsdown.config.ts'], { - 读取本地文件scripts/build.mjs:46
if (existsSync(libDir)) { - 读取本地文件scripts/build.mjs:47
for (const name of readdirSync(libDir).sort()) { - 读取本地文件scripts/build.mjs:49
const size = statSync(join(libDir, name)).size - 读取本地文件scripts/make-dev-copy.mjs:17
const base = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')) - 读取本地文件scripts/make-dev-copy.mjs:25
if (!existsSync(join(ROOT, file))) { - 读取本地文件scripts/make-dev-copy.mjs:31
if (existsSync(target)) rmSync(target, { recursive: true, force: true }) - 删除本地文件递归scripts/make-dev-copy.mjs:31
if (existsSync(target)) rmSync(target, { recursive: true, force: true }) - 写入本地文件scripts/make-dev-copy.mjs:32
mkdirSync(join(target, 'lib'), { recursive: true }) - 写入本地文件scripts/make-dev-copy.mjs:35
copyFileSync(join(ROOT, file), join(target, file)) - 写入本地文件scripts/make-dev-copy.mjs:37
copyFileSync(join(ROOT, 'cordis.patch.yml'), join(target, 'cordis.patch.yml')) - 写入本地文件scripts/make-dev-copy.mjs:43
writeFileSync(join(target, 'package.json'), JSON.stringify(pkg, null, 2)) - 写入本地文件scripts/make-dev-copy.mjs:44
writeFileSync(
扫描于 2026/11/3(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
界面预览
2 张 · 点击查看大图功能亮点
- 浏览器登录态接入
- PoW求解
- SSE流式输出
非官方 DSH(DeepSeek Harness)插件:把 chat.deepseek.com 网页模型接入为 LLM 服务商——浏览器登录抓取、PoW 求解、SSE 流式输出、基于提示词的工具调用。
最近更新
在 GitHub 查看全部 →v1.0.12026-10-18
这一版两件事:修一个只在深色主题下暴露的显示 bug,并把卡片 / 按钮 / 账号行的材质
统一到 dsh-our-free-model 那套语言。主色没变(仍跟随宿主的 --dsw-alias-brand-primary),
借的只是结构,不是配色。
· 修 bug:「网页版」页签的启动卡片在深色下是白底黑字
· 根因是 GuestTab 的内联样式用了三个 DSH 里不存在的令牌名:
--dsw-alias-fg-muted(4 处)/ --dsw-alias-fg-base / --dsw-alias-bg-l2。
名字不存在 ⇒ var() 永远落到硬编码兜底(#fafafa / #222),而那组兜底只对浅色主题成立。
这与 AGENTS.md 2.1 记的是同一类坑(那次是 --theme-*)。
· 对照证据:本文件 CSS 层用的是正确的 --dsw-alias-label-* / --dsw-alias-bg-layer-*;
dsh-our-free-model 里 --dsw-alias-label-secondary 出现 24 次、--dsw-alias-bg-layer-2 15 次,
而这三个错名字 0 次。
· 那 6 处内联样式改为类名(.dsw-startcard 系列),颜色全部回到语义层;顺带把两处
写死的错误色(#b3261e / rgba(179,38,30,.08))换成 var(--err) / var(--err-weak)。
· 观感:借 Our Free Model 的材质语言
从 dsh-our-free-model 的 client.js 里提取到的手法,逐条对应:
| 手法 | OFM 原值 | 本版 |
|---|---|---|
| 卡片顶部光条 | .ofm_chan::before,2px,linear-gradient(90deg, accent, transparent 78%) | 抽成 --chan-sheen |
| 玻璃底 | color-mix(in srgb, bg-layer-3 78%, transparent) | 抽成 --glass |
| 内高光 | inset 0 1px 0 rgb(255 255 255 / 5%) | 抽成 --sheen |
| 悬停 | translateY(-3px) + 投影加深 + 描边染主色 | 卡片 -3px、账号行 -2px |
| 动效曲线 | --ease-instrument: cubic-bezier(.16,1,.3,1) | 同(新增),另加 --ease-settle |
| 胶囊选中 | box-shadow: inset 0 0 0 1px bd2 | 标签页选中态 |
· 按钮默认长相改了:从实心主色改为玻璃描边,实心收敛到新增的 .dsw-btn.primary。
原因是一屏七八个实心主色块会抢走本该给「当前账号」和「告警」的注意力。
若不同意,把 .dsw-btn 的基态 + :hover + :active 三条规则整段换回原版即可 ——
只改基态会让 hover 从实心跳到几乎透明(color-mix(--accent 8%, --glass)),比不改更难看。
这段回退已实测:换回后三条规则与 cb61277 逐字相同,其余 8 项改动不受影响。
· 圆角档位对齐 OFM:--r-lg 12→16、--r-md 8→9,新增 --r-pill:999px。
这套比例让「容器」和「控件」一眼分得开。
· 验证
· node scripts/build.mjs exit 0;重建后 lib/ 无变化(产物与源码一致)。
· node scripts/test-offline.mjs 75/75 用例文件通过。
· node tests/check-bundle.mjs 产物核对全部通过;tests/check-smoke.mjs 通过。
· 渲染验证用的是从 lib/client.js 抽出的真实 CSS(浅深两套各渲一遍),不是只看源码。
· 没做的事
· 没动主色、没动布局与信息结构、没碰协议 / 传输 / 账号逻辑。
· 改动只在 src/client/index.ts(116+/46-)与构建产物 lib/。
v1.0.32026-10-18
传输层每次启动前会清一遍 web-login/transport-profile。旧实现是一句
rmSync(PROFILE_DIR, { recursive: true, force: true }) —— 在 Windows 上它是个定时炸弹:
清理 transport profile 失败:EPERM, Permission denied: \\?\C:\Users\\.dsh\web-login\transport-profile
报错看起来像权限/杀软问题,实际原因在别处。headless 浏览器是多进程的(主进程 +
GPU / renderer / utility / network 若干),而 Windows 的 TerminateProcess 不级联:
child.kill() 只收掉主进程,剩下的子进程成了孤儿,仍持有 --user-data-dir 下文件的
独占句柄。于是下一次启动删目录必失败 —— 而 force: true 只吞"文件不存在",不吞 EPERM。
外壳强杀(更新、崩溃重启)时尤其容易留下这种状态,一旦留下就是每次启动都失败。
· 改了三处
· 清理改「先改名、再删旧名」:renameSync 只要求父目录可写,不碰被占用的文件,
所以在"文件被锁"这种最常见卡点上仍然成功;改名后用 best-effort 删除,删不掉就随它去。
连改名都做不到(父目录本身被锁)时保留旧目录继续启动 —— 带上一次残渣也比整个
传输层起不来强。
· 收整棵进程树:Windows 上 cleanup() / 启动失败的各条出口改用
taskkill /PID /T /F,不再只 child.kill()。这是"别再留孤儿"那半边。
· 等上一批真的退出:新增一个只在 exit 事件上落定的状态位,清理前最多等 3 秒。
不靠"谁调了 cleanup"记账 —— 进程退出才是文件句柄已放开的唯一保证。
· 行为上要知道的两点
· 补丁的目标是绝不阻断启动,不是当万能清理器。删不掉是允许的结果,会留下一个
transport-profile.stale- 目录(几十 MB),下次启动再试。
· 非 Windows 平台不走 taskkill;改名路径通用,但那里本来就没有这个问题。
由 tests/check-browser-transport.mjs 的两条用例守着(check-bundle 另守"产物里
旧写法真的没了")。反向验证实测:把旧实现改回去,用例报的正是上面那句 EPERM。
· 另两件(同一批社区贡献)
· #20(m4ku1337,closes #14):looksLikeUnexecutedToolProgram 以前只认 tools.( 这种
PTC 程序体;issue #14 的现场是模型把要执行的命令行贴进 shell 语言标记的围栏当正文,
本轮零工具调用、正文又是完整句子收尾 ⇒ 两条判据都不满足 ⇒ 回合被判 completed
⇒ 用户看到「它说了要去做,然后什么都没做」。
现在补了 shell 语言围栏判据(大小写 / language- 前缀 / 参数都认)+ 注释行识别
(#、//、::、rem、块注释),带 22 项用例。
· #19 的洞察(Reuv1n):Windows 上 headless Edge 可能把启动交接给已有实例后自身退出,
而浏览器仍在、DevToolsActivePort 稍后才写出来。旧代码一看 exitCode !== null 就返回
undefined ⇒ 上层报「调试端口未就绪」⇒ 传输层整个不可用(看着像超时,其实启动成功了)。
⚠️ 那个 PR 只改了构建产物 lib/index.js(下次 build 就没了),所以没有直接合;
这条洞察已落进 src/browser-transport.ts,EPERM 那半边由 #22 覆盖。
没合的两个(#17 / #16):都是 i18n 功能 PR(英语默认 + 俄语),
属于产品决策且与当前界面改动冲突面很大,留给维护者单独评估,不随本版发布。
v1.0.22026-10-15
那晚(22:53)账号会话被吊销(AUTH/invalid token)。事后从调用台账里读出的轨迹:
22:31 → 22:53 共 49 次请求,间隔 8~12 秒(连续 22 分钟没有喘息)
22:40:00 22:40:11 22:40:23 22:40:31 22:40:39 22:40:47 22:40:54 22:41:02 …
真人在网页端做不到这种节奏(谁也不会 20 分钟里每 10 秒发一条)。而那一晚长休一次都没触发,
原因是长任务保护的"连续计数"有个 2 分钟的归零门槛,而实际节奏是「十几发一波、隔 1~4 分钟再来一波」
—— 每波都把计数洗掉了。
· 修的两个口子
· 归零门槛 2 分钟 → 10 分钟:真"人歇过"至少十几分钟,两分钟不叫歇。
· 新增滚动窗口限速(rateWindowMs / rateMaxInWindow,默认 30 分钟内最多 20 次):
超过就强制长休 60~180 秒。这个计数小间隙洗不掉 —— 它只看"这段时间一共发了多少次",
正是那晚的轨迹拦得住的那种判据。
- 两个字段进了 settings() / configure() / gate.json(面板保存与手改都认);
- 触发时日志会写明是"近 N 分钟已发 M 次",不再只有一个笼统的"长休"。
· 顺带
· npm 核验重试 6×10s → 12×15s:1.0.0 与 1.0.1 两次 Release 都是"npm 明明发成功、
60 秒内没等到 ⇒ 判失败 ⇒ Release 被跳过"。放宽到 3 分钟把传播抖动吃掉。
· (发布侧记录)1.0.1 的 Release 是事后补建的(删 tag 再推,触发幂等重跑)。
⚠️ 这不是"防封"的银弹:封号是行为特征问题,限速只是把最像脚本的那部分(连续小间隔)削掉。
真正的建议仍在 README「已知限制」:一个账号同时只开一个窗口,别长时间连轴转。
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。
README
由 DeepSeek 最新模型翻译生成中文 · English
把 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。
展开完整文档(README 剩余 1 节)
中文 · English
把 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- 连字符、裸 <invoke>、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 粘贴。
捕获成功后窗口自动关闭,面板显示已登录。
💡 这个按钮每次都会先清掉上次的登录状态 —— 登录用的是独立配置档(~/.dsh/web-login/browser-profile,不碰你日常 Edge 的 cookie 与历史),所以窗口一打开就是干净的登录页,不会带着上一个账号。
想省一次输入(凭证明明还有效、只是要重新捕获)就用账号行上的「重登」——那条路默认不清,浏览器里若还留着登录态会立刻复用、一个密码都不用敲。
⚠️ 但如果这条账号已经被标记失效(红标「需要重新登录」),它就会先清掉再让你重新登录 ——因为此时浏览器里那份登录态也已经不可用,复用只会把同一个坏凭证再抓一遍(会陷入点多少遍都失败的死循环)。
- 凭证只存在本机 ~/.dsh/web-login/deepseek-auth.json,不在仓库里;面板「当前账号 → 退出当前账号」一键清除(同时清掉浏览器分区里的登录态,保证真退出、可换号)
- 凭证丢了或校验不通过:点 从已登录窗口恢复(复用上次登录的持久化分区,无需重新登录)
- 非 Electron 环境(纯 web 配置档):用面板里的手动粘贴 Token通道
3. 选模型开跑
模型选择器 → 服务商 DeepSeek 网页版(免费) → DeepSeek 网页 · 快速模式,然后照常用智能体(工具调用、思考流、贴图都可用)。
⚠️ 一个账号同时只开一个聊天窗口:多开并发会触发网页端临时封禁(1 天)——要开多个窗口就换账号,或把多余的窗口换到别的服务商。详见已知限制。
4.(可选)开着「网页版」页签:让一个真页面一直活着
主区页签栏里(与 对话 / 轨迹 / LDW 并排)有一个 「网页版」 页签:里面是 DSH 自带的 Chromium 加载的 chat.deepseek.com 本体——真页面、真 JS、真指纹。它的作用不是"回显聊天"(发送仍走原有通路,网页端的回答不会显示在这里),而是保持一个真人网页端会话活着,并把它的登录态交给插件使用。
怎么用(一次就够):
1. 打开「网页版」页签 —— 它会自动向 DSH 申请内置浏览器(默认开,设置页「浏览器」页可关)。
首次会看到转圈 + 「正在恢复内置浏览器…」,几秒后页面就上来了;
2. 在里面登录一次(手机号 / 扫码都行)。登录完成后约 10 秒内:
- 插件把页面的登录态(localStorage + cookie)存到本机(~/.dsh/web-login/guest-storage.json);
- 同时把 token 与身份同步进账号库:新号会自动入库并切为当前账号(「账号」页能看到捕获时间 / 指纹头 / token 长度);
3. 以后重启 DSH 不用再登 —— 分区是内存的(DSH 原生设计),但插件会在挂载时把快照装回页面并刷新,页面直接回到登录态;
4. 想看手动入口:设置页 → 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-<uuid>,没有 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 时它不会跟着迁过去(账号库也不会 —— 用「导出备份 / 导入备份」搬),重启后会回到"关闭",表现为"以前会自己换号,现在不换了"。
导出的备份文件里是可完整登录的凭证,等同于账号本身 —— 别分享、别提交到仓库。
「登录新账号」和「退出」的区别(很容易搞混,但后果完全不同):
- 登录新账号(添加) —— 只清掉浏览器里的登录态,然后把新账号加入账号库但不切换。
你正在用的号不受影响;加完在列表里点「切换」才会用它。(多账号并存靠它。)
- 退出 —— 等于把该账号从账号库移除:本地凭证与浏览器登录态一起清掉,不是"只登出"。
想留住它就先「导出备份」。
另外「账号」页分成两个子页(登录状态 / 账号库),一次只看一半,不用滚很久。
ℹ️ 账号显示名用的是"网页端返回的屏蔽值":接口只给 19227、 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.<ext>(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.md 0.1.2)
- 模型漏写调用对象的闭合括号(批量调用时每个少一个 }),曾让整段调用 JSON 泄漏成正文 → 现在结构性补括号(仅当数组已闭合,被截断的流绝不补)+ 解析失败不再吐成正文(见 CHANGELOG.md 0.1.3)
- 会话被自己提前删除(建会话后立刻排定 1.5s 后删除 → PoW+建连超时就删掉了正在用的会话) → 现在删除只发生在流结束之后;会话失效还会换新会话透明重试(见 CHANGELOG.md 0.1.4)
- envelopeError 只看外层 code,把 data.biz_code 里的真实错误吞掉(invalid chat session id / user is muted 都被降级成看不懂的「非流式响应」) → 现在识别 biz_code,并区分「可恢复」「需等待」「重试没用」三类(见 CHANGELOG.md 0.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 |
| 正文里出现 <ds_system>…</ds_system> / <system>…</system> / <ide_result_status>…</ide_result_status> | 模型在模仿系统消息格式(与转写回声同类)。这些标签 DSH 从来不产生(app.asar、已装插件、~/.dsh 原始字节搜索 0 处;会话日志里只出现在模型的输出字段),所以一律当垃圾剥离、不上屏。清单只收有现场证据的名字,不做通配 —— 否则会吃掉正常回答里讨论这些标签的段落 |
| 回复在句中截断但没报错 | 服务端在句中截断、却仍然发了 FINISHED(实测 12s 内即断,不是 60s 上限)。这类截断没有可靠信号,只能靠启发式判断尾部字符:以「,」「、」「;」「:」(中英文逗号/顿号/分号/冒号)收尾 = 明显未写完 → 自动续写;以「。」「!」「?」「)」「」」等句末标点收尾 = 认为正常结束(… 也按正常结束处理,因为省略号也可能是有意的收束语气)。判定为未完时会自动发起新请求接着写并拼进同一条回答,不再出现「可能被截断」提示。真被切断(没有 FINISHED)时走另一条判据,不受上面的尾部规则影响 |
| 回复里出现 {"tool_calls":…} 或 <tool_calls> 标记 | 模型格式漂移。解析器已两族兼容 + 修复兜底;若仍出现请把原文贴进 issue(解析不出时不再把 JSON 吐进正文:本轮无其他正文则自动重试,已有正文则给一句提示) |
| 报错 DeepSeek 网页端错误(code 10):too many ref file | 这一次请求要引用的图片太多了。图片是请求级的(一次请求用 ref_file_ids 带一批),网页端对这一批有数量上限(实测 40 张通过 / 52 张被拒)。0.1.77 起默认只带最近的 24 张,被略过的会在 prompt 里标成 [earlier image omitted] 并给一句说明,所以正常情况下不会再撞上。⚠️ 如果是在旧版本上撞到的:那几轮会连续失败,且该会话此后每一轮都失败(图还留在历史里,每轮重发都超标)—— 但升级到 0.1.77 之后同一个会话就能继续用:新请求只带最近的 24 张,历史里那些旧图不再被引用。在升级之前,唯一出路是新建会话。⚠️ 0.1.79 起改说「份图片内容」而不是「张图片」—— 那个数字是内容条目(含模型读图产生的局部片段),不是你自己贴的张数;0.1.81 起这句提示按阶梯上屏,只在「首次」与「被略过的份数翻倍(且至少再多 10 份)」时出现,不会每轮刷 |
| 报 code 9 / invalid ref file id,而且此后每一轮都失败 | 请求带的图片引用被服务端拒了。0.1.78 起会自动两级降级(先清掉那几张的缓存重传、再不带图重发),正常情况下你不会再看到这个错误;若仍出现,说明两级都用尽了,换个会话继续即可 |
| 模型把要执行的程序写进正文、该轮什么也没做就结束了 | 冷启动的新会话 + 第三方预设可能触发:有些预设会在系统提示里写「你在 Programmatic Tool Calling 模式,所有动作都要靠 run_code 写 TypeScript 程序完成」,模型于是把程序的代码当正文贴出来、而不作为工具调用发出 —— 这一轮零工具调用,agent loop 就判定回合结束。0.1.74 起会自动识别这种形态(围栏代码块内含 tools.<名字>(…))并追加一轮纠正请求,只纠正一次;关掉 autoContinue 可一并关掉 |
| 发消息一直失败,日志里一串「N 张图片没能传给模型(Authorization Failed (invalid token))」 | 这个账号的登录态早就失效了(探活已经写进账号记录),而旧版本在发请求前不看这块牌子 —— 于是白跑一整轮:每张图都要先求一次 POW、再发一次上传。0.1.80 起请求发出前就会拦下来,直接给你一句「这个账号的登录态已失效(…),本次请求没有发出」并指出两条出路。⚠️ 判定只认授权类失败,断网 / 超时 / 5xx / 429 一律放行 —— 一次网络抖动不会锁住健康账号。0.1.80 同时让授权失败时不再逐张重试剩下的图(同一 token 上传剩下的一定也会被拒),并在告知语里写清「剩余 N 张未再尝试」 |
| 加完账号,账号库列表里没出现 | 面板每 3 秒会自动重读一次账号库(登录流程进行中),捕获一落地就会自己出现 —— 不用关掉设置页再打开(0.1.67 前必须重开)。空闲时是 30 秒一次,用来让探活补上的账号名 / 限制状态 / 失败标记自己更新。列表内容没变时不会重建 DOM,所以不会打断你正在点的按钮 |
| 回答看起来「说了一半就停了」(末尾有 [deepseek-web] 本轮有一部分「历史回放格式」的内容被过滤…) | 模型在正文里开始复读历史([Tool Result for …] / User: / Assistant: 这类转写格式),守卫从命中行起砍到结尾,所以后半段被丢掉了。0.1.70 起:① 只是在正文里引用一次工具结果不再触发(正常写作会保留 —— 之前会把整段回答一起吞掉);② 真被砍掉时会追加那行告知,不再静默。局限:单独写在行首的引用与真回声在字节层面无法区分,仍会被砍 —— 但你会看到提示。想彻底避免:提问时说一句「不要贴工具返回原文」 |
| 模型停住不动,只能手动敲「继续」 | 两种成因,0.1.70 起都不再静默:① 整轮只有思考(正文与工具调用都没有)—— 以前被当成「正常完成」直接结束回合,现在报可重试的 EMPTY_RESPONSE,DSH 会自动重发;② 回答被回声守卫砍掉后半段 —— 会在末尾追加一行告知(见上一条)。若重发仍失败,你会看到明确的失败提示 |
| 工具调用不触发 | 换说法或换 deepseek-reasoner;也可用面板「发送测试」确认链路 |
诊断工具:tools/inspect-session.mjs
怀疑「回复不对」时,不用猜 —— 直接读 DSH 的会话日志,看每一轮的原始事实:用了哪个模型、内容块的真实长度与首尾、结束原因、以及流式分块的类型与字节数。
node tools/inspect-session.mjs # 列出会话(时间 / 大小 / 事件数 / 标题)
node tools/inspect-session.mjs <会话ID前缀> # 诊断该会话的每一轮
node tools/inspect-session.mjs --search "关键词" # 按关键词找会话
它只读本机会话日志(不联网、不上传)。典型输出:
[14:04:01] 模型: deepseek-web/deepseek-chat ctx=1048576 [14:04:01] 分块原文(文本): ["I'll check what plugins exist"," for this in the DSH ecosystem", ", and also look at the current"," GUI's capabilities.\n\n}]}"
三条经验判据:
- 文本长度 远小于 分块字节数 → 解析层丢字
- 文本里出现 {"tool_calls":…} / <tool_calls> → 工具调用没被接住(格式漂移或 hold-back 失效)
- 同一 step 出现 多次 usage / finish → 触发了重试(通常是空响应)
报 bug 时请附这段输出(它不含凭证;如有敏感内容请先自行删减)。
上面三个真实事故(工具调用 JSON 泄漏、回答丢成碎片、合法 JSON 泄漏)都是靠它定位的。
诊断:传输层(net.fetch / TLS 指纹)
网页端请求默认由 Node 的 fetch(undici)发出,指纹与真实浏览器结构性不同。
想确认「换成 Chromium 网络栈」是否可行时,跑一次探测 —— 默认零额度,不生成、不发消息:
宿主进程的 HTTP 端点只有 DSH 自己的页面打得通(外部 curl 会撞同源守卫),所以走文件这条路:
echo '{"mode":"probe"}' > "$HOME/.dsh/web-login/probe-request.json"
重启 DSH,日志里会打出 deepseek-web: [net-fetch 探测] {...}
三步各看一个结论:
| 步骤 | 看什么 | 判定 |
| ① 指纹 | ja4 / http_version / http2_hash | 变成 t13d…h2… 且带 GREASE = 与 Chrome 一致 |
| ② 流式 | hasBody / chunks / abortedEarly | 三项都为真才能读 SSE(否则改造路线不成立) |
| ③ 鉴权 | status / body | 200 且能读出账号 = header/cookie 原样透传 |
(设置页「传输层(指纹)」卡里也有一键测试,走的就是这个接口。)
想连 DeepSeek 的 SSE 一起端到端验证(会消耗一点额度):把 probe 换成 stream。
文件被读取后会改名为 probe-request.json.done-<时间戳>,不会每次启动都重跑。
开发
npm ci # 安装开发依赖(首次 / 换版本后)
node scripts/build.mjs # 构建 host(lib/index.js) + client(lib/client.js)
node scripts/test-offline.mjs # 全量离线用例
node tests/check-bundle.mjs # 产物核对
node scripts/make-dev-copy.mjs <后缀> # 生成开发副本(见下)
构建走本地已安装的 tsdown(不再 npx 联网下载、不再要求 Bash,Windows 直接可用);缺依赖时会明确报错并提示执行 npm ci。
迭代注意(实测坑):当前 DSH 版本移除了热重载所依赖的 loader API,而 Node ESM 模块缓存以「解析后的文件路径」为键 —— 同一路径重新注入仍会命中旧模块实例。改代码后需换包名/换路径注入: node scripts/make-dev-copy.mjs a1 生成副本,配合 DSW_PLUGIN_ID 同步改 client 模块 id。
装配要点:包内 cordis.patch.yml 会自注册 entry,不要再手动 insert(会撞 duplicate loader entry id); DSH Desktop 用的是 desktop profile,而注入器的 junction 默认建在 profiles/web/,必要时手动补 junction。
文档索引
| 文档 | 内容 |
| README.en.md | English README |
| LICENSE · NOTICE | Apache-2.0 全文 · 版权与第三方声明 |
| docs/assets/ | 本文所有示意图(SVG 源文件,可自行改) |
| src/protocol.ts | 工具协议与流式过滤器(含每条修复策略的注释与实测案例) |
| src/webapi.ts | PoW / 会话 / SSE / 文件上传(含协议字段与端点注释) |
| src/login.ts | Electron 登录窗口与凭证捕获(含 AppKit token 解包等踩坑注释) |
贡献
欢迎 Issue / PR。提交前请先跑一遍上面的测试命令。请注意:
- 不要在 issue、日志或截图里附带 token / cookie 等凭证
- 报告工具调用格式问题时,请贴模型原始输出(含标点与反斜杠),那是最有价值的线索
致谢(协议情报来源)
本插件实现为原创代码,但网页端私有接口的行为(PoW 与 WASM 求解约定、SSE patch 流结构、文件上传与 ref_file_ids、DSML/XML 工具标记变体)参考了以下公开项目的文档与实现,并逐项实测验证。
这些项目的源码未包含在本仓库中:
- LLM-Red-Team/deepseek-free-api —— 最早的网页端逆向 API 实现
- Fly143/deepseek-free-api —— PoW 求解器、DSML 工具标记、SSE 格式注释
- ForgetMeAI/FreeDeepseekAPI —— 浏览器登录捕获、PoW WASM 调用约定
同时感谢 DeepSeek Harness 生态与 dsh-super-injector(运行时注入 / 侧挂开发链路)。
关于「装了这个插件之后封号了」
这个归因在群里出现过几次。先把上下文的各部分量级摆出来,再判断该怪谁。
0.9.2 更新:本插件自己这边确实有一个"只在插件里出现"的特征,已经删掉了。
每个请求都带 x-deepseek-harness: deepseek-harness (+https://github.com/…) ——这个头不是任何规范要求的(harness 自己的归属决策写明归属只用标准 User-Agent,并明确"别按类比复用别的头";DeepSeek 专属那条决策只拥有 -user-id / -session-id),而且只有插件会带:服务端一行 if 就能把"网页版"和"插件"两条流量分开。
姊妹项目 dsh-webin 的 README 把它列为最高风险单点(原话:"伪造的 x-deepseek-harness 头")。
0.9.2 起不再发送(同时删掉真实浏览器根本不发的写死 x-app-version),并由 tests/check-request-headers.mjs 用"发出去的头集合必须等于真实浏览器的集合"守着 ——多一个都算缺陷。
⚠️ 这仍然不是"封号由它造成"的证明:本地只能证明"发出去的头里不再有自报家门头"。
另外不是 PoW —— x-ds-pow-response 是官方自己的 wasm 跑在官方自己的页面里算出来的(0.6.42 起),同一算法、同一答案,服务端拿不到区分点。
本插件这一侧到底占多少
| 来源 | 量级 | 由谁决定 |
| 工具协议指令(本插件自己写的文本) | 3,045 字符 ≈ 950 token | 本插件,多个版本未变 |
| 工具目录(本插件负责渲染,内容来自 DSH 下发的工具) | 0.6.2 起长尾工具压成一行,同口径实测 -50% | 你装了多少工具插件 |
| 以上 + system 头合计(61 个工具的环境) | 约 6.4 万字符 ≈ 1.6 万 token | 大头是工具目录,不是协议 |
能记在本插件账上的,只有那 3,000 字符。
真正把上下文撑大的,通常是别的东西
- MCP 服务器:每接一个,它的工具 schema 就整份进上下文,而且是每一轮都在
- Skills:装得越多,system prompt 越厚
- 越狱 / 破甲类提示词:这类文本动辄上万 token,且会一直留在上下文里
- 全局注入文件:~/.dsh/prompt-inject.md(本机实测 41 KB)+ ~/.dsh/AGENTS.md(7 KB)—— 这两个文件是每轮整份进 system prompt 的,合计约 49 KB ≈ 1.2 万 token,是本插件协议文本的 16 倍
上下文越大 ⇒ 每轮请求越重 ⇒ 越容易撞上风控的体量与密度判定。这部分与本插件无关,本插件也管不到。
维护者本机实测(供参考,不是担保)
连续 5 天日常使用(含工具调用密集的会话)未被封禁;单日台账 178 次请求里有 6 次「网页版限流」,全部在退避后自动恢复。区别很重要:限流是常态、可自动恢复;封禁是另一回事。
怀疑自己中招,按这个顺序自查
1. 数一下装了哪些 MCP 服务器、哪些 skill —— ~/.dsh/profiles/desktop/cordis.patch.yml 里逐行可见
2. wc -c ~/.dsh/prompt-inject.md ~/.dsh/AGENTS.md —— 看这两个文件的体积
3. 打开设置页的「Token 统计」,看单轮输入量级(本机中位数在 10 万 token 上下,工具目录只占其中一小块)
4. 把用不到的 MCP / 插件行禁用掉(- id: xxx + disabled: true,重启生效),再观察
本插件能做的(压密度、串行工具调用、会话清理、长尾工具瘦身)都写在上面「请求节流」等章节里; 剩下的大头,只能从你自己的上下文里省。
免责声明
⚠️ 非官方项目:与 DeepSeek 无任何关联,未获其授权、认可或赞助。"DeepSeek" 为其权利人商标。
⚠️ 使用风险自负:本插件调用 chat.deepseek.com 的网页端私有接口(非官方 API),可能违反其服务条款,并可能导致账号被限流或封禁。请自行评估、遵守其条款,仅供学习研究与个人使用。
⚠️ 凭证安全:本仓库不含任何凭证;登录态由你在本机登录后捕获,存放于 ~/.dsh/web-login/。
⚠️ 按现状提供:接口随时可能变更导致失效,作者不提供任何担保(见 Apache-2.0 §7)。
许可证
Apache License 2.0 —— 含专利授权与专利报复条款;不授予商标权(§6)。版权与第三方说明见 NOTICE。
交流群
用法讨论、蹲更新,或者踩到坑想吐槽,欢迎加 QQ 群:
QQ 群号:1124773537
unofficial plugin · not affiliated with DeepSeek · use at your own risk预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
cv-superding/dsh-deepseek-web-login
把 chat.deepseek.com 网页版接进 DSH:用浏览器登录态不是 API Key驱动 agent…
相关插件 · 浏览器
智能体插件框架deepseek-ai/deepseek-harness
DeepSeek Harnessdsh是由 DeepSeek AI 开发的开源 agent harness智能体框架。
★ 270,422已汉化DSH
71.9
多设备 Agent 看板YaoApp/yao
✨ 所有 Agent 和工作区集中管理,覆盖你的每一台设备。任务看板追踪进度,桌面端、移动端、浏览器、API…
★ 8,147已汉化DSH
71.2
可扩展侧边栏工作台omdsh-dev/DSH-better-sidebar
DSH 的双工作台:原生右侧栏 + 自绘底部面板,并把 ctx.betterSidebar 服务开放给所有插件
★ 4,761已汉化DSH
71.1
浏览器自动化桥Tencent/BrowserSkill
你的 Agent。你的浏览器。连接,就能协作。
★ 9,928已汉化DSH
71
AI 小说创作工作台EthanYoQ/AI-Novel-Writer
AI 小说作家 / AI Novel Writer
★ 1,591已汉化DSH
70.7
智能体浏览器引擎platonai/Browser4
DeepSeek Harness 集成
★ 1,175已汉化DSH
70.4