DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Webin — DeepSeek Harness 插件(DSH Plugin)
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins
W

dsh-webin

Webin

非官方 DSH 插件:在真实浏览器(DSH webview guest)中操作 chat.deepseek.com——真实登录、真实输入、真实发送,并从页面捕获回复。可作为 DSH 模型提供商(LDW)使用。

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add github:cv-superding/dsh-webin#dfa52627e9bec84f269e9fa78e8e342a98dea0e3
README兼容性版本

兼容性与来源证明

Webin 以 dsh-webin 发布,当前版本为 0.2.25。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
web
发布来源
github
Registry 更新时间
2026/10/16

版本

0.2.25stable
2026/10/16
0.2.24stable
2026/10/15
0.2.23stable
2026/10/14
查看其余 6 个版本收起版本
0.2.17stable
2026/10/14
0.2.8stable
2026/10/14
0.2.5stable
2026/10/14
0.2.0stable
2026/10/13
0.1.0stable
2026/10/13
0.0.1stable
2026/10/12

相关插件

正在加载相关插件…

最新版
0.2.25
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
未提供
文件数
未提供
Surface
web
许可证
Apache-2.0
发布源
github
GitHub
★ 10
周下载
0
最近提交
2026/10/16
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 integrations-communication 分类下经过校验的插件。

Acp App@deepseek-ai/dsh-acp-appdsh ACP 配置文件包:基于 dsh-base 的仅限自动化的 JSON-RPC stdio 和进程生命周期管理Ab Publishdsh-ab-publish浏览器自动化发布工具,可登录任意网站并发布内容,整合为一个可发布的插件包Ab Wechat Scrapedsh-ab-wechat-scrape--- description: "树外 wechat_scrape 工具:获取微信公众号文章,过滤推广区块,并将每篇文章保存为 Markdown 文件,供此机器的 dsh 配置文件维护者使用。" kind: "package-reference" ---Ds Botds-botDS Bot:DeepSeek Harness 的一组长期运行的 Bot,支持主 Bot、Bot 间消息和群聊

README

dsh-webin

Unofficial DSH plugin — 用真实浏览器驱动多个 AI 网页版(DeepSeek / 通义千问 / …)。

核心思路是不设计协议,操作界面:打开真浏览器 → 用真实输入把字打进输入框 → 页面自己发请求 → 从页面 DOM 把回复读出来 → 工具结果当普通消息打回聊天框。

没有任何一行在「调 API」 —— 没有 create_pow_challenge、没有 x-ds-pow-response、 没有 x-hif-* 回放。这是不封号的原因。

为什么读 DOM 而不是拦 SSE

插件同时有两条读回复的路,但主路径是 DOM:

怎么读什么时候能用
DOM(主)从页面渲染出来的回复容器里读,并把 <pre><code> 还原成围栏代码块随时(内容在页面上,不依赖任何注入时机)
SSE(辅)包一层 window.fetch / XMLHttpRequest 观察页面自己的流只在请求发出之前就装上 hook 才行

SSE 那条路有个绕不过去的时序问题:要在页面脚本执行之前注入 hook, 唯一的办法是 preload。而 DSH 的 webview 门禁会把 webPreferences 清空后只设固定几项(lib/main.js),preload 不在里面 —— 所以我们只能等 dom-ready,那时页面请求早就发出去了。

后果就是「页面明明有回复,插件却收不到」。所以 DOM 成为主路径, 网络拦截只是辅助信号。

选择器用的是实测出来的真实类名(ds-markdown / ds-think-content / ds-pow-response),后面才是语义兜底 —— 不靠猜。

各版本改了什么见 CHANGELOG.md。

前置条件:宿主必须提供浏览器 guest 桥

⚠️ 本插件需要一个受控的真 Chromium,而这个能力只有带 Electron 通道的宿主才有。

它取桥的方式是:

const carrier = globalThis.dshDesktop        // src/client/index.ts:922
if (carrier?.protocolVersion !== 1) return undefined
const bridge = carrier.browser                // acquire / release
宿主有桥?说明
官方 DSH Desktop(Electron 应用)✅设计目标环境
DSH-X 启动器 + 浏览器里的 Web GUI❌浏览器无法注入 Electron preload
第三方 Electron 壳(未补桥)❌默认没有桥
自建 Electron 壳(已补 webviewTag + preload 注入)✅参见下方“没有官方 Desktop 时”

在无桥宿主上的行为:插件仍然能安装并加载,模型选择器里也会出现 LDW, 但发任何消息都会失败。现在它会明确告诉你原因(而不是误导成“标签没打开”), 并在 /status 的 hostCapability 字段里报 no-desktop-bridge。

# 一条命令确认当前宿主有没有桥(在 DSH 页面按 F12)
#   有桥 -> { protocolVersion: 1, browser: {...} }
#   无桥 -> undefined
globalThis.dshDesktop

安装

# 从本地目录装进 DSH 的 desktop profile
dsh plugin --profile desktop add link:F:/Code/DSH-Code/dsh-webin/dsh-webin

# 或从 npm(若已发布)
dsh plugin --profile desktop add dsh-webin

--profile 要填你实际在用的那个。上面写 desktop 是因为 官方 DSH Desktop 用的就是这个保留名;用 dsh web 的人则是 web。 装错 profile 的典型症状就是“插件看不见”。

装完重启 DSH(host 半没有热重载)。然后在对话区会出现 LDW 标签, 模型选择器里出现「LDW · DeepSeek 网页版」。

首次使用:切到 LDW 标签 → 登录一次 → 回到对话选 LDW 模型。

没有官方 Desktop 时

两条路:

  1. 装官方 DSH Desktop —— 它天生提供桥。

  2. 给一个 Electron 壳补桥 —— 只需两步:

    // 壳的 BrowserWindow:开 webviewTag + 注入 preload
    webPreferences: { contextIsolation: true, nodeIntegration: false, webviewTag: true, preload: PRELOAD }
    

    preload 向页面暴露 globalThis.dshDesktop = { protocolVersion: 1, browser: { acquire, release, onOpenRequested } }, 并在主进程用 session.on('will-attach-webview') 做租约门禁。

    参考实现:官方 apps/desktop-host, 或一份等价的本地实现(D:\deepseekwork\dsh-desktop-shell)。

    用持久分区(persist: 前缀)而非内存分区,还能顺带解决下方 L1(重启免登录)。

设置页

侧栏 → 设置 → 「LDW 网页版」。

平台:

  • 看每个平台的地址 / 模型 / 上下文窗口 / 登录状态
  • 添加自定义平台:贴一个 Provider JSON 即可,不用改代码、不用重启
  • 删除自定义平台(内置项删不掉,要改就用同 id 覆盖)

插件配置(11 项,改完立刻生效,不用重启):

分组项默认说明
防封号发送间隔2~4 秒网页版对请求密度敏感
每 N 次请求后长休关工具循环连跑十几轮时压低密度
单轮请求上限不限防止意外烧配额
行为等回复超时10 分钟
浏览器未打开时快速失败20 秒不让你干等满 10 分钟
工具结果截断4000 字符太大顶爆输入框,太小模型看不到
允许调用工具开关掉 = 纯聊天
登录态快照间隔30 秒
诊断详细日志关出问题时打开

每个数值都有边界。填超界会回退默认并告诉你哪项被修正 —— 不会静默生效, 也不会把「发送间隔填 0」(= 秒发 = 必封)这种值存进去。

登录状态显示「浏览器未打开」是正常的 —— 切到 LDW 标签就会变「已登录,浏览器在线」。 插件只在浏览器真的活着时才知道登录态,不猜。

状态

能力状态
真实浏览器驱动(DSH 自带 Chromium guest)✅
真实登录 + 登录态持久化(重启免登录)✅
真实发送(填框 + 点发送键 / 合成 Enter)✅
从页面 DOM 读回复(主路径,含思考)✅
还原 pre 代码块围栏(工具调用靠它)✅
观察页面自己的 SSE 取回复(辅助路径)✅
工具调用(JS 代码块协议)✅
多平台(DeepSeek / 通义千问 / 自定义)✅
每个 DSH 会话绑定独立网页对话(重启后接回)✅
差分发送(不重发历史)✅
设置页(平台 / 登录态 / 11 项配置 / 增删自定义)✅
离线测试865 项

多平台架构

一个平台 = 一个 Provider 对象

平台差异(选择器、评分规则、SSE 路径、模型表、上下文窗口)全部收敛到 一个 Provider(src/shared/providers.ts), 驱动层只认接口。新增站点 = 加一个对象。

const MY_PLATFORM: Provider = {
  id: 'my-platform',            // ← 同时是 workspace 名,决定登录态隔离
  name: 'LDW · 我的平台',
  homeUrl: 'https://example.com/',
  urlPattern: 'example\\.com',
  input: { selectors: ['textarea[placeholder*="输入"]', 'textarea'] },
  send: {
    selectors: ['button[type="submit"]', '[role="button"][aria-label*="发送"]'],
    signatures: [['send-button', 60]],   // 站点特征加分
    vetoes: ['toggle', '停止'],           // 命中即否决
  },
  completionPath: '/api/v1/chat/completions',
  models: [{ id: 'fast', name: '快速' }],
  contextWindow: 131_072,
  maxOutputTokens: 8_192,
}

登录态隔离靠 workspace,不改 DSH 一行

DesktopBrowserGuests.acquire(owner, workspace) 按 workspace 字符串缓存 partition (main.js:9636-9641):

let partition = this.partitions.get(workspace);
if (partition === void 0) {
  partition = `dsh-sidebar-browser-${randomUUID()}`;
  this.partitions.set(workspace, partition);
}

所以用 provider.id 当 workspace 名,各平台自然拿到不同 partition、 不同 cookie / localStorage ⇒ 登录态天然隔离,零侵入。

三个层次的可扩展性

层次做法适用
内置表在 BUILTIN_PROVIDERS 加一项贡献新平台
API 装载POST /dsh-webin/api/providers 贴一个 Provider JSON站点改版后修选择器,不改代码
磁盘持久化~/.dsh/webin/providers/<id>.json跨重启存活,与登录态同一套存储

自定义 provider 覆盖同 id 的内置项(改坏了删掉即可回退), 且加载时逐个校验(validateProvider())—— 一条坏的不影响其它。

运行时校验

TS 类型运行时被擦除,"必须实现接口"靠 validateProvider() 真正强制。 校验覆盖:id 格式(会用作 workspace 名)、 URL 合法性、正则可编译、models 非空、contextWindow 为正数…

加一个平台(三步)

1. 先探测真实 DOM —— 不要猜选择器:

npm run probe:dom    # dump 输入框/按钮结构 + 用当前选择器匹配

2. 照着 provider.example.json 填一份 JSON,然后:

$h = @{ Authorization = "Bearer <secret>" }   # 见 ~/.dsh/.credentials.yaml
$body = Get-Content provider.example.json -Raw
Invoke-WebRequest "http://127.0.0.1:19387/dsh-webin/api/providers" -Method Post `
  -Headers $h -ContentType "application/json" -Body $body

装好后立刻生效(host 会热重载路由),新平台出现在模型选择器里, 文件落在 ~/.dsh/webin/providers/<id>.json,跨重启存活。

3. 在 LDW 面板顶部的下拉里选中它,登录一次即可(登录态独立保存)。

移除:

Invoke-WebRequest "http://127.0.0.1:19387/dsh-webin/api/providers/my-platform" `
  -Method Delete -Headers $h

查看装载情况(含校验失败原因):

Invoke-WebRequest "http://127.0.0.1:19387/dsh-webin/api/providers" -Headers $h

⚠️ 内置的 qwen 选择器尚未实测,第一次用前请跑 npm run probe:dom 校准。 未校准的 provider 不会静默失败:发送脚本找不到目标时会带诊断清单报错。


「只回一个字」的根因:裸文本帧没被处理

现象

网页版里回复是完整长文,DSH 对话里只有开头一个字。用户实测报障两次。

定位过程(npm run probe:frames)

先发现页面用 XHR 而非 fetch 发 completion(POST [XHR] /api/v0/chat/completion), 于是把探测脚本改成拦 XHR 逐帧记录,拿到真实帧序列:

[0] {"request_message_id":1,...}                                      ← 元信息
[2] {"v":{"response":{...fragments:[{content:"1"}]}}}                  ← 初始快照
[3] {"p":"response/fragments/-1/content","o":"APPEND","v":"."}         ← 带路径增量
[4] {"v":" HTTP"}        ← ⚠️ 裸文本帧:没有 p、没有 o
[5] {"v":"("}            ← ⚠️ 正文主体全是这种,占 95% 以上
...
[131]{"p":"response/status","o":"SET","v":"FINISHED"}

正文的绝大部分是裸字符串帧(v 直接是要追加的文本,无路径)。 提取器只认「带 p 且路径匹配」的帧 ⇒ 每个回复只拿到快照里的第一个字。

早期测试没暴露是因为 8+8=? 的答案 16 恰好完整落在快照帧里。

修法

isBareTextDelta():无 p、o 缺省或 APPEND、v 为非空字符串 ⇒ text += v。 显式排除元信息帧(有 request_message_id/updated_at 的走不进这个分支) 和错误信封(有 code/data)。o: 'SET' 的裸值帧不按增量处理。

回归:npm run verify:extract(13 项)把实测帧序列灌进产物里的真实提取器。


会话路由:每个 DSH 会话绑定自己的网页对话

缺陷(用户实测发现,封号级)

换了几个 DSH 窗口聊天,网页版里全落在同一个会话窗口 —— 上下文互相串、多个话题混在一条流里,行为模式像机器人,不被封才怪。

语义

场景行为
DSH 会话首条在当前页发,落地 URL 由结果带回 host 存进游标
同一会话后续任务job 带 expectUrl(= 上次落地页),URL 相同则不导航(零开销)
切换 DSH 会话job 带各自的 expectUrl,client 先导航过去再发
老 client / 抓 URL 失败不绑定,行为不退化(当前页照发)

数据流

adapter.stream(sessionId)
  └─ cursors[sessionId].chatUrl → job.expectUrl
       ↓ client
     页面 URL ≠ expectUrl → loadURL(expectUrl) → 等输入框 → 发送
       ↓ 结果
     done → PUT result { text, chatUrl: location.href }
       ↓ host
     cursors[sessionId].chatUrl = result.chatUrl

⚠️ 存 chatUrl 必须在 diff.commit() 之后 —— commit 会整体重写游标, 放前面会被它用旧值覆盖(verify-session-route 的 ⭐ 断言就是抓这个的)。

实测

npm run verify:session-e2e:两次发送分别落在两个不同的对话 URL …/s/8530a042… 和 …/s/92020348…)。

npm run verify:session-route(14 项):两会话互不串、切回仍回原对话、 无 chatUrl 时不退化。


每次重启新建对话 → 网页版侧栏堆一排会话

现象

用户侧栏出现「从1数到800」×3、「8加8等于16」×2 —— 每次重启/重挂载都新建对话。

(其中一部分是探测脚本直接用真实账号发的 —— 已知污染,抱歉。)

根因

boot 阶段无条件 loadURL(homeUrl)。首页被页面当成「开新对话」。 更严重的连带后果:host 的差分游标记着「这会话发过 N 条」, 而页面里是空的新对话 ⇒ 下一轮只发增量,模型完全没有上下文。

修法

快照里存了 url(上次离开时所在页面)。boot 时优先导航到它, 并做平台域校验(urlPattern 匹配 hostname+pathname 才用,防串平台)。 非法/无快照时才退回 homeUrl。


差分发送:为什么网页端聊天记录不该出现超长提示词

问题

第一版实现里,每发一条消息,网页端聊天记录里都堆着一条超长提示词。

根因是位置搞错了:DSH 是标准 LLM 宿主,每一轮都把完整历史 (system + 全部 messages)交给 adapter,由 adapter 自己决定怎么变成 provider 的请求体。对 HTTP provider 这没问题 —— 每次请求本来就该带全量上下文。

但网页版不是无状态 API:页面自己维护着对话历史。把整段历史每轮都打进 输入框有两个后果:

  1. 网页端聊天记录里堆满重复的超长提示词(用户一眼就能看到);
  2. 上下文被重复计数 —— 模型看到的是「历史 + 又一份历史」, 有效上下文被砍半,还容易被判成刷屏。

为什么 harness 类工具没这问题

因为它们不是 LLM adapter,而是 harness —— 自己驱动 AI 页面, 系统提示词只在进入项目时发一次:

harness 类工具本插件(第一版)
架构位置Electron 外壳,自己驱动 AI 页面DSH 的 llm adapter
系统提示词初始化时只发一次每轮都随历史重发
每轮发送只发新消息flatten 全量历史
页面角色就是记忆体被当成无状态 API

本插件的做法

本插件处在 adapter 位置(DSH 要求这样),所以自己补上这层差分 —— src/host/adapter.ts 的 diffPrompt():

规则原因
首轮发 system + 全部消息页面需要一次拿到完整设定
后续轮只发新增消息页面已有历史,不重复
跳过页面自己产生的 assistant 回复那是页面吐出来的,重发会让记录里出现重复内容
历史变短(压缩/回退)→ 从头发否则页面缺上下文
system 指纹变了(换 agent/preset)→ 重发新设定必须传达
增量为空 → 直接返回 stop,不发纯重复没有发送意义
用 sessionId 做键多会话并行不互相污染

实测各轮长度(verify-diff.mjs 第 3 组断言):

首轮 227 字符(含系统提示词)
后续 33, 33, 33, 33 字符(只有增量)

回归保护:npm run verify:diff(30 项)锁死上述全部规则, 包括「第二轮不含旧回复」「连续多轮每轮都跳过自己的回复」这类容易写错的地方。

投递失败不得推进游标

diffPrompt() 是纯函数,游标推进靠返回的 commit(),调用方只在投递成功后调。

早期版本在算增量的同时就推进游标,于是发送失败/超时后:DSH 认为「这段内容发过了」, 下一轮只发之后的增量 —— 而页面里其实没有这段内容 ⇒ 中间那段上下文永久丢失, 模型从此缺一段记忆。

不推进 ⇒ 下次调用重新算出同样的增量,内容不丢。


任务生命周期:三个会真实撞上的坑

1. 插队(最严重)

发送成功后 pendingJobRef 仍置着,而轮询每 1.5s 跑一次。 早期版本没有闸门 ⇒ 页面还在生成时又领了下一个任务,把新 prompt 打进输入框发送。后果:生成被打断、两个回复混在一起、done 分不清归属。

修法:pendingJobRef 有值 ⇒ 本轮只问 host「是否被取消」,不领新活。

2. 取消后任务挂着

用户按停止 → host 侧 signal.aborted 抛错退出,但任务仍留在队列里 处于 claimed 状态:

  • hasClaimed() 永远 true ⇒ /status 误报「面板在干活」
  • 页面继续生成到结束 ⇒ 白烧配额
  • 之后回来的 done 还可能算成下一个任务的回复 ⇒ 串台

修法:queue.cancel() 标记任务;client 每轮轮询带 ?running=<jobId>, host 回 cancel 就去点页面的停止按钮。

3. 待办永久卡死

pendingJobRef 靠 done 事件清空。若事件丢失(页面导航 / hook 被清 / 回复永不来),它会一直挂着 ⇒ 发送循环永久罢工,用户看到「模型一直不回复」。

修法:5 分钟超时兜底,交回错误并解锁循环(比 adapter 的 10 分钟更早)。

停止键定位:靠实测,不靠猜

真浏览器 dump(npm run probe:stop)出来的关键事实:

生成中,输入框区域同时存在:
  发送键:ds-button--primary  ds-button--filled  ds-button--circle
  停止键:ds-button--outlinedNeutral  ds-button--outlined  ds-button--circle  ← 生成中才出现
特征归属用法
--circle两者共有只给 +10 分(不能靠它区分)
--filled / --primary发送键独有命中即排除
--outlined停止键独有+60 分(阈值 60)

而且生成中发送键不会消失(早期假设它消失,是错的)。

分数并列时不敢猜 —— 宁可不停止(只浪费一次配额), 也绝不能误点「深度思考」开关(那会改变后续所有请求的行为)。

实测结果:空闲时返回 no-stop-control(不误点),生成中命中 --outlinedneutral,score 90,候选唯一。


启动阶段机

早期用 documentCount === 1 / 2 / >= 3 计数决定做哪一步,有两个真实缺陷:

  1. 某步没发生就永久卡住:没有快照时 === 2 分支直接 return 不 reload, 计数停在 2 ⇒ hook 永不注入、ready 永不置位、插件永久失效。
  2. 后续导航打乱计数(用户点链接 / 页面跳登录页)。

改成显式阶段机:

boot → navigating → restoring → reloading → live

每个阶段只做一次,不依赖「第几次 dom-ready」。另外:

  • 每次 dom-ready 都重注入 hook(hook 是页面级的,导航后就没了; buildDeepSeekHook() 自带幂等标记,重复注入无害)
  • 加载失败回退到 boot(否则断网后永远不再前进)
  • 渲染进程崩溃回退到 boot(OOM 后 webview 重载,阶段机要能重来)

工具调用(JS 代码块协议)

为什么用 JS 代码块,不用 JSON

网页版没有原生 function calling,所以要让模型把调用写成代码块。 设计上有两条路:JSON 协议,或 JS 代码块协议。本插件选后者,理由是 JSON 要求模型做转义,而转义是失败之源:

JSON 协议JS 代码块协议
多行文本必须写成 \n(模型常写成真实换行 ⇒ 解析失败)反引号模板字符串,直接换行
值里的引号必须转义为 \"不用管
反斜杠路径C:\\Users\\… 双层转义不用管
需要补的补救条款一大堆(引号、换行、反斜杠…)无需

所以协议里直接写死:多行文本用反引号模板字符串,不做任何转义。 少一整类失败模式。

协议

需要调用工具时,输出一个 ```ldw 代码块,块外不要有任何文字:

```ldw
const r = await read({"path": "src/index.ts"});
log(r);
```

实测拿到的真实契约(没猜)

解压 ~/.dsh/sessions/*.jsonl.zstd(分帧 zstd,逐帧解压)拿到 4951 行、 851 行含工具调用的真实数据:

// tool-call:assistant content 里的块,arguments 是 JSON 字符串
{ "type": "tool-call", "id": "call_00_…", "name": "read_image",
  "arguments": "{\"file_path\":\"C:\\\\…\"}" }

关键:tool-result 不是 content block,而是 role: 'tool' 的独立消息:

{ "role": "tool", "toolCallId": "call_00_…",
  "content": [{ "type": "text", "text": "…" }], "isError": false }

字段名猜错 = 静默失败,所以先 dump 真货再写代码。

chunk 契约(实测序列)

block-start(text) → text-delta → block-end
→ block-start(tool-call) → tool-call-delta → block-end
→ finish(tool-calls)          ← 有调用时必须这个,否则 DSH 不执行

tool-call-delta 的字段名是 argumentsDelta,block-end.block 里是 arguments(不是 args)—— verify:tool-chunks 专门断言这些字段名。

项目初始化:只补 DSH 缺的那部分

harness 类工具在进入项目时会注入 6 类内容。逐条核对 DSH 已经给了什么 (详见 docs/project-init-compare.md):

#章节DSH 是否已给本插件
1平台信息⚠️ system 只提了一次 Windows(讲 exit code)补(cmd/pwsh 差异)
2项目目录✅ system 末尾 Your working directory is …不补
3项目介绍✅ AGENTS.md 走 user 消息自动进入不补
4工具清单⚠️ 走 options.tools,需自己格式化补
5技能清单✅ skill 工具在清单里不补
6MCP 清单✅ MCP 工具在清单里不补
—身份与原则✅ system 提供不补(重复会冲突)

为什么不全补:① 本插件是 adapter(system 由 DSH 给),重复注入 = 两份冲突指令; ② 体量是封号风险(实测记录:单次输入涨到 293k token,四个账号两天内被限制)。

体量控制

首轮 prompt: system + 工具协议 + 运行环境 + 工具目录 + 首条消息
后续轮 prompt: 只有增量(实测 11 字符)
60 个工具的工具段: 4,080 字符(对比 web-login 的 5 万)

工具结果超 4000 字符自动截断(防一次读大文件把 prompt 顶爆)。


当前进度

阶段内容状态
P0红框位置注册「LDW」标签✅ 已完成
P1LDW 面板内挂真实 Chromium guest + 加载 DeepSeek 网页版 + UA 洗白 + 登录态探测✅ 已完成(实测可登录,无「环境异常」)
P2注入 hook 观察页面自己的 SSE✅ 已完成(辅助路径 —— 见上文「为什么读 DOM 而不是拦 SSE」)
P3注册 deepseek-webin provider,网页回复 → StreamChunk → 进「对话」✅ 已完成
P4自动填输入框 + 触发发送 + 工具结果回传✅ 已完成(三段式发送:填充 / 客户端延迟 / 触发)
P5设置页(settings.section)+ 平台增删✅ 已完成
P6DOM 读回复成为主路径 + 围栏还原 + 思考读取✅ 已完成
P6登录态持久化(重启免登录)✅ 已完成(快照 + 目标域回写,见下节)
P7工具调用(JS 代码块协议)✅ 已完成(见「工具调用」节)
P8每会话独立网页对话 + 重启接回✅ 已完成(游标落盘 + 首轮开新对话)

为什么重启 DSH 后不用重新登录

约束:分区一定是内存分区

DSH 的 guest partition 由 DesktopBrowserGuests.acquire() 生成(main.js:9638):

partition = `dsh-sidebar-browser-${randomUUID()}`;   // ← 无 persist: 前缀

而 webview 门禁又强制 params.partition === lease.partition(main.js:9685):

if (lease === void 0 || lease.owner !== owner || lease.attached || params.partition !== lease.partition) {
  event.preventDefault();
  return;
}

所以插件不能自己换成 persist: 分区(会被门禁拒绝), 重启后 storage 必然是空的。唯一出路是「快照 + 在正确源上回写」。

坑:restore 必须在目标域上执行

早期实现在第一次 dom-ready(页面还是 about:blank)上跑 restore, 结果是每次重启都要重新登录。实测(npm run probe:origin):

[A] about:blank 上写 localStorage
    → SecurityError: Access is denied for this document.
[B] 目标域上写 → 跨 reload 保留 ✓

两层原因:

  1. about:blank 是 opaque origin,localStorage 直接抛 SecurityError;
  2. localStorage 按源隔离 —— 在 A 源写的值 B 源读不到。

正确时序

1. loadURL(homeUrl)          → 到目标域
2. dom-ready #2:restore      → 此时 localStorage 可写、且源正确
3. reload()                   → 带着登录态重新初始化
4. dom-ready #3+:注入 hook/探针

restore 脚本现在会回读校验并如实报告,不再静默吞异常:

{"ok":true,"restored":13,"verified":13,"failed":0,"cookies":2,
 "origin":"https://chat.deepseek.com"}

在错的源上会明确拒绝:

{"ok":false,"reason":"wrong-origin","origin":"null",
 "hint":"restore 必须在目标域上执行(about:blank 是 opaque origin,storage 不可写)"}

回归保护:npm run verify:restore(27 项)锁死时序与源校验, npm run probe:origin 在真浏览器上验证源隔离假设。

仍然存在的限制

  • httpOnly cookie 无法用 JS 回写(浏览器安全模型)。本站登录态主要靠 localStorage.userToken,所以不受影响。
  • 清空 ~/.dsh/webin/ 后需要重新登录(快照没了)。
  • 登录态快照按平台分槽(auth/deepseek.json、auth/qwen.json),互不覆盖。

本轮修复(压缩死锁)

症状

会话发任何消息都卡住,compaction/start 后长时间无响应,最久一次卡 6 分 12 秒才被中止:

[11:12:00] turn/start 21 → compaction/start → (96s) → aborted
[11:14:10] turn/start 22 → compaction/start → (4s)  → aborted
[11:14:36] turn/start 23 → compaction/start → (372s)→ aborted

根因(两个 bug 叠加)

1. contextWindow 写错,阈值低到每步都触发压缩(src/host/adapter.ts)

context: { contextWindow: 128_000 }   // ❌ 网页版真实是 1 Mi

compaction-basic 的判定是:

thresholdTokens = min(contextWindow × 0.8, contextWindow − maxTokens − headroomTokens)
contextWindowmaxTokensheadroom阈值该会话实测 359,267 tokens
修复前128,0008,19265,53654,272超阈值 6.6 倍 → 每步都压缩
修复后1,048,57632,76865,536838,860远低于阈值 → 不触发

2. 压缩的摘要调用被路由回 LDW 自己

compaction-basic 的 summarizeWithLlm() 用 routedTarget(session) 选模型 —— 也就是最近一次实际路由的 provider。上一轮把会话切到 deepseek-webin/ldw-think 后,压缩就变成「必须经浏览器发消息」。而发送恰好是坏的 ⇒ 摘要任务进了队列没人能完成 ⇒ 干等满 timeoutMs(10 分钟)。

修法:waitForJob() 增加无人领活快速失败——超过宽限期(20s)仍没有 client GET /job/next,立即判 WEBIN_FAILED 并说明原因,不再干等。

3. 发送路径失效(send-failed: 未找到发送按钮,合成 Enter 也无效)

旧 chat.ts 的三个缺陷:

  • 发送按钮只找 <button> —— DeepSeek 新版用 div[role="button"],永远命中不到
  • 兜底扫描只看 4 层祖先里的 <button> —— 同样漏掉 div[role=button]
  • 合成 Enter 只发 KeyboardEvent,没有二次尝试

用真 Chrome 加载真实页面 dump DOM 后(npm run probe:send),拿到的事实:

元素真实结构后果
输入框textarea[placeholder="给 DeepSeek 发送消息 "]旧选择器能命中
发送键div[role=button].ds-button--primary.ds-button--filled.ds-button--circle不是 <button>、无 aria-label、无文字(纯图标)→ 旧代码必然找不到
附件键div.ds-button--iconLabelPrimary.ds-button--icon.ds-button--capsule与发送键同尺寸相邻,点错会弹文件选择框
开关div.ds-toggle-button(深度思考 / 智能搜索)点了会改发送语义

只用「几何位置 + 邻近度」评分时,发送键 32 分 vs 附件键 31 分 —— 只差 1 分。 加入站点特征评分后:

候选修复前修复后
#1 发送键32214
#231(附件键)39
分差1175

4. 发送确认判据不可靠 → 改成不确认

早期尝试用「多信号校验」确认发送成功,但每个信号都不可靠:

hasStopControl() 靠文本找「停止生成」—— DeepSeek 上是纯图标,测不到。 而 grew(页面正文变长)在 dry-run 里误报为 true(页面动画/懒加载也在改文本)。

更糟的是:校验本身要等待,于是「填充 → 等 2.4s → 校验 → 可能重试」 被塞进一次 executeJavaScript —— 那是跨进程调用,页面一导航就整体死掉 (不 resolve、不 reject)⇒ 宿主永远等 ⇒ 用户看到「一直深度求索中」。

现在的做法(读参考实现后重构):发送三段式,每段都是短调用 ——

段做什么耗时
① buildFillScript只把字填进输入框几百毫秒
② 客户端 setTimeout随机延迟(防封号)2–4 秒
③ buildTriggerScript只点发送键(或合成 Enter)几十毫秒

等待放在客户端,不放在页面脚本里 —— 这样单次跨进程调用的存活时间极短, 导航最多毁掉一次短调用,不会让整条链路僵死。

发送是否成功不再自证:由后续的 DOM 轮询来确认「页面上真的出现了新回复」。

5. 交付判据:条数增长 或 内容变化

判断「本轮回复来了」用两个判据取「或」:

判据说明
条数比基线多主判据 —— 只有真的新增 assistant 消息才会增长
内容与基线不同兜底 —— 页面把回复渲染在选择器匹配不到的容器里时仍能交付

再加一道稳定性判据:连续两次读到相同内容才算生成完, 避免把「流式输出中的半截回复」交回 DSH。

6. 放弃门槛必须跟配置走

早期硬编码「5 分钟放弃」,但配置里 replyTimeoutMs 默认 10 分钟, 而且注释里记着「实测最慢一次网页回复 6 分 12 秒」—— 于是 6 分钟才回完的请求会在 5 分钟被提前掐断。

现在按 replyTimeoutMs * 0.8 算(留余量让 adapter 那次超时不会先触发),下限 60 秒。

7. 其他

问题修法
路由 / adapter 未释放 → 重载撞 duplicate prefix route改用 ctx.effect() 交回 disposer
无超时的 executeJavaScript 会静默死锁execJs() 统一加超时护栏(超时 resolve 成 sentinel,不 reject —— 抛出去会被外层 catch 吞掉反而变静默)
「已发送 · 收到 0 条」+ DSH 一直等异常分支必须交回结果(早期只打日志 ⇒ pendingJobRef 永挂着 ⇒ 循环僵死)
发送循环被 loggedIn 门控,误判即全链路失效去掉门控,改由发送脚本在页面上自行判断
登录误报(登录页也显示「已登录」)探针改用 __appKit_userInfo.id 做强证据 + 负面证据交叉判定
__appKit_userInfo.id 解不出(明明登录却报 hasUserId:false)真实形状是 {"value":{"id":...}},id 嵌在 value 里;两种形状都认
空快照覆盖有效快照(重启后仍要重新登录)未登录时 userToken={"value":null} —— 早期只查键是否存在,于是登录页的空快照盖掉了有效快照。改为 snapshotLooksLoggedIn() 解出值再判断
快照存了 cookie 但回写时被丢弃buildRestoreScript 补上 cookie 逐条回写(跳过属性残留片段)
ctx.logger 畸形会让整个 apply() 抛错 → 插件装不上safeLogger() 显式判类型,10 种畸形形态全部安全降级
排查困难host 加 POST /client/heartbeat,/status 反映面板在线数

6. 压缩摘要不依赖浏览器面板

即使 contextWindow 修正后不再频繁触发压缩,摘要本身也不该走浏览器: 摘要输入动辄几十万 token,塞进网页输入框会让页面卡死,网页版本身也有长度上限。

adapter 现在对 purpose === 'compaction' 的调用立即拒绝(实测 0ms 返回,不进队列), 并给出可操作指引:

压缩摘要不能走 LDW 网页版:摘要输入体量远超网页输入框上限,且会把整轮对话锁在浏览器上。
请把 compaction-basic 的 summarizationProvider / summarizationModel
配成普通 HTTP provider(例如 wb-out / deepseek-v4.1-flash)。

根治方式是在 preset 的 compaction 组里显式钉死摘要模型:

- id: compaction-basic
  name: '@deepseek-ai/dsh-compaction-basic'
  config:
    summarizationProvider: wb-out          # ← 空对时会回退到"最新已路由目标"(= LDW),
    summarizationModel: deepseek-v4.1-flash #   那就是死锁根源

dsh-compaction-basic/README.zh.md:70-71 原文:

与 summarizationModel 一起设置;空对使用最新已路由请求目标,再回退到 AgentOptions 对。

验证

npm run check        # 构建 + 全部离线校验(355 项)
npm run verify:e2e   # ⭐ 端到端:真浏览器 + 真登录态 + 真发送 + 真回复
npm run probe:chain  # 全链路真实验证(会消耗一次网页版额度)

npm run check 覆盖:contextWindow = 1 Mi、压缩阈值复算、无人领活快速失败、 压缩摘要立即拒绝、心跳路由、任务队列领活/交结果全链路、 卸载后重装不撞重复路由、10 种畸形 logger 下仍能加载、 差分发送的全部规则(30 项,含「第二轮不含旧回复」)、 多平台路由与模型不撞名、以及两个挂载点声明完整。

npm run verify:e2e 是真浏览器端到端验证 —— 起一个真 Chromium、 注入真登录态(含 cookie)、注入真 hook、用真发送脚本发一条消息、 轮询真 drain 脚本抓回复,最后断言 5 项全绿:

[1b] 登录态回写: {"restored":13,"cookies":2}
[4] 发送: stage=sent | signals: {"requestSent":true,...}
[5] stream×4 → DONE status=finished len=2
    网页版回复: 12
  ✓ hook 注入成功  ✓ 发送被确认  ✓ 收到 stream 增量
  ✓ 收到 done 事件  ✓ done 带正文        5/5 通过
prompt回复结果
1+1=? 只回答数字26/6
2+2=? 只回答数字46/6
3+3=? 只回答数字66/6

单次输出形如:

[2] hook 注入: installed
[3] 探针: {"loggedIn":true,"hasUserId":true}
[4] stage: sent | signals: {"requestSent":true,...}
[5] ✓ hook 捕获到 completion 请求(计数 1)
[6] stream 事件数: 4
  抓到的回复: 6
  ✓ hook 注入成功  ✓ 发送被确认  ✓ 捕获 completion 请求
  ✓ 收到 stream 增量  ✓ 收到 done 事件  ✓ done 带正文
  6/6 通过

另外几个探测脚本(只读或可指定 prompt):

命令作用
npm run probe:domdump 真实页面结构 + 用插件选择器匹配,验证选择器是否失效。加 --url <地址> 可校准任意平台
npm run probe:senddry-run 跑真实 buildSendScript()(拦截点击),验证候选评分
npm run probe:login分阶段验证登录判据(裸访问 / 注入后 / 延时探测),排查 loggedIn 误报
npm run probe:done逐秒 dump 事件队列,排查 done 事件为何不来

⚠️ 这三个脚本都要求本机有 Chrome/Edge,并会读取 ~/.dsh/webin/auth/default.json 的登录态。页面改版后先跑 probe:dom,比看 minified bundle 猜可靠得多。


架构

┌─ DSH Electron 主进程(app.asar/lib/main.js:9617-9803)──────────────┐
│  DesktopBrowserGuests(宿主自带,未修改)                            │
│    ipcMain.handle('dsh-desktop:browser-acquire') → { lease, partition }│
│    ipcMain.handle('dsh-desktop:browser-release')                     │
│    will-attach-webview:校验 about:blank#<lease> + partition          │
└──────────────────────────────────────────────────────────────────────┘
        ▲ window.dshDesktop.browser(宿主 preload 已暴露给主窗口主 frame)
        │
┌─ dsh-webin client 半(本插件)──────────────────────────────────────┐
│                                                                      │
│  ① 常驻宿主  shell.overlay  id='ldw-host'   ← root scope,永不卸载    │
│     └─ acquire(workspace) → <webview src="about:blank#<lease>">      │
│          → 真 Chromium guest → loadURL(chat.deepseek.com)            │
│                                                                      │
│  ② 占位符    conversation.view  id='ldw' order=20  ← 用户要的红框位置 │
│     └─ 只量自己的 rect 并广播(不含 webview)                         │
│                                                                      │
│  ① position:fixed 贴到 ② 的 rect 上;② 卸载时 rect=null → ① 移出视口  │
└──────────────────────────────────────────────────────────────────────┘

为什么拆成「常驻宿主 + 占位符」两半

因为 conversation.view 切标签会卸载组件。 四环证据:

#证据位置
Aconversation.view 声明为 kind: 'list'ui-conversation/lib/client.js:18209
B渲染点传 { only: viewId }ui-conversation/lib/client.js:16412-16421
Crenderer 对 only 是 filter + 单元素 map,不是 CSS 隐藏ui-renderer/lib/client.js:1193-1196
D全部 client bundle 里 keepAlive / Offscreen 零命中—

React 语义下,兄弟元素 key 从 e<chat> 变成 e<ldw> ⇒ 旧子树整体卸载。 若把 <webview> 直接放里面,用户一切到「对话」看回答,guest 就被销毁、 lease 被释放、轮询全停 —— 生成中的回答直接中断。这正是「把 LDW 当模型用」的核心场景。

shell.overlay 则是 kind: 'list' + scope: 'root',渲染点 renderSlot('shell.overlay', {}) 不传 only ⇒ 全部注册项常驻 (ui-layout/lib/client.js:312,617)。官方 ui-chat 的额度浮层就是这么挂的 (ui-chat/lib/client.js:12473)。

⚠️ 宿主容器绝不重新挂载:Electron 的 <webview> 一旦从 DOM 移除再插回, guest 会重新加载。所以宿主只改坐标(left/top/width/height + visibility), DOM 节点自始至终不动。切走时也不用 display:none(会让 webview 停止布局), 而是移出视口 + visibility:hidden。

运行时可用 cordis_inspect_query(client / Slots / listSubTree)确认两个挂载点:

shell.overlay     → occupants 含 { id: "ldw-host", order: 20, active: true }
conversation.view → occupants 含 { id: "ldw", order: 20, active: true }

为什么必须走 webview,不能自己开窗

插件跑在 dsh-desktop-host 子进程里,require('electron') 直接失败 (已实测:Cannot find module 'electron',process.type === undefined)。 所以拿不到 BrowserWindow / WebContentsView。

DSH 自带的 webview guest 通道是唯一可用且零新增内核的真浏览器路径。

guest 的三条硬约束(来自 main.js:9682-9707)

const id = params.src.startsWith("about:blank#") ? params.src.slice(12) : "";
const lease = this.leases.get(id);
if (lease === undefined || lease.owner !== owner || lease.attached
    || params.partition !== lease.partition) { event.preventDefault(); return; }
  1. src 必须精确等于 about:blank#<lease>
  2. partition 必须等于 acquire 返回的 partition
  3. 一个 lease 只能挂载一次(lease.attached 后再挂会被拒)→ 重开必须重新 acquire

开发

npm install       # 首次
npm run check     # 构建 + 全部校验(355 项)

拆开跑:

命令内容
npm run build构建 lib/index.js + lib/client.js
npm run check全部离线测试(865 项,不需要浏览器)
npm run verify:scrape⭐ DOM 读回复(53 项,真实 DOM 树桩)
npm run verify:stuck防僵死 / 交付判据(87 项)
npm run verify:fill填充与换行归一化(30 项)
npm run probe:dom真实页面结构 dump(只读)
npm run probe:send发送脚本 dry-run(只读,拦截点击)
npm run probe:chain全链路真实验证(⚠️ 真发送,消耗额度)

⚠️ 需要真浏览器的探针脚本尚未适配发送流程重构。

probe:send / probe:chain / probe:done / probe:frames / probe:stop / verify:e2e / verify:coldstart / verify:session-e2e 仍引用已删除的 buildSendScript()(改成三段式发送后不存在了)。

它们的文件头已写明怎么改(把 buildSendScript(...) 换成 buildTriggerScript(...)),但替换后需要真浏览器验证,所以没有自动改动 —— 用未验证的代码替换已运行的代码只会更糟。离线测试(npm run check)不受影响。

装进 profile(Desktop):

// ~/.dsh/profiles/desktop/package.json
"dependencies": { "dsh-webin": "link:F:/Code/DSH-Code/dsh-webin/dsh-webin" },
"dsh": { "profile": { "bundles": [ ..., "dsh-webin", ... ] } }

并建 junction:

New-Item -ItemType Junction -Path "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-webin" `
  -Target 'F:\Code\DSH-Code\dsh-webin\dsh-webin'

⚠️ profile 的 bundles 只在 Host 启动时读一次 —— 必须重启 DSH 才会加载本插件。

改完哪一半要怎么生效

改了什么生效方式
src/client/*(面板、发送脚本、hook)HMR 自动,刷新页面即可
src/host/*(provider、路由)必须重启 DSH(host 半没有 HMR)

⚠️ 用插件管理器「禁用再启用」不能替代重启:旧版本(无 disposer)留下的幽灵路由 不会被回收,会撞 webserver: duplicate prefix route "/dsh-webin/api"。 新版本已用 ctx.effect() 修好,但已经在跑的旧实例留下的路由仍要重启才清得掉。

排查「模型不回复」

按顺序看这三处:

# 1. 面板在不在线(online: 0 = 没面板在跑,任务必然无人领取)
$h=@{Authorization="Bearer <secret>"}   # secret 见 ~/.dsh/.credentials.yaml 的 client-connection/browser-session
Invoke-WebRequest "http://127.0.0.1:19387/dsh-webin/api/status" -Headers $h -UseBasicParsing

# 2. 队列里有没有积压
Invoke-WebRequest "http://127.0.0.1:19387/dsh-webin/api/job/next" -Headers $h -UseBasicParsing
  1. 切到 LDW 标签,点面板上的**「诊断」**按钮 —— 会 dump 页面输入框与按钮结构。

挂载点自证

// cordis_inspect_query: platform=client, provider=Slots, method=listSubTree
{ "root": "shell.overlay" }      // 应含 { id: "ldw-host", order: 20, active: true }
{ "root": "conversation.view" }  // 应含 { id: "ldw", order: 20, active: true }

网页版自己停了怎么办:点它自己的「继续生成」

网页版会在服务端主动停止时把回复截断,界面上显示「已停止」并留一个 「继续生成」按钮。这时如果直接把已抓到的内容交付,用户看到的是 「任务没做完,DSH 却报已完成」。

处理顺序:

  1. 点页面自己的「继续生成」(优先)—— 这是同一条消息的续写, 正文连续、立即生效。最多 3 次。
  2. 点不到(没有该按钮 / 分数并列不敢猜)⇒ 退回原看门狗: 静默 12 秒且只有思考时,发一条新消息「请继续」,最多 3 次。
  3. 都不行 ⇒ 正常交付(有什么交什么)。

两道闸门都放在交付之前,且计数独立 —— 一个手段失败不消耗另一个的额度。

回复不完整 / 服务端不可用时怎么办

网页版在服务端不可用(「服务器暂时不可用」「请求过于频繁」)时会直接掐断 生成,留下半截内容。插件不会把这半截当成功交付,判据两条:

判据含义
fenceOpen回复停在未闭合的代码围栏里 ⇒ 一定没写完(工具调用整段在围栏里,所以能直接挡住「半截工具调用」)
serverError页面上明确写着服务端错误

命中任一条 ⇒ 本轮不算成功,按顺序处理:

  1. 提醒用户:面板状态栏 + 控制台
  2. 重试(最多 3 次):先点页面自己的「继续生成」;点不到就重发原 prompt —— 服务端限流通常几秒就恢复
  3. 重试耗尽 ⇒ 交付现有内容,但在正文开头加一条明确警告,说明本次不完整、 可能被截断、建议重发

三种补救手段(续写按钮 / 看门狗「请继续」/ 不完整重试)计数各自独立 —— 一个手段失败不消耗另一个的额度。

新会话的任务没人领?(WEBIN_FAILED)

常驻宿主全局只有一个(挂 shell.overlay、root scope、永不卸载),所以整个 窗口只有一个领活循环;而 sessionId 来自每个 DSH 会话各自的占位符,写进同一个 模块级 ref —— 最后一个挂载的覆盖它。

于是只拿得到一个 sessionId,其余会话的任务永远匹配不上 ⇒ 20 秒无人领 ⇒ 「LDW 面板未连接」。

现在的行为:领活分两轮 —— 本会话的任务优先拿,本会话没有就回退领走。 回退是安全的,因为 client 会按 expectUrl / freshChat 自己导航到正确的网页对话。

/status 里每条任务带 claimedVia:

值含义
session会话匹配(正常)
fallback会话对不上,为了不让任务没人领而回退领走

频繁出现 fallback 说明会话身份长期对不上,值得查。

网页版在生成,DSH 却报「没发出去」?

「早期放弃」判据(发出后 30 秒无动静就报错)必须被 generating 一票否决:

信号含义可信度
generating(停止按钮在)页面正在生成 ⇒ 消息一定送到了高 —— 直接反映页面状态
DOM 有没有变化可能读不到(思考阶段还没有回复容器)低 —— 会误判

网页版处在思考阶段时,.ds-markdown 还没出现,插件的 DOM 读取什么都看不到 (条数没增长、内容没变化、思考区也没读到)—— 三个条件同时成立就会误判成 「没发出去」。所以只要页面在生成,就绝不放弃。

「页面是否在生成」靠什么判断

不能靠 aria-label —— DeepSeek 的停止键是纯图标(无 aria-label、无文字), aria-label*="停止" 这类选择器永远匹配不到。曾经因此让 isGenerating() 恒返回 false,连带「生成中不交付」闸门从未生效。

正确判据是校准过的 class 特征(与 buildStopScript 同源):

特征含义
ds-button--outlined停止键独有 ⇒ 命中即判定生成中
ds-button--filled / ds-button--primary发送键独有 ⇒ 排除(生成中它不会消失)
aria-label / title 含 stop / 停止语义兜底(页面改版后可能补上)

找不到任何特征时返回 false(不阻止交付 —— 宁早勿卡)。

已知限制

L1:guest partition 不持久(需重启后重新登录)

main.js:9638:

partition = `dsh-sidebar-browser-${randomUUID()}`;   // ⚠️ 没有 persist: 前缀

没有 persist: 前缀 ⇒ 内存分区,连磁盘都不落。官方 README 原文: 「Cookie 与 Web storage 不跨应用重启保留」(ui-sidebar-browser/README.zh.md:110)。

本插件用纯插件方案兜底:登录后每 30s 把 guest 的 localStorage(userToken / appKit* / hif_*)+ 非 httpOnly cookie 存到 ~/.dsh/webin/auth/default.json, 下次启动导航前回写。

⚠️ httpOnly cookie 用 JS 读不到 —— 若 DeepSeek 的登录态主要靠它,这条路会不完整。 那种情况下只能改 DSH 那一行(9638 加 persist: 前缀),代价是升级会被覆盖。

L2:LDW 必须全局单例

并发多窗口实测 6 分钟封 1 天。单轮体量同理,继续用 DSH 自己的 compaction 压上下文 (现已修正 contextWindow,见上)。

设计取舍:为什么不用现成的传输层方案

早期实现走过「手工构造 HTTP 请求」的路线,后来全部推倒 —— 那条路是封号之源:

丢掉的东西为什么
伪造的 x-deepseek-harness 头最高风险单点,每个请求都发
about:blank 代理 + 页面内 PoW需要 --headless=new --no-sandbox 等自动化 flag,指纹特征明显
手工拼 origin/referer/cookie在页面里会被浏览器静默丢弃,实际发出去的是 Origin: null + 无 Cookie
传输层开关(HTTP / 浏览器二选一)真浏览器方案里不存在这个概念

换真浏览器也解决不了的:并发多窗口(实测双窗口 6 分钟封 1 天)与单轮体量。 LDW 必须全局单例,并继续用 DSH 自己的 compaction 压上下文。


致谢

本项目的设计思路受益于以下项目,在此致谢:

  • cuckoo-code(GPL-3.0-only)
  • dsh-deepseek-web-login(Apache-2.0)

本项目为独立实现,全部代码自行编写,许可为 Apache-2.0(见 LICENSE), 与上述项目的许可不冲突。

许可

Apache-2.0(见 LICENSE)。


与 DSH 能力的适配

LDW 作为 DSH 的模型 adapter 接入,因此 DSH 的能力自动可用,无需本插件逐个实现:

  • 工具调用:DSH 把当前会话可用的全部工具(内置工具、MCP 工具、Skill 提供的工具) 以统一 schema 传给 adapter;LDW 将其转换成工具目录发给网页模型, 模型输出的调用由 DSH 执行后回灌 —— 你装的 MCP 服务器、启用的 Skill 在 LDW 模型下同样能用
  • 上下文管理:会话历史、压缩(compaction)由 DSH 管理,LDW 只做增量发送
  • 斜杠命令:由 DSH 的命令系统处理,与模型无关,天然可用
  • 多会话:每个 DSH 会话绑定独立的网页对话(见上文会话路由)

免责声明## 免责声明

本项目仅供个人学习与研究使用。使用者需自行承担以下风险:

  • 用自动化方式驱动 AI 网页版可能违反服务条款,存在账号被限制的风险
  • 本项目通过真实浏览器 + 真实用户操作降低风险(不发伪造请求头、不做 PoW 计算、 串行发送、差分发送控制体量),但无法保证不被风控识别
  • 请勿用于商业用途、批量操作或任何违反服务条款的场景

使用即表示你已知悉并接受上述风险。