dsh-cookie-janitor
DSH 启动越来越慢、最后白屏或直接起不来?多半是浏览器里堆积了几十条过期的登录 cookie。
这个插件在请求进入应用之前把它们清掉。装上之后不需要任何操作,也不会改变登录状态。
问题是什么
DSH 的浏览器认证给每个监听地址签一条 cookie,名字是算出来的:
cookie 名 = "dsh-auth-" + base64url(sha256("host:port"))
名字里带了端口。而端口是变的:
- 桌面版启动时写死了
--port 0,让操作系统挑空闲端口(@deepseek-ai/dsh-desktop-host的runProfile({ args: ["--no-open","--host","127.0.0.1","--port","0"] })); - 命令行
dsh --profile web --port 0同理。
所以每次启动都是一个新端口、一个新名字。
cookie 的归属范围只到主机,不含端口。浏览器对 127.0.0.1 发出的每一个请求,都会把历史上所有 dsh-auth-* 一起塞进 Cookie 头——哪怕那些端口早就没人监听了。
cookie 的有效期是 30 天,且不因使用而续期。所以:
30 天内启动 N 次,就堆 N 条无用的 cookie。
单条约 221 字节。Node.js 的请求头上限是 16 KiB,超过就返回 431 Request Header Fields Too Large,而且是在应用代码跑起来之前——页面拿不到任何有用信息,表现就是白屏、加载卡住、或认证失败。
实测:73 条时 Cookie 头 16277 B 仍能通过,74 条时 16500 B 拿到 431。因为 Host、User-Agent 等头也占同一份额度,实际触顶会略早于这个数,按 70 条左右估算。一天开几次、开一个月,就会撞上。
它做什么
三层,按发生顺序:
| 层 | 时机 | 动作 |
|---|---|---|
| 请求入口 | 每个请求 | 从 Cookie 头里摘掉属于其他端口的 dsh-auth-*,应用根本看不到它们 |
| 响应出口 | 同一请求 | 给被摘掉的那些下发 Max-Age=0,让浏览器把垃圾从库里删除 |
| 溢出兜底 | 431 发生前 | 如果请求头已经大到 Node 拒绝解析,就把 431 换成一次 302 回到原地址,并附上清除指令——用户看到的是刷新一下,而不是崩溃 |
当前正在用的那条 cookie 永远不动。 删掉它会把你从正在使用的实例里登出。
清完之后,第二次请求的 Cookie 头就只剩一条,之后不再增长。
安装
从 Git 仓库装:
dsh plugin --profile web add github:yan3536/dsh-cookie-janitor
或先克隆到本地再用绝对路径装(改源码调试时用这个):
git clone https://github.com/yan3536/dsh-cookie-janitor
dsh plugin --profile web add /绝对路径/dsh-cookie-janitor
装完完全退出 DSH 再启动(不是刷新页面,是退出进程)。插件挂在 Host 的 HTTP 服务上,只有在启动时才能装上。
桌面版用户把 --profile web 换成 --profile desktop;但桌面版 profile 被 Electron 独占,必须先完全退出 DSH 再执行命令,否则会得到 Open DeepSeek Harness Desktop once to initialize its profile, then fully quit it before running dsh plugin --profile desktop.
卸载:
dsh plugin --profile web remove dsh-cookie-janitor
配置
全部可选,默认值就是推荐值。想改就写进 profile 的 cordis.patch.yml:
- id: cookie-janitor
name: dsh-cookie-janitor
config:
keepForeign: 0 # 允许保留几条非当前端口的 cookie;0 = 全清
pruneRequests: true # 关掉就只剩溢出兜底
rescueOverflow: true # 关掉则 431 原样返回
maxHeaderBytes: 0 # >0 时把 Node 的头上限抬到这个字节数
attachTimeoutMs: 15000
maxHeaderBytes 只是把症状往后推——真正止血的是 keepForeign: 0。除非你有特殊理由(同一浏览器要同时连多个 DSH 实例),否则别动它。
想同时连多个实例的话,把 keepForeign 设成你实际并用的实例数,插件会按浏览器发送顺序保留最新的那几条。
会不会误删
不会。判定完全按 DSH 自己的算法来:
- 只碰
dsh-auth-开头的 cookie,其他 cookie(主题、语言、第三方)一律不动; - 当前
Host对应的那条永远保留; - 被删的那些,其对应的监听端口在当前进程里根本不存在,留着也没有任何用处。
如果哪天 DSH 换了 cookie 命名规则,插件会识别不到任何 dsh-auth-*,于是什么都不做——最坏情况是退回原状,不会误删。
手动清理(不装插件)
如果只想先把眼前的崩溃解决掉,在浏览器里清掉 127.0.0.1 的站点数据即可:
- 打开
http://127.0.0.1:<端口>; - 地址栏左侧的站点信息 → Cookie 和网站数据 → 删除;
- 或者开发者工具 → Application → Cookies → 逐个删掉
dsh-auth-*。
但这只治一次。下次启动还是会产生新的一条,只是重新开始攒。
离线自测
node test/smoke.mjs
24 个用例,不依赖 DSH、不联网:
- 16 个纯函数(cookie 名推导、authority 归一化、Cookie 头解析、清理策略的六种情形、过期指令拼装、原始报文与 target 抓取);
- 6 个端到端(真实
node:http服务器 + 裸 socket):正常请求透传、干净请求逐字节不变、20 KiB 头被救成 302、关掉兜底后恢复原生 431、放宽上限生效、关掉清理后原样透传; - 2 个健壮性(没有 web 服务时静默、畸形配置不抛)。
兼容
在 DSH 0.2.0-rc.2 上实测通过。Node ≥ 20(用到 Buffer.toString('base64url') 与 replaceAll)。
插件不声明 engines.dsh。DSH 的兼容检查一旦看到这个键就按它独占判定,而预发布版(0.2.0-rc.2)低于 0.2.0,写上 >=0.2.0 反而会把当前能用它的版本挡在外面。宁可让它跑,也不要在清单层面自己设卡。
插件只用 webServer 服务的两个公开成员:server(node:http 的 Server 实例)和 protocol('http:' 或 'https:')。这两个在 dsh-host-webserver 的 lib/index.js 里是明文类字段与 getter(:281、:305-307),运行期一定存在。
它不复用任何未导出的内部符号:cookie 名是自己按 sha256(authority) + base64url 复算的,因为 BrowserAuth、COOKIE_PREFIX、cookieName 三者都没有从 @deepseek-ai/dsh-client-connection 导出(该包 lib/index.js:1072 的 export 列表里没有它们)。
如果哪天 DSH 改了 cookie 命名规则,插件会扫不到任何 dsh-auth-*,于是什么都不做——最坏情况是退回原状,不会误删。
已知边界
- 只覆盖 Host 自己的 HTTP 监听。桌面版主窗口走
dsh-app://自定义协议,它的认证 cookie 由主进程内存变量持有、经请求头注入,不写浏览器 cookie 库,因此不在此插件的处理范围内——那条路径本来也不会堆积。 - 清理发生在下次请求时,不是安装的那一瞬间。装完第一次打开页面,插件才会看到并清掉旧 cookie。如果此刻请求头已经超限,走的是「溢出兜底」那条路:一次 302,然后清干净。
- 不改 DSH 的端口策略。
--port 0是给远程访问留的正规能力,插件只在 cookie 侧做减法。
许可
MIT。
No comments yet. Be the first to write one.