dsh-deepseek-web-login 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3165 -0
- package/LICENSE +202 -0
- package/NOTICE +12 -0
- package/README.en.md +510 -0
- package/README.md +673 -0
- package/cordis.patch.yml +6 -0
- package/docs/assets/architecture.svg +264 -0
- package/docs/assets/banner-dsh-deepseek-web-login.jpg +0 -0
- package/docs/assets/hero.svg +53 -0
- package/docs/assets/login-flow.svg +307 -0
- package/docs/assets/panel-preview.svg +64 -0
- package/docs/assets/qq-group.jpg +0 -0
- package/docs/assets/screenshot-settings.png +0 -0
- package/docs/assets/screenshot-usage-stats.png +0 -0
- package/docs/assets/tool-bridge.svg +276 -0
- package/lib/client.js +2165 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +9585 -0
- package/lib/index.js.map +1 -0
- package/package.json +79 -0
- package/scripts/build.mjs +59 -0
- package/scripts/build.sh +6 -0
- package/scripts/make-dev-copy.mjs +49 -0
- package/scripts/prepare.mjs +25 -0
- package/tools/changelog-section.mjs +42 -0
- package/tools/inspect-session.mjs +223 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,3165 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
本项目遵循大致语义化版本;日期为本地时间。
|
|
4
|
+
|
|
5
|
+
## 0.2.0 — 2026-09-24
|
|
6
|
+
|
|
7
|
+
> 定位(见 `0.2.0-规划.md`):账号健康与可用性 —— 让多账号「看得见(限流/用量)、信得过
|
|
8
|
+
> (AUTH 不再一票制锁死)、切得动」。0.2.0-a 修复批已作为 0.1.84 先行发布。
|
|
9
|
+
|
|
10
|
+
### 新功能
|
|
11
|
+
|
|
12
|
+
- **F1 限流状态进探活与面板**:`users/current` 的响应体本来就带 `chat.is_muted / mute_until`
|
|
13
|
+
(probe.ts 注释 2026-09-12 实测有效,只是从没接过)。探活现在顺手解析并写回账号记录的
|
|
14
|
+
`limit` 字段 —— 「被限到 X 点解除」**提前**出现在账号徽章上,不用等生成请求撞 muted 才知道;
|
|
15
|
+
`is_muted=false` 时**清掉**旧标记(提前看见解除);响应里没有 chat 字段时不动(没探到 ≠ 没受限)。
|
|
16
|
+
- **F2 AUTH 失效先复核再标记**:0.1.80 起「已知失效的账号请求前拦截」的标记是**一票制** ——
|
|
17
|
+
一次端点级误判就把健康账号锁死(实测 2026-09-22:94ms 被拒、同 token 2 秒前刚成功)。
|
|
18
|
+
现在收到 AUTH 先用**同一份凭证**做只读探活复核:复核也失败才标记(确认失效);
|
|
19
|
+
复核通过不标记(本轮仍按失败计、用户重发即可,日志留痕)+ 顺手清掉更早的失败标记(解锁);
|
|
20
|
+
复核自身网络失败不标记(误标锁死 ≫ 漏标白跑)。
|
|
21
|
+
- **F3 按账号用量面板**:台账卡片新增「按账号」行(近 24h 各账号调用次数/失败数,
|
|
22
|
+
按调用量排序、显示备注名)。数据本来就在台账里,此前只用于 gap 计算,没返回。
|
|
23
|
+
|
|
24
|
+
### 修复
|
|
25
|
+
|
|
26
|
+
- **R6** busy 判据剔除英文裸短语 `try again later`:它是**限流**文案的常见尾巴,
|
|
27
|
+
留着会把限流误判成「并发生成」、退避从 20s 渐长退化成 5s 固定。实测过的 busy 文案
|
|
28
|
+
("A message is being generated, please try again later.")含 being generated,剔除安全。
|
|
29
|
+
- **R9a** `pickJsonFile` 加 10 分钟兜底超时:没有 cancel 事件的旧环境里用户关掉选择框后
|
|
30
|
+
promise 永远挂着。
|
|
31
|
+
- **R9b** `make-dev-copy.mjs` 改为先校验全部构建产物再删旧目录(旧写法先删后校,
|
|
32
|
+
产物缺失时旧副本已经被删才报错,想回退也没得回退)。
|
|
33
|
+
- **图片引用按内容 key 记账**:`sentRefIds` 从按 fileId 改成按内容寻址的 attachmentId
|
|
34
|
+
(adapter 新增与 `refFileIds` 平行的 `refKeys` 入参)。uploadCache 因 TTL/条数封顶驱逐后
|
|
35
|
+
重传会拿到**新 fileId**,按 id 记账会把同一张图记成「两张」,重新打开每轮重发的口子。
|
|
36
|
+
|
|
37
|
+
### 未做(及理由)
|
|
38
|
+
|
|
39
|
+
- **R2**(updateAccount 并发守卫):回读确认 `writeJsonAtomic` 全链同步
|
|
40
|
+
(writeFileSync + renameSync),Node 单线程下 read-modify-write 天然原子 —— **判据不成立**,
|
|
41
|
+
复核代理的「成立」结论被推翻。
|
|
42
|
+
- **R5**(throttleStreak 全局不按账号分桶):`noteThrottled` 调用点在纯字节流的 SSE 解析层,
|
|
43
|
+
拿不到认证上下文;要分桶得给解析函数穿透 auth,改动面与收益(退避时长估错,方向保守)不成比例。
|
|
44
|
+
|
|
45
|
+
### 工程
|
|
46
|
+
|
|
47
|
+
- `LedgerSummary` 补齐 `byAccount` 字段(tsc --noEmit 进 CI 后类型面收紧,新增返回字段必须进接口)。
|
|
48
|
+
|
|
49
|
+
### 验证
|
|
50
|
+
|
|
51
|
+
- 新增 `tests/check-auth-recheck.mjs` 6 条行为用例(F1 ×4 + F2 ×2,apply 级驱动真实 adapter)。
|
|
52
|
+
- `check-bundle` 产物断言 +6(F1 解析/写回、F2 复核顺序与出口、R6 双向、refKeys 接线、F3 客户端渲染),
|
|
53
|
+
4 条老断言按意图重写(0.1.61/0.1.83 的形态被新实现打破)。
|
|
54
|
+
- 反向验证 **6/6 精确命中**(含一条先假绿后修真的:F2a 原等待信号被 F1 残留数据污染,
|
|
55
|
+
改坏也不红 —— 换成「宿主日志信号」后有了牙齿,并顺带给复核成功分支补了清旧标记的产品行为)。
|
|
56
|
+
- 全量离线回归 **50/50**;`tsc --noEmit` 0 错误。
|
|
57
|
+
|
|
58
|
+
## 0.1.84 — 2026-09-24
|
|
59
|
+
|
|
60
|
+
**0.2.0-a 修复批**(依据 `BUG-审查-2026-09-23.md` 第六节 R2~R9 逐行复核结论)+ 全库首次接入类型检查。
|
|
61
|
+
|
|
62
|
+
修复
|
|
63
|
+
- **R3 闸门看门狗(中高)**:宿主丢弃 generator(不再驱动也不 return)时许可永不释放,
|
|
64
|
+
tail 永久卡死、此后所有请求排队不响应,只能重启 DSH。现在许可持有超 15 分钟(可配),
|
|
65
|
+
下一次 acquire 会强制回收 + 告警日志。检查点放在 acquire 进入时(无需定时器);
|
|
66
|
+
**只在串行模式生效** —— 并发模式同时活着的许可可能不止一个,全局跟踪会误杀。
|
|
67
|
+
- **R7 Windows「先清登录态」静默失效**:浏览器进程还占着 profile 目录时 rmSync 必 EBUSY,
|
|
68
|
+
旧实现吞掉后又被 `partitionCleared || browserProfileCleared` 掩盖成"已清"。
|
|
69
|
+
现在:清 profile 失败先杀残留浏览器进程重试;logout 的判定从 `||` 改为分区维度按环境能力考核
|
|
70
|
+
(桌面外环境没有分区可清,不该拖后腿),profile 没清就如实说"相当于没退出"。
|
|
71
|
+
- **R7 登录主循环**:只看 `signal.aborted` 不看子进程退出码 —— 用户关掉浏览器后空转到超时。
|
|
72
|
+
现在看 `child.exitCode` 提前退出并提示。
|
|
73
|
+
- **relogin 分支添加模式泄漏**:`endAddAccount()` 在落库之后,落库抛错会泄漏到下一次无关捕获。
|
|
74
|
+
挪进 finally(F08 同款保护,relogin 分支此前漏了)。
|
|
75
|
+
- **R8 手动粘贴 token fail-open**:粘贴 localStorage 包装 JSON(value 为 null / 坏 JSON)时,
|
|
76
|
+
旧写法把整段 JSON 当 token 落盘。现在解出空 ⇒ 明确拒绝并提示怎么改;裸 token 行为不变。
|
|
77
|
+
|
|
78
|
+
工程
|
|
79
|
+
- **CI 接入 `tsc --noEmit`**:tsdown 只转译不查类型(0.1.82 的 P0-1 就是这么漏的)。
|
|
80
|
+
全库 2026-09-24 首次清零(71 个存量错误:59 个 `.ts` 导入扩展名由 tsconfig 开关统一解决,
|
|
81
|
+
12 个真错误逐个修:writeIndex 参数、浏览器 reason 联合类型、ClientContext、
|
|
82
|
+
writable 断言、process.versions 边界、abandoned 类型、@types/react)。
|
|
83
|
+
|
|
84
|
+
验证
|
|
85
|
+
- 新增看门狗行为用例 3 条(强制回收+告警 / 正常释放不误判 / 并发模式不误杀)、
|
|
86
|
+
R8 用例 4 条(check-login-token.mjs,全部零网络)
|
|
87
|
+
- 反向验证 8/8 精确命中(S1/S2 改坏后表现为「下一个 acquire 永久挂起」的 unsettled 警告 ——
|
|
88
|
+
这正是 R3 锁死症状的直接复现)
|
|
89
|
+
- tsc --noEmit = 0 错误;全量离线回归 49/49
|
|
90
|
+
|
|
91
|
+
## 0.1.83 — 2026-09-23
|
|
92
|
+
|
|
93
|
+
**修**:链式投喂的后续轮不再重复引用历史图片(此前每轮都把最近 24 张重挂一遍)。
|
|
94
|
+
|
|
95
|
+
- 现象(用户从网页端发现):同一批图被挂到每一条新消息下,重复很多次。上传本身有缓存、只有一次,
|
|
96
|
+
重复的是**引用**(`ref_file_ids`)。根因:`adapter.ts` 的 `refFileIds: rounds === 0 ? refFileIds : []`
|
|
97
|
+
本是给「自动续写」用的判据(第 2 轮不重带),而**链式投喂每轮都是新的 adapter 调用** ⇒ `rounds` 恒为 0
|
|
98
|
+
⇒ 顺带每轮都带。
|
|
99
|
+
- **真机实测**(新增 `tests/probe-image-chain.mjs`)证明这批是纯冗余:服务端按 parent 链回溯时,
|
|
100
|
+
**历史消息的附件也在上下文里** —— 两张不同布局的图、第二轮都不带 `ref_file_ids`,仍都答对四个角
|
|
101
|
+
(4/4,单组蒙对 1/256)。
|
|
102
|
+
- 改法:判据从「第几轮」改成「**服务端手里有没有**」,位置移到 `webapi.ts` 的决策点(`decideFeed` 之后)——
|
|
103
|
+
有父链(`chained`)时只发"服务端还没见过的"(按会话维护 `sentRefIds`,**请求被接受之后**才记账);
|
|
104
|
+
`restart` / 换号 / 新会话 ⇒ 发全部。⚠️ 是**差集**而不是清空:本轮新贴的图只存在于增量里,漏了就是功能坏。
|
|
105
|
+
- 附带效果:请求体不再带一批重复 id,网页端不再重复挂图;会话内图片引用也不再单调累积
|
|
106
|
+
(这曾是 `code 10 / too many ref file` 的一个可疑上游因素,未单独验证)。
|
|
107
|
+
|
|
108
|
+
验证:新增 `tests/check-image-chain-send.mjs` 8 条(含「新贴的图不能丢」与「restart 必须全发」两侧);
|
|
109
|
+
`check-bundle` +6 条产物断言;反向验证 7/7 精确命中;全量离线回归 48/48。
|
|
110
|
+
|
|
111
|
+
## 0.1.82 — 2026-09-23
|
|
112
|
+
|
|
113
|
+
全库审查(25 个模块 / 14653 行)确认的缺陷一次性修完。**三条 P0 里有两条是"功能一直不可用但没人发现"**。
|
|
114
|
+
|
|
115
|
+
**P0**
|
|
116
|
+
- **设置页的「图片上限」滑块从来就没生效过**:前端 POST 了 `maxRefImages`,而宿主 `/gate` 的
|
|
117
|
+
patch 白名单里没有它 ⇒ `patch` 为空 ⇒ 直接返回 400「没有可更新的字段」,滑块弹回默认值。
|
|
118
|
+
这正是防 `code 10 / too many ref file` 把整个会话打死的那道阀门(0.1.77 引入它时只接了显示层)。
|
|
119
|
+
- **每次「重登」都会清空账号元数据**(备注名 / 分组 / 显示名 / `serverId`):重登只传
|
|
120
|
+
`{ id: target }`,而 `carried` 的取值源 `existing` 是靠 **serverId 或 token 相等**解析的 ——
|
|
121
|
+
重登**必然换 token**、CDP 捕获又**不带身份** ⇒ `existing` 恒为 `undefined` ⇒ 白名单全部落空。
|
|
122
|
+
0.1.75 修的是"去重命中"那条路径,而用例恰好也测的是那条,两边一起错位。现在 `upsertAccount`
|
|
123
|
+
**优先认 `patch.id` 指向的记录**。
|
|
124
|
+
- **「重登意图」会劫持下一次无关的登录**:`endRelogin()` 全库只有 1 个调用点,而重登意图在
|
|
125
|
+
`commitCapturedAuth` 里**优先于**添加模式、TTL 60 分钟。点了「重登」又关掉窗口,这一小时内
|
|
126
|
+
用「登录新账号」登另一个号 ⇒ 新号凭证被写进旧记录(旧身份还留着)。
|
|
127
|
+
现在 `/login/add`、`/accounts/switch`、`/logout` 都会清掉它,重登分支也会一并消费添加模式。
|
|
128
|
+
|
|
129
|
+
**P1**
|
|
130
|
+
- **浏览器登录(CDP)先落库、后校验身份** ⇒ 每次登录都给同一个账号新增一条记录,`serverId`
|
|
131
|
+
这条去重键永远补不上。改为**先 `validateAuth` 拿身份 → `withVerifiedIdentity` → 再落库**,
|
|
132
|
+
身份回写补上 `serverId`。
|
|
133
|
+
- **跑一次网络诊断会把传输层悄悄退回 Node**:`finally` 里写的是无参 `setFetchImpl()`
|
|
134
|
+
(语义是"还原为 Node 全局 fetch"),而默认配置是 Chromium 网络栈 ⇒ 一次性丢掉 TLS 指纹,
|
|
135
|
+
设置页却仍显示 `chromium`,直到重启。现在按进来时那一个还原,不一致还会标红。
|
|
136
|
+
- **图片提示标记与"真的发出去了"不一致**:`keptKeys` 此前按**裁剪列表**预先算,上传失败的图
|
|
137
|
+
仍留在集合里 ⇒ prompt 写 `[image attached]`,而 `ref_file_ids` 里没有它,模型对着没送出去的
|
|
138
|
+
图瞎猜。现在按**上传成功(含缓存命中)**累计。
|
|
139
|
+
- **`__skipImages`(不带图重发)会清空 `lastImageKeys`** ⇒ "这批 key 被拒过"的知识被抹掉,
|
|
140
|
+
下一轮又从中毒缓存出发,重演"2 次注定被拒 + 1 次无图"。
|
|
141
|
+
|
|
142
|
+
**P2**
|
|
143
|
+
- 上传缓存的 `get` 补上作用域校验(与 `set` 的 `expectScope` 对称,防并发时 A/B 账号引用串号)。
|
|
144
|
+
- XML 捕获态下**调用块之后的正文不再凭空消失**(此前 `flush` 只回吐 calls,那段既不回吐也不
|
|
145
|
+
计入 rejected ⇒ 回答"凭空中断"且日志零线索)。
|
|
146
|
+
- `appendSink` 补 `else` 兜底:`sink` 未定时按正文发射,不再静默丢字(与 `appendToLastFragment` 对齐)。
|
|
147
|
+
- 批量删会话的失败分三态:5xx / 429 / 网关 HTML 不再被当成"服务端不支持批量"永久关掉(那会让
|
|
148
|
+
此后每批退化成 N 个请求,把请求密度抬上去)。
|
|
149
|
+
- 账号目录里混入 `.json` / `..json` 这类非法文件名时,不再让**整个账号库读不出来**。
|
|
150
|
+
- 闸门等待的计时器改为"可取消"(不是 `unref` —— 被 `await` 的 promise 一旦失去 ref,
|
|
151
|
+
事件循环可能空转退出,表现为进程挂在 `await` 上永不结算;这个错法是用例当场抓出来的)。
|
|
152
|
+
- 界面:`renderKv` 挪到「宿主进程」那行 push 之后(此前那行**永远不显示**,而它正是"窗口打不开"
|
|
153
|
+
时唯一能说明原因的证据);登录按钮在轮询期间保持禁用(防等待中再点一次开第二个窗口);
|
|
154
|
+
台账迷你图真的把失败时段标出来(此前恒传空集)。
|
|
155
|
+
|
|
156
|
+
**还没做(建议下一步,本次刻意不动)**:构建链路(tsdown)**不做类型检查**,P0-1 里
|
|
157
|
+
`saveGate({ maxRefImages })` 与声明的参数类型不符却一路构建通过、发到线上 —— 给 CI 加一步
|
|
158
|
+
`tsc --noEmit` 能拦住这一整类"字段名/类型对不上"的问题,但它需要新增 devDependency 并改锁文件,
|
|
159
|
+
不适合和这批修复混在一起发。
|
|
160
|
+
|
|
161
|
+
## 0.1.81 — 2026-09-22
|
|
162
|
+
|
|
163
|
+
**截断提示改成「阶梯」上屏**(修 0.1.79 没修干净的地方)。
|
|
164
|
+
0.1.79 的抑制用的是 `总条数:保留数` 这个**精确签名**,而「总条数」几乎每轮都在涨 ——
|
|
165
|
+
模型每 `read_image` 一次就多一条内容 ⇒ 签名变了 ⇒ 提示照旧每轮上屏
|
|
166
|
+
(现场:29 份 → 30 份又来一遍,用户说「这句话的频率还是有点高」)。
|
|
167
|
+
|
|
168
|
+
- 新的判据:**首次必说明;之后只有「被略过的份数」比上次翻倍、且至少再多 10 份时才再说一次。**
|
|
169
|
+
一条会话里它是 **O(log n)** 次而不是 O(n) 次(按 100 份估算:约 5~6 次)。
|
|
170
|
+
- 保留份数变了(用户改了 `maxRefImages`)⇒ 情况本身变了,重新说明一次。
|
|
171
|
+
- 文案顺手压短(去掉一句重复的铺陈),日志照旧每轮都记。
|
|
172
|
+
- ⇒ **通用判据:抑制重复的"签名"里只要含一个单调增长、且变化频繁的量,抑制就等于没做。**
|
|
173
|
+
|
|
174
|
+
## 0.1.80 — 2026-09-22
|
|
175
|
+
|
|
176
|
+
**已知授权失效的账号,请求不再发出。** 现场(09-21):探活在 22:42 就判定了 token 失效并写进
|
|
177
|
+
账号记录,但**请求路径没人看那块牌子** —— 22:50 仍拿它去跑,14 张图逐个走一次 POW + 一次上传
|
|
178
|
+
(**28 次注定失败的请求**;而且这些无效请求同样暴露在风控下),直到撞上 AUTH 才停。
|
|
179
|
+
判据本身早就写好了(`probe.ts` 的 `lastProbeFailed`),只是从来没有被调用。
|
|
180
|
+
|
|
181
|
+
- 现在在**发起请求前**检查:命中即抛 `AUTH`,**一个请求都不发**,并给出两条出路 ——
|
|
182
|
+
重新登录(正解),或点「校验全部」重新确认(用于"其实还能用"的误判情形)。
|
|
183
|
+
- 判据只认**授权类**失败(`Authorization Failed` / `invalid token` / `HTTP 401·403` /
|
|
184
|
+
中文「登录态已失效」)。断网、超时、5xx、**429** 一律放行 —— 否则一次网络抖动就会把健康账号
|
|
185
|
+
锁住。代价不对等:漏拦只是白跑一轮(后面还有 AUTH 兜底),误拦是功能坏了。
|
|
186
|
+
- 标记被清掉(重新登录成功 / 探活通过)后自动恢复发请求,用例里钉了这条。
|
|
187
|
+
|
|
188
|
+
**图片上传遇授权失败时中止剩余。** 同一个 token 上传剩下的图必然同样被拒,继续试纯粹是白跑
|
|
189
|
+
请求(每张一次 POW + 一次上传)。第一张撞 `AUTH` 就停,并把这句写进告知语
|
|
190
|
+
(「剩余 N 张未再尝试」)—— 否则文案只报 1 张,看着像小事。
|
|
191
|
+
⚠️ 只对 `AUTH` 短路:5xx 之类的偶发失败照常试完,不把偶发放大成"整批放弃"。
|
|
192
|
+
|
|
193
|
+
## 0.1.79 — 2026-09-21
|
|
194
|
+
|
|
195
|
+
### 修:图片截断提示每轮刷屏、且量词误导
|
|
196
|
+
|
|
197
|
+
**症状**(用户实测,drawio 配图会话):同一句「本轮只带了最近的 24 张图片,更早的 14 张没有随请求发送」
|
|
198
|
+
在 **19 分钟里上屏 52 次**;用户反问"我顶多发了 6 张图" —— 量词「张」让他以为是在说自己贴的图。
|
|
199
|
+
|
|
200
|
+
**实测**(新增排查工具 `tests/count-session-images.mjs`):读会话历史数出来 **40 个唯一图片内容**
|
|
201
|
+
(插件日志里 38 → 末轮 40,吻合)。这 40 份里用户手贴的只有个位数,
|
|
202
|
+
**其余绝大多数是模型自己 `read_image` 读进来的局部校验片段**
|
|
203
|
+
(`_chk_1.png` / `_probe_top.png` / `_zoom_midcap.png` / `_docx_fig4_top.png` …),
|
|
204
|
+
再加上迭代产生的新版本(同一个 `zh_compare.png` 挂着两个不同 hash)。
|
|
205
|
+
⇒ **数字没错,错的是叫法**:它是「图片**内容条目**」,不是「图片张数」。
|
|
206
|
+
|
|
207
|
+
**修法**:
|
|
208
|
+
|
|
209
|
+
1. **换量词 + 说清来源**:改说「N **份图片内容**」,并点明"这个数字指的是图片内容条目、
|
|
210
|
+
不是你贴的张数:模型每读一次图、或同一张图被重新渲染/裁剪出新内容,都会多算一条"。
|
|
211
|
+
2. **同样规模只上屏一次**:新增 `lastTrimSignature`(`总条数:保留数`),规模没变就只写日志;
|
|
212
|
+
规模**变了**才再提示一次(说明情况在恶化)。日志仍每轮都记,排查不受影响。
|
|
213
|
+
|
|
214
|
+
**新增排查工具** `tests/count-session-images.mjs`。
|
|
215
|
+
⚠️ 顺带记一个坑:`session.v3.jsonl.zstd` 是**多帧 zstd**,`zstdDecompressSync()` 与
|
|
216
|
+
`createZstdDecompress()` **都只解第一帧**(只出 231 字节 = session 头),极易误判成"这个会话没图片"。
|
|
217
|
+
必须按魔数 `28 B5 2F FD` 切帧、逐帧解,解不出的段当假阳性跳过。
|
|
218
|
+
|
|
219
|
+
**⚠️ 没改的**:截断本身不能去掉 —— 服务端对单次请求能引用的图片数有硬上限(实测 40~52),
|
|
220
|
+
40 份内容已贴着线,不截断整轮会被拒(`code 10` 的形态)。
|
|
221
|
+
|
|
222
|
+
## 0.1.78 — 2026-09-21
|
|
223
|
+
|
|
224
|
+
### 修:`code 9 / invalid ref file id` 会让整个会话卡死 → 两级降级自救
|
|
225
|
+
|
|
226
|
+
**症状**:从某一轮起整轮失败,报 `DeepSeek 网页端错误(code 9):invalid ref file id`,
|
|
227
|
+
而且**此后每一轮都失败** —— 图留在 DSH 的消息历史里,每轮重新收集、重新引用、再撞一次,
|
|
228
|
+
用户除了丢掉整个会话没有别的出路。
|
|
229
|
+
|
|
230
|
+
**根因不在「重复 id」上**(那是 0.1.66 修过的另一种形态)。本次现场:同样的操作在另一个账号上正常,
|
|
231
|
+
出问题的那个账号 `capturedAt` 刚刷新过 —— 但**因果并未证实**。所以本版不去猜「哪份凭证有问题」,
|
|
232
|
+
而是让这条错误**可自愈**。
|
|
233
|
+
|
|
234
|
+
**修法**:
|
|
235
|
+
|
|
236
|
+
1. **`code 9` 的两个含义分开了**(`src/webapi.ts`):上传时是 `unsupported file type`(换名字重传即可),
|
|
237
|
+
发请求时才是 `invalid ref file id`。后者单独给稳定错误码 `INVALID_REF_FILE`。
|
|
238
|
+
2. **闸门外包一层降级重试**(`src/adapter.ts`),每级各一次:
|
|
239
|
+
- 被拒 → 丢掉那几张的上传缓存、**重新上传**拿新 `file_id` 再试
|
|
240
|
+
- 仍被拒 → **不带任何图片**重发,至少让这一轮继续下去
|
|
241
|
+
3. **只在「一个字都还没吐给上层」时才重试** —— generator 已 yield 的内容撤不回来,
|
|
242
|
+
重试会让用户看到重复输出。
|
|
243
|
+
4. **缓存是定点清理**(只清被拒的那几条),不是全清 —— 全清会让下一轮把所有历史图重传一遍。
|
|
244
|
+
|
|
245
|
+
### 顺带:捕获凭证时留痕(`src/auth.ts` / `src/login.ts`)
|
|
246
|
+
|
|
247
|
+
捕获时若发现「只拿到 token、cookie 与请求头都为空」,给凭证记一条 `captureWarning` 并写进日志。
|
|
248
|
+
**只记录、不拦截** —— 实测鉴权只用 token,缺 cookie 本身是合法的(手工粘 token 的账号就是这样),
|
|
249
|
+
拿它拦人会把正常路径一起误伤。留痕是为了下次再遇到这类问题时,能立刻定位到「这份凭证可疑」。
|
|
250
|
+
|
|
251
|
+
## 0.1.77 — 2026-09-19
|
|
252
|
+
|
|
253
|
+
### 修:一次请求能带的图片数没有上限 → `code 10 / too many ref file` 会让整个会话作废
|
|
254
|
+
|
|
255
|
+
(问题由群友的实测报告定位,本版按它给的方向修复;报告内容已逐条核对代码,属实。)
|
|
256
|
+
|
|
257
|
+
**现象**:长会话里反复读图(`read_image` / 截图迭代),从某一步起**每一轮都失败**:
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
本轮运行失败
|
|
261
|
+
DeepSeek 网页端错误(code 10):too many ref file
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**根因**:图片是**请求级**的 —— 一次请求用一个 `ref_file_ids` 带一批,网页端对这一批的数量有上限。
|
|
265
|
+
而插件把整段历史里去重后的图片**全量**塞进去,**一个长度阀门都没有**。去重救不了它:
|
|
266
|
+
`attachmentId` 是内容寻址的,重新截图 / 重新渲染的预览图每次内容都变 ⇒ 新 id ⇒ 历史里只增不减。
|
|
267
|
+
越过上限后服务端拒收**整轮**,而图还留在历史里 ⇒ **该会话此后每一轮都失败**,用户只能丢掉全部上下文。
|
|
268
|
+
|
|
269
|
+
**定量边界**(来自群友实测;那份日志在对方机器上,我们无法独立复核,但它主动标注了不确定性):
|
|
270
|
+
最后一次成功 40 张、第一次失败 52 张 ⇒ 真值落在 **(40, 52]**。
|
|
271
|
+
|
|
272
|
+
**修法**:
|
|
273
|
+
|
|
274
|
+
- 按时间只带**最近的 N 张**。新增配置 `maxRefImages`:默认 **24**、区间 `[0, 100]`,`0` = 不限制
|
|
275
|
+
—— 24 给已知安全线留了 16 张余量。
|
|
276
|
+
- **标记同步收敛**:被略过的图不再写 `[image attached]`,改写 `[earlier image omitted]`。
|
|
277
|
+
只截 id 不截标记的话,模型会以为它收到了那些图,转而对着没送出去的图瞎猜。
|
|
278
|
+
标记**按图片在消息里的实际顺序**逐张给出(不能写成一堆 attached 再一堆 omitted,
|
|
279
|
+
否则模型搞不清被省略的是哪几张)。
|
|
280
|
+
- **告知用户,而且措辞不是报警**:
|
|
281
|
+
|
|
282
|
+
> `[deepseek-web] 本轮只带了最近的 24 张图片,更早的 36 张没有随请求发送。网页端对一次请求能引用的图片数量有上限(实测 40~52 之间),超了整轮都会被拒,所以按时间留最近的几张 —— 这是正常的长度控制,不是错误。`
|
|
283
|
+
|
|
284
|
+
用报错口吻会让人以为出了问题,而在弄清原因之前,他很可能就把一个本可以继续用的会话丢掉了。
|
|
285
|
+
- 设置页新增「图片上限」滑块(`0` 那一档明确标成"不限制(不推荐)")。
|
|
286
|
+
|
|
287
|
+
**覆盖**:`check-image-refs` +7 条(截断取最近几张、标记与实发严格一致且按顺序、
|
|
288
|
+
提示不是报警口吻、不超限时行为与改动前一致、恰好等于上限不触发、可配、`0` = 不限制);
|
|
289
|
+
`check-bundle` +4 条产物断言;反向验证 **7/7 精确命中**;全量离线回归 **44/44**。
|
|
290
|
+
|
|
291
|
+
> 顺带修掉一条被本次改动打破的老断言(0.1.66 那条断的是 `notice: imageNotice(...)` 的固定形态,
|
|
292
|
+
> 现在改成先收集 notices 再一次性回传)—— 按意图重写而不是跟着代码改。
|
|
293
|
+
|
|
294
|
+
## 0.1.76 — 2026-09-17
|
|
295
|
+
|
|
296
|
+
### 改:prompt 上限默认值 150 万 → 40 万(它其实是风控阀门)
|
|
297
|
+
|
|
298
|
+
`maxPromptChars` 决定**每次请求最多带多少字符的转写**。而网页端是**无状态**的 —— 每一轮都要
|
|
299
|
+
把整段对话重新发一遍 —— 所以这个数字直接等于**单次请求的体量**,它不只是"能记多长的对话"。
|
|
300
|
+
|
|
301
|
+
把它默认顶在 150 万字符(纯中文 ≈100 万 token),等于默认就允许"一次顶满 1M 上下文"。
|
|
302
|
+
实测同一个会话里单次输入从 9.7k token 涨到 **293k**;我们自己的四个账号在两天内陆续被限制,
|
|
303
|
+
体量是主要嫌疑。
|
|
304
|
+
|
|
305
|
+
所以默认降到 **40 万**(≈27 万 token,长任务够用)。**可调范围不变**(仍是 `[12 万, 150 万]`)。
|
|
306
|
+
|
|
307
|
+
> ⚠️ 确实需要更长的转写,仍可以自己往上调,但请把它理解为「**拿账号稳定换更长的记忆**」:
|
|
308
|
+
> 真要长上下文,更划算的是把「上下文投喂」切成 `chained`(只发增量、历史交给服务端维护),
|
|
309
|
+
> 而不是抬高这个天花板 —— 后者是**每一轮**都要多付的成本。
|
|
310
|
+
|
|
311
|
+
改动落在:`src/gate.ts` 的 `DEFAULT_MAX_PROMPT_CHARS`;`src/adapter.ts` 里三处兜底改成引用同一个
|
|
312
|
+
常量(原先各自硬编码 `1_500_000` —— "同一个默认值散在三处"本身就是隐患)。README 中英在配置表
|
|
313
|
+
和「配置」一节都写明了这层取舍,面板上那个旋钮的初始位置也随之落在区间中部、不再顶格。
|
|
314
|
+
|
|
315
|
+
顺带把 `tests/check-request-gate.mjs` 里一条**把默认值写死成 `1_500_000`** 的断言按意图重写:
|
|
316
|
+
改为断言"默认值必须落在可调区间内、且**不等于上限**"(顶格 = 默认就把阀门开到最大,正是本版要改掉的)。
|
|
317
|
+
写死数值等于把"当时恰好取了这个数"当成期望值 —— 与 0.1.72 那条把 bug 固化下来的断言同族。
|
|
318
|
+
|
|
319
|
+
## 0.1.75 — 2026-09-17
|
|
320
|
+
|
|
321
|
+
### 修:重登把账号"弄没了"—— 两个独立的真 bug
|
|
322
|
+
|
|
323
|
+
用户实测:"点了重登,账号变成未识别账号;再发个消息显示 API 密钥无效,账号也退出了。"查下来是两件事。
|
|
324
|
+
|
|
325
|
+
**① 重登把显示名抹掉了**(这才是"账号看起来退出了"的原因)
|
|
326
|
+
|
|
327
|
+
库里三个账号对照:
|
|
328
|
+
|
|
329
|
+
| 账号 | user 字段 |
|
|
330
|
+
| --- | --- |
|
|
331
|
+
| `acc_cdc0000f` | `{"display": "137******78"}` |
|
|
332
|
+
| `acc_ec650e63` | `{"display": "lidi*********+mn1@gmail.com"}` |
|
|
333
|
+
| `acc_d14996c7` | `null` —— 但 `serverId` 还在,说明它本来是有身份的 ← 就是它 |
|
|
334
|
+
|
|
335
|
+
`upsertAccount()` 在带"用户附加的元数据"时用的白名单里**没有 `user`**。而重登抓回来的凭证是
|
|
336
|
+
`unverified` 状态 —— **服务端还没校验,拿不到账号信息** ⇒ 合并时 `user` 为空 ⇒
|
|
337
|
+
**把原记录里可辨识的显示名覆盖没了**:界面从「137\*\*\*\*\*\*78」退化成「未识别账号 (acc_xxxx)」。
|
|
338
|
+
|
|
339
|
+
修法:`user` **单独做深合并**(新值优先、缺的字段沿用旧值),不再走那条 `??` 链。
|
|
340
|
+
这与之前那句"重登一次,账号从分组里掉出来"是**同一个疏漏的另一半** —— 那次给白名单补了 `groupId`。
|
|
341
|
+
|
|
342
|
+
> `user.display` 是服务端在**校验通过**时才给的一次性信息,被抹掉后要等下次校验成功才补得回来 ——
|
|
343
|
+
> 也就是说这个 bug 能自愈,但要等。
|
|
344
|
+
|
|
345
|
+
**② 「重登」在浏览器登录态已失效时是个死循环**(这是点了没反应的原因)
|
|
346
|
+
|
|
347
|
+
「重登」的设计是**复用浏览器 profile 里的登录态**(卖点:不敲密码)。但那份登录态**已经失效**时,
|
|
348
|
+
复用等于把同一个坏 token 再抓一遍。日志实证(连点两次完全一样):
|
|
349
|
+
|
|
350
|
+
```
|
|
351
|
+
14:53:20 点「重登」
|
|
352
|
+
14:53:22 1 秒内「已捕获 token」 ← 复用来的,人不可能 1 秒登完
|
|
353
|
+
14:53:27 校验 → invalid token
|
|
354
|
+
14:54:05 再点一次 → 14:54:07 又是同一个坏凭证
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
修法:**账号已被标记失效时,重登不再复用** —— 先清掉 profile 与登录分区,让浏览器窗口从零打开、
|
|
358
|
+
你真正登录一次;账号健康时的复用行为**完全不变**。界面上的按钮提示与进度文案改为跟随宿主返回,
|
|
359
|
+
不再写死"不清理登录态"。
|
|
360
|
+
|
|
361
|
+
> 不是 bug 的部分:DSH 界面上的「API 密钥无效」是它对 `AUTH` 类错误的统一文案(服务端确实回了
|
|
362
|
+
> `Authorization Failed (invalid token)`);账号**没有被删除**,那个红标是 0.1.61 有意加的提示
|
|
363
|
+
> (否则切到死号时界面完全静默,你不知道是哪个账号出问题)。
|
|
364
|
+
|
|
365
|
+
新增 `tests/check-relogin-integrity.mjs`(7 条);`check-bundle` 加 4 条产物断言,并把一条老断言按
|
|
366
|
+
**意图**重写(它数的是"`clearLoginState()` 恰好出现 2 次",多一个入口就假红 —— 等于把"当时有几处"
|
|
367
|
+
当成了期望值);反向验证 **7/7 精确命中**;全量离线回归 **44/44**。
|
|
368
|
+
|
|
369
|
+
## 0.1.74 — 2026-09-17
|
|
370
|
+
|
|
371
|
+
### 修:模型把 `run_code` 的代码写成正文 → 那一轮零工具调用 → 会话"停下来了"
|
|
372
|
+
|
|
373
|
+
现场(11:00 那次会话):连续三轮里模型都在 reasoning 里写着"现在真正发出工具调用",
|
|
374
|
+
输出的却是一段带围栏的 `ts` 裸代码块 —— **正文里连协议标记都没有**,整会话 `tool_calls`
|
|
375
|
+
零命中。没有工具调用 ⇒ agent loop 判定回合结束 ⇒ 你看到的是"它停下来了"。
|
|
376
|
+
模型自己在 reasoning 里也承认了:「我把 TypeScript 代码写成了正文文本,而不是作为工具调用发出」。
|
|
377
|
+
|
|
378
|
+
**根因不在适配器**:同一个 preset、同一个插件、同一个模型,另一个长会话里 `run_code`
|
|
379
|
+
被正常调用了 **178 次**。差别在于那个是长会话(历史里全是正确的调用形态可以模仿),
|
|
380
|
+
这次是新建会话、冷启动第一轮就选错了方向。
|
|
381
|
+
|
|
382
|
+
真正的原因是**第三方 preset 注入的框架与工具调用协议冲突**:染神 preset 的
|
|
383
|
+
`tool-bootstrap.mjs` 往系统提示里加了一句 PTC 说明 ——「你在 Programmatic Tool Calling 模式,
|
|
384
|
+
所有动作必须通过 `run_code` 写 TypeScript 程序完成」;同 preset 的 persona 里还写着
|
|
385
|
+
`One complete deliverable per turn: numbered steps or code blocks` 与
|
|
386
|
+
`When rules conflict, choose the reading that still produces the deliverable`。
|
|
387
|
+
模型于是把"写出程序"当成了"执行程序"。
|
|
388
|
+
|
|
389
|
+
**这一版加运行时兜底**:识别出这种形态后,自动追加**一轮纠正**请求。
|
|
390
|
+
|
|
391
|
+
- 判据 `looksLikeUnexecutedToolProgram()`(`src/protocol.ts`):正文里出现围栏代码块、
|
|
392
|
+
且块内含 `tools.<名字>(…)` 形态的调用(那是程序体的特征);没有围栏时只认带 `await` 的形态。
|
|
393
|
+
围栏块还要过 80 字符的长度门槛 —— 短于这个多半只是"提到某个 API"而不是写程序。
|
|
394
|
+
- 触发条件(全部满足才动手):本轮**零工具调用** + 正文命中判据 + `purpose === 'chat'` +
|
|
395
|
+
没被中止。**只给一次机会**,纠正不成就正常收尾 —— 不把请求密度打上去。
|
|
396
|
+
- 纠正指令直接对抗那句 PTC 措辞:*即使系统提示要求你写 TypeScript 程序,那个程序也必须
|
|
397
|
+
放进工具调用的 arguments 里 —— 只有作为工具调用发出,它才会真的被执行。*
|
|
398
|
+
- 与「自动续写」是**互斥的两条分支**:续写管"话没说完",这个管"话说完了、但该发的动作没发出去",
|
|
399
|
+
两者要发的内容完全不同。它同样挂在 `autoContinue` 开关下,关掉续写就一并关掉。
|
|
400
|
+
|
|
401
|
+
新增 `tests/check-unexecuted-program.mjs`(10 条,正反两侧都钉);
|
|
402
|
+
`check-auto-continue` 加 5 条行为用例(其中 3 条是防误伤:普通正文 / 无关代码块 /
|
|
403
|
+
**本轮已拿到工具调用**时的健康形态);`check-bundle` 加 3 条产物断言;
|
|
404
|
+
反向验证 **8/8 精确命中**;全量离线回归 **43/43**。
|
|
405
|
+
|
|
406
|
+
## 0.1.73 — 2026-09-17
|
|
407
|
+
|
|
408
|
+
### 修:「浏览器窗口登录」主按钮不清登录态(四个登录入口里唯一的疏漏)
|
|
409
|
+
|
|
410
|
+
四个登录入口里**三个都做了前置清理**,只有客户端那个主按钮是"裸"的 —— 它直接调
|
|
411
|
+
`/login/browser`,没有任何清理。而登录用的是**独立 profile**
|
|
412
|
+
(`<DSH_HOME>/web-login/browser-profile`,**跨次保留**)⇒ 里面若还留着上次那个账号的登录态,
|
|
413
|
+
Edge 一打开就是已登录:**你以为在登录,抓回来的却是旧号**。
|
|
414
|
+
|
|
415
|
+
- 客户端主按钮改为带 `fresh: true`;`/login/browser` 在该标志下先清登录态
|
|
416
|
+
(**按需**,不是路由的默认行为)。
|
|
417
|
+
- 把「清 profile + 清登录分区」收口成 `src/login.ts` 的 `clearLoginState()` ——
|
|
418
|
+
`/login/add` 原先自己写那两行,**这次的疏漏正是这种重复代码漂移出来的**,一并收口。
|
|
419
|
+
- 「重登」**仍然有意不清**:留着登录态才能"一打开就复用、一个密码都不用敲",
|
|
420
|
+
这是当初特意做的取舍 —— 想省输入的场景走它。
|
|
421
|
+
|
|
422
|
+
新增 `tests/check-login-fresh.mjs`(5 条)+ `check-bundle` 4 条产物断言;
|
|
423
|
+
反向验证 4/4 精确命中;全量离线回归 42/42。
|
|
424
|
+
|
|
425
|
+
> 💡 顺带澄清:登录用的那只 Edge **一直**是独立 profile(`--user-data-dir` / `--disable-sync` /
|
|
426
|
+
> `--no-service-autorun`),**不读也不写你日常 Edge 的 cookie 与历史** —— 隐私上早就隔离了,
|
|
427
|
+
> 这次修的只是"那个 profile 里残留的登录态没被清"。
|
|
428
|
+
|
|
429
|
+
## 0.1.72 — 2026-09-16
|
|
430
|
+
|
|
431
|
+
### 修:账号库标题退化成 `acc_97768033`(0.1.71 的回归)
|
|
432
|
+
|
|
433
|
+
0.1.71 把「按组分区」挪到宿主算时,喂给 `partitionByGroup` 的是**原始账号记录**,
|
|
434
|
+
而它返回的 `accounts` 会**原样**交给客户端渲染 —— 而 `title` / `display` / `isActive`
|
|
435
|
+
都是**响应加工字段**,原始记录里没有。于是面板上:
|
|
436
|
+
|
|
437
|
+
- 标题退化成内部 id(`acc_97768033`),显示名与「✅ 当前」徽章一起消失;
|
|
438
|
+
- 分区本身是好的(排序、置顶、未分组兜底都正常)—— 坏的只是"渲染字段"这一层。
|
|
439
|
+
|
|
440
|
+
修法:先 map 出「可直接渲染的视图」,再把**视图数组**喂给 `partitionByGroup`
|
|
441
|
+
(它是泛型 `T extends GroupableAccount`,只要求 `id` + `groupId`,视图满足)。
|
|
442
|
+
顶层 `accounts` 继续返回 —— 客户端有"没有 sections 就平铺"的兜底路径,且重建签名覆盖它。
|
|
443
|
+
|
|
444
|
+
**为什么原来的测试没抓住**:
|
|
445
|
+
|
|
446
|
+
- `check-account-groups.mjs` 的 17 条是**纯函数**用例,而 `partitionByGroup` 对
|
|
447
|
+
"喂原始记录还是喂视图"一视同仁(两者都有 `id` + `groupId`)—— 差别只在**接线层由谁喂**;
|
|
448
|
+
- 更糟的是那条产物断言当时写成 `sections: partitionByGroup(list, groups, activeId)`,
|
|
449
|
+
**把 bug 固化成了期望值**。
|
|
450
|
+
|
|
451
|
+
新增 `tests/check-accounts-view.mjs`(9 条,用真实 `apply()` 调 `/accounts`):
|
|
452
|
+
|
|
453
|
+
- `title` 非空、不等于内部 id、且是掩码后的标识;`display` 非空;
|
|
454
|
+
- `sections` 里的账号同样带 `title` / `isActive` / `cookieMeta` / `lastVerifiedAt`
|
|
455
|
+
(回归的哨兵:标题退化与「当前」徽章消失都在这里抓);
|
|
456
|
+
- 同时守住「分区 / 置顶 / 未分组兜底仍然工作」,免得修这个把那个砍了。
|
|
457
|
+
|
|
458
|
+
产物断言改成表达**意图**:`sections` 必须喂视图数组 + 不得再出现
|
|
459
|
+
`partitionByGroup(list, …)`。反向验证 4/4 精确命中。
|
|
460
|
+
|
|
461
|
+
## 0.1.71 — 2026-09-16
|
|
462
|
+
|
|
463
|
+
### 新增:账号库分组、备注与状态刷新;按钮文案名实相符
|
|
464
|
+
|
|
465
|
+
账号多起来之后列表固定按**捕获时间倒序**排(新号永远插最前、常用的那个一路往下沉),
|
|
466
|
+
也没法把「工作号 / 备用号」分开看。这一版补齐。
|
|
467
|
+
|
|
468
|
+
- **分组**:新建 / 改名 / 删除,账号归组(每行一个下拉),列表按组分区、组可折叠
|
|
469
|
+
(折叠状态记在本地,重建列表不会丢),未分组的落进「未分组」兜底。
|
|
470
|
+
- **当前账号所在的组自动置顶** —— 正在用的号不会沉到看不见的地方。组内仍是捕获时间倒序。
|
|
471
|
+
- **组定义单独存** `~/.dsh/web-login/groups.json`,账号记录里只有一个 `groupId` 指针。
|
|
472
|
+
因此**删组不删账号**:删组只删定义、不遍历账号文件;组被删后指针悬空的账号一律落进
|
|
473
|
+
「未分组」,一个都不会从列表里消失。
|
|
474
|
+
- **「校验全部」**:对库里每个账号跑一次只读探活(`users/current`,**零额度**),
|
|
475
|
+
串行执行并有并发互斥;用来刷新登录态、补上账号名、清掉已恢复的失败标记。
|
|
476
|
+
- 「重命名」→「**备注**」(它写的一直是备注名,改的只是名字,不是功能)、
|
|
477
|
+
「重新登录」→「**重登**」。
|
|
478
|
+
|
|
479
|
+
**分组只影响显示,不影响行为**:切号、会话复用槽、会话清理策略一律不看它 ——
|
|
480
|
+
分组是你的文件夹,插件的调度逻辑保持简单可预期。
|
|
481
|
+
|
|
482
|
+
⚠️ 面板上「校验全部」(本版新增)与「刷新状态」(原有,刷 `/status` 那张卡)是两回事,别混。
|
|
483
|
+
|
|
484
|
+
## 0.1.70 — 2026-09-16
|
|
485
|
+
|
|
486
|
+
### 修:回答「说了一半就停了」的两种成因(都属于静默丢内容)
|
|
487
|
+
|
|
488
|
+
同一类症状、两个不同根因 —— 都是「模型其实说了,但内容没到用户眼前」。
|
|
489
|
+
|
|
490
|
+
**① 思考跑飞:整轮只有思考、没有正文也没有工具调用 → 被当成「正常完成」**
|
|
491
|
+
|
|
492
|
+
- 现象:模型停在半路,只能手动敲「继续」才动。实测某个会话 16:12–16:36 的 15 轮里中了 **2 轮**。
|
|
493
|
+
- 根因:`outputChars` 把**思考通道的字数也算作「有输出」** ⇒ 跳过空响应分支、直接报 `stop`
|
|
494
|
+
(正常完成)⇒ agent loop 认为这回合干完了。
|
|
495
|
+
- 修法:判据只看**正文**。思考写了一堆、正文与工具调用都没有时,报 `EMPTY_RESPONSE`(**可重试**)
|
|
496
|
+
⇒ DSH 自动重发;重发仍失败时你会看到明确的失败提示,而不是静默停住。
|
|
497
|
+
- 顺带把一行会误导人的日志分档:`toolCallCount > 0` → 「正常形态」;否则 → 「内容可能全在思考通道」。
|
|
498
|
+
(旧文案一律说「内容可能全在思考通道」,实测把读日志的人带偏成「每天上百次故障」;
|
|
499
|
+
真实比例是 885 条助手消息里 **3 条**真空转,另有 **266 条**是健康的调工具轮 ——
|
|
500
|
+
工具调用是被工具过滤器从正文流里取走的,所以「正文 0 字 + 有工具调用」是正常形态。)
|
|
501
|
+
|
|
502
|
+
**② 回声守卫砍掉后半段 → 现在会告诉你**
|
|
503
|
+
|
|
504
|
+
- 现象:回答写到一半断掉(实测断在「问题项 4」处),界面无任何提示,`turn/end` 还是「正常完成」。
|
|
505
|
+
- 根因:转写回声守卫的设计是「**命中一行就从该行起砍到结尾**」,而判据过宽 ——
|
|
506
|
+
模型在**正文里正常引用一次**工具结果当证据(`[Tool Result for call_…]`)也会命中,
|
|
507
|
+
于是整段回答(问题项 4、5 与结论)被一起丢掉。
|
|
508
|
+
- 修法两条:
|
|
509
|
+
- **收窄判据**:行内转写特征拆「强 / 弱」两档。弱档(`[Tool Result` / `[status:` / `[System]`)
|
|
510
|
+
不再一律砍,改为**先扣住、再看后文**:后面 2 行普通内容仍无回声 ⇒ 判为正文、放行;
|
|
511
|
+
相邻两行都是转写特征 ⇒ 才判回声。强档(`truncated]` / `Assistant truncated` /
|
|
512
|
+
`[N chars omitted]`)是 prompt 自己的截断占位符,正常回答不会引用 ⇒ 照旧立即判回声。
|
|
513
|
+
- **不静默**:真被砍掉时,回答末尾追加一行
|
|
514
|
+
`[deepseek-web] 本轮有一部分「历史回放格式」的内容被过滤(未上屏),回答可能因此不完整。`
|
|
515
|
+
- **局限(写清楚)**:单独放在**行首**的引用与真回声在字节层面无法区分,仍会被砍 ——
|
|
516
|
+
但你会看到上面那行提示,不会再对着一个断掉的回答不知道发生了什么。
|
|
517
|
+
想彻底避免:提问时说一句「不要贴工具返回原文」。
|
|
518
|
+
- 用例:`check-transcript-echo.mjs` 16 → **21 条**(新增:单行引用不误伤、分块下内容与顺序不变、
|
|
519
|
+
引用 + 空行 + 正文仍放行、连续两行仍判回声、引用后紧跟行首标记仍判回声);
|
|
520
|
+
`check-bundle.mjs` 新增 3 条产物断言。
|
|
521
|
+
|
|
522
|
+
## 0.1.69 — 2026-09-16
|
|
523
|
+
|
|
524
|
+
### 修:账号库里两个不同账号显示成同一个(二次屏蔽把可辨识部分抹掉了)
|
|
525
|
+
|
|
526
|
+
- **根因**:网页端返回的 `user.display` **本身就是屏蔽过的**(形如 `192******27`、
|
|
527
|
+
`lidi*********+mn1@gmail.com`),插件存盘存的就是它;面板又调了一次 `maskIdentifier`,
|
|
528
|
+
而它对邮箱只保留本地部分前 3 个字符 ⇒ 两个 Gmail 账号都变成 `lid***@gmail.com`,
|
|
529
|
+
看起来像同一个号。**这恰好违背该函数自身「保留可辨识部分」的目的。**
|
|
530
|
+
- **修法**:`maskIdentifier` 改为**幂等** —— 值里已经含 `***` 就原样返回。
|
|
531
|
+
一处改动同时修好三个调用点(面板标题 / 列表 display / 校验结果 display)。
|
|
532
|
+
**未屏蔽过的原始值照旧屏蔽**,安全网不丢。
|
|
533
|
+
- **说清楚一件事**:**"把账号显示全"做不到** —— 原始邮箱/手机号压根不经过插件
|
|
534
|
+
(接口只给屏蔽值)。这版修的是"重名",不是"显示全"。
|
|
535
|
+
- 实测(本机 7 个账号):修前只有 5 个互不相同的显示名,修后 **7 个全不同**。
|
|
536
|
+
- 用例:`logic-test.mjs` 新增一组幂等断言(含"两个 Gmail 不能相等"的回归)。
|
|
537
|
+
|
|
538
|
+
## 0.1.68 — 2026-09-15
|
|
539
|
+
|
|
540
|
+
### 修:经 read_image 等工具返回的图片一律传不上去(服务端按文件名后缀判类型)
|
|
541
|
+
|
|
542
|
+
用户看到界面上反复出现「有 2 张图片没能传给模型(code 9:unsupported file type)」,
|
|
543
|
+
但那些图本身是完好的 PNG。真机 A/B 定位到:**服务端按 multipart 里的文件名后缀判类型**,
|
|
544
|
+
`content-type` 说了不算 —— 同一份 PNG 字节,只改文件名:
|
|
545
|
+
|
|
546
|
+
| 文件名 | 结果 |
|
|
547
|
+
| --- | --- |
|
|
548
|
+
| `image.png` | 成功 |
|
|
549
|
+
| 纯 64 位 hex(无后缀) | 被拒:`code 9 unsupported file type` |
|
|
550
|
+
| `<64位hex>.png` | 成功 |
|
|
551
|
+
| 不传 name(走缺省) | 成功 |
|
|
552
|
+
|
|
553
|
+
而宿主给图的 `name` 是两套的:`user/message` 与 `agent/inbox` 给的是 `image.png`,
|
|
554
|
+
`tool/result`(`read_image` 之类工具返回)给的是**纯 sha256、没有扩展名**。
|
|
555
|
+
旧实现把 `ref.name` 原样透传 ⇒ **凡是经工具返回的图,一律被拒**
|
|
556
|
+
(本机 09-14 有 36 次、09-15 有 6 次,全部静默降级成纯文本)。
|
|
557
|
+
|
|
558
|
+
修法:新增纯函数 `imageUploadName(name, mediaType)`(`src/protocol.ts`)——
|
|
559
|
+
只保留受支持的图片后缀(png / jpg / jpeg / webp / gif),其余按 `mediaType` 重建为
|
|
560
|
+
`image.<ext>`;并只取基名(宿主若给的是路径,别把目录带进 multipart 的文件名)。
|
|
561
|
+
`uploadRequestImages` 改用它,不再透传 `ref.name`。
|
|
562
|
+
|
|
563
|
+
测试:`check-image-refs.mjs` 13 项(新增 5 项:纯 hex 名 / 缺 name / jpeg / `.bmp` / 带路径);
|
|
564
|
+
`check-bundle.mjs` 新增一组产物断言(按调用点匹配,三条子句各自做过"改坏必红"的反向验证)。
|
|
565
|
+
真机复现脚本:`tests/probe-upload-name.mjs`(需已登录凭证;只上传、不建会话)。
|
|
566
|
+
|
|
567
|
+
注意:这与 0.1.66 的「同一张图在历史里重复出现 → `ref_file_ids` 去重」是**两个不同**的
|
|
568
|
+
失败形态(那个修的是 `biz_code 9 / invalid ref file id`),两条修复都在,别删任何一条。
|
|
569
|
+
|
|
570
|
+
### 文档
|
|
571
|
+
|
|
572
|
+
- README 两版补上两条已知问题:上传文件名必须带受支持后缀;DSH 前端把单个 `$` 当行内公式。
|
|
573
|
+
|
|
574
|
+
## 0.1.67 — 2026-09-15
|
|
575
|
+
|
|
576
|
+
### 修:新登录的账号不会自己出现在账号库里,要关掉设置页再打开
|
|
577
|
+
|
|
578
|
+
用户反馈:加完一个账号,账号库列表还是旧的,"退出一下才能刷新"。
|
|
579
|
+
|
|
580
|
+
根因是**账号库列表不在状态轮询里**。面板每 2 秒轮询一次,但只读 `/status` 并渲染
|
|
581
|
+
**当前账号**的信息;列表本身只有 `loadAccounts()` 被显式调用的那几个时刻才重读
|
|
582
|
+
(初始化 / 切号 / 移除 / 重命名 / 重新登录 / 添加 / 导入)。而登录捕获**是异步落地的**:
|
|
583
|
+
|
|
584
|
+
- CDP 那条路(宿主在 utility 进程时的常态)要等用户在浏览器里登录完;
|
|
585
|
+
- 能开 Electron 窗口那条路更直接 —— `openLoginWindow()` 是"开窗即返回",
|
|
586
|
+
捕获发生在响应之后。
|
|
587
|
+
|
|
588
|
+
于是"加完账号"的捕获很可能晚于那次 `loadAccounts()`,列表就停在旧快照上,
|
|
589
|
+
**而且此后不会再更新**(轮询不碰账号库)。
|
|
590
|
+
|
|
591
|
+
最直接的证据是代码里原有的一句注释(「登录新账号」的处理里):
|
|
592
|
+
「期间保持轮询更密一点,万一捕获是异步落地的也能及时刷出来」——
|
|
593
|
+
作者本来就指望轮询兜住这件事,但轮询只读 `/status`,**那个兜底从未生效**。
|
|
594
|
+
本次修的就是"让它成真"。
|
|
595
|
+
|
|
596
|
+
**修法**:轮询里按节拍重读 `/accounts`。
|
|
597
|
+
|
|
598
|
+
| 时机 | 间隔 | 为什么 |
|
|
599
|
+
|---|---|---|
|
|
600
|
+
| 登录流程进行中(窗口开着 / 刚操作过 5 分钟内) | **3 秒** | 捕获一落地就能看见 |
|
|
601
|
+
| 空闲 | **30 秒** | 够让探活补上的账号名 / 限制状态 / 失败标记自己出现,又不必一直读盘 |
|
|
602
|
+
|
|
603
|
+
配套(`src/account-sync.ts`,纯函数):
|
|
604
|
+
|
|
605
|
+
- **内容签名守卫**:`accountsSignature()` 只取界面真正渲染的东西(账号数组 + 当前账号 id),
|
|
606
|
+
内容没变就**不重建 DOM** —— 否则每 3 秒会把用户正在悬停的按钮、正在输入的
|
|
607
|
+
重命名框换掉。整包 `JSON.stringify` 而不是逐字段挑,**按构造就是完整的**:
|
|
608
|
+
将来 `/accounts` 多返回一个会被渲染的字段,这里不用改也不会漏。
|
|
609
|
+
- **后台同步静默**:轮询的偶发失败不写进 `accountsMsg`(那是给用户操作反馈用的,
|
|
610
|
+
被后台失败覆盖会更困惑;下一次成功就自然好了)。
|
|
611
|
+
|
|
612
|
+
### 同源的第二个缺口(一并覆盖)
|
|
613
|
+
|
|
614
|
+
探活会更新记录(补上账号名、限制状态、失败标记),而**账号名是捕获之后由校验拿到的**
|
|
615
|
+
(见 `index.ts` 里那段回写)。此前若面板一直开着,那个名字永远不会出现;
|
|
616
|
+
现在 30 秒内会自己补上,不用再重开面板。
|
|
617
|
+
|
|
618
|
+
### 测试与验证
|
|
619
|
+
|
|
620
|
+
- 新增 `tests/check-account-sync.mjs`(12 项):重读节拍(登录中 / 空闲 / 从未同步过 /
|
|
621
|
+
空闲必须明显更慢)、签名(同内容同签名、新增账号、切换当前账号、顺序变化、
|
|
622
|
+
探活补上的四个字段、`footprint` 必须被排除、畸形响应、循环引用不炸)。
|
|
623
|
+
- `check-bundle` 新增一组产物断言(轮询调用点 / 签名守卫 / 共用渲染入口 /
|
|
624
|
+
后台同步的 catch 必须为空 / 判据函数真的进了产物)。
|
|
625
|
+
- 反向验证 **12 处 12 中**(源码 5 + 产物 5 + 复原 2):把轮询那句去掉、
|
|
626
|
+
把签名守卫去掉、让后台同步不再静默、让渲染入口不算签名、把判据模块从产物里改名 ——
|
|
627
|
+
每一处都精确只红对应那一条。
|
|
628
|
+
- 全量回归 **39/39 个用例文件**通过。
|
|
629
|
+
|
|
630
|
+
## 0.1.66 — 2026-09-15
|
|
631
|
+
|
|
632
|
+
### 修:同一张图被引用两次 → `ref_file_ids` 出现重复 id(图会永久卡死整条会话)
|
|
633
|
+
|
|
634
|
+
外部提交了一份三条 bug 的审计,逐条核实后**只有这一条属代码缺陷**(另两条见下),
|
|
635
|
+
但它的触发条件是真实的:`collectImageRefs()` 会**有意**走进 `tool-result` 内嵌的图片
|
|
636
|
+
(网页端不看图,模型得调 `read_image`,而工具结果里带着图片本体),
|
|
637
|
+
于是同一张 `attachmentId` 会在同一份历史里出现多次。
|
|
638
|
+
实测本机会话 `install-plugin/session-c1fb8208-*` 里 seq=17 与 seq=23
|
|
639
|
+
两个 `tool/result` 各自内嵌同一张 `sha256:23c18a56…`(89962 B JPEG)。
|
|
640
|
+
|
|
641
|
+
`attachmentId` 是**内容寻址**(sha256)⇒ 两次是同一个缓存 key ⇒ 旧实现在缓存命中时
|
|
642
|
+
把**同一个 file_id 又推一遍**,请求体变成 `ref_file_ids: [id, id]`。
|
|
643
|
+
服务端不接受重复 id(`biz_code 9 / invalid ref file id`),被拒后**该会话后续每一轮都失败**
|
|
644
|
+
(图留在历史里),只能新开对话。
|
|
645
|
+
|
|
646
|
+
**修法**:`uploadRequestImages()` 里按 `attachmentId` 去重(每个附件只产出一个 file id)。
|
|
647
|
+
去重按 attachmentId 而不是 fileId —— 不同附件即使内容相同也各有各的 id,不该在这里合并。
|
|
648
|
+
|
|
649
|
+
### 修:图片没能送出去时,界面上什么都看不到
|
|
650
|
+
|
|
651
|
+
上传失败时旧实现只写一行 `logger.warn` 然后降级成纯文本,当轮 completion 正常 `FINISHED`
|
|
652
|
+
—— **用户只会以为「模型看不懂图」**。实测本机 09-14 有 36 次上传被服务端以
|
|
653
|
+
`code 9 unsupported file type` 拒绝,全程无声。
|
|
654
|
+
(注意同一个数字在两个端点含义不同:上传端 code 9 = `unsupported file type`,
|
|
655
|
+
completion 端 code 9 = `invalid ref file id`。)
|
|
656
|
+
|
|
657
|
+
**修法**:`uploadRequestImages()` 把失败原因随结果回传,适配器在**回答开头**插一句
|
|
658
|
+
`⚠️ [deepseek-web] 有 N 张图片没能传给模型(原因),本轮回答只基于文字内容。`
|
|
659
|
+
与 F28「工具调用被丢弃要告知模型」同一原则:本该处理的输入丢了,就必须说出来。
|
|
660
|
+
|
|
661
|
+
### 修:续写判据漏掉「;」「、」—— 截断点落在它们后面会静默少一段
|
|
662
|
+
|
|
663
|
+
`looksMidSentence()` 只在尾部是「,」「:」等少数分隔符时才判"未写完",
|
|
664
|
+
而 **`;`(全角分号)与 `、`(顿号)落到了「字母/数字/汉字以外的字符 → false」那条兜底上**,
|
|
665
|
+
被判成已写完。实测(把源码函数体切出来直接求值):`;` → false、`、` → false,
|
|
666
|
+
而 `,`/`:`/`;` → true —— 同一个文件里两套标准。
|
|
667
|
+
|
|
668
|
+
为什么这次要紧:服务端**会在句中截断却照发 FINISHED**(README 有记录),
|
|
669
|
+
此时 `cutByServer` 为 false,这条判据是**唯一**防线 ⇒ 截断点落在分号/顿号后就是静默丢内容。
|
|
670
|
+
两个恰恰是最强的"还没写完"信号(列举到一半、分句列到一半)。
|
|
671
|
+
|
|
672
|
+
**修法**:把尾部标点拆成两个显式集合 —— `MID_SENTENCE_TAIL`(分隔符,判未完)
|
|
673
|
+
与 `COMPLETE_TAIL`(终止符,判已完),不再靠字符串 includes 混在一起。
|
|
674
|
+
`…` 有意留在"已完"里:省略号既可能是没说完、也可能是有意的收束语气,
|
|
675
|
+
误判 true 会让一句正常收尾的话被要求"接着写",感知上比偶发漏判更打扰。
|
|
676
|
+
|
|
677
|
+
### 测试与验证
|
|
678
|
+
|
|
679
|
+
- 新增 `tests/check-image-refs.mjs`(8 项):去重(同图 ×2 / ×3)、不同图不误合、
|
|
680
|
+
无图不碰附件服务、上传失败要告知、宿主没接线也要告知、正常情况**不得**出现提示、超长原因截断。
|
|
681
|
+
这条路径此前**零覆盖** —— 真实上传要 PoW(sha3 wasm) + 真网络,所以按本文件已有的
|
|
682
|
+
`streamCompletion` 惯例,把上传做成可注入依赖(`deps.uploadImage`)。
|
|
683
|
+
- `tests/check-auto-continue.mjs` +4 项:`;`、`、`、`。`(长回答不续写)、`…`(刻意不续写),
|
|
684
|
+
且每条都带"样本必须 > 40 字"的自证断言(短样本会被 F29 门槛吃掉、用例等于没测)。
|
|
685
|
+
- `check-bundle.mjs` +3 组产物断言(去重循环 / 失败回传 / 注入缝 / 提示进正文 / 两个集合的内容),
|
|
686
|
+
全部按**调用点**写。
|
|
687
|
+
- 反向验证 **18 处 18 中**(源码 8 + 产物 7 + 复原 3):去重失效精确红 2 条、
|
|
688
|
+
失败不回传精确红 2 条、`;` 精确红 1 条、`、` 精确红 1 条;把 `。` 也放进 MID 会让
|
|
689
|
+
9 条老用例同时报警(说明"放宽过头"拦得住)。
|
|
690
|
+
- 全量回归 **38/38 个用例文件**通过。
|
|
691
|
+
|
|
692
|
+
### 核实结论:外部审计的另两条
|
|
693
|
+
|
|
694
|
+
- **「服务端句中截断 + FINISHED 无法区分」** —— 代码描述属实(且比它说的更宽,见上),
|
|
695
|
+
但属于**启发式固有局限**,代码注释里早就承认了,不是新发现的缺陷。
|
|
696
|
+
- **「更新检查无退避」** —— 描述属实但**定性不对**:`/update-check` 是**手动按钮**
|
|
697
|
+
(`client/index.ts` 的 click),没有任何自动轮询 ⇒ 不存在放大限流的风险;
|
|
698
|
+
「短超时 + 优雅失败、不卡设置页」是 `update-check.ts` 明写的设计目标。不改。
|
|
699
|
+
- 顺带修正一处:`randomUUID` 那条结论对、依据可以更硬 —— `protocol.ts:9` 是
|
|
700
|
+
`import { randomUUID } from 'node:crypto'`,**显式 Node 实现**,压根不经过浏览器
|
|
701
|
+
`crypto`,与"安全上下文"无关。
|
|
702
|
+
|
|
703
|
+
### 说明
|
|
704
|
+
|
|
705
|
+
本机调用台账 1123 条里 `PROVIDER_ERROR` 只有 1 条(且非图片轮次),
|
|
706
|
+
所以「completion 端 code 9」这个失败形态**在本机从未出现过** ——
|
|
707
|
+
去重修的是"迟早会中"的隐患,不是你已经踩到的那个坑。
|
|
708
|
+
真正影响本机的是上传端那个 `unsupported file type`(成因未定位,需要真机复现)。
|
|
709
|
+
|
|
710
|
+
## 0.1.65 — 2026-09-15
|
|
711
|
+
|
|
712
|
+
### 修:「重新登录这个账号」会新增一条同名记录,旧那条还一直报错
|
|
713
|
+
|
|
714
|
+
用户实测:点「重新登录这个账号」→ 重登成功 → 账号库里**多出一条同名账号**,
|
|
715
|
+
旧那条仍然挂着「❌ 需要重新登录」,同时当前账号也被顶到了新记录上。
|
|
716
|
+
|
|
717
|
+
两层根因,缺一不可:
|
|
718
|
+
|
|
719
|
+
1. **`/login/relogin` 完全没用到传进来的 `id`** —— 它只调了 `beginAddAccount()`,而那个函数
|
|
720
|
+
的语义只是"别切换当前账号",**不保证更新同一条记录**。更糟的是添加模式有 15 分钟 TTL,
|
|
721
|
+
而用户从点按钮到真正登录完成隔了 34 分钟(实测)⇒ 意图早过期,捕获退回"写入并设为当前",
|
|
722
|
+
于是既多了一条记录、又换掉了当前账号。
|
|
723
|
+
2. **捕获时拿不到身份,去重无从下手** —— 捕获只有 token/cookie,没有 `serverId`;
|
|
724
|
+
而重新登录必然换新 token(旧 token 匹配不上)⇒ `upsertAccount` 只能新增一条。
|
|
725
|
+
身份是**捕获之后**校验才拿到的,那时已经晚了一步。
|
|
726
|
+
|
|
727
|
+
**修法**:新增"重新登录意图"(`beginRelogin(id)` / `pendingReloginTarget()`):
|
|
728
|
+
|
|
729
|
+
- 记住**要更新哪一条记录**,捕获时按 `patch.id` 落库(优先于 serverId / token 去重),
|
|
730
|
+
所以不会再新增,也不切换当前账号;
|
|
731
|
+
- 成功捕获时显式清掉旧的 `lastVerifyError` —— 否则那条记录会一直显示「需要重新登录」
|
|
732
|
+
(`upsertAccount` 的字段继承用 `??`,传 `undefined` 清不掉,所以用 `updateAccount` 单独清);
|
|
733
|
+
- 意图 TTL 放宽到 **60 分钟**(用户可能开着登录窗口去干别的);
|
|
734
|
+
- 认得出"这次捕获是另一个号"时**放行成普通捕获**,绝不覆盖别人的记录;
|
|
735
|
+
- 目标记录已被移除时同样放行,不凭空把它写回来。
|
|
736
|
+
|
|
737
|
+
### 改:「重新登录」按钮挪进右侧动作列
|
|
738
|
+
|
|
739
|
+
原来它跟在失败说明后面、**独占一整行**,占地方又不整齐(用户反馈)。
|
|
740
|
+
现在失败说明只留一行短文字,按钮挪到 切换/重命名/移除 那一列的最前面(它是这行最该点的)。
|
|
741
|
+
|
|
742
|
+
测试:`check-account-add` 13 → 21 项(新增 8 条重新登录用例:原地更新、身份不符放行、
|
|
743
|
+
token 未变时清失败标记、TTL 过期、意图只用一次、目标已移除、与添加模式并存);
|
|
744
|
+
`check-ui-copy` 6 → 9 项(按钮位置、失败块不许放按钮、⚠️ 也算醒目);`check-bundle` 新增一组产物断言。
|
|
745
|
+
反向验证 8 发 8 中(源码 5 处 + 产物 3 处)。全量 37/37 用例文件通过。
|
|
746
|
+
|
|
747
|
+
> ⚠️ 已经产生的重复记录不会自动消失:请在账号库里把**旧的那条**(捕获时间早、标着
|
|
748
|
+
> ❌ 需要重新登录)手动「移除」。本版之后重新登录只会原地更新。
|
|
749
|
+
|
|
750
|
+
## 0.1.64 — 2026-09-15
|
|
751
|
+
|
|
752
|
+
### 改:报错要醒目、重要提示要有标注(界面可读性)
|
|
753
|
+
|
|
754
|
+
用户原话:「账号切换失败的时候,报错应该明显一点,还有一些重要的注释什么的,也要标注,可以有 emoji」。
|
|
755
|
+
|
|
756
|
+
1. **失败/成功消息自动变醒目**。新增 `createMsgNode()`:给它的 `textContent` 赋值时按内容自动选样式 ——
|
|
757
|
+
命中「失败 / 错误 / 无法」→ **红框红字**(`.dsw-msg.err`),以「已…」开头 → 绿框,其余保持灰色小字。
|
|
758
|
+
为什么自动判定、而不是每个调用点传 kind:账号库 / 传输层 / 上下文三张卡加起来 40 多个赋值点,
|
|
759
|
+
靠人手传迟早会漏 —— 而漏掉的那个往往正是报错(这次就是"账号切换失败"混在灰字里没人看见)。
|
|
760
|
+
2. **去掉用户可见文案里的 markdown 星号**。界面是纯文本,写 `**可完整登录的凭证**` 会**原样显示**出星号
|
|
761
|
+
(你这次截图里就是)。6 处改成 emoji + 纯文本,顺带修掉一条 host 日志里的同类写法;
|
|
762
|
+
并新增 `tests/check-ui-copy.mjs` 把这条规矩做成自动检查(我在这条上栽过不止一次)。
|
|
763
|
+
3. **重要标注加 emoji**:徽章 `✅ 当前` / `❔ 未校验` / `⏳ 受限至 X` / `❌ 需要重新登录` /
|
|
764
|
+
`❌ 登录态校验失败` / `⚪ 未登录`;cookie「未记录」、导出备份的凭证风险等关键注释前加 `⚠️`。
|
|
765
|
+
|
|
766
|
+
测试:新增 `check-ui-copy.mjs`(6 项:星号扫描、自动醒目接线、`.node` 接线、徽章标注、凭证警告);
|
|
767
|
+
`check-bundle` 新增一组产物断言(样式判定、`.node` 接线、emoji 标记,并要求旧的星号形态必须消失);
|
|
768
|
+
`check-cookie-meta` 里那条 cookie 文案断言同步更新(行为未变,只是多了一个 ⚠️ 前缀)。
|
|
769
|
+
全量 37/37 用例文件通过。
|
|
770
|
+
|
|
771
|
+
## 0.1.63 — 2026-09-15
|
|
772
|
+
|
|
773
|
+
### 修:链式投喂三处「看不见但会让人误判」的缺陷
|
|
774
|
+
|
|
775
|
+
起因:设置页「上下文」卡的状态行写着"链式投喂正在跑…链上已发 168 段",
|
|
776
|
+
我据此怀疑按钮状态不对(**结论是我看错了图,按钮没问题**),
|
|
777
|
+
但顺着这条线翻出三处真缺陷 —— 都属于"没有报错、但会让你和我判断错"的那一类:
|
|
778
|
+
|
|
779
|
+
1. **`/status` 的字段读错了层级(0.1.62 引入)**。宿主把 `contextMode` / `contextChain`
|
|
780
|
+
放在 `status.config` 里,客户端却写成了 `status.contextMode` ——
|
|
781
|
+
于是"跟着状态轮询刷新"这段**从来没生效过**:面板不重开、不切换模式,
|
|
782
|
+
链的段数就永远停在打开那一刻。现在读 `status.config?.contextMode`。
|
|
783
|
+
2. **链状态没按模式过滤**。`/context-mode` 的响应无条件回 `contextChainInfo()`,刚切到
|
|
784
|
+
「每轮全量」时会短暂出现"每轮全量 + 链式投喂正在跑"的自相矛盾组合。现在只在链式模式下回;
|
|
785
|
+
并且**切到全量时立刻丢弃当前链** —— 否则以后切回链式会从一个已经过期的父消息续链,
|
|
786
|
+
模型上下文会错位(这点比界面难看更严重)。
|
|
787
|
+
3. **决策原因没有日志(可诊断性)**。0.1.62 里 `decideFeed` 返回的 `reason` 只写在类型注释里、
|
|
788
|
+
没接进 logger ⇒ 链式投喂在生产日志中**完全不可见**,出问题时无法回答
|
|
789
|
+
"这一轮到底发了增量,还是退回全量、因为哪条判据"。现在 webapi 在**决策原因变化时**
|
|
790
|
+
回调一次(避免每轮刷屏),adapter 打一行:
|
|
791
|
+
|
|
792
|
+
```
|
|
793
|
+
deepseek-web: 上下文投喂=链式:本轮只发增量 152 字(历史由服务端维护)
|
|
794
|
+
deepseek-web: 链式投喂退回全量重发(原因=not-appended)
|
|
795
|
+
deepseek-web: 上下文投喂=每轮全量:重发完整 prompt
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
测试:`check-context-chain` 10 → 15 项(决策回执四条:首轮原因、原因不变不重复上报、
|
|
799
|
+
历史改写后退回、`resetContextChain` 后重新可见);`check-bundle` 新增一组产物断言
|
|
800
|
+
(客户端读取层级、链按模式过滤、切全量清链、回执接线、adapter 日志文案)。
|
|
801
|
+
反向验证 8 发 8 中(源码三处 + 产物五处,各自改坏都精确变红)。
|
|
802
|
+
|
|
803
|
+
## 0.1.62 — 2026-09-14
|
|
804
|
+
|
|
805
|
+
### 新:上下文投喂方式可切换(设置页「上下文」标签)
|
|
806
|
+
|
|
807
|
+
起因是用户的两个问题:网页端每条消息都重发一大段提示词、以及"上下文到底有没有带过去"。
|
|
808
|
+
查代码后事实是这样:插件一直把 `parent_message_id` 写成 `null`(`webapi.ts`),
|
|
809
|
+
**每条消息都是会话里的根消息、没有父链**,服务端按消息树回溯上下文时回溯到空 ——
|
|
810
|
+
所以历史只能由我们每轮重发。浏览器不是这么干的:参考实现里
|
|
811
|
+
`nextParentMessageId = history?.parentMessageId ?? finalAssistantMessageId`、
|
|
812
|
+
`isFirstMessage = parent_message_id === null`,**只有会话第一条的 parent 是 null**。
|
|
813
|
+
|
|
814
|
+
新增 `contextMode` 设置(设置页「上下文」标签,两个按钮,即时生效 + 落盘):
|
|
815
|
+
|
|
816
|
+
- **`full`(默认)**:每轮重发全量 prompt。与 0.1.61 及以前**行为完全一致**,不改变任何既有用户。
|
|
817
|
+
- **`chained`**:后续轮只发**增量**,`parent_message_id` 指向上一条回答的 `message_id`
|
|
818
|
+
(取自 SSE 首帧 `event: ready` 的 `response_message_id`,真实样本形如
|
|
819
|
+
`{"request_message_id":1,"response_message_id":2,"model_type":"default"}`)。历史由服务端维护。
|
|
820
|
+
|
|
821
|
+
代价说清楚:链式投喂下**工具协议只存在于链首那条消息里**,一旦服务端丢掉早期上下文,
|
|
822
|
+
模型可能不按 JSON 约定发工具调用。所以判据是"能省则省、一有不确定就退回全量",
|
|
823
|
+
以下任一情形都**重新起链**(发全量 + `parent=null`,只是多花点 token,不会上下文错位):
|
|
824
|
+
新会话 / 会话轮换 / 切号 / 固定头(系统提示+工具目录)变化 / 历史非严格追加(压缩、改写、回退)/
|
|
825
|
+
本轮无新增 / 增量超预算 / 上一轮流失败·取消·没拿到 `message_id`。
|
|
826
|
+
|
|
827
|
+
实现:新增纯逻辑模块 `src/context-feed.ts`(判据 + 设置读写);`protocol.ts` 拆出
|
|
828
|
+
`serializePromptParts()`(交出 `head` + **未截断**的 `entries` + `full`,`serializePrompt` 变为它的包装,
|
|
829
|
+
两者逐字节一致);`webapi.ts` 的 `parent_message_id`/`prompt` 改由决策结果提供,链状态与复用会话同生命周期
|
|
830
|
+
(退役/切号/失败即作废);`adapter.ts` 把 `promptParts` 随请求传下去。
|
|
831
|
+
|
|
832
|
+
测试:`tests/check-context-feed.mjs`(判据 28 项)、`tests/check-context-chain.mjs`
|
|
833
|
+
(接线与生命周期 10 项,假 transport + 假 SSE)、`logic-test.mjs` +3(ready 帧取 `response_message_id`)、
|
|
834
|
+
`check-bundle.mjs` +2 组产物断言(四条接线 + 设置可切换)。反向验证 10 发 10 中。
|
|
835
|
+
|
|
836
|
+
## 0.1.61 — 2026-09-14
|
|
837
|
+
|
|
838
|
+
### 修:切到失效账号后「API 密钥无效」却毫无提示(三处接线)
|
|
839
|
+
|
|
840
|
+
反馈:切换到一个一天多没用过的账号后每轮都报「API 密钥无效」(AUTH),切回另一个号就正常;
|
|
841
|
+
用户质问"保存好的账号为什么要重新登录、不能用为什么不给提示"。
|
|
842
|
+
|
|
843
|
+
**机制**:账号库存的是**捕获那一刻的 token 快照**,有效期由服务端控制、本机无法续期,
|
|
844
|
+
过期只能重新登录换新。但下面三处接线缺失,让人完全无从判断哪个号还能用:
|
|
845
|
+
|
|
846
|
+
1. **AUTH 失败不回写账号状态** —— `lastVerifyError` 只由 30 分钟一次的探活写入,
|
|
847
|
+
而探活只扫当前活跃账号;刚切到死号、探活还没轮到它时界面完全静默。
|
|
848
|
+
⇒ 现在 `recordCallOutcome` 收到 `code === 'AUTH'` 立刻回写 `lastVerifyError`,
|
|
849
|
+
账号库马上出现红标「需要重新登录」。
|
|
850
|
+
2. **「未校验」标记永久粘住** —— `unverified` 在"捕获但未校验"时置位,而探活成功只更新
|
|
851
|
+
`lastVerifiedAt`、从不清它。实测账号库 **5/5 全挂「未校验」**,连"最近校验 6 分钟前"
|
|
852
|
+
的那个也挂着 —— 标与数据自相矛盾、信息量归零,真出问题时反而看不出来。
|
|
853
|
+
⇒ 探活成功时显式写 `unverified: false`(`normalizeRecord` 只保留 `=== true`,标记自然消失)。
|
|
854
|
+
3. **切换账号不校验** —— 死号要等下一轮请求才暴露。⇒ 切号前先对目标账号做一次零额度探活
|
|
855
|
+
(只读 `users/current`),失败则**不切换**并返回「该账号登录态校验未通过…请重新登录后再切换」。
|
|
856
|
+
|
|
857
|
+
测试:check-accounts 新增"`updateAccount` 能清 unverified"用例(22 项);check-bundle 新增
|
|
858
|
+
0.1.61 产物断言(三处接线按调用点写,`unverified: false` 钉在探活 patch 内部);
|
|
859
|
+
反向验证四发四中(探活清标记 / AUTH 回写 / 切号探活 / unverified 归一化语义)。
|
|
860
|
+
|
|
861
|
+
## 0.1.60 — 2026-09-14
|
|
862
|
+
|
|
863
|
+
### 修:暂存兜底方向的反面代价 —— 思考被归进正文(F28)
|
|
864
|
+
|
|
865
|
+
0.1.59 把"无线索暂存"的兜底方向改成一律归正文(F27,防止回答被吞进思考)。
|
|
866
|
+
当天实测反例:同一个"孤儿未结算"场景下,方向一改,错法就翻了个面 ——
|
|
867
|
+
两条消息的 text 块里塞着**整段 `<analysis>…<summary>…</summary>` 复盘**
|
|
868
|
+
(其中一条 27397 字全是复盘、没有一句对用户说的话 —— 模型不会把整条回答写成纯复盘),
|
|
869
|
+
用户在界面上看到"思考内容当正文输出"。
|
|
870
|
+
|
|
871
|
+
**改法**:兜底方向不变(原则仍是"看不到回答比看到思考更糟"),但加一条**标签判据** ——
|
|
872
|
+
暂存文本以 `<analysis>` / `<summary>` / `<thinking>` 等思考的标准包装为主体时归思考;
|
|
873
|
+
不含标签的孤儿仍按正文收尾(0.1.59 修的场景不受影响)。
|
|
874
|
+
|
|
875
|
+
**顺带加取证开关**:设环境变量 `DSH_WEB_LOGIN_DUMP_SSE=1`(需重启 DSH 生效),
|
|
876
|
+
把原始 SSE 逐行落盘到 `~/.dsh/deepseek-web/frames/`,给通道错位类问题定论用
|
|
877
|
+
(统计与日志只能看到结果,只有原始帧能看到服务端到底怎么标的)。
|
|
878
|
+
|
|
879
|
+
### 修:短回答被误判「句中被截」(F29)
|
|
880
|
+
|
|
881
|
+
`looksMidSentence` 的判据是"尾部是汉字/字母/数字 ⇒ 被截",对**完整的短答**
|
|
882
|
+
("我在""在吗"—— 天然以汉字收尾)恒为真,会白打 1~2 次续写请求。
|
|
883
|
+
host 日志里整天都有"回答在句中被截且自动续写额度已用尽(尾部:"在吗")"就是它。
|
|
884
|
+
现在短于 40 字的回答不再走这条判据;服务端真截断(无 FINISHED)不受影响、仍会续写。
|
|
885
|
+
|
|
886
|
+
### 修:收尾日志误导 + 续写轮丢弃静默
|
|
887
|
+
|
|
888
|
+
1. 「回答在句中被截且自动续写额度已用尽」这行打在收尾检查处,**不看用途白名单、
|
|
889
|
+
也不管续写是否真的发生过** —— 标题/压缩这类内部调用(白名单本就拦住、从未续写)也会打,
|
|
890
|
+
排查时会被它骗(实测同一天 8 条,全部是"第 1 轮就结束")。现在按事实分档:
|
|
891
|
+
内部用途打 info「按约定不自动续写」,真用尽额度才打 warn。
|
|
892
|
+
2. 续写轮里工具调用解析失败被丢弃时,原先只记宿主日志 —— 模型和用户都不知道
|
|
893
|
+
"这次调用发生过但没执行"(用户只能在网页端看到乱码)。现在补一行可见提示进正文,
|
|
894
|
+
模型下轮能据此重发。首轮丢弃的整步重试链路不变。
|
|
895
|
+
|
|
896
|
+
## 0.1.59 — 2026-09-14
|
|
897
|
+
|
|
898
|
+
### 修:收尾兜底可能把「正文」吞进思考(F27,修订 0.1.57 的兜底方向)
|
|
899
|
+
|
|
900
|
+
反馈:DSH 里问"在吗?",回答"在的,YG。有什么需要我做的?"**整段显示在「思考」里、正文空白**
|
|
901
|
+
(host 日志:`第 1 轮流结束:[本轮 0 字 / 累计 0 字 / 耗时 1359ms] finish=FINISHED`)。
|
|
902
|
+
|
|
903
|
+
**排查**:抓 6 轮"在吗?"的真实帧,**6/6 全部正常**(快照都带 `THINK` fragment,
|
|
904
|
+
思考 50~286 字 + 正文 11~24 字,从未出现"正文 0 字")。所以正常路径没问题 ——
|
|
905
|
+
这个现象要么是模型这轮把回答写进了思考,要么是 0.1.57 那个**暂存兜底**选了错的方向。
|
|
906
|
+
|
|
907
|
+
**不管哪种,兜底方向都得改**。0.1.57 的兜底是"开了思考就归思考",理由是"避免以正文本名
|
|
908
|
+
补发思考"。但两种错法的代价不对等:
|
|
909
|
+
|
|
910
|
+
| 错法 | 后果 |
|
|
911
|
+
|---|---|
|
|
912
|
+
| 思考被显示到正文 | 内容都在,用户读得出这是思考 |
|
|
913
|
+
| **正文被吞进思考** | **用户看不到回答 —— 明确的功能故障** |
|
|
914
|
+
|
|
915
|
+
**改法**:
|
|
916
|
+
|
|
917
|
+
1. `finish()` 的暂存兜底改为**一律归正文**(无线索时,宁可思考上屏也不让回答消失);
|
|
918
|
+
2. 把 `response/fragments/-1/elapsed_secs`(真实帧里紧跟 THINK fragment、值=思考耗时)
|
|
919
|
+
作为**"刚结束的是一段思考"的直接证据** —— 快照丢失时靠它把暂存认回思考通道,
|
|
920
|
+
所以 0.1.57 修好的那个场景不受影响;
|
|
921
|
+
3. host 日志新增诊断:本轮正文 0 字时点名(`⚠️ 本轮正文 0 字 —— 内容可能全在思考通道`),
|
|
922
|
+
下次一眼可判。
|
|
923
|
+
|
|
924
|
+
### 测试
|
|
925
|
+
- `logic-test.mjs` 55 → **57 项**(新增 2 条 + 修订 1 条语义)
|
|
926
|
+
- 反向验证 **2/2**(源码);F25 的真实帧实验 6 个变体仍全绿(改动没有让 F25 回归)
|
|
927
|
+
- 产物断言 +1;全量 **34/34** 个用例文件通过
|
|
928
|
+
|
|
929
|
+
## 0.1.58 — 2026-09-14
|
|
930
|
+
|
|
931
|
+
### 修:会话标题变成重复垃圾(F26)
|
|
932
|
+
|
|
933
|
+
侧栏里的会话标题长这样:
|
|
934
|
+
|
|
935
|
+
```
|
|
936
|
+
在吗在吗在吗
|
|
937
|
+
AI助手的记忆功能AI助手的记忆功能AI助手的记忆功能
|
|
938
|
+
安装 archify skills安装 archify skills安装 archify skills
|
|
939
|
+
TCP 三次握手原因解析TCP 三次握手原因解析TCP 三次握手原因
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
实测 **13 个会话里 11 个中招**,而且清一色是 `deepseek-web` 生成的
|
|
943
|
+
(两个走 fallback 的标题是正常的)。
|
|
944
|
+
|
|
945
|
+
**根因**:自动续写(本是为"回答在句中被截"设计的)**没有区分调用用途**。
|
|
946
|
+
标题天然**以汉字结尾**("…原因解析"的"析"),而 `looksMidSentence` 的判据正是
|
|
947
|
+
"尾部是汉字/字母/数字 → 大概率被截" ⇒ **对标题恒为真** ⇒ 每轮都被判成"句中被截" ⇒
|
|
948
|
+
适配器自动发起续写、要求模型"接着写" ⇒ 模型把标题**重复一遍** —— 每轮一次,
|
|
949
|
+
直到续写额度(默认 2 轮)用尽。host 日志里能直接看到这个三段式:
|
|
950
|
+
|
|
951
|
+
```
|
|
952
|
+
第 1 轮流结束:[本轮 12 字 / 累计 12 字] finish=FINISHED,尾部是句中
|
|
953
|
+
回答疑似在句中被截,自动续写(第 1/2 轮)……
|
|
954
|
+
第 2 轮 …(第 2/2 轮)……
|
|
955
|
+
第 3 轮 … 自动续写额度已用尽,按正常完成上报(尾部:"TCP 三次握手原因解析"×3)
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
**附带代价**:每次标题生成多发 1~2 个请求(多消耗额度、也抬高请求密度)——
|
|
959
|
+
按这 11 个会话算,白发了约 22 个请求。
|
|
960
|
+
|
|
961
|
+
**修法**:新增 `allowsAutoContinue(purpose)` 白名单,**只有 `chat`(或未指定用途)才续写**;
|
|
962
|
+
`session-title` / `compaction` 这类内部调用不再续写,标题回到"只生成一次"。
|
|
963
|
+
|
|
964
|
+
### 测试
|
|
965
|
+
- `check-auto-continue.mjs` 23 → **27 项**,含一条**自证断言**(确认标题确实满足
|
|
966
|
+
"句中被截"的判据 —— 否则这条用例可能没测到点子上)
|
|
967
|
+
- 反向验证 **3/3**(源码:去掉白名单 / 放宽松 / 收过紧)+ **2/2**(产物)
|
|
968
|
+
- 产物断言新增 1 条(按调用点匹配:`eligible` 里必须真的带上用途白名单)
|
|
969
|
+
- 全量 **34/34** 个用例文件通过
|
|
970
|
+
|
|
971
|
+
## 0.1.57 — 2026-09-14
|
|
972
|
+
|
|
973
|
+
### 修:首帧快照丢失时整段思考上屏(F25,用真实帧复现并修掉)
|
|
974
|
+
|
|
975
|
+
现象:新建会话后第一条回答里,**整段思考被当正文发出去**(实测一轮 2062 字符)。
|
|
976
|
+
09-13 修过一次(F24),但那次修的是 `response/thinking_content` 那条路径 ——
|
|
977
|
+
**抓真实帧后发现服务端根本不发这个事件**,所以那次改动对线上没有效果。
|
|
978
|
+
|
|
979
|
+
**真实帧**(2026-09-14 抓包,连续 4 轮完全同构):
|
|
980
|
+
|
|
981
|
+
```
|
|
982
|
+
快照(fragments=[{type:"THINK", content:…}]) ← 思考的归属只靠这一帧
|
|
983
|
+
-1/content → 380× 裸续段 ← 思考
|
|
984
|
+
response/fragments APPEND [{type:"RESPONSE"}] ← 正文从这里开始
|
|
985
|
+
-1/content → 110× 裸续段 ← 正文
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
也就是说,**思考完全依赖「首帧快照里的 THINK fragment」**。一旦那份快照没能进入状态机
|
|
989
|
+
(整帧丢失,或服务端先发了 `fragments: []` 的快照),`fragments` 就一直是空的,
|
|
990
|
+
而旧实现在「无 fragment 可续」的分支里**无条件当正文发射** ⇒ 整段思考上屏。
|
|
991
|
+
同一个根因还解释了「正文块开头缺几个字」(缺掉的正是随快照丢掉的片段)。
|
|
992
|
+
|
|
993
|
+
**验证方式**:把抓到的真实帧里的快照帧删掉再回放 —— 修复前 `thinking=0 / text=818`
|
|
994
|
+
(思考 661 字全在 text 里),修复后 `thinking=657 / text=161`。三处反向验证
|
|
995
|
+
(删快照 / 快照 fragments 清空 / 只丢快照)修复前全部复现、修复后全部归位。
|
|
996
|
+
|
|
997
|
+
**修法**(`createSseState({ thinkingEnabled })`):
|
|
998
|
+
|
|
999
|
+
1. 请求开了思考、却还没有任何 fragment 可续时,文本先进**暂存**,不再直接当正文发射;
|
|
1000
|
+
2. 一旦出现 fragment(快照或 APPEND),按它结算暂存 —— 真实帧证明「第一个 RESPONSE
|
|
1001
|
+
fragment 之前的内容必是思考」,所以这段归思考;
|
|
1002
|
+
3. 流结束时仍没等到 fragment(被中止/掐断)→ 开了思考就按思考收尾,不静默丢字;
|
|
1003
|
+
4. **未开思考时行为完全不变**(没有歧义,仍即时发射,快速模式不受影响)。
|
|
1004
|
+
|
|
1005
|
+
### 测试
|
|
1006
|
+
- `logic-test.mjs` 50 → **55 项**(4 条 F25 用例 + 1 条顺序断言,含「暂存确实生效」的自证)
|
|
1007
|
+
- 反向验证 **6/6** 处精确命中;其中一处(结算点从 APPEND 挪到 finish)**只靠内容断言看不出来**
|
|
1008
|
+
—— 是「思考必须先于正文到达」这条顺序断言把它逼出来的。
|
|
1009
|
+
- 产物断言新增 1 条(按调用点匹配,验证生产链路真的把思考开关传进了状态机)。
|
|
1010
|
+
|
|
1011
|
+
## 0.1.56 — 2026-09-14
|
|
1012
|
+
|
|
1013
|
+
### 修:退出时泄漏网页端会话(每次运行必留一个,实测堆了几十个)
|
|
1014
|
+
|
|
1015
|
+
**现象**:DeepSeek 网页端侧栏堆出一批标题 = DSH 会话主题的对话(如「SSH插件开发讨论」
|
|
1016
|
+
「彻底删除会话插件」「在吗」)。
|
|
1017
|
+
|
|
1018
|
+
**证据**:
|
|
1019
|
+
- 残留标题与 `~/.dsh/sessions/<id>/` 里的 `session/title` 记录**一一对应** ⇒ 确认是本插件建的;
|
|
1020
|
+
- 启动次数与残留量级相符:09-11 = 40 次、09-12 = 33、09-13 = 20、09-14 = 5;
|
|
1021
|
+
- 残留**分散在多个账号**上(09-12 那天用了 5 个账号),网页端只显示当前登录账号的会话,
|
|
1022
|
+
所以肉眼看到的还只是其中一份。
|
|
1023
|
+
|
|
1024
|
+
**根因**:复用槽(`reuseSlot`)与待删队列都只活在进程内存里,而卸载时的 teardown 只关登录窗口 ——
|
|
1025
|
+
进程一退出(尤其被强杀),槽里那个会话与队列里排着的会话全部静默丢失,**永远不会被删**。
|
|
1026
|
+
由于会话是 20 轮复用、槽位始终有一个"在用"会话,所以**每次运行至少留一个**。
|
|
1027
|
+
|
|
1028
|
+
**修法**(三块,缺一不可):
|
|
1029
|
+
|
|
1030
|
+
1. **退出收尾**:teardown 新增 `disposeSessionReuse()` —— 把槽里的会话**交回它自己的清理回调**
|
|
1031
|
+
(排队待删),再 `flush()` 尽力清空队列。此前只清槽不排队,等于白丢。
|
|
1032
|
+
2. **「欠删除」落盘**:新增 `src/session-journal.ts`,把"还欠一次删除"的会话按账号记到
|
|
1033
|
+
`~/.dsh/web-login/sessions-in-use.json`(进槽记一条、进队列记一条、**确认删掉才销账**)。
|
|
1034
|
+
3. **启动补删**:下次启动读这份记录,把上次遗留的会话按账号排进清理器。
|
|
1035
|
+
进程还活着的(多实例共用 DSH_HOME)、账号已被移除的、以及 `keep`/`deleteWebSessions: false`
|
|
1036
|
+
这几种情况都**不动**;账号没了的记录直接丢弃(没凭证,留着也删不掉)。
|
|
1037
|
+
|
|
1038
|
+
删除回执才有销账:`deleteOne` / 批量删现在都返回"服务端是否真的接受"(不再只看 `resp.ok`,
|
|
1039
|
+
而是同时检查 HTTP 200 上裹的业务错误信封),**删失败就留着记录,下次启动再补**。
|
|
1040
|
+
|
|
1041
|
+
### 修:清理设置会被设置页的下一次保存冲掉
|
|
1042
|
+
|
|
1043
|
+
`sessionCleanup` / `cleanupBatch` / `cleanupDelayMs` / `cleanupGapMs` 这几个字段
|
|
1044
|
+
**从没被初始化进请求闸门**,而设置页保存时写的是 `gate.settings()` 的返回值 ——
|
|
1045
|
+
于是用户只是调了下「请求间隔」,清理设置就被从 `gate.json` 里**静默抹掉**,
|
|
1046
|
+
重启后回到内置默认(用户视角:「我明明拖过滑块,过两天又变回去了」)。
|
|
1047
|
+
现在这几个字段随闸门一起创建、一起落盘(`createRequestGate` 新增对应选项)。
|
|
1048
|
+
|
|
1049
|
+
### 测试
|
|
1050
|
+
|
|
1051
|
+
- 新增 `tests/check-session-journal.mjs`(**18 项**):落盘往返与容错、扫尾决策的四种分支、
|
|
1052
|
+
「删除回执没来之前不销账」、事件链(`queued` / `deleted` / `leased`)、
|
|
1053
|
+
退出收尾确实把槽里的会话交了出去。
|
|
1054
|
+
- `tests/check-request-gate.mjs` +1:清理设置必须随闸门落盘(改间隔不能把它抹掉)。
|
|
1055
|
+
- **反向验证 8 处**:每处实现分别改坏 → 恰好红掉对应断言(含"多进程共享 DSH_HOME 时不乱删"
|
|
1056
|
+
与"删失败也要留着记录"两条容易写假的地方)。
|
|
1057
|
+
|
|
1058
|
+
### 仍然存在的边界(写清楚,不假装没有)
|
|
1059
|
+
|
|
1060
|
+
强杀时**队列里排着的那批**仍会随进程丢失(退出时的 `flush()` 只是尽力而为);
|
|
1061
|
+
但它们的记录还在 `sessions-in-use.json` 里,**下次启动会补删**。
|
|
1062
|
+
真删不掉(例如账号已从账号库里移除)的会话,插件无能为力。
|
|
1063
|
+
|
|
1064
|
+
内置默认值**未变**(清理延迟仍是 1~2 分钟、批量 6~10 个);要放慢/加快在设置页调区间即可
|
|
1065
|
+
(延迟允许 5 秒 ~ 10 分钟)。
|
|
1066
|
+
|
|
1067
|
+
## 0.1.55 — 2026-09-13
|
|
1068
|
+
|
|
1069
|
+
### 修:思考内容被当成正文上屏(真实会话 7/268 条中招)
|
|
1070
|
+
|
|
1071
|
+
**现象**:跑 `deepseek-reasoner` 时偶发**整段思考直接显示成回答内容**(用户看到大段英文内心
|
|
1072
|
+
独白,如 `me analyze the situation. The user wants to…`)。一次真实会话(268 条 assistant
|
|
1073
|
+
消息)里 **7 条**中招,最长的一段 **14877 字符**。
|
|
1074
|
+
|
|
1075
|
+
**根因**(`src/webapi.ts`):思考阶段服务端是**分两步**下发的 —— 先 `response/thinking_content`
|
|
1076
|
+
发开头一小段(建立当前通道 `sink = 'thinking'`),接着用 `response/fragments/-1/content`
|
|
1077
|
+
发**思考的其余全部**。而这条路径走的 `appendToLastFragment()` 在 `fragments` 为空时无条件
|
|
1078
|
+
"当正文发射",**完全没有使用 `sink`** —— 于是整段思考进了正文通道。
|
|
1079
|
+
|
|
1080
|
+
旁证:这些上屏的正文块**开头都缺 2~4 个字符**(日志原文是 `" me analyze the situation…"`,
|
|
1081
|
+
本该是 `"Let me analyze the situation…"`)—— 缺掉的正是先走 `thinking_content` 的那一小段。
|
|
1082
|
+
两处现象由同一个分支同时解释。
|
|
1083
|
+
|
|
1084
|
+
**修法**(两处,缺一不可):
|
|
1085
|
+
1. `appendToLastFragment()` 在 `fragments` 为空时改为**按 `sink` 归属**;`sink` 未建立时
|
|
1086
|
+
**保持旧行为**(当正文)—— 服务端也可能首帧就发 `-1/content`,那种情况没有通道信息可用,
|
|
1087
|
+
当正文是唯一能避免丢字的兜底。
|
|
1088
|
+
2. `case` 分支里**只有真的续到 fragment 上**才把 `sink` 切成 `'fragments'`;否则紧接着的
|
|
1089
|
+
裸续段会又退回正文,等于没修。
|
|
1090
|
+
|
|
1091
|
+
**验证**:`tests/logic-test.mjs` 新增 4 条(核心 1 条 + 防回归 3 条),**46 → 50 项全过**;
|
|
1092
|
+
两处修法**分别**回退,都精确地只让那 1 条变红(说明两处都必要);离线回放 6 组候选帧序列,
|
|
1093
|
+
修复前复刻出与线上日志**逐字符同构**的形态(`thinking="Let"` / `text=" me analyze…"`),
|
|
1094
|
+
修复后整段回到思考通道。
|
|
1095
|
+
|
|
1096
|
+
(同一会话里还出现过 4 次 `user is muted` 账号限流,但**没有证据**表明两者相关 ——
|
|
1097
|
+
限流只影响稳定性。)
|
|
1098
|
+
|
|
1099
|
+
## 0.1.54 — 2026-09-13
|
|
1100
|
+
|
|
1101
|
+
### 修:CI / Release 被 `setup-node` 的 npm 缓存卡死(0.1.53 因此没能发布)
|
|
1102
|
+
|
|
1103
|
+
**现象**:v0.1.53 的 CI 与 Release **全部失败**,三个平台都一样,且都停在同一步 ——
|
|
1104
|
+
`actions/setup-node@v4`,连"装依赖"都没轮到。
|
|
1105
|
+
|
|
1106
|
+
**原因**:审计 F20 我给 CI 固定了 Node 小版本,顺手加了 npm 缓存开关。
|
|
1107
|
+
但 `setup-node` 的 npm 缓存**要求仓库里有 `package-lock.json`**,没有就报
|
|
1108
|
+
`Dependencies lock file is not found` 并让整个 job 失败 —— 而锁文件恰好还没生成
|
|
1109
|
+
(本机 registry 慢到每个 packument 要 5 分钟,跑不完完整依赖树)。
|
|
1110
|
+
|
|
1111
|
+
**修法**:两个工作流都去掉缓存开关,并保留注释说明"锁文件就绪后再打开"。
|
|
1112
|
+
|
|
1113
|
+
**顺带加了一条自相矛盾的守卫**(`check-round2`):没有 `package-lock.json` 时,
|
|
1114
|
+
`ci.yml` / `release.yml` 都不许出现真的 npm 缓存键 —— 这条断言能直接拦住这次的错误。
|
|
1115
|
+
反向验证有个细节值得记:正则必须只认 **YAML 键**(行首缩进后的那一段),
|
|
1116
|
+
否则注释里那句"不能开 npm 缓存"会把断言喂饱而假红。**同一类坑今天踩了两次**
|
|
1117
|
+
(上午是注释里的 `npx` 字面量),两处都已按"断言只认调用点/键形态"修正。
|
|
1118
|
+
|
|
1119
|
+
**本版无行为改动**:源码与 0.1.53 完全一致,只有版本号与两个工作流文件;
|
|
1120
|
+
`lib/` 因为版本号会被内联进产物所以重新构建过。
|
|
1121
|
+
|
|
1122
|
+
## 0.1.53 — 2026-09-13
|
|
1123
|
+
|
|
1124
|
+
### 第二批收尾三条:F10(建连无限等待)、F08(登录轮询重叠)、F20(构建/平台契约)
|
|
1125
|
+
|
|
1126
|
+
**F10(高)建连阶段限时 + 「创建过的会话」全部归还**
|
|
1127
|
+
|
|
1128
|
+
先核对发现:N04 重写 `streamWebCompletion` 时已经落了审计要求的三件事(建立阶段 45 秒限时、
|
|
1129
|
+
只在等 `next()` 期间计 idle、失败即退役、放行结构化 `AdapterLlmError`),
|
|
1130
|
+
但**这两条路径一条断言都没有** —— 所以本轮先补齐缺口,另外补了两处真正的收口:
|
|
1131
|
+
|
|
1132
|
+
- 建连期限抽成可注入的 `connectTimeoutMs`(默认仍是 45 秒)。原来硬编码,
|
|
1133
|
+
没法离线验证「挂住必须被中断」。
|
|
1134
|
+
- **`wait()` 也包住 `openCompletion`**:建连期限靠 `abort` 生效,而 `abort` 只对
|
|
1135
|
+
「肯配合 signal 的传输」立即生效。包一层之后,即使底层 promise 永远不结算,
|
|
1136
|
+
等待也会在 abort 的瞬间结束 —— 否则「限时」形同虚设,会一直挂在 `await` 上。
|
|
1137
|
+
- **外层记录本次调用创建过的全部会话**(`owned` 集合 + 收尾遍历):超时/取消会
|
|
1138
|
+
*放弃*进行中的 `openCompletion`,它之后才返回的会话必须有人认领。
|
|
1139
|
+
收尾之后才建出来的(`finalized` 分支)立即归还。
|
|
1140
|
+
|
|
1141
|
+
**F08(高)登录轮询:串行、至多提交一次、关窗后不写回**
|
|
1142
|
+
|
|
1143
|
+
旧写法是 `setInterval(() => void (async () => {…})())`:
|
|
1144
|
+
|
|
1145
|
+
- 一轮超过 2 秒时会有**多轮同时在跑**(校验是网络请求,很容易超);
|
|
1146
|
+
- 没有"完成"守卫,也没有 `catch`(写盘失败=未处理拒绝);
|
|
1147
|
+
- 落库动作没有串行保护 →「添加新账号」模式会被**消费两次**,
|
|
1148
|
+
第二次退化成普通切换(把你正在用的号顶掉);
|
|
1149
|
+
- 窗口关闭只 `clearInterval`,拦不住**已经在 `await` 里**的那一轮 → 关窗后照样写回。
|
|
1150
|
+
|
|
1151
|
+
现在抽成 `startCapturePoll`(可离线验证),三条不变量都在里面:
|
|
1152
|
+
串行(上一轮跑完才排下一轮)、至多提交一次(`committed` 先占位再提交,
|
|
1153
|
+
即便 commit 自身抛错也不重试,但会报给 UI 和日志)、停止后丢弃迟到的校验结果;
|
|
1154
|
+
校验另加 15 秒超时(旧写法一次挂住的校验会一直占着这一轮)。
|
|
1155
|
+
|
|
1156
|
+
顺带:`commitCapturedAuth` 的 add 分支改成 `try/finally` 消费添加模式 ——
|
|
1157
|
+
旧写法把 `endAddAccount()` 放在 `upsertAccount` 之后,落库抛错时模式会一直挂着,
|
|
1158
|
+
与模块注释承诺的"只消费一次"相反。
|
|
1159
|
+
|
|
1160
|
+
**F20(中)构建/依赖与平台契约**
|
|
1161
|
+
|
|
1162
|
+
- **`scripts/build.mjs` 取代 Bash 入口**:Windows 上只有 Node/npm、没有 Bash 时
|
|
1163
|
+
`npm run build` 会直接失败。现在 `npm run build` = `node scripts/build.mjs`,
|
|
1164
|
+
用当前 Node 执行本地 tsdown 的 CLI(不经过 shell)。`scripts/build.sh` 保留为薄包装。
|
|
1165
|
+
- **不再用 npx 兜底**:缺依赖时会联网下载,断网就构建不了;版本还是范围,
|
|
1166
|
+
同一份源码在不同时间可能解析到不同的依赖树。现在缺依赖就明确报错并提示 `npm ci`。
|
|
1167
|
+
- **依赖固定精确版本**(`@types/node` 24.13.3 / `tsdown` 0.22.14 / `typescript` 5.9.3,
|
|
1168
|
+
均已核验 registry 可解析),并声明 `engines.node = ^22.18.0 || >=24.11.0`
|
|
1169
|
+
(跑 `.ts` 源码与 tsdown 0.22.14 的共同下限)。顺手删掉了没人用、且缺本地依赖必失败的
|
|
1170
|
+
`build:client` 脚本。
|
|
1171
|
+
- **`scripts/test-offline.mjs` + `scripts/test-files.mjs`**:按文件名扫全量离线用例
|
|
1172
|
+
(`tests/check-*.mjs` + `logic-test.mjs`),新增用例自动纳入;
|
|
1173
|
+
明确排除 `probe-*.mjs`(那几个会打真实接口、需要有效账号)。
|
|
1174
|
+
- **CI 改为三平台矩阵**(ubuntu / windows / macos),固定 Node 小版本(`24.21.0`,
|
|
1175
|
+
因为 tsdown 要求 `>=24.11.0`,写 `24` 可能装到构建不了的早期版本),
|
|
1176
|
+
跑「装依赖 → 构建 → 全量离线用例 → 产物核对」;release 也走同一套工具链。
|
|
1177
|
+
|
|
1178
|
+
**遗留(明确未做)**:`package-lock.json` 尚未生成 —— 本机对 registry 的访问
|
|
1179
|
+
慢到每个 packument 要 5 分钟左右,完整依赖树跑不完。CI 因此暂时用
|
|
1180
|
+
`node scripts/install-deps.mjs`(内部按有无锁文件选 `npm ci` / `npm install`,
|
|
1181
|
+
回退时会打印警告)。生成命令:`npm install --package-lock-only`。
|
|
1182
|
+
|
|
1183
|
+
### 测试
|
|
1184
|
+
|
|
1185
|
+
`check-round2.mjs` 38 → **53 项**(F10 ×3、F08 ×6、F20 ×6)。
|
|
1186
|
+
反向验证 F10 3 处、F08 5 处、F20 6 处(其中 4 处一次批量改坏,另 1 处此前真实变红过)。
|
|
1187
|
+
|
|
1188
|
+
两处「改坏后仍然绿」的发现值得记下:
|
|
1189
|
+
|
|
1190
|
+
1. **同一个清理动作在两个层级各有一份**(`openCompletion` 的失败分支 / 生成器收尾遍历;
|
|
1191
|
+
`tracked` 的迟到分支 / `leaseSession` 的取消分支),互相兜底 ——
|
|
1192
|
+
反向验证必须**两层一起改**才会红,测试注释已写明。
|
|
1193
|
+
2. 建连超时的测试夹具需要**留一个 ref 的句柄**占住事件循环:
|
|
1194
|
+
`arm()` 的超时定时器是 `unref()` 的(刻意不为了超时吊住进程),
|
|
1195
|
+
夹具里没有其它 ref 句柄时 Node 会直接退出,表现为 "unsettled top-level await",
|
|
1196
|
+
看起来像实现挂了(真实运行环境里 DSH 自己就有活的事件循环,所以 unref 是对的)。
|
|
1197
|
+
|
|
1198
|
+
## 0.1.52 — 2026-09-13
|
|
1199
|
+
|
|
1200
|
+
### 老账三条:F22(写盘失败不释放)、F16(请求体边界)、F15(CDP 未结算)
|
|
1201
|
+
|
|
1202
|
+
**F22(中)另存为失败要 abort**
|
|
1203
|
+
|
|
1204
|
+
`createWritable()` 成功之后,`write`/`close` 抛错时旧实现直接返回 failed、**没有 `abort`** ——
|
|
1205
|
+
Chromium 会留下未提交的临时文件与句柄(模拟磁盘写满时实测 `abort` 一次都没被调用)。
|
|
1206
|
+
现在留住 writable 引用、失败即 abort;成功路径走完清掉引用(不对已关闭的 writable 再动手)。
|
|
1207
|
+
|
|
1208
|
+
**F16(中)请求体:生命周期、超时、结构化错误 + 路径导入的边界**
|
|
1209
|
+
|
|
1210
|
+
`readJsonBody` 三处:
|
|
1211
|
+
|
|
1212
|
+
- 旧实现只监听 `data`/`end`/`error` —— 客户端只 `close`/`aborted`(不发 `end`)时 **Promise 永不结算**,
|
|
1213
|
+
监听器一起挂着。现在 `close`/`aborted` 也算结束,并且每种结局都清监听。
|
|
1214
|
+
- 没有应用层 deadline:一个慢连接可以永远占着。现在 10 秒。
|
|
1215
|
+
- 超限旧实现是 `destroy()` 后 `resolve(undefined)`,调用方只能报"缺少 payload",客户端更可能只看到断连。
|
|
1216
|
+
现在抛带状态码的 `BodyError`,handler 统一回 413/400/408,并把 `shouldKeepAlive` 置 false。
|
|
1217
|
+
|
|
1218
|
+
**关于路径导入,这里没有按审计建议删掉它**:客户端在**优先用** path 路线,理由是
|
|
1219
|
+
"明文凭证不进 HTTP"(`/accounts/export-json` 那段也确认过同样的取舍)。删掉会让凭证回到渲染进程 ——
|
|
1220
|
+
那比审计担心的"渲染进程可让宿主读任意文件"更严重。所以**保留 path、自己加边界**:
|
|
1221
|
+
只接受**普通文件**(挡掉目录 / FIFO / 设备这类会阻塞或异常的东西),并限定大小。
|
|
1222
|
+
上限取 **2 MiB**:实测单个账号 1.1–1.8 KB、账号数上限 500 → ≈850 KB,2 MiB 留 2 倍余量
|
|
1223
|
+
(审计建议的 120 KiB 会挡住正常备份)。前端同样检查一道是为了不白传。
|
|
1224
|
+
更彻底的做法是"宿主批准的一次性句柄/令牌",需要新的宿主 API,本版没做(代码里留了说明)。
|
|
1225
|
+
|
|
1226
|
+
**F15(中)CDP:协议错误与连接关闭都要结算**
|
|
1227
|
+
|
|
1228
|
+
- `{id, error:{…}}` 旧实现把 `message.result`(undefined)当成功 resolve → 调用方拿着 undefined
|
|
1229
|
+
继续跑、真正的错误信息全丢。现在 reject。
|
|
1230
|
+
(与 `Runtime.evaluate` 的 `exceptionDetails` 区分:那是**执行结果**,由调用方检查。)
|
|
1231
|
+
- 连接 `close` 旧实现不清 pending → 每个在途命令各自等到超时;建连阶段也不监听 `close`。
|
|
1232
|
+
现在连接关闭立刻拒绝全部在途命令并清定时器;建连失败/超时主动收掉 socket
|
|
1233
|
+
(不再留下"晚到的 open"造成的孤立连接)。
|
|
1234
|
+
- `send` 同步抛错时也不再留一个永远不结算的 pending。
|
|
1235
|
+
- HTTP 探测(`/json/version`、`/json/list`)收口成统一 helper:**每次请求各自 2 秒超时**
|
|
1236
|
+
(旧写法只有外层循环的 deadline,一次请求挂住就再也回不到循环条件);轮询开头检查 `signal`,
|
|
1237
|
+
并从 `browserLogin` 一路传入。
|
|
1238
|
+
- 页面筛选改成 `new URL(url).origin === DS_BASE` **严格相等**:`includes('deepseek.com')`
|
|
1239
|
+
会命中 `chat.deepseek.com.evil.example` 这类域名。
|
|
1240
|
+
|
|
1241
|
+
### 测试
|
|
1242
|
+
|
|
1243
|
+
- `check-round2.mjs` 30 → **38 项**:F22 一条、F16 三条(只 close 也结算 / 超限 413 / 正常解析)、
|
|
1244
|
+
F15 四条(协议错误 reject / 关连接立刻结算 / send 抛错不留 pending / origin 严格比较)。
|
|
1245
|
+
为可测性导出 `readJsonBody`、`BodyError`、`CdpClient`、`isDeepSeekPage`(与 `checkedWasmUrl` 同类做法)。
|
|
1246
|
+
- 反向验证 **6 处全红**,复原后全绿。
|
|
1247
|
+
- 33 个测试文件全绿。
|
|
1248
|
+
|
|
1249
|
+
### 未处理
|
|
1250
|
+
|
|
1251
|
+
F20(构建脚本的平台依赖)、F10(建流缺整体期限)、F08(登录轮询的 in-flight/代际约束)。
|
|
1252
|
+
|
|
1253
|
+
## 0.1.51 — 2026-09-13
|
|
1254
|
+
|
|
1255
|
+
### 老账四条:F06(图片缓存跨账号)、F19(台账口径)、F21(迁移留明文)、F23(诊断落原文)
|
|
1256
|
+
|
|
1257
|
+
**F06(高)图片上传缓存:按账号隔离 + 真的淘汰**
|
|
1258
|
+
|
|
1259
|
+
缓存是 `attachmentId → fileId`,两个独立问题:
|
|
1260
|
+
|
|
1261
|
+
- **没有账号维度**:`fileId` 是**归属某个账号**的服务端对象。切号之后旧缓存还在,
|
|
1262
|
+
会把上一个账号的 `fileId` 复用出去。(服务端是否接受是它的事 —— 不能据此断言"跨账号能读到图",
|
|
1263
|
+
但"把我们这边两个号的引用串了"本身就已经是错的。)
|
|
1264
|
+
- **TTL ≠ 淘汰**:只检查"被查中的那一项"是否过期;一直换新图、不换旧 key 时,旧项永久滞留。
|
|
1265
|
+
|
|
1266
|
+
缓存逻辑抽成 `ImageUploadCache`(**导出以便单测** —— 图片上传要过真实 PoW,端到端测不了):
|
|
1267
|
+
切号整批清空;每次使用前清掉**所有**过期项;条数封顶(默认 256);
|
|
1268
|
+
`set` 带作用域校验,防"上传 await 期间账号被切走"造成的串号。
|
|
1269
|
+
另外把「取消」从"降级为纯文本继续跑"改成**照常传播**。
|
|
1270
|
+
|
|
1271
|
+
**F19(中)台账:hours 取整 + 间隔口径**
|
|
1272
|
+
|
|
1273
|
+
`?hours=1.5` 直接 `RangeError`(路由只 clamp 没取整,`new Array(1.5)` 抛错)。
|
|
1274
|
+
现在先 `Number.isFinite` 判断、再 `Math.floor` 并夹到 1–72。
|
|
1275
|
+
|
|
1276
|
+
间隔口径修正(原来两处都不对):`at` 是**结束**时刻,所以"这次等了多久" = `本次开始 − 上次结束`;
|
|
1277
|
+
直接拿相邻 `at` 相减,会把上一轮的生成耗时算进"等待"里。另外**失败的调用也要计入**
|
|
1278
|
+
(失败同样占用了等待窗口),并按**账号分组**(两个号的节奏互不相干,混在一起只会把分布拉平)。
|
|
1279
|
+
|
|
1280
|
+
**F21(中)迁移不再留明文副本**
|
|
1281
|
+
|
|
1282
|
+
旧版把凭证文件 `rename` 成 `.migrated-<时间戳>` **留档** —— 那是明文、可登录的凭证,
|
|
1283
|
+
于是"退出登录"删掉账号库记录之后它还在磁盘上照样能用,"已登出"就成了谎话。
|
|
1284
|
+
现在**确认新记录落盘后删掉源文件**;写入校验失败会抛错,`migrateLegacyAuthIfNeeded` 记下原因、
|
|
1285
|
+
启动时写进日志(`legacyMigrationError()`),不再静默。
|
|
1286
|
+
**选择删源文件而不是留档**:误删的保护交给账号库的导出备份 —— 与 `removeAccount` 已有策略一致。
|
|
1287
|
+
|
|
1288
|
+
**F23(中)丢弃载荷默认只记元信息**
|
|
1289
|
+
|
|
1290
|
+
模型吐坏的工具调用参数,里面完全可能带命令里的 token、文件内容、个人信息。以前默认把整段原文写进
|
|
1291
|
+
`~/.dsh/deepseek-web/rejected.jsonl`,而那个路径**硬编码 homedir、无视 `DSH_HOME`**
|
|
1292
|
+
(我们自己的测试就被它坑过:写到了真实用户目录)。
|
|
1293
|
+
现在只记「时间 / 模式 / 原因 / 长度 / sha256」,写在 `<DSH_HOME>/web-login/diagnostics/rejected-meta.jsonl`,
|
|
1294
|
+
目录 0700、文件 0600、上限 4 MB。要采集完整内容应另做"用户显式开启 + 有效期 + 清理策略"的独立诊断,
|
|
1295
|
+
不能默认打开(本版没做)。⚠️ 摘要挡不住低熵内容被猜出来 —— 它只是"不存原文",不是"内容不可还原"。
|
|
1296
|
+
|
|
1297
|
+
### 测试
|
|
1298
|
+
|
|
1299
|
+
- `check-round2.mjs` 25 → **30 项**:F06 三条(按账号隔离 / 淘汰与封顶 / await 期间切号拒绝写入)、
|
|
1300
|
+
F19 一条(小数、负数、0、999、±Infinity、NaN 都不抛错)、F23 一条(只记元信息 + 写在 DSH_HOME 下)。
|
|
1301
|
+
- `check-accounts.mjs` 20 → **21 项**:F21 一条(迁移写入失败时必须留下原因,且**不能删**旧凭证)。
|
|
1302
|
+
- 两条既有断言写的是**旧行为**,按新行为改了期望(不是放松实现):
|
|
1303
|
+
「旧文件改名留档」→「旧文件被删除、无任何明文副本」;
|
|
1304
|
+
「间隔只统计 chat 成功调用」→「含失败调用 + 按结束时刻算等待」(`samples` 1→2,`min` 30_000→19_800)。
|
|
1305
|
+
- 反向验证 **9 处全红**(F06 三处、F19 两处、F21 两处、F23 两处),复原后全绿。
|
|
1306
|
+
- 33 个测试文件全绿。
|
|
1307
|
+
|
|
1308
|
+
### 未处理
|
|
1309
|
+
|
|
1310
|
+
F08 / F10 / F15 / F16 / F20 / F22。
|
|
1311
|
+
|
|
1312
|
+
## 0.1.50 — 2026-09-13
|
|
1313
|
+
|
|
1314
|
+
### 第一轮审计 F04(第二轮复核:**声称修了但生产身份链未接通**):user.id 没进去重键
|
|
1315
|
+
|
|
1316
|
+
库里是**两级去重**:先按 `serverId` 匹配同一个账号,再按 `token` 匹配。但真实登录路径
|
|
1317
|
+
(浏览器捕获 / 分区恢复 / 手动 token)拿到的 `user.id` **只被塞进 `user` 字段**、
|
|
1318
|
+
**从来没写进 `serverId`** —— 于是 token 一刷新,第一级去重就失效,同一个号在库里堆成好几条。
|
|
1319
|
+
既有测试是**手工传 `serverId`** 才通过的:那条链在生产上根本没接上,测试把它绕过去了。
|
|
1320
|
+
|
|
1321
|
+
**身份归一**(`auth.ts` 新增两个 helper):
|
|
1322
|
+
|
|
1323
|
+
- `withVerifiedIdentity(auth, user)` —— 凭证**落库前**用:带上 `serverId: user.id`、清 `unverified`;
|
|
1324
|
+
- `refreshVerifiedIdentity(id, token, user)` —— 记录**已在库里**时用(`/status` 迟到校验、探活):
|
|
1325
|
+
记录不存在或 token 已变 → 什么都不做,**绝不新建、绝不切号**(与 F05/N02 同一条纪律)。
|
|
1326
|
+
|
|
1327
|
+
接入点共五处:浏览器窗口登录、分区恢复、手动粘贴 token、`/status`(与 N02 同一处代码,顺带补 serverId)、
|
|
1328
|
+
探活成功分支。
|
|
1329
|
+
|
|
1330
|
+
**兼容旧记录**:`serverId` 是后加的字段,老库里可能只有 `user.id`。去重时加一层
|
|
1331
|
+
「只认**没有 serverId**、且 `user.id` 相同」的匹配 —— 否则老用户重登一次就会多出一条。
|
|
1332
|
+
(备份文件自报的 id 仍然不参与授权,那条路径由 N01 的 `importAccounts` 单独把关。)
|
|
1333
|
+
|
|
1334
|
+
### 测试
|
|
1335
|
+
|
|
1336
|
+
- `check-round2.mjs` 22 → **25 项**:三条 F04 用例 —— 同账号 token 刷新后重登库里仍只有一条
|
|
1337
|
+
(且 token 已更新)、旧记录(只有 `user.id`)能被认出来、`/status` 的迟到校验也要落 `serverId`。
|
|
1338
|
+
输入用的是**真实登录形状**(而不是手工塞 `serverId`),正是原来被绕过的那条链。
|
|
1339
|
+
- `check-bundle.mjs` 新增一条产物断言(三个调用点特征)。
|
|
1340
|
+
- 反向验证 **3 处全红**:`withVerifiedIdentity` 不写 serverId / 去重不兼容旧记录 /
|
|
1341
|
+
`refreshVerifiedIdentity` 不写 serverId;复原后全绿。
|
|
1342
|
+
- 33 个测试文件全绿。
|
|
1343
|
+
|
|
1344
|
+
### 未处理
|
|
1345
|
+
|
|
1346
|
+
旧项 F06 / F08 / F10 / F15 / F16 / F19 / F20 / F21 / F22 / F23。
|
|
1347
|
+
|
|
1348
|
+
## 0.1.49 — 2026-09-13
|
|
1349
|
+
|
|
1350
|
+
### 第二轮审计第五批:N02(迟到的 /status 校验把当前账号切回去)
|
|
1351
|
+
|
|
1352
|
+
`GET /status` 会顺带做一次登录态校验,返回后用 `writeAuth({ ...auth, user: check.user })`
|
|
1353
|
+
补全账号的展示信息。问题在于 **`writeAuth` 的语义是「写入并设为当前账号」**,
|
|
1354
|
+
而这个校验请求是异步的(超时 15s),等待期间用户完全可能:
|
|
1355
|
+
|
|
1356
|
+
- **切到另一个账号** → 迟到的结果把当前账号**切回去**("我明明切了 B,面板又跳回 A");
|
|
1357
|
+
- **把该账号删掉** → `writeAuth` 内部 upsert,把已删除的凭证**复活**。
|
|
1358
|
+
|
|
1359
|
+
刷新元信息不该有这两个副作用。现在只在「仍存在、且 token 匹配」的记录上更新元数据:
|
|
1360
|
+
|
|
1361
|
+
```ts
|
|
1362
|
+
const target = listAccounts().find((item) => item.token === auth.token)
|
|
1363
|
+
if (target) {
|
|
1364
|
+
updateAccount(target.id, {
|
|
1365
|
+
user: { ...target.user, ...check.user },
|
|
1366
|
+
unverified: undefined,
|
|
1367
|
+
lastVerifiedAt: new Date().toISOString(),
|
|
1368
|
+
})
|
|
1369
|
+
}
|
|
1370
|
+
```
|
|
1371
|
+
|
|
1372
|
+
顺带记录 `lastVerifiedAt`、并清掉 `unverified` 标记(`normalizeRecord` 只在
|
|
1373
|
+
`unverified === true` 时才保留该字段,所以传 `undefined` 等于清除)。
|
|
1374
|
+
探活模块 `probe.ts` 早就是「按 target 更新、不切号」的写法 —— 这次是让 `/status` 这条路径与它一致。
|
|
1375
|
+
|
|
1376
|
+
**关于「整块替换」**:审计给的是完整的 `apply()`(786 行)。我先把它与当前实现**逐行 diff**,
|
|
1377
|
+
发现**只有这一处差异(+3/−1)**,于是只改这一处 —— 等价,但把引入意外变化的风险降到最低。
|
|
1378
|
+
|
|
1379
|
+
### 测试
|
|
1380
|
+
|
|
1381
|
+
- `check-round2.mjs` 20 → **22 项**:两条 N02 用例,都通过真实 `apply()` 注册 `/status` handler,
|
|
1382
|
+
再用注入的 fetch 把校验请求**悬挂住**,然后分别「切到 B」「删掉 A」,最后释放响应。
|
|
1383
|
+
含三处自证:拿到了 handler、校验请求确实发出并挂起、元信息确实被刷新(证明这条路径真的走到了)。
|
|
1384
|
+
- `check-bundle.mjs` 新增一条产物断言。注意断言的写法:`item.token === auth.token` 这类比较
|
|
1385
|
+
在产物里有 **3 处**(`upsertAccount` 内部也有),只拿它做断言会被库内部代码骗过;
|
|
1386
|
+
`unverified: void 0` 只出现在**这一个调用点**(探活模块用的是 `lastVerifyError`)。
|
|
1387
|
+
- 反向验证:恢复成 `writeAuth({ ...auth, user: check.user })`(命中数强制 = 1)→ 两条断言变红,复原后绿。
|
|
1388
|
+
- 33 个测试文件全绿。
|
|
1389
|
+
|
|
1390
|
+
### 未处理
|
|
1391
|
+
|
|
1392
|
+
F04(`serverId` 的生产身份链是断的),以及旧项 F06 / F08 / F10 / F15 / F16 / F19 / F20 / F21 / F22 / F23。
|
|
1393
|
+
|
|
1394
|
+
## 0.1.48 — 2026-09-13
|
|
1395
|
+
|
|
1396
|
+
### 第二轮审计第四批:N05(失效 WASM 地址阻断 discovery 恢复)
|
|
1397
|
+
|
|
1398
|
+
**N05(高)资源下载收口到统一实现;失效时连「已解析的地址」一起清**
|
|
1399
|
+
|
|
1400
|
+
`resolveWasmUrl` 命中 key 就直接返回缓存地址,而 `loadWasmModule` 的 rejection 只清
|
|
1401
|
+
`wasmModuleCache`、**没清 `resolvedWasmUrl`**。于是「保留了 discovery 能力」并不等于
|
|
1402
|
+
「失效后真的会再进 discovery」:地址已经 404 了,请求还会一直对着它打 ——
|
|
1403
|
+
唯一的兜底(页面发现)永远走不到。
|
|
1404
|
+
|
|
1405
|
+
- 新增 `readOfficialResource(url, max, signal)`,把三处下载收口成一个实现:
|
|
1406
|
+
**只走官方 HTTPS 域**(`deepseek.com` / `*.deepseek.com`,无凭据、标准端口);
|
|
1407
|
+
**`redirect: 'error'` 拒绝全部重定向**;**分块累计字节上限**(首页 2 MiB / 脚本 8 MiB /
|
|
1408
|
+
WASM `MAX_WASM_BYTES`),超限抛错并 `reader.cancel()`,不再先读完 `arrayBuffer()` 再检查大小;
|
|
1409
|
+
独立超时(下载 15s、探测 10s)。
|
|
1410
|
+
- `loadWasmModule` 的失败回调现在**同时清编译缓存与已解析地址**;`isReachable` 也先过白名单。
|
|
1411
|
+
|
|
1412
|
+
**关于「拒绝重定向」的可用性代价 —— 已实测(不只静态推理)**
|
|
1413
|
+
|
|
1414
|
+
直接跟随重定向会被当成"任意跳转都放行",所以这里选择拒绝。代价是若官方某天需要 CDN 跳转,
|
|
1415
|
+
就会拒掉合法请求。**用真实资源实测过(不发任何账号请求)**:
|
|
1416
|
+
|
|
1417
|
+
| 检查项 | 结果 |
|
|
1418
|
+
| --- | --- |
|
|
1419
|
+
| 默认 WASM 地址(`fe-static.deepseek.com`…) | HTTP 206(range 探测)、**无重定向** |
|
|
1420
|
+
| 完整下载 | 200、`redirected: false`、26612 字节、魔数 `00 61 73 6d` |
|
|
1421
|
+
| `WebAssembly.compile` | 通过 |
|
|
1422
|
+
| `resolveWasmUrl` 解析耗时 | 257ms(命中默认地址,未进 discovery) |
|
|
1423
|
+
|
|
1424
|
+
结论:**当前部署完全满足「无跳转」约束** —— 默认地址与首页(discovery 起点)都无重定向。
|
|
1425
|
+
|
|
1426
|
+
**本节初稿有一处描述已更正(2026-09-13 当日内)**:初稿写「首页在不带浏览器 UA 时返回 429」,
|
|
1427
|
+
那是把**一次偶发限流**当成了「UA 相关」的稳定行为 —— 典型的「一次观测当结论」。
|
|
1428
|
+
随后用 Node 默认 UA(也就是 `activeFetch` 的真实行为)连测**两次都是 HTTP 200 / 无重定向**,
|
|
1429
|
+
带浏览器 UA 也是 200 → 结论是**与 UA 无关**,不需要给 discovery 补 UA。
|
|
1430
|
+
(那次 429 出现在短时间内连续发多个探测请求之后,属临时限流,不是稳定特征。)
|
|
1431
|
+
|
|
1432
|
+
### 测试
|
|
1433
|
+
|
|
1434
|
+
- `check-round2.mjs` 18 → **20 项**:两条 N05 用例(下载 404 / 编译失败),走真实 `createPowHeader`
|
|
1435
|
+
链路 + 注入 fetch,并用 `range` 头区分「探测」与「下载」。断言链含三处自证:
|
|
1436
|
+
第一轮必须真的失败、探测与下载都真的发生过、第一轮探测通过(说明地址确实进了缓存)。
|
|
1437
|
+
- `check-bundle.mjs` 新增两条产物断言(下载统一入口 + 限字节;失效时清地址缓存)。
|
|
1438
|
+
- 反向验证:只移除 `if (resolvedWasmUrl?.url === url) resolvedWasmUrl = null` 一句
|
|
1439
|
+
(命中数强制 = 1)→ 两条新断言变红,而 `check-sse-wasm` **仍全绿** ——
|
|
1440
|
+
这是覆盖空洞,不是"原用例没用"。复原后全绿。
|
|
1441
|
+
- 33 个测试文件全绿。
|
|
1442
|
+
|
|
1443
|
+
### 未处理
|
|
1444
|
+
|
|
1445
|
+
N02(迟到的 `/status` 校验把当前账号切回去)、F04(`serverId` 的生产身份链是断的)。
|
|
1446
|
+
|
|
1447
|
+
## 0.1.47 — 2026-09-13
|
|
1448
|
+
|
|
1449
|
+
### 第二轮审计第三批:N03(tool_result 跨流残片泄漏)+ N06(续写漏记 usage)
|
|
1450
|
+
|
|
1451
|
+
这两条改的是**同一处 `streamImpl`**,按审计要求用组合后的代码块**一次应用** ——
|
|
1452
|
+
分两次打补丁会互相覆盖。
|
|
1453
|
+
|
|
1454
|
+
**N03(高)伪系统标记的剥离改成「有状态」流式过滤器**
|
|
1455
|
+
|
|
1456
|
+
旧写法对正文每一包调用**无状态**的 `stripSystemMarkers(guarded.text)`。
|
|
1457
|
+
完整字符串上的跨行正则正确,**不代表跨 push 有效**:开始标签、正文、结束标签落在
|
|
1458
|
+
不同的 push 里就失去共同上下文。用真实 createAdapter 流链实测:
|
|
1459
|
+
|
|
1460
|
+
| 送入方式 | 结果 |
|
|
1461
|
+
| --- | --- |
|
|
1462
|
+
| 整段一次 | 能剥 |
|
|
1463
|
+
| 只有开标签(未闭合) | 能剥 |
|
|
1464
|
+
| **逐字符** | **泄漏 —— 正文里出现完整的 `<tool_result>…</tool_result>` 及其内容** |
|
|
1465
|
+
|
|
1466
|
+
新增 `SystemMarkerStreamFilter`,跨 push 维持「正在捕获的标签 / 围栏状态 / 尾部缓冲」;
|
|
1467
|
+
流内与**轮末残余**(`drainTextPipeline(..., false)` 之后)都交给它。
|
|
1468
|
+
围栏代码块内的示例仍然保留(那是给人看的,不剥)。
|
|
1469
|
+
|
|
1470
|
+
顺带在反向验证时看清一个事实:真实流链上**一包送入也会泄漏** —— 因为 `echoGuard`
|
|
1471
|
+
会先把整段扣住,残余到**轮末**才放行,而轮末那句 drain 原本不做标记剥离。
|
|
1472
|
+
所以「流内 + 轮末」两处**都要**换成有状态过滤器,只改一处仍然漏。
|
|
1473
|
+
|
|
1474
|
+
**N06(中)逐轮记账服务端真实 token**
|
|
1475
|
+
|
|
1476
|
+
旧写法整次调用二选一:`reportedTokens > 0 ? max(0, reportedTokens - 输出估算) : estimateTokens(prompt)`。
|
|
1477
|
+
只要**任何一轮**上报过 `totalTokens`,其它轮次的输入成本就从账本里消失 ——
|
|
1478
|
+
审计复现:第一轮上报 100、第二轮不上报,总量恰好还是 100。
|
|
1479
|
+
|
|
1480
|
+
现在按轮记账(`usageRounds`):有上报的轮次用真值并减掉**该轮**的输出估算,
|
|
1481
|
+
没上报的轮次仍按字符估算;两轮都上报时总量精确等于上报值之和。
|
|
1482
|
+
|
|
1483
|
+
**SystemMarkerStreamFilter 的「安全前缀」(自查发现,非审计条目)**
|
|
1484
|
+
|
|
1485
|
+
逐行过滤在没有换行时会一直缓冲到轮末 —— 实测一段 60 字符、全程无换行的文本
|
|
1486
|
+
**一个字都不上屏**,直到 flush 才整段吐出;前面若有 200 字符无换行,首字要等到第 201 个字符。
|
|
1487
|
+
现在:不在围栏内、不在捕获中、且剩余部分既无 `<` 也不是围栏候选开头时,直接吐出去
|
|
1488
|
+
(所有被识别的标记都以 `<` 开头,这样不可能漏掉标记,也不影响围栏状态判定)。
|
|
1489
|
+
|
|
1490
|
+
⚠️ **效果边界要说清**:真正决定上屏节奏的是上游 `TranscriptEchoGuard` ——
|
|
1491
|
+
它同样逐行分类(只按换行符切行),**整段没有换行**时会把内容先扣到轮末。
|
|
1492
|
+
这是**既存行为**(0.1.47 之前就是这样,不是本轮引入)。要改成逐字上屏,需要放宽 echoGuard
|
|
1493
|
+
的逐行 hold,并保证「行内回声判据的前缀」不被误放 —— 风险较高,**本轮未处理**。
|
|
1494
|
+
`tests/check-auto-continue.mjs` 里有一条用例**记录这个现状**,将来若改了它会提醒同步更新。
|
|
1495
|
+
|
|
1496
|
+
### 测试
|
|
1497
|
+
|
|
1498
|
+
- `check-auto-continue.mjs` 14 → **23 项**:N06 两条(部分上报 / 全部上报)、
|
|
1499
|
+
N03 六条(一包 / 逐字符 / **遍历全部切分点** / 围栏保留 / 无换行现状 / 尖括号必须缓冲)、
|
|
1500
|
+
安全前缀单测一条。
|
|
1501
|
+
- `check-bundle.mjs` 的产物断言同步更新:原来断言的 `reportedTokens > 0` 已被 N06 删除;
|
|
1502
|
+
新断言改的是**调用点** —— `usageRounds.push(roundUsage)` + `estimateTokens(round.prompt)`,
|
|
1503
|
+
以及 `systemMarkerFilter` 在流内与轮末的两处 push + flush。
|
|
1504
|
+
注意 `round.total !== undefined` 会被打包器改写成 `!== void 0`,所以不拿它做断言。
|
|
1505
|
+
- 反向验证 **6 处全部变红**:流内退回无状态剥离、轮末残余不剥、围栏保护失效、
|
|
1506
|
+
安全前缀失效、去掉「含 `<` 必须缓冲」、usage 退回整次二选一。
|
|
1507
|
+
- 33 个测试文件全绿。
|
|
1508
|
+
|
|
1509
|
+
### 未处理
|
|
1510
|
+
|
|
1511
|
+
N05(WASM 下载失败只清编译缓存,失效地址会阻断 discovery 恢复)、
|
|
1512
|
+
N02(迟到的 `/status` 校验把当前账号切回去)、F04(`serverId` 的生产身份链是断的)。
|
|
1513
|
+
|
|
1514
|
+
## 0.1.46 — 2026-09-13
|
|
1515
|
+
|
|
1516
|
+
### 第二轮审计第二批:N07(长历史静默截掉系统指令)+ N04(复用槽三处)
|
|
1517
|
+
|
|
1518
|
+
**N07(高)固定头不再按比例裁剪**
|
|
1519
|
+
|
|
1520
|
+
旧写法最终用 `headBudget = min(head.length, floor(maxChars * 0.62))` 兜底 ——
|
|
1521
|
+
只要 system 长到超过预算的 **62%**,就会从**中间**被 `truncateMiddle` 挖掉一块:
|
|
1522
|
+
**预算明明够放完整 system,也会静默丢掉系统指令**,模型照着残缺策略干活。
|
|
1523
|
+
|
|
1524
|
+
现在:固定头(system + 协议 + 工具目录)**独占它的长度**,剩余预算全给历史;
|
|
1525
|
+
固定头自己放不下就抛 `AdapterLlmError(..., 'CONTEXT_WINDOW_EXCEEDED')`。
|
|
1526
|
+
代价:以前"静默丢指令还能生成"的请求现在会明确失败 —— 这是有意的。
|
|
1527
|
+
另加 `maxChars` 校验(非安全整数或 < 128 直接 `RangeError`),免得小数一路漂到后面的算术里。
|
|
1528
|
+
|
|
1529
|
+
**N04(高)复用槽:缺在用保护 / 提前中断不退役 / 切号丢失清理归属**
|
|
1530
|
+
|
|
1531
|
+
三处独立问题,审计各自复现过:
|
|
1532
|
+
|
|
1533
|
+
1. **提前结束不退役**:`openCompletion` 只在 HTTP 失败分支退役;调用方拿到正文就 `return`
|
|
1534
|
+
(或取消)时,stream generator 的 `finally` 不管 —— 于是下一次请求会接着用一个
|
|
1535
|
+
"上一条流还没消费完"的会话。现在 `finally` 里 `!complete || poisoned || limit === 0` 就退役。
|
|
1536
|
+
2. **没有在用保护**:复用开启时新增**飞行互斥**(`reuseFlightTail`),同一时刻只有一个复用请求在跑,
|
|
1537
|
+
避免"轮换撞上并发请求正在消费的会话"。关闭复用(`sessionReuseTurns: 0`)保持原并发模式。
|
|
1538
|
+
3. **切号丢失清理归属**:槽位新增 `cleanup`,记录**这个会话归谁回收**。
|
|
1539
|
+
旧实现只在同账号轮换时返回 `retired`,跨账号切换直接覆盖旧槽 → 旧会话永远不会有人删。
|
|
1540
|
+
现在旧槽一律交回**它自己的** cleanup。
|
|
1541
|
+
|
|
1542
|
+
顺带修掉一处:`openCompletion` 的 catch 原本**无条件**把错误包成 `TRANSPORT`,
|
|
1543
|
+
会把 PoW/网络层带出来的 `AUTH` / `RATE_LIMIT` 结构化分类抹掉 —— 宿主于是按"可重试的传输错误"
|
|
1544
|
+
处理本该停下的情况。现在 `AdapterLlmError` 原样放行。
|
|
1545
|
+
|
|
1546
|
+
**并发代价(明确记录)**:复用开启(默认 20)时全程串行,含跨账号。
|
|
1547
|
+
默认 adapter 本来就串行,常规体验不变;若同时主动开启 `allowConcurrent`,吞吐会降低。
|
|
1548
|
+
关闭复用则完全不受影响。
|
|
1549
|
+
|
|
1550
|
+
### 测试
|
|
1551
|
+
|
|
1552
|
+
- `check-round2.mjs` 18 项(+7):N07 四条、N04 三条、以及"已是 AdapterLlmError 不得被包成 TRANSPORT"。
|
|
1553
|
+
- **按审计的 T-N04 改掉我那条期望写错的用例**:原断言是"不能回收上一个账号的会话"→ `deleted` 必须为空。
|
|
1554
|
+
但正确设计不是"永不回收",而是**用原账号的回调回收** ——
|
|
1555
|
+
「不能用 B 的回调删 A」不等于「永远不应回收 A」。原用例两个账号共用一个回调,所以断言不到"归属"。
|
|
1556
|
+
- 反向验证 **9 处全部变红**(N07 一处、N04 五处 + 早前的 N01/N08/N09/N10 四处)。
|
|
1557
|
+
|
|
1558
|
+
⚠️ 其中 N04e(catch 分类)**第一轮没红**——因为**没有任何用例覆盖它**。补了
|
|
1559
|
+
"powHeader 抛 AUTH,必须原样传出"这条才真的能区分。又一次印证:没红先问"用例真的存在吗"。
|
|
1560
|
+
|
|
1561
|
+
### 尚未处理
|
|
1562
|
+
|
|
1563
|
+
N03(tool_result 跨流残片)、N05(WASM 下载失败不失效缓存)、N02(迟到 /status 切回账号)、
|
|
1564
|
+
N06(续写漏记 usage)。⚠️ 审计明确指出 **N03 与 N06 修改同一个 `streamImpl`,必须用组合函数**,
|
|
1565
|
+
不能分两次互相覆盖。以及旧项 F06/F08/F10/F15/F16/F19/F20/F21/F22/F23 与 F04 的生产身份链。
|
|
1566
|
+
|
|
1567
|
+
## 0.1.45 — 2026-09-13
|
|
1568
|
+
|
|
1569
|
+
### 第二轮审计(N01–N10)第一批:N01 / N08 / N09 / N10
|
|
1570
|
+
|
|
1571
|
+
审计报告:`DSH-Round2-Audit.md`(10 项确认,7 项"高")。本版处理 4 条,其余见文末。
|
|
1572
|
+
|
|
1573
|
+
**N01(高)导入仍相信备份自报的 id —— 我上一轮声称修了 F03,其实没修**
|
|
1574
|
+
|
|
1575
|
+
- 根因:`normalizeRecord` 内部是 `raw.id ?? fallbackId ?? newAccountId()`,所以
|
|
1576
|
+
"调用时不传 fallbackId"**完全没有效果**;备份里写同一个 `id` 的两条**不同 token** 账号会互相覆盖
|
|
1577
|
+
(不需要路径穿越)。审计在原版上复现:连续导入两条,列表长度只有 1。
|
|
1578
|
+
- 改法:导入一律**生成本地主键**(`do { id = newAccountId() } while (ids.has(id))`),
|
|
1579
|
+
只有 token 命中才更新已有账号;`serverId` 也不再用于匹配 —— 它是备份自报字段,
|
|
1580
|
+
不能拿它授权覆盖不同 token 的账号。另加 500 条上限。
|
|
1581
|
+
- 代价:同账号刷新 token 的备份会多出一条,需要用户手动确认合并(但旧凭证安全)。
|
|
1582
|
+
|
|
1583
|
+
**N08(中)取消长休会白吃休息债务**
|
|
1584
|
+
|
|
1585
|
+
`if (needsBreak) { consecutive = 0; ... }` 原本发生在 `await sleep(...)` **之前**,
|
|
1586
|
+
于是"取消一次长休"等于把休息债务一笔勾销,下一次请求直接绕过长休
|
|
1587
|
+
(把"闸门可取消"的正确修复和长任务保护组合出了旁路)。现在清计数移到长休**真正走完且未被取消**之后。
|
|
1588
|
+
|
|
1589
|
+
**N09(中)认证信封接受空壳与错型**
|
|
1590
|
+
|
|
1591
|
+
`{code:0}`、`{data:null}`、`{code:"401",data:null}` 原本全部被判成功 ——
|
|
1592
|
+
"校验成功"与"拿到有效身份"脱节。现在要求:业务码必须是**数值**、`data` 必须是对象、
|
|
1593
|
+
且里面要能辨认出一个用户(id 或已知名称字段)。
|
|
1594
|
+
|
|
1595
|
+
⚠️ 这次**改了既存测试的期望**(不是放松实现):`check-user-display.mjs` 里原本断言
|
|
1596
|
+
`{code:0,data:{}}` 判成功 —— 那正是 N09 指出的空壳信封。已在用例里注明原因。
|
|
1597
|
+
|
|
1598
|
+
**N10(高)外部浏览器兜底能让宿主直接退出**
|
|
1599
|
+
|
|
1600
|
+
`login.ts` 的 `openExternalLogin` 在 `spawn()` 后立刻 `return {ok:true}`,
|
|
1601
|
+
外层 try/catch **只能接同步异常**;`error` 是 EventEmitter 在下一个事件循环异步发出的,
|
|
1602
|
+
没监听就是未处理错误 → 宿主进程退出(同类问题在 `browser-login.ts` 修过,这条路径漏了)。
|
|
1603
|
+
现在等 `spawn`/`error` 之一落地再返回;并加了 `setSpawnImpl` 测试缝(与闸门的 `now`/`sleep` 同一套做法)。
|
|
1604
|
+
|
|
1605
|
+
### 测试
|
|
1606
|
+
|
|
1607
|
+
- **新增 `tests/check-round2.mjs`**(11 项,按审计编号组织):
|
|
1608
|
+
N01 五条(含"同 token 幂等""非法输入零条""超 500 条拒绝")、
|
|
1609
|
+
`newAccountId` 唯一性、N08(取消后仍要长休)、N09 两条、N10 两条(error / spawn 两个分支)。
|
|
1610
|
+
- **按审计建议修了两处"假守护"**:
|
|
1611
|
+
- `check-accounts.mjs` 的 F02 用例原本用"持有文件句柄让 rename 失败"——那是**平台假设**
|
|
1612
|
+
(Linux 上打开 r 句柄不阻止 rename,夹具无效)。改成**注入确定的 rename 失败**并断言
|
|
1613
|
+
`injected === 1`(自证真的命中了目标 rename)。
|
|
1614
|
+
- `check-accounts.mjs:233` 原本只断言 `randomUUID()` 有横线 —— 那只证明 Node API 的输出。
|
|
1615
|
+
改成 200 次 `newAccountId()` 互不相同 + 形态合法。
|
|
1616
|
+
- 反向验证 4 处全部变红;`check-round2` 共 11 项、全量 33 个测试文件全绿。
|
|
1617
|
+
|
|
1618
|
+
### 尚未处理(下一批)
|
|
1619
|
+
|
|
1620
|
+
N02(迟到 /status 把账号切回去)、N03(tool_result 跨流残片)、N04(复用槽在用保护 + 回收归属,
|
|
1621
|
+
含审计给的 T-N04 测试替换)、N05(WASM 下载失败不失效缓存)、N06(续写漏记 usage)、
|
|
1622
|
+
N07(长历史固定比例裁剪截掉系统指令);以及旧项 F06/F08/F10/F15/F16/F19/F20/F22/F23/F21、F04
|
|
1623
|
+
(审计指出 `serverId` 的生产身份链**没接通**:`login.ts` 填的是 `user.id`,没有归一化到 `serverId`)。
|
|
1624
|
+
|
|
1625
|
+
## 0.1.44 — 2026-09-13
|
|
1626
|
+
|
|
1627
|
+
### 变更:token 统计改用**服务端上报的真实值**(不再纯靠字符估算)
|
|
1628
|
+
|
|
1629
|
+
上一版抓到 `accumulated_token_usage` 但没接入,因为不知道它是「本消息」还是「会话累计」。
|
|
1630
|
+
本次用**同一会话发两回合**的对照实验把它钉死了(真实请求,已落盘):
|
|
1631
|
+
|
|
1632
|
+
| 回合 | prompt | 服务端上报 |
|
|
1633
|
+
| --- | --- | --- |
|
|
1634
|
+
| 1 | 12,424 字符 | **6446** |
|
|
1635
|
+
| 2 | 9 字符 | **38** |
|
|
1636
|
+
|
|
1637
|
+
第 2 回合只报自己的 38(和独立跑同一 prompt 的结果完全一致)→ **是「本消息」总量**
|
|
1638
|
+
(含我们发的 prompt + 回复 + 服务端自身开销),不是会话累计。
|
|
1639
|
+
|
|
1640
|
+
**改法**:`parseWebSse` 把 patch 通道里的 `accumulated_token_usage` 记下来,
|
|
1641
|
+
通过 `finish` 事件的 `totalTokens` 带给适配器;适配器优先用它上报
|
|
1642
|
+
`inputTokens = max(0, 总量 - 我们对输出的估算)`——这样**总量恰好等于服务端给的数**,
|
|
1643
|
+
底部统计不再是估算。拿不到(协议变了/老请求)才退回按字符估算。
|
|
1644
|
+
|
|
1645
|
+
⚠️ **踩到的坑**:真实形态是 `{"p":"response","o":"BATCH","v":[{"p":"accumulated_token_usage","v":38}]}`
|
|
1646
|
+
—— 它在 **response/BATCH 的内层 op** 里,第一版补丁插在顶层 case,测试立刻抓到(全 undefined)。
|
|
1647
|
+
另外**快照里的同名字段初始恒为 0**(status 还是 WIP),绝不能用它当结果;
|
|
1648
|
+
判定脚本的第一版就是取了这个 0,把结论整个判反了。
|
|
1649
|
+
|
|
1650
|
+
### 提示词上限:本机已调到 40 万字符
|
|
1651
|
+
|
|
1652
|
+
`~/.dsh/web-login/gate.json` 的 `maxPromptChars` 已从 150 万改为 **40 万**(可用设置页随时改回)。
|
|
1653
|
+
这意味着单次请求最多带约 33 万字符的历史 —— 长任务里模型会更早"忘事",
|
|
1654
|
+
但不需要的历史不再重发。**重启 DSH 后生效。**
|
|
1655
|
+
|
|
1656
|
+
### 测试
|
|
1657
|
+
|
|
1658
|
+
- `check-sse-wasm.mjs` +5:用**真实抓到的响应逐字当夹具**(含那个初始为 0 的快照),
|
|
1659
|
+
覆盖 38 / 6446 / 无字段 / 脏值 / 只有快照时必须报「拿不到」。
|
|
1660
|
+
- `check-bundle.mjs` +1(上报真实值的调用点)。
|
|
1661
|
+
- 反向验证 5 处全部变红。
|
|
1662
|
+
|
|
1663
|
+
⚠️ 反向验证第 D 条第一轮没红,两个原因叠在一起:① 我的"改坏"是 `void value` 这种空操作;
|
|
1664
|
+
② 更要紧的是**那条用例本身写的断言(notEqual 0)根本区分不了任何东西**——
|
|
1665
|
+
patch 在快照之后到达、照样覆盖成 38,把实现改坏也测不出来。已重写成
|
|
1666
|
+
「只有快照、没有 patch 时必须报 undefined」,这才是能区分的形态。
|
|
1667
|
+
|
|
1668
|
+
## 0.1.43 — 2026-09-13
|
|
1669
|
+
|
|
1670
|
+
### 新增:设置页可调 **prompt 上限**(token 体量的总阀门)
|
|
1671
|
+
|
|
1672
|
+
**为什么它比"间隔"更值得调**:网页 API 是无状态的,**每一轮都要把整段对话历史重发一遍**,
|
|
1673
|
+
所以转写越长、单次请求越贵。同一个会话里实测单次输入估算从 9.7k token 涨到 **293k**,
|
|
1674
|
+
180 次请求累计约 2900 万。间隔只影响"多久发一次",这个才影响"每次发多少"。
|
|
1675
|
+
|
|
1676
|
+
- 设置页新增滑块(**12 万 ~ 150 万字符**,步长 2 万),改动即时生效并写入 `gate.json`。
|
|
1677
|
+
- 边界取值理由:上限就取原来的默认值(再大就有撑爆 1M 上下文的风险,纯中文 150 万字符 ≈ 100 万 token);
|
|
1678
|
+
下限取更早的默认值 12 万(长期在用,说明这个量级还能干活,工具目录占约 5.6 万)。
|
|
1679
|
+
- 界面上直接给取舍:调大 → 模型不容易"忘事"、但单次更贵;调小 → 省 token、长任务会丢早期上下文。
|
|
1680
|
+
|
|
1681
|
+
### 新增:界面上的「数字说明」
|
|
1682
|
+
|
|
1683
|
+
用户问过"缓存命中 0% 正常吗"。事实是:**网页端不提供缓存信息,所以我们从没上报过这个字段**
|
|
1684
|
+
——DSH 只能显示 0,它**不代表真的没命中**。这句话现在直接写在设置页上,
|
|
1685
|
+
免得以后反复困惑(同时说明底部 token 数目前是按字符估算的)。
|
|
1686
|
+
|
|
1687
|
+
### 调查结论:网页端**是**给用量数据的(尚未接入)
|
|
1688
|
+
|
|
1689
|
+
抓了一次真实 SSE 落盘(`/api/v0/chat/completion`),结论:
|
|
1690
|
+
|
|
1691
|
+
- ✅ 有用量字段:`{"p":"response","o":"BATCH","v":[{"p":"accumulated_token_usage","v":38}, …]}`
|
|
1692
|
+
—— 一个 9 字符 prompt 的请求服务端报 **38**,而我们按字符只估出 3。目前我们**没有解析它**。
|
|
1693
|
+
- ❌ **没有任何缓存字段**(把响应里所有键名穷举了一遍)→ 「缓存命中率」在网页端拿不到,
|
|
1694
|
+
这个指标永远只能是"未提供",除非改用官方 API。
|
|
1695
|
+
- ⚠️ 未定:`accumulated_token_usage` 是**本消息**的总量还是**整个会话**的累计(样本只有一个回合,
|
|
1696
|
+
两种情况数值相同)。这决定接入时能不能直接当 inputTokens 上报,需要再花 2 个请求确认。
|
|
1697
|
+
|
|
1698
|
+
### 测试
|
|
1699
|
+
|
|
1700
|
+
- `check-request-gate.mjs` +3(默认值 / 越界夹紧 / 落盘读回),并把 3 处整对象 `deepEqual`
|
|
1701
|
+
改成**取子集比对**(`pick(..., GATE_CORE)`)——`settings()` 每加一个字段就碎一次,已踩两次。
|
|
1702
|
+
- `check-bundle.mjs` +3(host 可调、client 有旋钮、**保存后即时推给 adapter 的调用点**)。
|
|
1703
|
+
- 反向验证 5 处全部变红。
|
|
1704
|
+
|
|
1705
|
+
## 0.1.42 — 2026-09-13
|
|
1706
|
+
|
|
1707
|
+
### 修复:模型复述的**跨行**工具结果被当正文显示
|
|
1708
|
+
|
|
1709
|
+
现场(会话 `15ac4c56` 记录 `[1480]`):正文里出现
|
|
1710
|
+
|
|
1711
|
+
```
|
|
1712
|
+
Found a real gap: … Fixing that, then doing the final pass.
|
|
1713
|
+
<tool_result>Path: F:/…/package.json
|
|
1714
|
+
<path>F:/…/package.json</path>
|
|
1715
|
+
<type>file</type>
|
|
1716
|
+
<content>
|
|
1717
|
+
1: { "name": "dsh-ssh-workspace", …
|
|
1718
|
+
</content>
|
|
1719
|
+
</tool_result>
|
|
1720
|
+
```
|
|
1721
|
+
|
|
1722
|
+
即模型把 SSH 插件一次读取工具的结果**整段复述**进了回答。已有的伪标记剥离器
|
|
1723
|
+
(`stripSystemMarkers`)**逐行**处理,匹配不到跨行的闭合标签,所以漏了出去。
|
|
1724
|
+
|
|
1725
|
+
改法:新增「跨行长标记」这一遍,在逐行循环之前对全文做一次(`[\s\S]` 而不是行内匹配)。
|
|
1726
|
+
围栏区间先算好再判断命中点是否落在代码块里,与逐行那遍的保护策略保持一致。
|
|
1727
|
+
|
|
1728
|
+
⚠️ `tool_result` 比 `ds_system` 那批更「可讨论」——用户正在开发**产出它**的那个插件,
|
|
1729
|
+
正常回答里可能出现简短示例。所以要求**正文 ≥ 120 字才剥**:真实回声是整份文件
|
|
1730
|
+
(实测那处上千字),随口举例不会有那么长。
|
|
1731
|
+
|
|
1732
|
+
### 测试
|
|
1733
|
+
|
|
1734
|
+
- `check-system-markers.mjs` +5(跨行回声剥离 / 短示例保留 / 围栏内保留 /
|
|
1735
|
+
未闭合形态 / 回声后面还有正文时不能连后面一起吃掉)
|
|
1736
|
+
- `check-bundle.mjs` +1(跨行伪标记的产物断言)
|
|
1737
|
+
- 反向验证 4 处全部变红
|
|
1738
|
+
|
|
1739
|
+
⚠️ 反向验证第一轮 A 没红:上面那条"回声在末尾"的用例里,**闭合规则与未闭合规则互为备份**,
|
|
1740
|
+
把闭合规则改坏也测不出来。补一条「回声后面还有正文」的用例才真正区分开,
|
|
1741
|
+
并顺带守住一个真实风险:**别把回声后面的正文一起吃掉**。
|
|
1742
|
+
|
|
1743
|
+
## 0.1.41 — 2026-09-12
|
|
1744
|
+
|
|
1745
|
+
### 修复:DSML 标记再次漏上屏;提示词不再写出私有标记字面量
|
|
1746
|
+
|
|
1747
|
+
用户两次报同一类问题。第二次的形态与 0.1.35 修过的**不是同一个**:
|
|
1748
|
+
|
|
1749
|
+
现场(会话日志记录 `[1354]`,`assistant/message` 的 text 块):
|
|
1750
|
+
```
|
|
1751
|
+
"Continuing. … Fixing both." + \n×4 + "<|DSML|calls>" + \n + "</|DSML|invoke>" + \n + "</|DSML|calls>"
|
|
1752
|
+
```
|
|
1753
|
+
即**完整的开标签 + 两个闭合标签,中间没有任何 invoke**。
|
|
1754
|
+
|
|
1755
|
+
**两个独立成因**:
|
|
1756
|
+
|
|
1757
|
+
1. `<|DSML|calls>` 命中 `XML_STARTER_RE` → 进捕获态 → 里面没有 invoke →
|
|
1758
|
+
`looksLikeToolCallBlock` 判否 → 捕获内容被当正文透出。而那条**降级透出路径原本没剥残片**
|
|
1759
|
+
(只有 `pending` 和 `flush` 两条路径剥)。现在三条透出路径都先过 `stripStrayToolMarkup`。
|
|
1760
|
+
2. **只在流式下出现**:hold-back 判定不认「半截 DSML 前缀」。`DSML_PREFIX` 要求
|
|
1761
|
+
「竖线 + DSML + 竖线」,而流到一半的 `<|DSML` 少最后一个竖线 → 归一化认不出 →
|
|
1762
|
+
`partialMarkerSuffixLength` 给 0 → **半截标记被当正文切出去**,后面几个字符补上就成了完整标记。
|
|
1763
|
+
已加兜底:把候选里的竖线与 `dsml` 擦掉再比对 starter 前缀。
|
|
1764
|
+
|
|
1765
|
+
顺带补上 `dsml-` 连字符变体(`<dsml-calls>`)——本文件其它地方早已声明兼容该变体。
|
|
1766
|
+
|
|
1767
|
+
### 提示词卫生:不再写出私有标记的字面量
|
|
1768
|
+
|
|
1769
|
+
`TOOL_PROTOCOL_INSTRUCTIONS` 的规则 6 原文里有 `<|DSML|>` **这个字面量**,而这段文字
|
|
1770
|
+
**每次请求都会随 prompt 进到网页端会话**(用户在 chat.deepseek.com 上就能看到它)。
|
|
1771
|
+
除了暴露给用户,点名一个 token 本身也有诱发模型吐它的风险。已改为描述性表述
|
|
1772
|
+
("the private delimiter-prefixed variants some DeepSeek surfaces use"),禁令强度不变。
|
|
1773
|
+
|
|
1774
|
+
⚠️ **网页端出现的 DSML 有一部分是我们改不了的**:服务端存的是模型的原始输出,
|
|
1775
|
+
客户端过滤只影响 DSH 这一侧的显示。另外 0.1.40 起同一会话复用 20 轮,
|
|
1776
|
+
这些内容会在**同一个对话里累积**(以前每轮一个会话、很快删掉),所以更容易被看到。
|
|
1777
|
+
|
|
1778
|
+
### 测试
|
|
1779
|
+
|
|
1780
|
+
- `check-dsml-stray.mjs` +6(退化块一次喂入 / 逐字符流式 / 裸包裹标签 / 尖括号对照 /
|
|
1781
|
+
真调用不被误伤 / 提示词不含私有标记)
|
|
1782
|
+
- `check-bundle.mjs` +2(提示词卫生 + 剥 DSML 裸包裹标签)
|
|
1783
|
+
- 反向验证 4 处全部变红
|
|
1784
|
+
|
|
1785
|
+
⚠️ 反向验证第一轮 A 没红:测试样本是我自己简写的,**分片边界挪了位**,没走到 hold 那条路径。
|
|
1786
|
+
换成与会话日志**逐字一致**的样本(含反引号与弯引号)后才真的覆盖到 —— 这也是本次的教训:
|
|
1787
|
+
分片相关的用例,样本必须逐字照抄现场。
|
|
1788
|
+
|
|
1789
|
+
## 0.1.40 — 2026-09-12
|
|
1790
|
+
|
|
1791
|
+
### 变更:网页端会话改为**多轮复用**(实测判定后推翻旧结论)
|
|
1792
|
+
|
|
1793
|
+
**问题**(用户在自己浏览器里发现的):DSH 里跑一个 agent 任务,chat.deepseek.com 的对话列表
|
|
1794
|
+
会不停冒出新对话、过一会儿又被删掉。查台账确认:**2026-09-12 一天建了 182 个网页端会话**,
|
|
1795
|
+
18 点那一小时 74 个,最密 8 个/分钟 —— 因为**每个 DSH 回合 = 建一个新会话,用完再删一个**。
|
|
1796
|
+
真人不会这样建删对话,这是比"间隔太小"强得多的机器特征。
|
|
1797
|
+
|
|
1798
|
+
**旧结论错在哪**:代码注释和 0.1.21 的提交说明里写着「不能复用会话:DSH 每次都交全量历史,
|
|
1799
|
+
复用会让服务端看到两份上下文、迅速撑爆窗口」——**这是一条推理,从来没有实测过**。
|
|
1800
|
+
|
|
1801
|
+
**实测判定**(真实请求,2026-09-12,同一会话内):
|
|
1802
|
+
- 先发「记住这个编号:ZC-7391-KX。只回复 OK。」→ 模型答 `OK`
|
|
1803
|
+
- 再发「刚才我让你记住的编号是什么?」→ 模型答 `不知道`
|
|
1804
|
+
|
|
1805
|
+
→ 服务端**没有**把同会话历史带进上下文。原因是每次 completion 都发 `parent_message_id: null`,
|
|
1806
|
+
每条消息都是会话里的**根消息**、没有父链,服务端按消息树回溯上下文时回溯到空。
|
|
1807
|
+
**复用是安全的。**
|
|
1808
|
+
|
|
1809
|
+
(另:判定实验第一版差点得出反向的假结论——账号被封时接口返回 `user is muted`、正文为空,
|
|
1810
|
+
脚本把"没复述出编号"当成了"没带历史"。已加自证断言:A 步必须真的答出内容,否则整轮判无效。)
|
|
1811
|
+
|
|
1812
|
+
**改法**:新增 `sessionReuseTurns`(默认 **20**)。同一账号连续 20 个回合共用一个网页端会话;
|
|
1813
|
+
达到上限或换账号才轮换,轮换时把旧会话交给清理器回收。失败的会话立即摘出复用槽(失败即弃),
|
|
1814
|
+
"会话失效 → 换新会话透明重试"的路径保持有效。`sessionReuseTurns: 0` 可回到旧行为。
|
|
1815
|
+
|
|
1816
|
+
按今天的数据估算:建会话数从 **182/天** 降到 **约 10/天**。
|
|
1817
|
+
|
|
1818
|
+
### 测试
|
|
1819
|
+
|
|
1820
|
+
- 新增 `tests/check-session-reuse.mjs`(7 项:复用/不删/轮换回收/关闭复用/失败即弃/失效重试/跨账号隔离)
|
|
1821
|
+
- `check-bundle.mjs` +1(会话复用的产物断言)
|
|
1822
|
+
- 反向验证 5 处全部变红
|
|
1823
|
+
|
|
1824
|
+
## 0.1.39 — 2026-09-12
|
|
1825
|
+
|
|
1826
|
+
### 修复:审计中危项 F11 / F12 / F13 / F14
|
|
1827
|
+
|
|
1828
|
+
**F11 闸门不可取消(中)**
|
|
1829
|
+
`gate.acquire()` 的两处等待(排队等前序、等间隔)此前都是死等,调用方的 `AbortSignal`
|
|
1830
|
+
根本没传进去。表现是:用户点「停止」之后,请求仍可能在闸门里干等 —— 普通间隔 2~4 秒,
|
|
1831
|
+
触发长任务保护后甚至是 60~180 秒。界面停了、闸门还在倒计时。
|
|
1832
|
+
|
|
1833
|
+
现在 `acquire(label, signal)` 接受信号,两处等待都可中断;被取消时会把自己**从队列里摘掉**
|
|
1834
|
+
(`releaseMine()`),否则后续排队的请求会在队尾上永久卡住 —— 闸门直接锁死。
|
|
1835
|
+
适配器已把 `options.signal` 传下去(这是宿主原本就有、只是没接的那根线)。
|
|
1836
|
+
|
|
1837
|
+
**F12 PoW WASM 地址无限制(高)**
|
|
1838
|
+
`auth.wasmUrl` 主要来自**导入的账号备份**,可被构造成任意地址 —— 审计已复现可打内网 /
|
|
1839
|
+
云元数据(169.254.169.254)/ `file:` 协议。Electron 的 `net.fetch` 支持比 Node fetch 更宽的
|
|
1840
|
+
协议,不能把后者的协议限制当成统一边界。
|
|
1841
|
+
|
|
1842
|
+
新增 `checkedWasmUrl()`:限 https + `*.deepseek.com` + `.wasm` 路径 + 无凭据 + 无显式端口,
|
|
1843
|
+
凭证地址、默认地址、页面发现到的地址**三处都过校验**;下载加 8MB 上限再编译。
|
|
1844
|
+
|
|
1845
|
+
⚠️ 这里**没有**照搬审计的「删掉 discovery」建议:实测 `wasmUrl` 其实**不是**浏览器抓来的
|
|
1846
|
+
(`browser-login.ts` 里恒为空),默认地址又带内容哈希(`sha3_wasm_bg.7b9ca65ddd.wasm`),
|
|
1847
|
+
官方一改就失效。discovery 是唯一的兜底路径,删了就没有恢复手段。所以保留发现能力,
|
|
1848
|
+
只把「能不能用」收白名单 —— 既挡 SSRF 又留后路。
|
|
1849
|
+
|
|
1850
|
+
**F13 SSE 多行 data 丢失(中)**
|
|
1851
|
+
SSE 规范允许一个事件里出现多个 `data:` 行,收齐后用 `\n` 拼接再整体解析。旧实现逐行
|
|
1852
|
+
`JSON.parse`,一旦服务端把一个 JSON 拆到多行,每行都解析失败 → 被 `catch { continue }`
|
|
1853
|
+
**静默丢弃**,表现为「流突然断了/少了一段」且没有任何报错。现在改为攒齐再解析,
|
|
1854
|
+
事件边界(空行 / 新 event 名 / 流末尾)都会收口;末尾没有空行也不丢。
|
|
1855
|
+
|
|
1856
|
+
**F14 spawn 异步错误未监听(高)**
|
|
1857
|
+
启动登录浏览器时只 catch 了同步异常;异步失败(`ENOENT` 程序不在、`EACCES` 没权限、
|
|
1858
|
+
被安全软件拦截)会变成**未捕获异常直接带崩宿主进程** —— 而这只是「登录窗口起不来」。
|
|
1859
|
+
现在监听 `child.on('error')`,并给 300ms 让它冒头(不必让用户干等 25 秒的调试端口超时)。
|
|
1860
|
+
|
|
1861
|
+
### 测试
|
|
1862
|
+
|
|
1863
|
+
- 新增 `tests/check-sse-wasm.mjs`(F12 白名单 15 例 + F13 解析 6 例)
|
|
1864
|
+
- `check-request-gate.mjs` +3(F11:已取消 / 取消后不锁死 / 排队中取消且队列仍流动)
|
|
1865
|
+
- `check-bundle.mjs` +1(F14 产物断言;该处无注入点,暂无单元测试)
|
|
1866
|
+
- 反向验证 5 处全部变红
|
|
1867
|
+
|
|
1868
|
+
## 0.1.38 — 2026-09-12
|
|
1869
|
+
|
|
1870
|
+
### 新增:长任务保护(连续跑满阈值后强制长休)
|
|
1871
|
+
|
|
1872
|
+
起因是实测到的一个形态:2026-09-12 一个 SSH 插件开发任务,**12 分钟里发了约 70 次请求**,
|
|
1873
|
+
其中大部分是"只调工具、不说话"的轮次(会话日志里 89 个 0 正文消息中有 88 个都产出了工具调用,
|
|
1874
|
+
属正常行为)。间隔设到 2~4 秒仍全程零停顿 —— 当天该账号两次被临时限制,第二次长达 3 天。
|
|
1875
|
+
|
|
1876
|
+
根子在于:间隔只管"两次之间空多久",**管不了一刻不停跑了多久**。
|
|
1877
|
+
把间隔从 2 秒调到 8 秒,请求还是均匀铺满整条时间轴,只是稀一点 ——
|
|
1878
|
+
它造不出真人那种"跑几轮 → 停下来看结果 → 再跑"的大段空白。
|
|
1879
|
+
|
|
1880
|
+
现在闸门会数连续请求:
|
|
1881
|
+
|
|
1882
|
+
连续 DEFAULT_LONG_RUN_THRESHOLD(15) 次 → 强制长休 60~180 秒(区间随机)→ 计数归零
|
|
1883
|
+
距上次请求超过 120 秒 → 视为"歇过了",计数归零("连续"指的是不停歇)
|
|
1884
|
+
longRunThreshold = 0 → 关闭
|
|
1885
|
+
|
|
1886
|
+
长休时会打日志(`已连续 15 次请求 —— 长休 87s 再继续`),免得用户以为卡住。
|
|
1887
|
+
设置落盘在 gate.json,重启后保留;设置页的节流日志也会显示当前配置。
|
|
1888
|
+
|
|
1889
|
+
测试:check-request-gate +5(触发长休 / 阈值 0 关闭 / 歇够归零 / 长休时长随机 / configure 与越界夹取)。
|
|
1890
|
+
反向验证三处全部变红。
|
|
1891
|
+
|
|
1892
|
+
⚠️ 两件值得记的事:
|
|
1893
|
+
1. 中途有一步补丁**断言失败没写进文件**,导致 gate.ts 引用了不存在的常量 ——
|
|
1894
|
+
于是那一轮反向验证的"变红"其实是 **import 报错造成的假红**。补丁脚本必须确认写入成功,
|
|
1895
|
+
反向验证前也要先确认基线是绿的。
|
|
1896
|
+
2. `settings()` 多了字段后,既有测试里 6 处 `deepEqual(settings(), {...})` 全碎了。
|
|
1897
|
+
其中 3 处改成**逐字段断言**(只断言这条用例真正关心的字段,以后再加字段也不会碎),
|
|
1898
|
+
另外 3 处(`configure()` 的返回值)必须补全字段。
|
|
1899
|
+
|
|
1900
|
+
## 0.1.37 — 2026-09-12
|
|
1901
|
+
|
|
1902
|
+
继续采纳审计:F01 / F05 / F07。
|
|
1903
|
+
|
|
1904
|
+
### 修 F01:账号 id 未校验,可路径穿越
|
|
1905
|
+
|
|
1906
|
+
`accountFilePath(id)` 直接 `join(accountsDir(), `${id}.json`)`,**没有任何校验**;
|
|
1907
|
+
而 `importAccounts` 会把**备份文件里的 id 原样当主键**,HTTP 路由也直接吃调用方传的 id。
|
|
1908
|
+
于是 id 写成 `../../../../Users/me/evil` 就能越界读、写、删账号目录之外的 JSON。
|
|
1909
|
+
|
|
1910
|
+
新增 `assertSafeAccountId()`:只挡"能跑出目录"的字符(路径分隔符、`..`、空、`\0`),
|
|
1911
|
+
**不强求格式** —— 历史 id 形态不止一种,按白名单收紧会误伤老账号。
|
|
1912
|
+
另外 `activeAccountId()` 加了 try/catch:索引文件若被外部改坏,当"未选择"而不是让异常炸穿。
|
|
1913
|
+
|
|
1914
|
+
### 修 F05:异步期间切号 → 限制/台账记到错误的账号
|
|
1915
|
+
|
|
1916
|
+
`recordCallOutcome` 在上报时**现取** `activeAccountId()`,而一次流式调用可能飞几十秒,
|
|
1917
|
+
期间用户完全可能切号 —— 结果是「被限制的号反而清白,正在用的号背了别人的处罚」,
|
|
1918
|
+
台账与设置页的「限制还剩多久」全都指错了人。
|
|
1919
|
+
|
|
1920
|
+
改为:适配器在**起飞前**(拿到闸门许可之后、开流之前)调一次 `currentAccountId()`,
|
|
1921
|
+
结果随 `noteCall` 回传;宿主优先用它,只在拿不到时才回退到"此刻"的当前账号。
|
|
1922
|
+
`currentAccountId` 是可选依赖,不注入也能工作(回退到旧行为)。
|
|
1923
|
+
|
|
1924
|
+
### 修 F07:批量清理会拿 A 的凭证去删 B 的会话
|
|
1925
|
+
|
|
1926
|
+
批量删除只发**一个** Authorization 头(`buildDsHeaders(batch[0].auth)`),
|
|
1927
|
+
却删除了**整批**的 sessionId。若一批里混了不同账号的会话:
|
|
1928
|
+
轻则整批被服务端拒绝;重则 `resp.ok` 时被当成全部成功 ——
|
|
1929
|
+
旧代码在 ok 时直接 `return`,不逐个校验每个 id 是否真被删掉。
|
|
1930
|
+
|
|
1931
|
+
改为:只有 `batch` 内 token 全部相同时才走批量,混号则退化为逐个删(逐个删用的是各自的 auth)。
|
|
1932
|
+
实测:混号 2 个 → 2 次逐个删;同号 3 个 → 仍合并成 1 个请求。
|
|
1933
|
+
|
|
1934
|
+
测试:check-accounts +2(F01)、新建 check-call-attribution 4 项(F05)、
|
|
1935
|
+
check-session-cleaner +2(F07)、check-bundle +3。反向验证三处全部变红。
|
|
1936
|
+
|
|
1937
|
+
⚠️ 测 F07 时的一个坑:逐个删是**异步串行**的,`fakeTimers.runAll()` 之后还要再
|
|
1938
|
+
让出一次事件循环(`settle()`)才能断言,否则会看到"只发了 1 次请求"的假象。
|
|
1939
|
+
## 0.1.36 — 2026-09-12
|
|
1940
|
+
|
|
1941
|
+
采纳用户提供的第三方审计(GPT6,23 项)里的三条,都是小而明确、证据齐的。
|
|
1942
|
+
|
|
1943
|
+
### 修 F09:认证响应未校验形状,HTML 200 被当成验证成功
|
|
1944
|
+
|
|
1945
|
+
`validateAuth` 里 `resp.json()` 抛错时把 json 置为 undefined,而 `envelopeError(undefined)`
|
|
1946
|
+
返回 undefined,于是径直走到 `ok: true` 并返回一个**空壳的 user({})**。
|
|
1947
|
+
也就是说:反爬页 / WAF 拦截页 / 空响应 —— 它们同样是 HTTP 200 —— 会被判为"验证通过"。
|
|
1948
|
+
|
|
1949
|
+
后果很实际:探活显示"通过"、账号看起来正常,0.1.31 加的「需要重新登录」按钮**永远不会触发**;
|
|
1950
|
+
什么都没确认到,却说成功。只读零额度请求偶发失败的代价只是一次重试,远比"误报成功"划算。
|
|
1951
|
+
|
|
1952
|
+
抽出纯函数 `classifyAuthEnvelope(json)`(validateAuth 要发网络请求,测不了这条分支):
|
|
1953
|
+
非对象 / 数组 / 业务错误码 / 既无 data 也无 code → 一律判失败。
|
|
1954
|
+
|
|
1955
|
+
### 修 F02:写凭证先删目标文件 —— 失败即丢凭证
|
|
1956
|
+
|
|
1957
|
+
`writeJsonAtomic` 旧写法是「先 `rmSync(file)` 再 `renameSync(tmp, file)`」。
|
|
1958
|
+
一旦 rename 失败(磁盘满 / 占用 / 权限),**原文件已经没了**,凭证直接丢失。
|
|
1959
|
+
实测 win32 + Node 22:`fs.renameSync` **本来就能直接覆盖已存在的目标文件**,
|
|
1960
|
+
所以那句 rmSync 既没必要、也是唯一的丢数据风险点。
|
|
1961
|
+
另外临时文件改为创建时就是 0600(旧写法先按默认权限落地、事后再 chmod,中间有暴露窗口)。
|
|
1962
|
+
|
|
1963
|
+
### 修 F18:head 超预算时被从中间挖空(残缺的 JSON Schema)
|
|
1964
|
+
|
|
1965
|
+
旧做法:先把工具目录渲染到 5.6 万字符,再发现 head 超过 `maxChars × 比例`,
|
|
1966
|
+
于是 `truncateMiddle` 从**中间**挖掉一块 —— 留下残缺的工具定义,
|
|
1967
|
+
模型会照着半截定义猜参数,比"干脆不列这个工具"更糟;而且 maxChars 越小时越容易触发。
|
|
1968
|
+
(0.1.33 只把比例从 0.45 提到 0.62,属于"把坑挪远",没解决根因。)
|
|
1969
|
+
|
|
1970
|
+
改为**反过来算**:先扣掉 system 与协议指令的固定开销,剩下的才是工具目录能用的额度,
|
|
1971
|
+
装不下就走 buildToolSection 自己的兜底(列出被省略的工具名),**head 永远完整**。
|
|
1972
|
+
实测:maxChars=2 万时 head 12,525 字符、无截断;maxChars=12 万时 61 个工具全在。
|
|
1973
|
+
|
|
1974
|
+
测试:check-user-display +4(F09)、check-accounts +2(F02)、check-tools-section +2(F18)。
|
|
1975
|
+
反向验证三处全部变红。
|
|
1976
|
+
|
|
1977
|
+
⚠️ 反向验证里 F18 **第一轮没红**,原因值得记:我把新测试插在了测试文件末尾
|
|
1978
|
+
`if (failures.length) { … process.exit(1) }` **之后**,于是失败被前面的报告块"吞掉",
|
|
1979
|
+
退出码仍是 0。**新测试必须插在报告/退出块之前**,否则测了也白测。
|
|
1980
|
+
|
|
1981
|
+
## 0.1.35 — 2026-09-12
|
|
1982
|
+
|
|
1983
|
+
### 修:工具调用标记的残片上屏(`voke> </|DSML|calls>`)
|
|
1984
|
+
|
|
1985
|
+
用户在使用中发现正文里冒出 `voke> </ | DSML | calls>` 这样的残留,手动停止了生成。
|
|
1986
|
+
会话日志里对应的是 `assistant/message` 的 **text 块**(会显示给用户的那块):
|
|
1987
|
+
|
|
1988
|
+
"I'll finish the install: … variable.\n\n\n\nvoke>\n</ calls>"
|
|
1989
|
+
|
|
1990
|
+
而同一条消息里的 **3 个工具调用全部解析成功** —— 也就是说:**调用抓到了,标记的残片却当正文吐了出去**。
|
|
1991
|
+
|
|
1992
|
+
模型经常**把包裹开始标签写丢、只留闭合标签**(`</|DSML|calls>`),或者只留下标签的后半截
|
|
1993
|
+
(`voke>` 是 `invoke>` 掉了头)。这些残片绕过了捕获逻辑(识别器只认 `<…invoke` / `<…calls>`
|
|
1994
|
+
这类**开始**形态),落进正文缓冲。
|
|
1995
|
+
|
|
1996
|
+
离线复现确认了**两条独立成因**,都修:
|
|
1997
|
+
|
|
1998
|
+
1. `findXmlToolCallEnd` 吞完裸 `invoke` 块后,遇到后面的孤立闭合标签**停下返回**,
|
|
1999
|
+
把它留在缓冲里 → 现在会把它一并吞进块内(收不全则继续等,由 `flush()` 兜底)。
|
|
2000
|
+
2. **`flush()` 把 hold 住的尾巴无条件吐出**。残片通常很短(`</|DSML|calls>` 只有 16 字符),
|
|
2001
|
+
小于 `HOLD_BACK_CHARS`(24) 就会被一直 hold 到流结束,然后原样上屏 → 现在吐出前先清理。
|
|
2002
|
+
|
|
2003
|
+
新增 `stripStrayToolMarkup()`(导出,便于单测),三道规则从明确到宽松:
|
|
2004
|
+
带 DSML 前缀的孤立闭合标签 / 前缀被吃光的 `</ calls>` / 只剩后半截的 `voke>`。
|
|
2005
|
+
主循环透传前与 `flush()` 各调一次 —— **两处互为备份**:反向验证里单独取消任一处都不会变红,
|
|
2006
|
+
同时取消才复现泄漏(已用这一点确认清理真的在起作用,而不是假绿)。
|
|
2007
|
+
|
|
2008
|
+
⚠️ 一个**刻意的取舍**(已写进测试注释):正文里孤立出现的 `</calls>` 也会被剥。
|
|
2009
|
+
它与残片形态完全一致、无法可靠区分;真实回答里几乎不会出现,而残片泄漏是实打实的 bug。
|
|
2010
|
+
|
|
2011
|
+
测试:新增 `check-dsml-stray` 12 项(现场复现 / 逐字符喂 / 退化 `</ calls>` / 截图的 `voke>` /
|
|
2012
|
+
`findXmlToolCallEnd` 的吞并 / 带 wrapper 的不回归 / **不误伤**普通英文里的 invoke 与正文里的 `<invoke>` /
|
|
2013
|
+
刻意的取舍 / 纯正文透传 / JSON 路径不受影响)。
|
|
2014
|
+
反向验证:C、D、E 三处变红(取消吞并 / 清理失效 / 两处同时取消),A、B 单独取消不变红是**预期**
|
|
2015
|
+
(互为备份),已用 E 确认。
|
|
2016
|
+
|
|
2017
|
+
## 0.1.34 — 2026-09-12
|
|
2018
|
+
|
|
2019
|
+
### 修:设置页保存的间隔上限,每次重启都丢(随机区间变固定间隔)
|
|
2020
|
+
|
|
2021
|
+
用户重启 DSH 后继续用,日志里打出:
|
|
2022
|
+
|
|
2023
|
+
deepseek-web: 距上次请求不足 2000ms(区间 2000~2000),等 908ms 再发「chat」
|
|
2024
|
+
|
|
2025
|
+
而 `gate.json` 里明明是 `2000~4000`。查下去是宿主启动时**漏传了 max**:
|
|
2026
|
+
|
|
2027
|
+
// src/index.ts(修复前)
|
|
2028
|
+
const gate = createRequestGate({
|
|
2029
|
+
minIntervalMs: savedGate?.minRequestIntervalMs ?? config.minRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS,
|
|
2030
|
+
// ← 没有 maxIntervalMs
|
|
2031
|
+
})
|
|
2032
|
+
|
|
2033
|
+
而 `createRequestGate` 的既有语义是「只收到 min 时,max 跟随 min」(为了兼容老配置里
|
|
2034
|
+
只写一个值 = 固定间隔)。于是**设置页保存的 2000~4000 随机区间,每次重启都变成固定 2000ms**,
|
|
2035
|
+
直到用户再去设置页动一下才恢复。
|
|
2036
|
+
|
|
2037
|
+
固定间隔正是最典型的机器特征 —— 用户以为自己开着随机区间,实际每次重启后都在用固定值跑,
|
|
2038
|
+
而且从界面上看不出来("已生效"读的是 `gate.settings()`,它显示的就是退化后的值)。
|
|
2039
|
+
|
|
2040
|
+
三处漏传一起补:启动闸门 / `adapterConfig` / 状态回传。
|
|
2041
|
+
(对照:0.1.30 加的三个**清理区间**在启动时是正确的,只有间隔这一对漏了。)
|
|
2042
|
+
|
|
2043
|
+
测试:`check-request-gate` 加 2 条 —— 一条**记录语义**(只传 min 时 max 跟随 min,
|
|
2044
|
+
不是默认 4000),一条守「保存 2000~4000 后重启仍是 2000~4000」。
|
|
2045
|
+
`check-bundle` 加 3 条产物断言。
|
|
2046
|
+
|
|
2047
|
+
⚠️ 反向验证又是**第一版没红**:我最初写的是 `/maxIntervalMs:/`,
|
|
2048
|
+
但 gate.ts 内部也有 `options.maxIntervalMs ?? …`,被一起打进产物,所以宿主漏传照样通过。
|
|
2049
|
+
改成匹配**调用点**(`/maxIntervalMs:\s*\w+\?\.maxRequestIntervalMs/`)才真的守住。
|
|
2050
|
+
→ **产物断言要精确到调用点,别只匹配字段名。**
|
|
2051
|
+
|
|
2052
|
+
## 0.1.33 — 2026-09-12
|
|
2053
|
+
|
|
2054
|
+
### 修:DSH 的 61 个工具里,有 26 个从没告诉过模型
|
|
2055
|
+
|
|
2056
|
+
DSH 下发给插件的 `tools` 数组共 **61 个工具**,而我们把它写进 prompt 时有两道砍:
|
|
2057
|
+
|
|
2058
|
+
- `MAX_TOOLS_SECTION_CHARS = 24_000` → 只装下 **35 个**,剩下 **26 个完全没告知模型**。
|
|
2059
|
+
⚠️ 截断是**按字母序**发生的(工具按名排序),所以被砍的是
|
|
2060
|
+
`write`(w)、`web_search`、`web_fetch`、`subagent`、`subagent_fork`、`todo_write`、`skill`、
|
|
2061
|
+
`read_image`、全部 `ssh_*`/`sftp_*`、`tunnel_start/stop`、`update_goal`;
|
|
2062
|
+
而极少用的 `db_tx_rollback`、`db_list_connections`、`job_list` 反而留下。
|
|
2063
|
+
**`write` 恰恰是 `~/.dsh/deepseek-web/rejected.jsonl` 里失败最多的工具**
|
|
2064
|
+
(27,616 字符 unbalanced、13,480 字符 echo)。模型看不到它的参数定义,只能猜。
|
|
2065
|
+
- `MAX_DESCRIPTION_CHARS = 400` → **17 个工具的描述被砍**,`pwsh` 3010→400(丢 87%)、
|
|
2066
|
+
`workflow` 2500→400(丢 84%)。**丢掉的正是「遇错该怎么办」的指引**:沙箱拒绝不是命令的 bug
|
|
2067
|
+
(别换方式重试)、命名管道不可用时 `stdio:'pipe'` 的 spawn 会报 EPERM、只读沙箱下
|
|
2068
|
+
.NET 静态调用/Add-Type/COM/反射会失败;`workflow` 丢的是 `agent()`/`pipeline()`/`parallel()` 的钩子签名。
|
|
2069
|
+
|
|
2070
|
+
改三处(**必须联动,单改一处会更糟**):
|
|
2071
|
+
|
|
2072
|
+
MAX_DESCRIPTION_CHARS 400 → 3_200 (长描述基本不再砍)
|
|
2073
|
+
MAX_TOOLS_SECTION_CHARS 24_000 → 56_000 (实测需 50,942,留约一成余量)
|
|
2074
|
+
head 占比 0.45 → 0.62 (见下)
|
|
2075
|
+
|
|
2076
|
+
⚠️ **第二道闸**:`serializePrompt` 里 `headBudget = maxChars * 0.45 = 54,000`。
|
|
2077
|
+
只把工具段预算调大会让 head(system + 协议 + 工具段 ≈ 63.5k)超过它,
|
|
2078
|
+
被 `truncateMiddle` **从中间挖空** —— 比原来的尾部省略更糟,留下的是残缺的 JSON Schema。
|
|
2079
|
+
所以 head 占比同时提到 0.62(转写仍余约 5.6 万字符,历史可截,工具定义不可截)。
|
|
2080
|
+
|
|
2081
|
+
兜底改为**不再静默**:真超预算时列出被省略的工具名,并要求模型"别猜参数,向用户确认"。
|
|
2082
|
+
|
|
2083
|
+
验证(用 DSH 真实下发的 61 个工具离线跑):工具段 51,022 字符、**缺失 0 个**、
|
|
2084
|
+
17 个长描述全部完整保留、prompt 合计 63,771 字符且**没有触发中段截断**。
|
|
2085
|
+
代价:prompt 头部从约 3.7 万字符涨到约 6.3 万字符;工具段在最前且固定,前缀缓存友好。
|
|
2086
|
+
|
|
2087
|
+
测试:新增 `check-tools-section` 14 项(这个模块此前**零覆盖**,所以这个 bug 一直没被发现)。
|
|
2088
|
+
反向验证四处全部变红:描述上限改回 400 / 预算改回 24_000 / 兜底改回静默 / head 占比改回 0.45。
|
|
2089
|
+
⚠️ 第一轮反向验证里 D **没变红** —— 因为那条用例的 `merged` 根本没超过 `maxChars`,
|
|
2090
|
+
压根没走到截断分支。已改为把转写撑长,并反转断言(必须出现 `chars omitted`,
|
|
2091
|
+
以证明"真的走到了那段逻辑",否则就是无效用例)。
|
|
2092
|
+
|
|
2093
|
+
## 0.1.32 — 2026-09-12
|
|
2094
|
+
|
|
2095
|
+
### 修:模型自造的伪标记 `<ide_result_status>` 漏上了屏
|
|
2096
|
+
|
|
2097
|
+
用户在会话里看到正文中间夹着一段:
|
|
2098
|
+
|
|
2099
|
+
Core backend written. Now I need to verify the module-resolution question…
|
|
2100
|
+
<ide_result_status>Tool ran without output or errors</ide_result_status>
|
|
2101
|
+
|
|
2102
|
+
**先穷搜确认它不来自任何一方**(都是原始字节搜索,不是 grep):
|
|
2103
|
+
|
|
2104
|
+
| 搜哪儿 | 结果 |
|
|
2105
|
+
| --- | --- |
|
|
2106
|
+
| DSH 的 `app.asar`(168 MB) | **0 处** |
|
|
2107
|
+
| 全部已装插件(`node_modules`) | **0 处** |
|
|
2108
|
+
| `~/.dsh` 全树 | **0 处** |
|
|
2109
|
+
| 连那句话本身(`Tool ran without output`) | **0 处** |
|
|
2110
|
+
| 会话日志(757 条记录)里的位置 | **只在 assistant 的输出字段**;212 条 `tool/result`、用户消息、系统消息里一处都没有 |
|
|
2111
|
+
|
|
2112
|
+
→ 结论:**这是模型自己编的**(模仿它见过的协议格式),与 0.1.11 处理过的
|
|
2113
|
+
`<ds_system>Tool result for call_1a2b3c</ds_system>` 是同一类。所以修法就是把它加进剥离清单。
|
|
2114
|
+
|
|
2115
|
+
顺带把这一层从「硬编码两个正则」改成**数据驱动的标签清单**:
|
|
2116
|
+
|
|
2117
|
+
const IMITATED_MARKER_TAGS = ['ds_system', 'system', 'ide_result_status'] as const
|
|
2118
|
+
|
|
2119
|
+
- 闭合形态用**反向引用**(`<(tag…)>…</\1>`)要求首尾同名,避免 `<a>…</b>` 错配被连内容吃掉。
|
|
2120
|
+
- 刻意**不做通配**(`<[a-z_]+>`):用户的正常回答可能就在讨论这些标签,通配会把它们一起吃掉。
|
|
2121
|
+
**清单只收有现场证据的名字**,每加一个都要有现场 + 穷搜证据。
|
|
2122
|
+
- 快速退出改为 `hasImitatedMarker()`,语义与原来的 `includes` 串联一致。
|
|
2123
|
+
|
|
2124
|
+
### 顺带记下一个 10 分钟的教训(方法层面)
|
|
2125
|
+
|
|
2126
|
+
排查过程中我一度"复现"出缺字(正文少了开头的 `<tool_call`),**但那是我探针脚本自己的 bug**:
|
|
2127
|
+
我手写了一遍 SSE 解析逻辑去"复现",结果复现的是我写的 bug。
|
|
2128
|
+
|
|
2129
|
+
**正确做法**:`createSseState()` 本来就是导出的,直接拿**插件自己的解析器**离线回放抓到的原始帧。
|
|
2130
|
+
用它回放 56 帧真实数据的结果是:正文 114 字完整、思考 14 字、**分歧 0 次** —— 解析完全正确。
|
|
2131
|
+
→ 教训:**验证"是不是适配器的 bug"时,必须用适配器自己的代码,不能另写一份逻辑去模拟。**
|
|
2132
|
+
|
|
2133
|
+
### 测试
|
|
2134
|
+
|
|
2135
|
+
- `check-system-markers` 从 7 项扩到 **13 项**:新增现场 B(剥掉 + 保留上下文文字)、夹在正文中间、
|
|
2136
|
+
半截截断、**清单外标签 `<tool_call>` 必须原样通过**(剥了会把真工具调用吃掉)、
|
|
2137
|
+
围栏内保留、三种标记混排。
|
|
2138
|
+
- **反向验证三处,全部变红 ✅**:去掉 `ide_result_status`/把真协议 `tool_call` 也加进清单/
|
|
2139
|
+
让快速退出永远跳过剥离。
|
|
2140
|
+
⚠️ 第一轮我把"反向验证"写错了方向(把 `if (!hasImitatedMarker)` 改成 `if (false)` ——
|
|
2141
|
+
那只是关掉快路径、**行为不变**,测试绿是对的)。改成 `if (hasImitatedMarker(text)) return`
|
|
2142
|
+
才真正模拟"永远不剥",立刻变红。**反向验证本身也要核对方向。**
|
|
2143
|
+
- `check-bundle` 加 3 条产物断言(清单是数据驱动的 / 含新标记 / 老的 `ds_system` 不回退)。
|
|
2144
|
+
|
|
2145
|
+
## 0.1.31 — 2026-09-12
|
|
2146
|
+
|
|
2147
|
+
### 把"登录态还能撑多久"从完全不可观察变成至少能看一半
|
|
2148
|
+
|
|
2149
|
+
起因:讨论"账号长期不用会不会掉、要不要加心跳"。查证后**不加心跳**(实测没有任何东西可以刷新:
|
|
2150
|
+
只读端点全部 `set-cookie: 0`、token 也不轮换),改为做两件**不需要多发任何请求**的事。
|
|
2151
|
+
|
|
2152
|
+
**① 捕获时记下 cookie 的过期构成**
|
|
2153
|
+
|
|
2154
|
+
新增 `src/cookies.ts`(纯函数,21 项单测)。两条捕获路径的形状不同,都认:
|
|
2155
|
+
|
|
2156
|
+
| 来源 | 字段 | 会话级 |
|
|
2157
|
+
| --- | --- | --- |
|
|
2158
|
+
| CDP `Storage.getCookies`(真实 Edge/Chrome) | `expires`(秒) | `-1` |
|
|
2159
|
+
| Electron `session.cookies.get()`(插件自开窗口) | `expirationDate`(秒) | 字段缺失 |
|
|
2160
|
+
|
|
2161
|
+
- `0` / 非数 / 负值**一律当会话级** —— `0` 会被算成 1970 年,界面显示"已过期"会让人以为号坏了。
|
|
2162
|
+
- 界面上写成 `5 项 · 1 会话级 · 4 持久级 · smidV2 还剩 399 天`。
|
|
2163
|
+
- ⚠️ 文案里明确写了它**不是登录态寿命**:实测真正鉴权的是 `token`(只发 token 不带 cookie 能通过,
|
|
2164
|
+
只发 cookie 不带 token 直接被拒 `40002 Missing Token`),所以这只是浏览器侧的上界。
|
|
2165
|
+
- 没有记录时 `summarizeCookieLife` 返回 `undefined` 而**不是全 0 对象** ——
|
|
2166
|
+
界面必须区分"没记录"(老记录 / 手动粘 token)和"记到了、全是会话级",两者文案完全不同。
|
|
2167
|
+
|
|
2168
|
+
**② 探活失败从"报状态"改成"给动作"**
|
|
2169
|
+
|
|
2170
|
+
- 徽章 `校验失败` → **`需要重新登录`**(原来只写失败,用户不知道要干嘛、也看不出这号还能不能用)。
|
|
2171
|
+
- 那一行直接出**「重新登录这个账号」**按钮,并写明失败原因与时间。
|
|
2172
|
+
- 新增 `POST /login/relogin`:与 `/login/add` 的**唯一区别是不清浏览器登录态**。
|
|
2173
|
+
加新号必须先清干净(否则窗口一打开就是旧账号、抓回来还是它);修同一个号正相反 ——
|
|
2174
|
+
留着才可能一打开就复用上,一个密码都不用敲。真掉了也没关系,窗口里重新登录一次即可。
|
|
2175
|
+
- 落库仍走**添加模式**(只入库、不切换):修好它但不顶掉你正在用的账号;若它本来就是当前账号,
|
|
2176
|
+
当前账号不变、只是凭证被换成新的 —— 这正是期望行为。
|
|
2177
|
+
|
|
2178
|
+
### 测试
|
|
2179
|
+
|
|
2180
|
+
- 新增 `check-cookie-meta` **21 项**:两种来源形状 / `session: true` 优先 / `0` 与非数当会话级 /
|
|
2181
|
+
过滤与 `buildCookieHeader` **产物逐个对齐**(两边过滤条件不许漂移)/ 读写磁盘时的规整 /
|
|
2182
|
+
"没记录"与"全会话级"的区别 / 剩余时间的天·小时·已过期。
|
|
2183
|
+
- **反向验证四处,全部变红 ✅**:只认 `expires`(丢 Electron 那条路径)/ 把 `0` 当有效时间 /
|
|
2184
|
+
没记录时返回全 0 对象 / `pickCookieMeta` 忽略过滤条件。
|
|
2185
|
+
- `check-bundle` 加 **9 条产物断言**(含"失败徽章必须是行动指令""必须说明不是登录态寿命")。
|
|
2186
|
+
|
|
2187
|
+
## 0.1.30 — 2026-09-12
|
|
2188
|
+
|
|
2189
|
+
### 会话清理:三个参数改成「上下限 + 随机」(防"删除时机有固定规律")
|
|
2190
|
+
|
|
2191
|
+
用户提的:会话清理也该有滑块 —— 攒够几个删、几秒后删、删多快(间隔),并且上下限都要能调、
|
|
2192
|
+
取值要随机(一下子删一大批会出问题)。
|
|
2193
|
+
|
|
2194
|
+
原来是三个**死值**:正好攒到第 8 个动手、正好等 90 秒、批量删被拒后逐个删**连发不等待**。
|
|
2195
|
+
固定值方差≈0 本身就是最明显的机器特征。现在:
|
|
2196
|
+
|
|
2197
|
+
| 参数 | 默认区间 | 谁在重抽 |
|
|
2198
|
+
|---|---|---|
|
|
2199
|
+
| `cleanupBatch` 攒够几个 | `6~10` 个 | **每轮清理**重抽 |
|
|
2200
|
+
| `cleanupDelayMs` 最长等多久 | `60~120` 秒 | **每轮清理**重抽 |
|
|
2201
|
+
| `cleanupGapMs` 删除间隔 | `0.8~2.5` 秒 | **每删一个**重抽 |
|
|
2202
|
+
|
|
2203
|
+
- 默认区间的**均值刻意落在原来的固定值上**(8 / 90s / 1.65s),所以行为不会有突然的跳变。
|
|
2204
|
+
- 三个区间都**复用 `gate.ts` 的常量**(单一事实来源),不在 webapi 里重复定义一遍默认值。
|
|
2205
|
+
- 边界收敛走 `normalizeCleanupRange`:非数忽略、按 bounds 夹住、**上下限颠倒时自动交换**。
|
|
2206
|
+
- 区间只在 `deferred` 模式显示与生效(`immediate` 是"老行为":固定 1.5s / 每次一个;`keep` 不清理)。
|
|
2207
|
+
|
|
2208
|
+
### 「删除间隔」是这次最要紧的一个
|
|
2209
|
+
|
|
2210
|
+
批量删是首选(N 个会话 1 个请求),但服务端**不接受批量删时会永久退化为逐个删** ——
|
|
2211
|
+
那时如果连发,几十个删除请求会瞬间打过去,比"攒批"本身更像脚本。所以:
|
|
2212
|
+
相邻两个删除请求之间按 `cleanupGapMs` 停一下,上限设 0 = 不等待(老行为)。
|
|
2213
|
+
顺便把**批量删与逐个删都串行化**:上一轮没删完时,下一次 flush 只会排队,不会插进来并发发请求。
|
|
2214
|
+
|
|
2215
|
+
⚠️ 兼容性:**没给区间**(老调用方 / 老配置 / `immediate` 模式)→ 间隔为 0、不加额外等待,
|
|
2216
|
+
老行为一字不变。这个约束是现有测试逼出来的(`check-session-cleaner` 的旧用例传死值、不传区间,
|
|
2217
|
+
我第一版给它们加上了间隔 → 两处失败 → 改成"没给区间就不加间隔")。
|
|
2218
|
+
|
|
2219
|
+
### 界面
|
|
2220
|
+
|
|
2221
|
+
「防风控」页在会话清理模式下面多三行,每行是一对上下限滑块 + 右侧合成值(如 `6~10 个`):
|
|
2222
|
+
拖动中只更新数字、松手才提交(与既有的间隔滑块一致);只在「延迟」模式显示。
|
|
2223
|
+
两条说明分别解释"取值随机"和"删除间隔只作用于逐个删"。
|
|
2224
|
+
|
|
2225
|
+
### 测试
|
|
2226
|
+
|
|
2227
|
+
- 新增 `check-cleanup-ranges` 12 项:区间重抽 / 上下限相等=固定值 / 批量删被拒后退化为逐个删**且中间确实会等**
|
|
2228
|
+
(不是连发)/ 串行化(上一轮没删完时下一次 flush 不插进来)/ 单批超上限会拆成多次 / 间隔为 0 时退回老行为。
|
|
2229
|
+
- **反向验证抓到一处真缺口**:第一版测试用 `t.drain()`(把延迟清理的定时器也一起触发),
|
|
2230
|
+
于是"第二轮批次=4"是**碰巧成立**的,"阈值被重抽"根本没被验证到 —— 反向验证(把重抽去掉)
|
|
2231
|
+
没有变红才发现。改成每入队一步都只等异步链跑完(不触发定时器)、并断言**批次大小的确切序列**
|
|
2232
|
+
`[2] → [2, 4]`;重做反向验证三处(不重抽 / 间隔恒 0 / 去掉串行化)**全部变红 ✅**。
|
|
2233
|
+
教训:**"没变红"不等于"补丁没生效",可能是测试用错了方式** —— 必须查到底。
|
|
2234
|
+
- `check-bundle` 加 6 条产物断言(三个区间能落盘 / 走 normalizeCleanupRange / 三滑块存在 /
|
|
2235
|
+
初始隐藏 / 两条说明文案)。
|
|
2236
|
+
|
|
2237
|
+
## 0.1.29 — 2026-09-12
|
|
2238
|
+
|
|
2239
|
+
### 修:账号名显示成一串 id(两个叠在一起的取值 bug)
|
|
2240
|
+
|
|
2241
|
+
用户实测反馈:手机号注册的账号在账号库里显示成 `9d6***13`(其实是被掩码的**用户 id**),
|
|
2242
|
+
刚「登录新账号」加进来的那个更是直接显示内部 id `acc_cd8e05ec`。
|
|
2243
|
+
|
|
2244
|
+
接口 `GET /api/v0/users/current` 其实**返回了手机号**:
|
|
2245
|
+
|
|
2246
|
+
{ id, token, email: "", mobile_number: "183******78", area_code: "+86", chat: { is_muted, mute_until } }
|
|
2247
|
+
|
|
2248
|
+
是我们的取值代码有两个 bug,而且它们叠在一起、单看任一个都"像是好的":
|
|
2249
|
+
|
|
2250
|
+
1. **用 `??` 串回退链,而 `??` 不跳过空字符串。** 这个账号没设邮箱,接口给的是 `email: ""`,
|
|
2251
|
+
于是 `"" ?? mobile ?? …` 的结果就是 `""` —— 整条链当场被挡住,`display` 永远是空。
|
|
2252
|
+
必须改成**按"有内容"取**(跳过 undefined / null / 空白)。新增 `pickUserDisplay()`。
|
|
2253
|
+
2. **字段名找错了**:手机号是 `mobile_number`,我们找的是 `mobile`。
|
|
2254
|
+
所以即使修好第 1 点,还是拿不到。
|
|
2255
|
+
|
|
2256
|
+
顺带修的两处:
|
|
2257
|
+
|
|
2258
|
+
- **捕获时就把身份写回记录。** `/login/browser` 本来就会做一次零额度的只读校验,
|
|
2259
|
+
顺手把结果存下来即可。否则新加的账号要等下一次探活(最长 30 分钟)才有名字 ——
|
|
2260
|
+
用户加完账号一刷新,看到的就是那串 hex。
|
|
2261
|
+
- **探活成功时也写回身份**,库里闲置的账号会自己长出名字。
|
|
2262
|
+
用"旧值打底 + 新值覆盖"合并:新一次只带回 id、没带回 display 时,
|
|
2263
|
+
不会把已经拿到的好名字冲掉。
|
|
2264
|
+
|
|
2265
|
+
兜底也改了:**一个名字都拿不到时,不再把内部 id(`acc_cd8e05ec`)当名字摆出来**
|
|
2266
|
+
(用户看到一串 hex 只会以为是 bug),改成 `未识别账号(cd8e05ec)`,留一小截后缀,
|
|
2267
|
+
多个未识别账号之间仍然能区分。
|
|
2268
|
+
|
|
2269
|
+
⚠️ 顺带更正一处**我们之前写错的结论**:0.1.26 的注释写"受限期间 `users/current` 依然 200,
|
|
2270
|
+
所以探活探不出限制"。返回 200 是对的,但**响应体里就带着 `chat: { is_muted, mute_until }`**
|
|
2271
|
+
—— 也就是说探活其实探得出来,只是还没接上。三处注释已更正(probe.ts / accounts.ts / index.ts)。
|
|
2272
|
+
接上之后就不必"等一次失败的生成"才能显示倒计时(留给 0.1.30)。
|
|
2273
|
+
|
|
2274
|
+
测试:新增 `check-user-display` 14 项(空串必须继续往后找 / 空白跳过 / 实测响应形状 /
|
|
2275
|
+
优先级顺序 / 非字符串值 / 全空返回空串 / title 的三档回退与「未识别」)。
|
|
2276
|
+
反向验证:把 `mobile_number` 从候选里删掉 → 5 项立刻变红。
|
|
2277
|
+
⚠️ 另一轮反向验证(把**调用点**改回 `??` 链)**没有变红** —— 因为测试直接测函数、绕过了调用点,
|
|
2278
|
+
这是个真缺口;已用产物断言补上(产物里必须出现 `pickUserDisplay` / `mobile_number` / `未识别账号`)。
|
|
2279
|
+
|
|
2280
|
+
## 0.1.28 — 2026-09-12
|
|
2281
|
+
|
|
2282
|
+
### 修:账号库根本没有「再加一个账号」的入口;并把过长的「账号」页拆成两个子页
|
|
2283
|
+
|
|
2284
|
+
用户反馈两件事,第一件追下去是真 bug。
|
|
2285
|
+
|
|
2286
|
+
**① 账号库缺入口(而且两个退出按钮都会顺手删库)**
|
|
2287
|
+
|
|
2288
|
+
```
|
|
2289
|
+
退出当前账号 / 退出并登录其它账号
|
|
2290
|
+
→ POST /logout → logout() → clearAuth() → removeAccount(active.id)
|
|
2291
|
+
```
|
|
2292
|
+
|
|
2293
|
+
也就是说「退出」= **把该账号从账号库删掉**(0.1.26 刻意的设计:登出的语义就是凭证不该留在磁盘上),
|
|
2294
|
+
但**界面从没说清这一点**;而账号库卡里只有列表 / 切换 / 重命名 / 移除 / 导出 / 导入 ——
|
|
2295
|
+
**没有任何"加一个账号"的入口**。合起来的后果:这个"账号库"其实攒不到第二个号,
|
|
2296
|
+
只能靠手动粘贴 Token 或导入备份,更像"一个账号 + 备份恢复"。
|
|
2297
|
+
|
|
2298
|
+
改动:
|
|
2299
|
+
- 账号库卡新增「**登录新账号(添加)**」,配套新增 `POST /login/add`。
|
|
2300
|
+
它只清"登录态存放处"(独立浏览器 profile + 登录分区),**不动账号库里的任何账号**
|
|
2301
|
+
—— 这正是它与 `/logout` 的区别。不清的话新窗口一打开就是旧账号,抓回来还是它,等于没加。
|
|
2302
|
+
- 新增 `src/account-add.ts`:**添加模式**。捕获到凭证后统一走 `commitCapturedAuth()`:
|
|
2303
|
+
默认仍是 `writeAuth()`(写入并设为当前,老行为一字不变);添加模式下只 `upsertAccount()`
|
|
2304
|
+
(入库),**当前账号原样不动**。两条硬约束:
|
|
2305
|
+
- **一次有效**:无论成败都消费掉标志,绝不泄漏到后面某次无关的捕获上;
|
|
2306
|
+
- **15 分钟 TTL**:登录窗口可能被丢在那儿不管,过期自动失效。
|
|
2307
|
+
- 边界:库里本来没有当前账号时(比如刚退过又直接点添加),反而**会**设为当前 ——
|
|
2308
|
+
否则会留下"库里有账号却没选中"的僵局。这不违反"不自动切换":那条规则针对的是**别顶掉正在用的号**。
|
|
2309
|
+
- `login.ts` 里 **5 处**捕获写回全部改走 `commitCapturedAuth`;
|
|
2310
|
+
但 `/status` 里那次"补全账号展示信息"的写回**故意保留 `writeAuth()`** ——
|
|
2311
|
+
它是对当前账号的元数据刷新、不是新捕获,让它消费掉添加模式会是个极难查的 bug(源码里留了注释)。
|
|
2312
|
+
- 「退出」的文案改成明说"= 把该账号从账号库移除,不是只登出",并指向「登录新账号」。
|
|
2313
|
+
|
|
2314
|
+
**② 「账号」页拆成两个子页:登录状态 / 账号库**
|
|
2315
|
+
|
|
2316
|
+
- 二级页签用**分段胶囊**(一级是下划线大标签)—— 两层长得不一样,一眼能看出自己在第几层;
|
|
2317
|
+
两层长得一样的话,用户会以为回到了同一层。
|
|
2318
|
+
- 分组:登录状态 = 登录状态卡 + 当前账号卡;账号库 = 账号库卡 + 手动粘贴 Token
|
|
2319
|
+
(手动贴 token 是"再加一个号"的另一条路,与「登录新账号」并列)。
|
|
2320
|
+
- 与一级标签同一原则:用 `hidden` 属性切换,不靠 CSS 类 —— 万一样式没加载,
|
|
2321
|
+
退化成"两页都显示"(难看但能用),而不是"除了一页全空白"。
|
|
2322
|
+
- 深色主题的坑:选中态用 `--bg2` 铺在 `--bg1` 上,而深色里这两档只差 7 级、几乎看不出,
|
|
2323
|
+
必须再补一圈 `--bd2` 描边才立得住(实测不加就跟没选中一样)。
|
|
2324
|
+
|
|
2325
|
+
测试:新增 `check-account-add` 13 项(标志语义 / TTL 失效 / 默认语义不变 / 添加模式不切换 /
|
|
2326
|
+
`created` 判定 / 空库边界 / **只生效一次** / 反复进出不串味),并做了**两轮反向验证**:
|
|
2327
|
+
把"不切换"与"一次有效"分别改坏,测试立刻变红(3 项 / 1 项)。
|
|
2328
|
+
`check-bundle` 加 5 条产物断言。
|
|
2329
|
+
|
|
2330
|
+
## 0.1.27 — 2026-09-12
|
|
2331
|
+
|
|
2332
|
+
### 改:导出/导入备份改成弹**系统文件对话框**(不再让人手打路径)
|
|
2333
|
+
|
|
2334
|
+
用户反馈:导入要在一个输入框里手打备份文件路径;导出只能落到插件目录、不能自己选位置。
|
|
2335
|
+
两者都应该弹一个系统对话框让人选。
|
|
2336
|
+
|
|
2337
|
+
**先得解决一个架构限制**:插件宿主跑在 Electron 的 **utility 进程**里,那里
|
|
2338
|
+
`require('electron')` 只暴露 `net` 与 `systemPreferences`(0.1.22 的能力探测实测),
|
|
2339
|
+
而 `dialog` 是**主进程**模块 —— 也就是说"弹系统文件框"宿主侧根本做不到,
|
|
2340
|
+
只能由**渲染进程**(插件界面所在之处)用 Chromium 自己的能力完成:
|
|
2341
|
+
|
|
2342
|
+
| 场景 | 机制 | 为什么用它 |
|
|
2343
|
+
|---|---|---|
|
|
2344
|
+
| 打开文件 | `<input type="file">` | 任何 Chromium 都支持。Chromium 150 起跨域 iframe 被禁止弹 File System Access 文件框,这个不受影响,是最可靠的手段 |
|
|
2345
|
+
| 另存为 | `showSaveFilePicker()` | File System Access API,Electron 有实现(Electron 30+ 那个已知问题只是对话框里多一行提示文案,不是不可用) |
|
|
2346
|
+
| 取真实路径 | `window.__DSH_DESKTOP_FILE_PATH__` | DSH 注入的 preload 桥(`webUtils.getPathForFile`),注释写明"只解析操作者选中的、真实落盘的 File" |
|
|
2347
|
+
|
|
2348
|
+
**改动**
|
|
2349
|
+
|
|
2350
|
+
- 新增 `src/file-picker.ts`:三项能力各自做**探测 + 优雅回退**,都能缺失。
|
|
2351
|
+
- **导出**:先弹系统「另存为」,位置和文件名由用户定;内容在用户选完之后才去取。
|
|
2352
|
+
拿不到系统对话框(宿主未注入 / 平台拒绝)时**回退**到原来的"宿主写 `exports/` 并回显路径",
|
|
2353
|
+
功能不失效、只是不能选位置 —— 回退时界面会说明原因。
|
|
2354
|
+
- **导入**:点按钮弹系统「打开」框选文件,**删掉**"请填写备份文件路径"输入框。
|
|
2355
|
+
拿得到真实路径时**只把路径交给宿主**(宿主自己读文件,凭证明文不进 HTTP);
|
|
2356
|
+
拿不到才退回"界面读内容再交给宿主"。
|
|
2357
|
+
- 新增 `POST /accounts/export-json`:供界面「另存为」取内容。
|
|
2358
|
+
⚠️ 这条路会把明文凭证交给渲染进程 —— 是在"让用户自己选位置"与"凭证不出宿主"
|
|
2359
|
+
之间的**显式取舍**(本机回环 + DSH 同源守卫),理由写在路由与 `exportAccounts()` 的注释里。
|
|
2360
|
+
凭证不出宿主的那条 `/accounts/export` **始终保留**作为回退。
|
|
2361
|
+
- 备份文件名改为 `deepseek-accounts-YYYYMMDD-HHMMSS.json`。
|
|
2362
|
+
不能直接拿 `toISOString()` 当文件名 —— 它带冒号,在 Windows 上是非法文件名字符。
|
|
2363
|
+
|
|
2364
|
+
**两个不显眼但会踩的点(都写进注释、并由测试守住)**
|
|
2365
|
+
|
|
2366
|
+
1. **必须"先弹框、再取内容"。** Chromium 的瞬时用户激活有时限,反过来
|
|
2367
|
+
(先 `await` 取备份再弹框)在真机上会被判成"没有用户手势"而直接抛错。
|
|
2368
|
+
所以 `saveWithPicker()` 收的是**回调**而不是现成的文本 —— 顺序锁在函数里,调用方想写错都难。
|
|
2369
|
+
测试断言的是**调用顺序本身**(`picker → produce → write → close`),并做了反向验证:
|
|
2370
|
+
把顺序改反,这条测试立刻变红(实测报了 `实际顺序 produce → picker → write → close`)。
|
|
2371
|
+
2. **取消不是错误,且取消时不该去取内容。** 用户点了取消却仍然把明文凭证从宿主拉进界面,
|
|
2372
|
+
是无谓的多暴露一次。`AbortError` 被单独识别成 `cancelled`,与真失败分开处理。
|
|
2373
|
+
|
|
2374
|
+
测试:新增 `check-file-picker` 23 项(建议文件名的非法字符、路径桥各种坏值容错、
|
|
2375
|
+
另存为四种结果分类、顺序断言、取消时不取内容、打开框的 DOM 清理与取消、
|
|
2376
|
+
导入来源的 path / content / unreadable 三分支);`check-bundle` 加 5 条产物断言,
|
|
2377
|
+
其中一条是**负向**的:产物里不许再出现"要导入的备份文件路径"。
|
|
2378
|
+
|
|
2379
|
+
## 0.1.26 — 2026-09-12
|
|
2380
|
+
|
|
2381
|
+
### 把账号当一等公民管理(借鉴 workbuddy-switch 的 5 项 + 1 项检查更新)
|
|
2382
|
+
|
|
2383
|
+
参考项目 <https://github.com/changexbc/workbuddy-switch>(MIT,227★)管理 WorkBuddy /
|
|
2384
|
+
CodeBuddy CLI / CodeBuddy CN IDE 三个目标的账号切换、积分到期、签到、Token 保活、自动更新。
|
|
2385
|
+
逐条判断后,**只搬适用的部分**:
|
|
2386
|
+
|
|
2387
|
+
**① 账号库(多账号并存 + 一键切换 + 导入导出)**
|
|
2388
|
+
|
|
2389
|
+
以前只有一份凭证文件,换号的代价是「退出 → 清浏览器分区 → 重新登录 → 等捕获」,
|
|
2390
|
+
期间原来的号也回不去。现在:
|
|
2391
|
+
|
|
2392
|
+
- `accounts/<id>.json` 一账号一文件 + `accounts.json` 索引(当前指针);原子写 + 0600。
|
|
2393
|
+
- **旧单账号文件自动迁移**(只在库为空且旧文件存在时跑一次,旧文件改名留档)。
|
|
2394
|
+
- 去重按 `serverId` → token:同一个号重复捕获是**更新**而不是新增。
|
|
2395
|
+
- 移除 = **删除凭证文件**(不做"改名归档"):「登出」的语义就是这份凭证不该再留在磁盘上。
|
|
2396
|
+
- 导入/导出:导出**写到本机文件并只回传路径**,不把明文 token 塞进 HTTP 响应。
|
|
2397
|
+
|
|
2398
|
+
**② 受限状态可视化 + 恢复倒计时**
|
|
2399
|
+
|
|
2400
|
+
`isMutedError` / `muteUntilMs` 早就在解析 `mute_until` 了,但只在**失败那一刻**拼进错误消息。
|
|
2401
|
+
现在把它记到账号上(`limit.untilMs`),设置页常驻显示「还剩 X 小时 Y 分(MM/DD HH:mm 解除)」,
|
|
2402
|
+
30 秒粒度自己走定时器刷新(不产生请求)。
|
|
2403
|
+
|
|
2404
|
+
> ⚠️ 这个状态**只能从"生成被拒"里学到** —— 账号受限期间 `users/current` 依然返回 200。
|
|
2405
|
+
> 所以探活探不出限制,两者是两件事。
|
|
2406
|
+
|
|
2407
|
+
**③ 登录态主动探活(只读、零额度、可关)**
|
|
2408
|
+
|
|
2409
|
+
参考项目是"操作前不足阈值就刷新 token";我们这边没有 refresh token 可刷(网页端 token
|
|
2410
|
+
只能靠重新登录拿新的),所以做的是**尽早发现失效**:启动后 20 秒探一次、之后每 30 分钟一次
|
|
2411
|
+
(`probeIntervalMs`,0 = 关闭),走只读的 `users/current`。失败**只提示、不阻断**。
|
|
2412
|
+
结果写回**发起探活时那个账号**(按 token 匹配)—— 探活期间切了号也不会记到新账号头上。
|
|
2413
|
+
|
|
2414
|
+
**④ 本地调用台账(请求密度 + 失败分类)**
|
|
2415
|
+
|
|
2416
|
+
节流"到底有没有效"不能靠感觉。台账按天记 JSONL(只留近 7 天、只记元信息,不含对话内容与凭证),
|
|
2417
|
+
设置页看两个数:**相邻对话间隔**(中位/p90/最短 —— 最短间隔直接对应"有没有连环请求")
|
|
2418
|
+
与**失败分类**(限流 / 账号被限制 / 鉴权 / 网络)。间隔只统计 `purpose === 'chat'`:
|
|
2419
|
+
会话标题生成是 DSH 自己发的旁路请求,算进去会让分布失真。
|
|
2420
|
+
|
|
2421
|
+
**⑤ 检查更新(比对 GitHub Releases)**
|
|
2422
|
+
|
|
2423
|
+
插件装不了包,所以只做"检查 + 给链接"。8 秒超时 + 优雅失败(GitHub 在国内常连不上,
|
|
2424
|
+
失败要如实说"没查到",不能把设置页卡住)。走当前传输层。
|
|
2425
|
+
|
|
2426
|
+
**⑥ 新增第 5 个标签「关于」**
|
|
2427
|
+
|
|
2428
|
+
版本与更新 / 数据位置 / **为什么没有自动换号**。
|
|
2429
|
+
最后一条是刻意放给使用者看的:风险说明只写在代码注释里等于没写。
|
|
2430
|
+
|
|
2431
|
+
### 刻意**没做**的(各有原因,不是遗漏)
|
|
2432
|
+
|
|
2433
|
+
| 项 | 为什么不做 |
|
|
2434
|
+
|---|---|
|
|
2435
|
+
| **自动轮换换号** | 参考项目切的是"CLI 下次启动用哪个账号",服务端看不到;我们每次对话都实时发请求 —— 自动换号是极强的机器行为特征,与本插件在传输层指纹 / 随机间隔 / 会话清理上"降低机器可识别性"的努力**直接冲突**。且同一服务商会关联多账号,处置可能更重。**只做手动切换。** |
|
|
2436
|
+
| 会话复制 | 我们的对话存在 DSH 本地、每轮全量重发、网页端不留会话 —— **架构上不需要**(换账号对话本来就跟着走) |
|
|
2437
|
+
| 自动签到 | DeepSeek 网页端没有签到机制 |
|
|
2438
|
+
| Token 多维统计图表 | 免费网页端不返回 token 计数,数据支撑不住 |
|
|
2439
|
+
|
|
2440
|
+
上述判断同时写在 `src/accounts.ts` 的模块注释里(带 ⚠️ 风险小节),由 `check-bundle` 守住。
|
|
2441
|
+
|
|
2442
|
+
### 其它
|
|
2443
|
+
|
|
2444
|
+
- `auth.ts` 退化成"账号库门面"(`readAuth` / `writeAuth` / `clearAuth`),
|
|
2445
|
+
适配器、登录流程、诊断等**所有调用点一行未改**。
|
|
2446
|
+
- `paths.ts` 单独拆出(避开 auth ↔ accounts 的 import 环)。
|
|
2447
|
+
- adapter 新增**一个**统一钩子 `noteCall`(受限记录与台账共用,不是两个钩子)。
|
|
2448
|
+
- `AdapterLlmError` 带上 `mutedUntilMs` 绝对值 —— 不让调用方去解析错误文案里的时间。
|
|
2449
|
+
|
|
2450
|
+
测试:新增 `check-accounts`(16 项)、`check-smoke`(8 项),`check-logout` 适配账号库;
|
|
2451
|
+
`check-bundle` 加 15 条产物断言(含"源码注释里必须写明为何不做自动换号")。
|
|
2452
|
+
|
|
2453
|
+
## 0.1.25 — 2026-09-12
|
|
2454
|
+
|
|
2455
|
+
### 修:副标题在切换标签时突然折成两行(并断在连字符处)
|
|
2456
|
+
|
|
2457
|
+
用户实测:切到「账号」页时副标题变两行、切到「模型」页又变回一行。
|
|
2458
|
+
|
|
2459
|
+
**根因不是样式写错,是这行文字正好压在折行边界上**:
|
|
2460
|
+
|
|
2461
|
+
- 原副标题 78 字,单行需要 **558px**;而宿主设置面板的内容宽约 **560px**。
|
|
2462
|
+
- 「账号」页(3 张卡)比「模型」页高 → 面板出现纵向滚动条 → 内容宽度少十几像素
|
|
2463
|
+
→ 最后一个词掉到第二行,而且断在 `deepseek-web` 的**连字符**处,看着像故障。
|
|
2464
|
+
|
|
2465
|
+
实测复现(同一字体栈,量 `getBoundingClientRect` 的高度):
|
|
2466
|
+
|
|
2467
|
+
| 容器宽 | 旧版 | 新版 |
|
|
2468
|
+
|---|---|---|
|
|
2469
|
+
| 560px | 1 行 | 1 行 |
|
|
2470
|
+
| **545px** | **2 行** | 1 行 |
|
|
2471
|
+
| 520px | 2 行 | 1 行 |
|
|
2472
|
+
| 480px | 2 行 | 1 行 |
|
|
2473
|
+
|
|
2474
|
+
**改动**
|
|
2475
|
+
|
|
2476
|
+
- 副标题缩到约 **467px**(58 字),留 ~90px 余量 —— 切标签、缩放窗口都不会再翻行;
|
|
2477
|
+
provider 信息保留,只是不再单列一段。
|
|
2478
|
+
- 给 `deepseek-web` 套 `.dsw-nobreak{white-space:nowrap}`:万一将来更窄,
|
|
2479
|
+
也只会整词换行,不会再断成 `deepseek-` / `web`。
|
|
2480
|
+
- 源码里留了宽度预算注释("以后改这句话保持单行 ≲470px")与量法。
|
|
2481
|
+
|
|
2482
|
+
测试:`check-bundle` 新增 1 条产物断言(`.dsw-nobreak` 存在)。
|
|
2483
|
+
|
|
2484
|
+
## 0.1.24 — 2026-09-12
|
|
2485
|
+
|
|
2486
|
+
### 设置页拆成标签页:一次只显示一页,不再一页到底
|
|
2487
|
+
|
|
2488
|
+
7 张卡堆在一页,「找某一项」要滚很久。按「什么时候会用到它」拆成 4 页:
|
|
2489
|
+
|
|
2490
|
+
| 标签 | 内容 |
|
|
2491
|
+
|---|---|
|
|
2492
|
+
| 账号 | 登录状态 · 当前账号(退出 / 换号)· 手动粘贴 Token |
|
|
2493
|
+
| 模型 | 可用模型 · 连通性测试 |
|
|
2494
|
+
| 防风控 | 请求节流(并发开关 · 间隔区间 · 会话清理) |
|
|
2495
|
+
| 传输层 | 传输层(指纹)+ 一键测试 |
|
|
2496
|
+
|
|
2497
|
+
- 标签栏样式取自同类插件(下划线指示 + 未选中 70% 透明度),装进**带边框的圆角容器**;
|
|
2498
|
+
颜色全部走 `--dsw-alias-*` 令牌,浅色/深色自动跟随,不写第二份样式。
|
|
2499
|
+
- **操作反馈条提到标签栏之上常驻** —— 拆页之后,在「账号」页点按钮的反馈若落在别的页里
|
|
2500
|
+
就等于看不见,所以它不归属任何一页。
|
|
2501
|
+
- 顺手调了两处顺序:**「登录状态」排在「当前账号」之前**(先结论、后细节);
|
|
2502
|
+
**「可用模型」排在「连通性测试」之前**(测试卡的模型下拉正是来自这张列表)。
|
|
2503
|
+
- 无障碍:`role=tablist / tab / tabpanel` + `aria-selected`;页容器用 `hidden` 属性切换
|
|
2504
|
+
(不靠 CSS 类,避免样式缺失时所有页同时显示)。
|
|
2505
|
+
|
|
2506
|
+
测试:`check-bundle` 新增 4 条产物断言(页签骨架、四个页签齐全、反馈条、传输层卡),
|
|
2507
|
+
防止以后重构把标签页弄丢。
|
|
2508
|
+
|
|
2509
|
+
> 注:README 里的设置页截图是拆页之前拍的,重启后可以换新的。
|
|
2510
|
+
|
|
2511
|
+
## 0.1.23 — 2026-09-12
|
|
2512
|
+
|
|
2513
|
+
### 传输层可切换:默认改走 Chromium 网络栈(消除「非浏览器客户端」特征)
|
|
2514
|
+
|
|
2515
|
+
0.1.22 的 net.fetch 诊断三项全绿后,把主请求路径接上。三方指纹对比(同机同日实测):
|
|
2516
|
+
|
|
2517
|
+
| | JA4 | cipher 列表哈希 | ALPN |
|
|
2518
|
+
|---|---|---|---|
|
|
2519
|
+
| Node fetch(undici) | `t13d5212h1_…` | — | **h1** |
|
|
2520
|
+
| Chrome(本机 152) | `t13d1517h2_8daaf6152771_cb7bf5808d99` | `8daaf6152771` | h2 |
|
|
2521
|
+
| **net.fetch(Electron 43)** | `t13d1516h2_8daaf6152771_806a8c22fdea` | **`8daaf6152771`** | h2 |
|
|
2522
|
+
|
|
2523
|
+
cipher 列表哈希与 Chrome **逐字节一致**,ALPN 与 cipher 数量(15)也都对上;
|
|
2524
|
+
唯一差异是扩展数 16 vs 17 —— Electron 43 内置 Chromium 150、本机 Chrome 是 152,
|
|
2525
|
+
差两个大版本,属正常。流式(`response.body` + AbortSignal)与鉴权(只读 `users/current` 200)均通过。
|
|
2526
|
+
|
|
2527
|
+
**改动**
|
|
2528
|
+
|
|
2529
|
+
- 新增 `src/transport.ts`:`chromium` / `node` 二选一 + 设置持久化
|
|
2530
|
+
(`<DSH_HOME>/web-login/transport.json`)+ 环境降级判定。
|
|
2531
|
+
降级只看**能力**(拿不到 `electron.net.fetch`),**不做「请求失败后换一条重试」** ——
|
|
2532
|
+
完成请求重发可能就是一次重复生成,代价比"切错了手动改回来"大得多。
|
|
2533
|
+
- 宿主启动时按设置**一次性注入**;webapi 保持与环境无关(不 require electron,单测天然不碰)。
|
|
2534
|
+
- 设置页新增「传输层(指纹)」卡:切换即时生效,外加**一键测试**(调诊断端点,零额度,
|
|
2535
|
+
直接回显 ①指纹 ②流式 ③鉴权 三项结论)。环境不支持 Chromium 时该选项自动禁用并说明原因。
|
|
2536
|
+
- 新增接口 `GET/POST /deepseek-web-login/api/transport`;`/status` 的 config 带上 `transport`。
|
|
2537
|
+
- 启动日志打印 `传输层=chromium|node`,降级时显式标注。
|
|
2538
|
+
|
|
2539
|
+
⚠️ **行为变更**:Chromium 网络栈会跟随**系统代理**,而 Node fetch 完全无视代理。
|
|
2540
|
+
若梯子关闭时系统代理仍指向 `127.0.0.1:7897`,切到 Chromium 后请求会失败 —— 设置页切回 Node 即可。
|
|
2541
|
+
这条提示常驻在卡片说明里。
|
|
2542
|
+
|
|
2543
|
+
测试:新增 `check-transport` 11 项 —— 默认值、路径跟随 DSH_HOME、读写往返、损坏/非法值容错、
|
|
2544
|
+
非 Electron 环境降级且**如实标记 degraded**、切换后注入层真的跟着变、降级后 fetch 不能丢、
|
|
2545
|
+
反复切换稳定、`apply` 能纠正外部对注入层的改动。
|
|
2546
|
+
|
|
2547
|
+
## 0.1.22 — 2026-09-12
|
|
2548
|
+
|
|
2549
|
+
### 新增:可注入传输层 + `net.fetch` 诊断(为「请求从哪出去」做验证)
|
|
2550
|
+
|
|
2551
|
+
**为什么**:实测 Node 的 `fetch`(undici)与 Chrome 的 TLS / HTTP2 指纹是**结构性差异** ——
|
|
2552
|
+
JA4 的 `h1` vs `h2`、Node 完全不带 GREASE、cipher 55 个 vs 15 个、扩展集合完全不同。
|
|
2553
|
+
也就是说请求在 **TLS 层**就能被判定为「非浏览器客户端」,而这几项**调参修不了**。
|
|
2554
|
+
参考项目 cuckoo-code(106★)与 deepseek-pp(1849★)都**不让 Node 发请求**(前者内嵌浏览器、
|
|
2555
|
+
后者 hook 用户浏览器),从未被风控 —— 印证「请求从哪出去」才是关键差异。
|
|
2556
|
+
|
|
2557
|
+
**发现**:DSH 本体是 Electron,插件宿主是 **utility 进程**。官方文档写明 `net` 模块适用
|
|
2558
|
+
Main + Utility,且 utility 的网络请求默认走 Chromium 的 system network context。
|
|
2559
|
+
实测能力探测确认:utility 进程里 `require('electron')` 只暴露 `net` 与 `systemPreferences`,
|
|
2560
|
+
其中 **`net.fetch` 是 function** —— 于是不必引入 uTLS / curl-impersonate,换一处传输层即可。
|
|
2561
|
+
|
|
2562
|
+
本次只做**验证**,不动主请求路径:
|
|
2563
|
+
|
|
2564
|
+
- `src/webapi.ts`:新增 `setFetchImpl()` / `fetchImplKind()`,9 处请求统一改走可注入的 `activeFetch`。
|
|
2565
|
+
⚠️ 它是**每次现取**(`injectedFetch ?? fetch`),而不是在模块加载那一刻固化 —— 固化写法会让
|
|
2566
|
+
「模块加载后再替换 `globalThis.fetch`」失效(单测正是这么打桩的),请求会绕过桩件**真的出网**;
|
|
2567
|
+
这类回归**不会让任何功能报错**,只会让一批测试静默失去意义。已单独加测试守住。
|
|
2568
|
+
- `src/net-diagnostics.ts`(新):三步**零额度**探测 ——
|
|
2569
|
+
① TLS/HTTP2 指纹 ② **能否读流式响应**(`response.body` + AbortSignal,用本地分块服务确定性验证;
|
|
2570
|
+
拿不到 body 就等于整条改造路线不成立)③ 鉴权(只读 `users/current`,不生成内容)。
|
|
2571
|
+
另有一档 `stream`:额外来一次迷你 completion,端到端验证 DeepSeek 的 SSE(会消耗一点额度)。
|
|
2572
|
+
- 触发方式:`POST /deepseek-web-login/api/diagnostics/net-fetch`(body `{"mode":"probe"|"stream"}`);
|
|
2573
|
+
或往 `<DSH_HOME>/web-login/probe-request.json` 写 `{"mode":"probe"}` 后重启 DSH ——
|
|
2574
|
+
宿主进程的 HTTP 端点只有 DSH 自己的同源页面打得通(从外部 curl 会撞同源守卫,实测任何路径都 403),
|
|
2575
|
+
这条路不依赖 HTTP。读完把文件改名为 `*.done-<时间戳>`(不删文件)。
|
|
2576
|
+
- 启动时打印一次能力清单(`process.type` / Electron 版本 / `require('electron')` 暴露了哪些 API),
|
|
2577
|
+
排查环境问题时一眼可见。
|
|
2578
|
+
|
|
2579
|
+
测试:新增 `check-fetch-injection`(5 项)与 `check-net-diagnostics`(13 项)。
|
|
2580
|
+
后者对流式探针做了**正反两向**验证("整体缓冲的响应必须被判为不可用"),避免出现永远绿灯的假判据。
|
|
2581
|
+
|
|
2582
|
+
**主请求路径仍未改动** —— 是否切到 `net.fetch`,等诊断结果确认后再决定。
|
|
2583
|
+
|
|
2584
|
+
## 0.1.21 — 2026-09-12
|
|
2585
|
+
|
|
2586
|
+
### 改进:把「机器特征」压下去(行为侧)
|
|
2587
|
+
|
|
2588
|
+
参考同类项目 cuckoo-code(同场景、从未被风控,分析见仓库外文档 `cuckoo-code 借鉴分析.md`)后做的两处行为优化。
|
|
2589
|
+
|
|
2590
|
+
- **请求间隔:固定值 → 随机区间**(默认 `2000~4000ms`)。
|
|
2591
|
+
固定间隔的方差≈0,在统计上就是明显的「定时器特征」;cuckoo-code 用的正是 2000~4000 随机区间。
|
|
2592
|
+
- 设置页改为**两个滑块**(下限 / 上限)+ 三个区间档位(1500~2500 / 2000~4000 推荐 / 5000~9000)。
|
|
2593
|
+
- **老配置兼容**:只设了 `minRequestIntervalMs`(0.1.20 的写法)时,上限自动跟随下限
|
|
2594
|
+
—— 语义仍是「固定间隔」,升级后行为不变。
|
|
2595
|
+
- 上下限被拖成非法组合(上限 < 下限)时自动纠正,不会出现负区间。
|
|
2596
|
+
- **临时会话:每轮建+删 → 攒批集中清理**(默认 `sessionCleanup: 'deferred'`)。
|
|
2597
|
+
「每轮新建一个临时会话、用完立刻删掉」是最强的机器行为特征之一(真人不会每 30 秒建删一次对话)。
|
|
2598
|
+
- `deferred`:攒够 `sessionCleanupBatchSize`(默认 8)个、或等满 `sessionCleanupDelayMs`
|
|
2599
|
+
(默认 90 秒)后集中清理;清理时**优先用一个请求批量删**
|
|
2600
|
+
(服务端支持的话 N 个会话只花 1 个请求),不支持则自动回退逐个删且此后不再尝试。
|
|
2601
|
+
- `immediate`(老行为,1.5s 后逐个删)/ `keep`(不删,请求最少但网页端会留下临时会话)。
|
|
2602
|
+
- 设置页新增「会话清理」三选一。
|
|
2603
|
+
- **为什么不直接复用会话**:DSH 每次交全量历史,而网页端会话有状态,复用会让上下文翻倍撑爆窗口,
|
|
2604
|
+
所以只能优化删除侧(原因写在代码注释里)。
|
|
2605
|
+
|
|
2606
|
+
### 测试
|
|
2607
|
+
|
|
2608
|
+
- `check-request-gate` 19 → **23 项**:区间随机性(不同随机源得到不同等待,证明不是固定值)、
|
|
2609
|
+
上下限相等退化为固定、老配置兼容、非法组合自动纠正。
|
|
2610
|
+
- 新增 `check-session-cleaner` **10 项**:三档策略、攒批阈值不提前触发、延迟触发、
|
|
2611
|
+
**批量删 3 个只花 1 个请求**、业务错误回退逐个删且此后不再尝试批量、网络异常仍可重试批量、
|
|
2612
|
+
keep 不发任何请求、清理失败不抛错。
|
|
2613
|
+
- `check-bundle` 补 3 条产物断言。
|
|
2614
|
+
|
|
2615
|
+
|
|
2616
|
+
## 0.1.20 — 2026-09-12
|
|
2617
|
+
|
|
2618
|
+
### 新增:设置页直接可调(开关 + 滑块)
|
|
2619
|
+
|
|
2620
|
+
- 新增「**请求节流(防风控)**」卡片:**允许并发**开关、**最小间隔**滑块(0~30 秒,步长 500ms)、
|
|
2621
|
+
三个快捷档位(1500 / 3000 推荐 / 8000)。开关打开时轨道显示为**红色**(风险提示)。
|
|
2622
|
+
- 改完**即时生效**(闸门新增运行时 `configure`,不必重启),并自动落盘到
|
|
2623
|
+
`${DSH_HOME:-~/.dsh}/web-login/gate.json`。
|
|
2624
|
+
- 优先级:**设置页保存的值 > cordis config > 内置默认** —— 设置页是用户的显式操作,
|
|
2625
|
+
不该被配置文件里的旧值盖回去。
|
|
2626
|
+
- 新增 host API:`GET /deepseek-web-login/api/gate`(读,含档位与上限)、
|
|
2627
|
+
`POST`(写:校验 → 应用 → 落盘)。落盘失败时如实返回 `persisted: false` 并注明「重启后会回到旧值」,
|
|
2628
|
+
而不是假装成功。
|
|
2629
|
+
- 适配器改为接受外部注入的闸门实例(宿主与设置页共享同一个,改完立即作用于后续请求);
|
|
2630
|
+
缺省仍可自建,单测不受影响。
|
|
2631
|
+
|
|
2632
|
+
### 测试
|
|
2633
|
+
|
|
2634
|
+
- `check-request-gate` 13 → **19 项**:`configure` 运行时改设置与范围夹取、`clampInterval` 边界、
|
|
2635
|
+
推荐档位必须包含默认值(防止「推荐」按钮指到别的值)、设置文件写入读回、
|
|
2636
|
+
损坏与非法字段容错、**落盘值与 configure 结果一致**(模拟重启后行为不变)。
|
|
2637
|
+
|
|
2638
|
+
## 0.1.19 — 2026-09-12
|
|
2639
|
+
|
|
2640
|
+
### 新增:请求节流与并发开关(防风控)
|
|
2641
|
+
|
|
2642
|
+
- **`minRequestIntervalMs`(默认 `3000`)**:两次网页端调用之间的最小间隔,
|
|
2643
|
+
按上一次调用**结束**时刻计算(不是开始 —— 长回答之后不会白等)。
|
|
2644
|
+
- **`allowConcurrent`(默认 `false`)**:是否允许同一账号并发请求;默认**串行**,多个调用按 FIFO 排队。
|
|
2645
|
+
- 新增 `src/gate.ts`(`createRequestGate`),闸门包在**每一次**模型调用外层(`gatedStream`),
|
|
2646
|
+
因此 DSH 的会话标题生成(`options.purpose === 'session-title'`)与压缩等辅助调用**一并受控**。
|
|
2647
|
+
- 设置页「登录状态」卡新增「请求节流」一行,显示当前生效值;`/status` 同步返回这两项配置。
|
|
2648
|
+
|
|
2649
|
+
### 为什么要默认开启(实测证据)
|
|
2650
|
+
|
|
2651
|
+
- 从插件日志反推 272 轮调用的起止时间,发现 **16 对真重叠**:重叠的一方总是主回答
|
|
2652
|
+
(数百字 / 6~40 秒),另一方只有 **8~17 字 / 1~3 秒** —— 那正是 **DSH 的会话标题生成**。
|
|
2653
|
+
也就是说,主回答还在跑的时候,同一个账号上已经又发出一个请求了。
|
|
2654
|
+
- 网页端同一账号同时只能生成一条,并发生成会被拒(`A message is being generated…`);
|
|
2655
|
+
更严重的是**账号级限制** —— 实测双窗口并发生成不到 6 分钟即触发 **1 天**的临时封禁。
|
|
2656
|
+
- 默认值取 3000ms 的依据:272 轮里非重叠情况下的相邻调用间隔**中位数为 3.9s**,
|
|
2657
|
+
3s 的下限既压住密集连发,又不会让正常步骤明显变慢。
|
|
2658
|
+
调值建议见 README「请求节流」一节(1500 / 3000 / 8000 三档)。
|
|
2659
|
+
|
|
2660
|
+
### 测试
|
|
2661
|
+
|
|
2662
|
+
- 新增 `check-request-gate` **13 项**:串行与并发开关、首次调用不等待、间隔从「结束」起算、
|
|
2663
|
+
间隔设 0、FIFO 顺序、release 幂等、**流抛错与消费者中断都不漏名额**、
|
|
2664
|
+
未迭代的 generator 不占位,以及一组对照实验(串行下标题必须等主回答结束 /
|
|
2665
|
+
并发模式下确实复现重叠 —— 防止「假绿灯」)。
|
|
2666
|
+
- `check-bundle` 补 3 条产物断言(闸门进了产物、覆盖 session-title、设置页显示节流策略)。
|
|
2667
|
+
|
|
2668
|
+
## 0.1.18 — 2026-09-12
|
|
2669
|
+
|
|
2670
|
+
### 修复(界面)
|
|
2671
|
+
|
|
2672
|
+
- **浅色主题下设置页仍是黑底**(用户实测):样式表全程引用 `var(--theme-text, #ddd)` 这套**宿主并不存在**
|
|
2673
|
+
的变量名 → 每个颜色都落到兜底值,而兜底全是深色(`#151922` / `#0f1115` / `#2a2f3a`),
|
|
2674
|
+
于是切任何主题都是黑底 + 灰字,几乎读不出来。
|
|
2675
|
+
- 改为宿主真实令牌 **`--dsw-alias-*`**(共 90 个,值由 DSH 按当前主题重定义,插件继承即自动跟随)。
|
|
2676
|
+
- 再加一层兜底:`var(--dsw-alias-xxx, var(--fb-xxx))`,`--fb-*` 由 `prefers-color-scheme` 切换 ——
|
|
2677
|
+
即使令牌改名/缺失,也不会再出现「浅色主题里的黑界面」。
|
|
2678
|
+
|
|
2679
|
+
### 改进(观感)
|
|
2680
|
+
|
|
2681
|
+
- 正文由 `ui-monospace`(等宽铺满,观感像终端日志)改为系统无衬线栈;等宽只留给命令、代码与输入框。
|
|
2682
|
+
- 卡片:圆角 12px + 主题描边;信息表按行细分隔线;徽章改胶囊形并用半透明语义色。
|
|
2683
|
+
- 按钮:主按钮改用 `brand-primary` + `label-primary-foreground`(浅色下黑底白字、深色下白底黑字,自动反色),
|
|
2684
|
+
并区分默认 / 幽灵 / 危险 / 二次确认四态,带 hover 过渡;输入框聚焦有描边。
|
|
2685
|
+
- 连通性测试输出改为左侧 3px 语义色条;模型列表去掉虚线分隔;滚动条轻量美化。
|
|
2686
|
+
|
|
2687
|
+
### 测试
|
|
2688
|
+
|
|
2689
|
+
- 用到的 15 个 `--dsw-alias-*` 变量名逐个与宿主令牌清单比对 → 缺失 0;
|
|
2690
|
+
产物核对与全部用例继续通过。
|
|
2691
|
+
|
|
2692
|
+
## 0.1.17 — 2026-09-11
|
|
2693
|
+
|
|
2694
|
+
### 改进(限流)
|
|
2695
|
+
|
|
2696
|
+
- **连续被限流时退避渐长**:原来是固定 20s,重试全落在限流窗口里 → 反复失败。
|
|
2697
|
+
现在按连续次数递增:20s → 40s → 80s…(上限 90s,再叠 0~30% 抖动避免多请求扎堆),
|
|
2698
|
+
5 分钟没再被限就重新计数。
|
|
2699
|
+
- 节流退避与并发退避(5s)继续分开:节流是账号级,等太短等于白撞。
|
|
2700
|
+
|
|
2701
|
+
### 测试
|
|
2702
|
+
|
|
2703
|
+
- `check-session-lifecycle` 15 → **16 项**:连续两次节流的退避必须递增(且落在 20s~90s+抖动区间)。
|
|
2704
|
+
|
|
2705
|
+
## 0.1.16 — 2026-09-11
|
|
2706
|
+
|
|
2707
|
+
### 修复
|
|
2708
|
+
|
|
2709
|
+
- **模型复读 prompt 里的截断占位符**(用户实测 17:2x,会话 1.2M tok 已超模型 1M 窗口,
|
|
2710
|
+
中段历史必然被截):正文里出现 `truncated]` / `[Assistant truncated]`,并被模型接着往下写。
|
|
2711
|
+
这些占位符来自 DSH 核心的压缩标记与 serializePrompt 的省略标记(`[N chars omitted]`),
|
|
2712
|
+
会话超长后就躺在 prompt 里,模型照抄。
|
|
2713
|
+
- `ECHO_INLINE_SIGNATURES` 增补:`[truncated]` / `assistant truncated` / `[N chars omitted]`。
|
|
2714
|
+
- 行级新增:单独一行 `truncated]`(占位符被拦腰切开的残片)也判回声。
|
|
2715
|
+
- 围栏代码块内不判(讨论截断机制的正常回答不受影响)。
|
|
2716
|
+
|
|
2717
|
+
### 测试
|
|
2718
|
+
|
|
2719
|
+
- `check-transcript-echo` 13 → **16 项**:占位符复读(正文保留)、`[N chars omitted]`、分块到达。
|
|
2720
|
+
|
|
2721
|
+
## 0.1.15 — 2026-09-11
|
|
2722
|
+
|
|
2723
|
+
### 修复
|
|
2724
|
+
|
|
2725
|
+
- **转写回声换了个前缀就绕过了守卫**(用户实测 17:07,install-plugin 工作区):
|
|
2726
|
+
模型输出的不再是以 `[Tool Result` 开头的行,而是
|
|
2727
|
+
```
|
|
2728
|
+
Assistant: [Tool Result for call_7b1a7d39a2e54bc0b8f1]
|
|
2729
|
+
direct ERR fetch failed
|
|
2730
|
+
```
|
|
2731
|
+
加了 `Assistant: ` 前缀后,行首不再匹配回声特征 → 被当成「正文里偶尔出现的
|
|
2732
|
+
`User:` 字样」放行,还把后面那行也一起带出来。
|
|
2733
|
+
- 新增 **行内任意位置** 的转写特征判据(`ECHO_INLINE_SIGNATURES`):
|
|
2734
|
+
`[Tool Result` / `[status:` / `[Truncated]` / `[System]` / `[Assistant]`
|
|
2735
|
+
—— 只要一行里含有这些标记就判回声,不再要求行首。
|
|
2736
|
+
- 光秃秃的 `Assistant:` / `User:`(冒号后没有内容)视为模型在起一行假转写,直接拦下
|
|
2737
|
+
(以前它会被扣住,然后在 flush 时当作正文放行)。
|
|
2738
|
+
|
|
2739
|
+
### 测试
|
|
2740
|
+
|
|
2741
|
+
- `check-transcript-echo` 10 → **13 项**:带头像前缀的回声(整段 / 分块到达)、
|
|
2742
|
+
裸 `Assistant:`、以及原有判据不回归。
|
|
2743
|
+
|
|
2744
|
+
## 0.1.14 — 2026-09-11
|
|
2745
|
+
|
|
2746
|
+
### 修复
|
|
2747
|
+
|
|
2748
|
+
- **账号级节流被误判成不可重试,整轮直接失败**(用户实测 16:11,SSE error 事件):
|
|
2749
|
+
服务端说 `消息发送过于频繁,请稍后重试`,而并发那条判据匹配的是 `请稍后再试` —— **差一个字**,
|
|
2750
|
+
于是落到 `PROVIDER_ERROR`(不可重试),只能手点「继续」。
|
|
2751
|
+
- 新增 `isThrottled()`:`过于频繁` / `太频繁` / `too many requests` / `rate limit` / `稍后重试` / `限流`。
|
|
2752
|
+
- 归为可重试的 `RATE_LIMIT`,退避 **20s**(并发那条只给 5s:撞得越勤越可能延长限制)。
|
|
2753
|
+
- **两种 RATE_LIMIT 的文案不再串台**:事件新增 `rateLimitKind: 'concurrent' | 'throttled'`,
|
|
2754
|
+
适配器据此给准确说明 —— 节流不再被说成「另一个窗口正在用同一账号生成」,
|
|
2755
|
+
并明确告诉用户「这不是封号,登录态有效,等几分钟或降低步骤密度」。
|
|
2756
|
+
|
|
2757
|
+
### 测试
|
|
2758
|
+
|
|
2759
|
+
- `check-session-lifecycle` 13 → **15 项**:`isThrottled` 判据(且不误伤会话失效/mute)、
|
|
2760
|
+
节流 SSE 事件必须带 `RATE_LIMIT` + `rateLimitKind=throttled` + 退避 ≥20s。
|
|
2761
|
+
- `check-auto-continue` 13 → **14 项**:节流文案必须说「限流」、不得出现「另一个窗口」,且透出退避时间。
|
|
2762
|
+
|
|
2763
|
+
## 0.1.13 — 2026-09-11
|
|
2764
|
+
|
|
2765
|
+
用户实测反馈串起来的几个问题(回答错位、静默少一段、网页端声明上屏、莫名反复续写),
|
|
2766
|
+
根因都在「流式文本管线的收尾」这一段,本版一并修掉。
|
|
2767
|
+
|
|
2768
|
+
### 修复
|
|
2769
|
+
|
|
2770
|
+
- **轮末吐净顺序反了 → 正文最后一段前后颠倒**(实测:正文停在「…原来的 `pelican.svg` 保持」,
|
|
2771
|
+
而下一段却以「不动,」开头)。三层缓冲的吐净顺序改为流水线**反序**(回声守卫 → 声明剥离 → 工具调用过滤器):
|
|
2772
|
+
每层扣住的是「它收到的尾部」,所以越深的层扣住的文本越早。错位文本此前还会作为「半截回答」
|
|
2773
|
+
进续写 prompt,让后续几轮越滚越乱。
|
|
2774
|
+
- **剥离网页端免责声明**:DeepSeek 网页端在**每一轮**回复末尾自动追加
|
|
2775
|
+
`本回答由 AI 生成,内容仅供参考,请仔细甄别`。它有两个后果:① 自动续写的缝正好落在它后面,
|
|
2776
|
+
于是它卡在两条回答中间上屏;② 它以「甄别」(汉字、无标点)结尾 → 句中截断判据**每轮恒为真**
|
|
2777
|
+
→ 明明正常收尾也被判成「被截」→ 反复续写(实测一轮 5901 字的完整回答被续写到 14326 字)。
|
|
2778
|
+
- 新增 `BoilerplateFilter`:流式剥离,**恒定扣留 len-1 个字符**。声明会被 SSE 切成任意小包
|
|
2779
|
+
(实测有「 AI」「 生成」「,」「内容」),只扣「声明前缀」时切点落在中间就会漏。
|
|
2780
|
+
- **轮末尾巴必须也走完下游各层**:新增 `drainTextPipeline()` —— 三层按反序吐净后,
|
|
2781
|
+
对残余补跑「剥声明 / 剥伪系统标记」。此前声明恰好 23 字 ≈ 滤波器 24 字扣留窗口,
|
|
2782
|
+
整段从轮末尾巴漏出去(会话 `6c0dbc47` 实测:它作为只有单个 delta 的独立 text 块,跟在工具调用后面)。
|
|
2783
|
+
- `boilerplate` 必须排在回声守卫**之前**:守卫会扣住「最后一个换行之后的整行」,
|
|
2784
|
+
而声明常待在那里,放在它后面就永远看不到。
|
|
2785
|
+
- **没收到 `response/status: FINISHED` 也按截断处理**:截断判据原来只看句末标点,
|
|
2786
|
+
截在标点 / 反引号 / 代码块收尾处会**静默**少一段(实测 `要我直接开跑 \`dev_plugin_status\` 和 \`` 到此为止)。
|
|
2787
|
+
- **每轮诊断日志**:`第 N 轮流结束:[本轮 X 字 / 累计 Y 字 / 耗时 Zms] finish=…`,
|
|
2788
|
+
区分「服务端截断(无 FINISHED)」与「判据认为没写完」;剥掉声明时另打一行。
|
|
2789
|
+
|
|
2790
|
+
### 测试
|
|
2791
|
+
|
|
2792
|
+
- 新增 `tests/check-flush-order.mjs`(3 项):三层反序吐净 = 与原文逐字一致;正序**必然**错位。
|
|
2793
|
+
- 新增 `tests/check-web-disclaimer.mjs`(9 项):整段剥离、**从中间切开**、小包拼接、
|
|
2794
|
+
前缀扣留后 flush 放行、两处声明、正常文本不受影响、**声明落在轮末尾巴(工具调用后)**、
|
|
2795
|
+
**声明是整轮最后一句话**、尾巴非声明时逐字保留。
|
|
2796
|
+
- `check-auto-continue` 8 → **13 项**:补「无 FINISHED 也续写」「反引号结尾也续写」
|
|
2797
|
+
「FINISHED + 句号不续写」「轮末带声明不算截断且不上屏」「续写轮带声明两处都剥」。
|
|
2798
|
+
- 六个文件断言 39 → **56 项**;产物核对 45 → **48 项**;全部测试文件通过。
|
|
2799
|
+
- 真实数据验证(直连网页端跑完整流水线):原始流含声明时,输出与「原文去掉声明」**逐字相等**。
|
|
2800
|
+
|
|
2801
|
+
## 0.1.12 — 2026-09-11
|
|
2802
|
+
|
|
2803
|
+
### 新增
|
|
2804
|
+
|
|
2805
|
+
- **自动续写**(应用户要求:两种模式都不再出现「已达到输出 token 上限」提示)
|
|
2806
|
+
- 回答在句中被截时,适配器**自动**发起新请求让模型接着写(等价于用户手动说「继续」,
|
|
2807
|
+
但无需用户参与),并把续写内容**无缝拼进同一条回答**。绝大多数截断被无声补全。
|
|
2808
|
+
- 每一轮的过滤器/回声守卫在轮次收尾时吐净、续写轮用全新实例 ——
|
|
2809
|
+
否则跨请求的行缓冲会让续写内容与 hold 的尾部乱序。
|
|
2810
|
+
- 续写 prompt = 原对话 + 已输出的半截回答(作为 assistant 消息)+ 明确的继续指令
|
|
2811
|
+
(从断点直接续写、不重复、不加开场白)。
|
|
2812
|
+
- 续写轮次失败(网络/频控)→ 保留已上屏部分并正常收尾,不让整轮失败。
|
|
2813
|
+
- 已发起工具调用的轮次不续写(等工具结果)。
|
|
2814
|
+
- 配置:`autoContinue`(默认开)、`maxContinuations`(默认 2,每轮是一次新的网页端请求;
|
|
2815
|
+
调高会消耗更多免费额度)。
|
|
2816
|
+
- **永远不再报 max-tokens**:续写额度用尽仍被截时,按正常完成(stop)上报 + 日志留痕。
|
|
2817
|
+
用户可手动说「继续」。「已达到输出 token 上限」提示从此不会出现。
|
|
2818
|
+
|
|
2819
|
+
### 测试
|
|
2820
|
+
|
|
2821
|
+
- 新增 `tests/check-auto-continue.mjs`(8 项):句中截断→续写拼接、
|
|
2822
|
+
续写 prompt 带半截回答与指令、正常结束不触发、autoContinue/maxContinuations 关闭、
|
|
2823
|
+
多轮续写、工具调用后不续写、续写失败保留已输出部分。
|
|
2824
|
+
- 断言总数 117 → **125**;产物核对 44 项。
|
|
2825
|
+
|
|
2826
|
+
## 0.1.11 — 2026-09-11
|
|
2827
|
+
|
|
2828
|
+
### 修复
|
|
2829
|
+
|
|
2830
|
+
- **模型吐出成串的伪系统标记,全部上屏**(用户实测"回答过程中出现 `<ds_system>Tool call made.</ds_system>` 正常吗")
|
|
2831
|
+
- 现场:一条正文里连续 **13 个** `<ds_system>Tool result for call_1a2b3c</ds_system>`,
|
|
2832
|
+
调用 ID 还是字母递增编造的(1a2b3c→4d5e6f→7a8b9c…)。这是模型在**模仿**系统消息格式
|
|
2833
|
+
—— 与「转写回声」同类问题,但形态是 XML 标签而不是 `[Tool Result]` 行。
|
|
2834
|
+
- 修复:新增 `stripSystemMarkers`(第三道网,在工具调用过滤器与转写回声守卫之后)——
|
|
2835
|
+
剥掉正文中的 `<ds_system>…</ds_system>` / `<system>…</system>` / 半截标记,
|
|
2836
|
+
围栏代码块内不剥(正常回答可能讨论这些标记)。
|
|
2837
|
+
- **服务端在句中截断却报 FINISHED → 适配器假装正常完成**(用户实测"最后回复总是回复不完")
|
|
2838
|
+
- 现场:以 FINISHED 收笔但正文停在 `…用官方模型`、`…read-re` 等句中,
|
|
2839
|
+
最后一个 text-delta 与 finish 事件间隔仅 5-7ms(流是真的结束了,不是解析器丢字)。
|
|
2840
|
+
- 修复:`mapFinish` 新增启发式 —— FINISHED 但正文**在句中被截**(尾部无句末标点、
|
|
2841
|
+
或 `**` 粗体标记未闭合、或逗号/冒号/分号收尾)→ 报 `max-tokens` 而非 `stop`,
|
|
2842
|
+
让 UI 至少提示「可能被截断」。
|
|
2843
|
+
- 注:截断本身是**服务端行为**(12s 内即断,不是 60s 上限;可能是网页端单条回复的
|
|
2844
|
+
输出 token 静默限制或内容过滤),客户端无法恢复,只能如实标记。
|
|
2845
|
+
|
|
2846
|
+
### 已知问题(记录在案,暂不修)
|
|
2847
|
+
|
|
2848
|
+
- **思考碎片化**:实测会话日志中思考内容只有 25-27 字符的碎片
|
|
2849
|
+
(`" duplicate"`、`" adapter"`、`" classified"` 等孤立词汇),
|
|
2850
|
+
而模型实际输出了大段推理。说明 SSE 解析器对思考流的某个新格式不兼容,需要账号解除后
|
|
2851
|
+
用线上探针定位具体协议变化。当前表现为「思考过程看起来断断续续」。
|
|
2852
|
+
- 思考里反复出现的单独「写」字是模型的**自身行为**(思维链里的"现在开始写"标记),不是 bug。
|
|
2853
|
+
|
|
2854
|
+
### 测试
|
|
2855
|
+
|
|
2856
|
+
- 新增 `tests/check-system-markers.mjs`(7 项):真实事故 13 个标记全剥、单个/半截/<system> 剥掉、
|
|
2857
|
+
围栏内不剥、无标记原样通过、嵌套在其他文本中只剥标记本身。
|
|
2858
|
+
- 断言总数 117 → **124**;产物核对 44 项。
|
|
2859
|
+
|
|
2860
|
+
## 0.1.10 — 2026-09-11
|
|
2861
|
+
|
|
2862
|
+
### 修复
|
|
2863
|
+
|
|
2864
|
+
- **模型把 arguments 写成数组时,整个调用被静默丢弃**(严重,现场实测:
|
|
2865
|
+
`~/.dsh/deepseek-web/rejected.jsonl` 抓到 249 字符的完整现场)
|
|
2866
|
+
- 现场:`{"tool_calls":[{"name":"pwsh","arguments":[{…}}]}` —— 两个错误叠加:
|
|
2867
|
+
① `arguments` 写成了**数组**(规范是对象);② args 数组没闭合就写了 `}`
|
|
2868
|
+
(`}}` 里缺一个 `]`)。
|
|
2869
|
+
- 旧的结构性修复用 `( braced-deficit ) 个 `}` 盲补 —— 但未闭合的是**数组**,
|
|
2870
|
+
`}` 闭不上它 → 补出的候选仍是非法 JSON → 整个调用被丢掉。
|
|
2871
|
+
- 修复 ①:`rebuildToolCallJson` —— **栈引导重排**:扫描时维护容器栈,
|
|
2872
|
+
遇到不匹配的闭合符就**插入缺失的容器闭合**使其匹配(有 8 次上限保护)。
|
|
2873
|
+
一个算法同时覆盖:每个元素少写 `}`(事故 #4)、数组/对象闭合顺序错乱(本次)、
|
|
2874
|
+
外层少写收尾 `}`。
|
|
2875
|
+
- 修复 ②:`parseToolCallJson` 把 `arguments: [{…}]`(单元素数组)解包成真正的参数对象
|
|
2876
|
+
—— 否则即使结构修好,参数也会被序列化成 `"[{…}]"`,工具拿到的是垃圾。
|
|
2877
|
+
- 重排的逗号规则必须带**前瞻**(后面紧跟 `{"name":` 才是元素边界):
|
|
2878
|
+
调用对象内部的键分隔逗号(`"name"` 与 `"arguments"` 之间)在栈上深度相同,
|
|
2879
|
+
不带前瞻会把每个调用对象拦腰补坏(第一版就踩了这个洞)。
|
|
2880
|
+
- 安全闸门保持:扫描结束时**仍在字符串内**(流被 60s 上限截断的典型特征)→ 拒绝修补。
|
|
2881
|
+
|
|
2882
|
+
### 测试
|
|
2883
|
+
|
|
2884
|
+
- 真实事故原文(249 字符,逐字取自 rejected.jsonl)固化为回归:
|
|
2885
|
+
必须解析出 1 个 `pwsh` 调用、`command` 逐字还原(含 `$env:` 与 Windows 路径)。
|
|
2886
|
+
- `arguments` 数组解包(闭合完整版)+ 合法调用不受影响的对照。
|
|
2887
|
+
- 断言总数 114 → **117**;产物核对 44 项。
|
|
2888
|
+
|
|
2889
|
+
## 0.1.9 — 2026-09-11
|
|
2890
|
+
|
|
2891
|
+
### 修复
|
|
2892
|
+
|
|
2893
|
+
- **两个窗口共用同一网页账号时,第二个窗口整轮失败**(用户实测)
|
|
2894
|
+
- 现象:`DeepSeek 网页端返回错误:A message is being generated, please try again later.(PROVIDER_ERROR)`
|
|
2895
|
+
- 含义:网页端**同一账号同时只能生成一条消息**。这不是封号(封号是 `user is muted`),
|
|
2896
|
+
但旧实现把这类 SSE 错误事件一律抛成**不可重试**的 `PROVIDER_ERROR` → 那一轮直接失败。
|
|
2897
|
+
- 修复:新增 `isBusyGenerating` 语义归类 —— SSE 错误事件与业务信封里的
|
|
2898
|
+
「being generated / try again later / 请稍后再试」统一映射为**可重试**的 `RATE_LIMIT`
|
|
2899
|
+
(`providerRetryAfterMs = 5s`),交给 dsh-llm-retry 自动重发;短暂碰撞可以自愈。
|
|
2900
|
+
错误文案也改成人话:说明是并发占用、建议另一窗口换 provider 或换账号。
|
|
2901
|
+
|
|
2902
|
+
### 说明(实测账号状态)
|
|
2903
|
+
|
|
2904
|
+
- 本次用户的新账号在登录后约 **6 分钟**即被 mute(两个窗口并发跑)—— 免费网页端对
|
|
2905
|
+
自动化调用的限流阈值远比想象低。**并发会加速 mute**:两个 agent 同时跑 = 请求量翻倍。
|
|
2906
|
+
- muted 与「同时只能生成一条」是两回事:前者要等解除(错误信息带解除时间),
|
|
2907
|
+
后者等几秒重试即可。
|
|
2908
|
+
|
|
2909
|
+
### 测试
|
|
2910
|
+
|
|
2911
|
+
- `check-session-lifecycle` 新增 3 项(10 → 13):真实并发拒绝文案的分类、
|
|
2912
|
+
SSE 错误事件带 `RATE_LIMIT` + 5s 重试间隔、业务信封里的同款映射。
|
|
2913
|
+
- 断言总数 111 → **114**;产物核对 44 项。
|
|
2914
|
+
|
|
2915
|
+
## 0.1.8 — 2026-09-11
|
|
2916
|
+
|
|
2917
|
+
### 变更
|
|
2918
|
+
|
|
2919
|
+
- **面板文案如实化**(用户反馈:缺失项读起来像风险提示)
|
|
2920
|
+
- 旧文案把「未捕获 cookie / 指纹头」写成「(可能仍可用)」,含义模糊;实际含义只是
|
|
2921
|
+
「你走的是手动粘贴 token 那条路」——该路径本来就没有这两项。
|
|
2922
|
+
- 新增「**凭证来源**」一行,明确区分两条路径(手动粘贴 token / 浏览器登录捕获);
|
|
2923
|
+
缺失项后直接说明「已验证不影响请求」,并给出「若频繁遇到 AUTH/40003 就改用浏览器登录」的建议。
|
|
2924
|
+
- 未登录时的「登录方式」改为按**实际能力**显示(插件自开窗口 / 真实浏览器 + 调试协议 /
|
|
2925
|
+
手动 token),不再笼统写成「非 Electron 环境」。
|
|
2926
|
+
- 依据:端到端实测(2026-09-11)——仅凭 64 字符 Bearer token(无 cookie、无 x-hif-*)时,
|
|
2927
|
+
登录态校验、PoW 求解、真实生成、会话回收**全部通过**。
|
|
2928
|
+
|
|
2929
|
+
### 测试
|
|
2930
|
+
|
|
2931
|
+
- 产物核对新增 2 项:client 必须出现「凭证来源」且**不得**再出现「可能仍可用」;
|
|
2932
|
+
未登录时必须说明可用的登录路径。
|
|
2933
|
+
- 断言总数仍为 111(本次是展示层改动,逻辑未变);产物核对 42 → **44** 项。
|
|
2934
|
+
|
|
2935
|
+
## 0.1.7 — 2026-09-11
|
|
2936
|
+
|
|
2937
|
+
### 修复
|
|
2938
|
+
|
|
2939
|
+
- **DSH 更新后「浏览器窗口登录」打不开**(严重,用户实测:升级 DSH 后按钮点了没反应)
|
|
2940
|
+
- 现场证据(host 日志):
|
|
2941
|
+
`deepseek-web api /login/browser failed: Cannot read properties of undefined (reading 'fromPartition')`
|
|
2942
|
+
- 根因:**DSH 把插件宿主从 Electron 主进程挪到了 utility 进程**。探针实测
|
|
2943
|
+
`process.type === 'utility'`、Electron 43.3.0、`process.parentPort` 存在;
|
|
2944
|
+
utility 进程里 `require('electron')` 拿不到 `BrowserWindow` / `session`(主进程专属 API)。
|
|
2945
|
+
而旧的 `electronAvailable()` **只检查 `process.versions.electron`** → 假阳性通过 →
|
|
2946
|
+
随后炸在 `session.fromPartition`。
|
|
2947
|
+
- 另外:新架构**没有**给插件暴露任何「开窗口 / 开外部 URL」的通用服务
|
|
2948
|
+
(`desktopRuntime` 只有 openTerminal / pickDirectory / openProfileCreateWindow 这类专用接口),
|
|
2949
|
+
所以窗口式登录在新宿主里无法实现。
|
|
2950
|
+
|
|
2951
|
+
### 新增
|
|
2952
|
+
|
|
2953
|
+
- **真实浏览器 + CDP 登录**(取代无法使用的 Electron 窗口)
|
|
2954
|
+
- 拉起系统里真实的 **Edge / Chrome**(独立 profile `<DSH_HOME>/web-login/browser-profile`,
|
|
2955
|
+
不碰你日常浏览器的登录态),用 Chrome DevTools 协议读取:
|
|
2956
|
+
`localStorage.userToken`(AppKit 包装自动解包)、`Storage.getCookies`(cookie 串,
|
|
2957
|
+
免去 DPAPI 解密)、`Network.*` 里 `/api/*` 的**真实请求头**(x-hif-* / x-client-*)、
|
|
2958
|
+
`navigator.userAgent`。你只需在弹出的浏览器里正常登录,其余全自动。
|
|
2959
|
+
- 真实浏览器不会被网页端判「使用环境异常」(实测:无头 Edge 打开 chat.deepseek.com
|
|
2960
|
+
正文正常、无该提示)。
|
|
2961
|
+
- **必须用 `--remote-debugging-port=0`**:Windows 保留了大量端口区间
|
|
2962
|
+
(实测 8792-9897、10001-10100、50000-50059 等),硬编码端口会 `bind()` 失败
|
|
2963
|
+
(WSAEACCES 10013,Chromium 报 "Cannot start http server for devtools");
|
|
2964
|
+
端口 0 由系统分配,真实端口写在 `<profile>/DevToolsActivePort`。
|
|
2965
|
+
- **登录能力自检 + 面板展示**:`/status` 新增 `loginCapability`(宿主进程类型 / 能否开窗口 /
|
|
2966
|
+
有无可用浏览器),面板「登录状态」区直接显示「宿主进程」与当前登录方式。
|
|
2967
|
+
按钮文案随之自适应(「浏览器窗口登录」/「用 Microsoft Edge 登录」/ 禁用并提示走手动 token)。
|
|
2968
|
+
- `openExternalLogin`(用默认浏览器打开)不再依赖 Electron `shell`:
|
|
2969
|
+
非主进程时回退到 `cmd /c start`、`open`、`xdg-open`,任何宿主都能用。
|
|
2970
|
+
- 退出账号时**连带清掉浏览器登录 profile**(否则换号会复用旧登录态)。
|
|
2971
|
+
|
|
2972
|
+
### 变更
|
|
2973
|
+
|
|
2974
|
+
- `unwrapStoredToken` 移到 `auth.ts`(浏览器登录与 Electron 登录共用,避免循环 import),
|
|
2975
|
+
并显式处理实测形态 `{"value":null,"__version":"0"}` → 空串(绝不能把字符串 "null" 当 token)。
|
|
2976
|
+
- `electronAvailable()` 语义收紧为「真的能开窗口」;新增纯函数 `canOpenElectronWindowWith()`
|
|
2977
|
+
便于用事故现场参数做单测。
|
|
2978
|
+
|
|
2979
|
+
### 测试
|
|
2980
|
+
|
|
2981
|
+
- 新增 `tests/check-browser-login.mjs`(15 项):utility 进程必须判为不能开窗口(事故现场参数)、
|
|
2982
|
+
主进程/renderer/字符串模块/非 Electron/加载抛错五类判定、端口必须为 0、
|
|
2983
|
+
启动参数(独立 profile、不带 --headless)、`DevToolsActivePort` 解析(CRLF/越界/非法)、
|
|
2984
|
+
`{"value":null}` 解包、cookie 串拼装、真实请求头挑选、浏览器可执行文件存在性。
|
|
2985
|
+
- 断言总数 96 → **111**;产物核对 38 → **42** 项。
|
|
2986
|
+
|
|
2987
|
+
## 0.1.6 — 2026-09-11
|
|
2988
|
+
|
|
2989
|
+
### 修复
|
|
2990
|
+
|
|
2991
|
+
- **点「浏览器窗口登录」被网页端判为「使用环境异常」**(严重,本次用户实测)
|
|
2992
|
+
- 现象:登录窗口里显示
|
|
2993
|
+
「使用环境异常 —— 当前页面的使用环境可能存在数据和隐私泄露风险,为保障安全,
|
|
2994
|
+
建议您使用我们的官方产品。」
|
|
2995
|
+
- 根因:Electron 的默认 UA 里带**应用名与 `Electron/<版本>`** 字样,网页端一眼识别出
|
|
2996
|
+
「这不是普通浏览器」就拒绝服务。
|
|
2997
|
+
(探针证实:服务端对 Electron UA 与干净 Chrome UA 返回**完全相同的 HTML**,
|
|
2998
|
+
说明判定发生在**页面内 JS**,所以两处都要清。)
|
|
2999
|
+
- 修复:
|
|
3000
|
+
1. 登录窗口报**干净 Chrome UA**(`buildLoginUserAgent`,Chromium 大版本取运行时真实值),
|
|
3001
|
+
在 `session.setUserAgent` 与 `webContents.setUserAgent` 上同时设置,且**必须在 loadURL 之前**;
|
|
3002
|
+
2. 清 **UA-CH**(`sec-ch-ua` / `sec-ch-ua-full-version-list`)里的非浏览器品牌 ——
|
|
3003
|
+
只改 UA 字符串不够,Chromium 还会通过 client hints 把品牌列表发出去;
|
|
3004
|
+
3. 页面主世界里抹掉 `navigator.userAgentData.brands` 的非浏览器品牌与 `navigator.webdriver`;
|
|
3005
|
+
4. 捕获到的 UA 现在也是干净的(它会用于后续 API 请求,与网页端保持一致)。
|
|
3006
|
+
|
|
3007
|
+
### 新增
|
|
3008
|
+
|
|
3009
|
+
- **「用我的默认浏览器登录」兜底入口**(`POST /login/external`)
|
|
3010
|
+
- 网页端连干净指纹的 Electron 窗口也拦、或者用户就是想用自己的日常浏览器时用它:
|
|
3011
|
+
用系统默认浏览器打开 chat.deepseek.com,配合已有的「手动粘贴 Token」卡完成登录。
|
|
3012
|
+
- 说明:外部浏览器里的登录态插件抓不到(没有 webRequest 钩子),所以这条路径必须配合手动 token。
|
|
3013
|
+
- **指纹可观测**:面板新增「指纹清理 / 页面看到 UA / 页面品牌 / webdriver」四项。
|
|
3014
|
+
服务端不区分 UA,判定在页面内 —— 看不到「页面实际看到了什么」就只能靠猜,所以把它读出来展示。
|
|
3015
|
+
|
|
3016
|
+
### 测试
|
|
3017
|
+
|
|
3018
|
+
- 新增 `tests/check-login-fingerprint.mjs`(7 项):UA 不得含 Electron/应用名、版本取自运行时、
|
|
3019
|
+
缺版本不崩、UA-CH 品牌清理、干净 UA 原样保留、品牌被清空时的兜底、无关头不被改动。
|
|
3020
|
+
- 断言总数 89 → **96**;产物核对 34 → **38** 项。
|
|
3021
|
+
|
|
3022
|
+
## 0.1.5 — 2026-09-11
|
|
3023
|
+
|
|
3024
|
+
### 新增
|
|
3025
|
+
|
|
3026
|
+
- **退出当前账号 / 退出并登录其它账号**(用户反馈「怎么没有退出当前账号功能」)
|
|
3027
|
+
- 退出功能其实一直存在,但按钮只塞在「手动粘贴 Token」那张卡的角落里 → 实际上找不到。
|
|
3028
|
+
现在独立成一张 **「当前账号」卡**(在登录状态卡下方):显示当前账号与登录时间,
|
|
3029
|
+
提供 **退出当前账号** 与 **退出并登录其它账号** 两个按钮。
|
|
3030
|
+
- 退出需**二次确认**(首次点击变「确认退出?」,3 秒内再点一次才执行)——按钮不再可能被误触。
|
|
3031
|
+
- 未登录时按钮自动禁用;非 Electron 环境下「换号」按钮禁用并给出改用 token 的提示。
|
|
3032
|
+
|
|
3033
|
+
### 修复
|
|
3034
|
+
|
|
3035
|
+
- **退出账号其实没有真退出**(严重)
|
|
3036
|
+
- 旧实现只删本地凭证文件,**不清浏览器分区**里的 chat.deepseek.com 站点数据。后果:
|
|
3037
|
+
1) 退出后点「从已登录窗口恢复」会把**同一个账号**原样抓回来,看起来像「退不掉」;
|
|
3038
|
+
2) 点「浏览器窗口登录」打开的是已登录页面,**根本没法换号**。
|
|
3039
|
+
- 现在 `logout()` 会 `clearStorageData({ origin: 'https://chat.deepseek.com' })`(cookie/localStorage/
|
|
3040
|
+
IndexedDB/CacheStorage/ServiceWorker),并且**等清理完成再返回** —— 否则紧接着打开的登录窗口
|
|
3041
|
+
会带着旧 cookie 开起来,又登录回同一个账号。
|
|
3042
|
+
- **卸载/热重载插件会把用户登出**(隐患)
|
|
3043
|
+
- 卸载钩子里调的是 `logout()`(连带删除凭证)。改为只关登录窗口(`closeLoginWindow()`):
|
|
3044
|
+
卸载插件不等于登出。
|
|
3045
|
+
|
|
3046
|
+
### 测试
|
|
3047
|
+
|
|
3048
|
+
- 新增 `tests/check-logout.mjs`(6 项):凭证写入/读回、非 Electron 下分区清理返回 false 不抛错、
|
|
3049
|
+
`closeLoginWindow` 不删凭证(卸载即净的回归)、`logout` 真删文件且留下可读结果、幂等、退出后
|
|
3050
|
+
`readAuth()` 必须为 undefined(无从恢复旧账号)。
|
|
3051
|
+
- 断言总数 83 → **89**;产物核对 34 项(新增退出/换号相关 5 项)。
|
|
3052
|
+
|
|
3053
|
+
## 0.1.4 — 2026-09-11
|
|
3054
|
+
|
|
3055
|
+
### 修复
|
|
3056
|
+
|
|
3057
|
+
- **会话被自己提前删掉,导致整轮失败**(严重,本次用户实测)
|
|
3058
|
+
- 现象:网页快速模式下某一轮直接失败
|
|
3059
|
+
`DeepSeek 网页端返回了非流式响应(content-type: application/json):
|
|
3060
|
+
{"code":0,"msg":"","data":{"biz_code":1,"biz_msg":"invalid chat session id"}}`
|
|
3061
|
+
- 根因:`streamWebCompletion` 在建会话**之后立刻**调用 `onDeleteSession`,而它内部是
|
|
3062
|
+
「延迟 1.5s 删除」——只要 PoW 求解 + 建连超过 1.5s,completion 发出时会话已被自己删掉。
|
|
3063
|
+
更隐蔽的是**生成进行到一半会话消失**,服务端可能直接掐断流 —— 正是我们一直在追的
|
|
3064
|
+
「说半句就停 / 工具调用没收全」那类截断的一个来源。
|
|
3065
|
+
- 修复:删除只发生在 `finally`(流正常结束 / 报错 / 调用方中止都算),会话在整个请求期间保持存活。
|
|
3066
|
+
- 反证:同一场景下旧时序为 `create → delete → pow → completion`(删除早于请求,故障成因成立),
|
|
3067
|
+
新时序为 `create → pow → completion → delete`。已固化为断言。
|
|
3068
|
+
- **服务端的真实错误被吞掉**(严重)
|
|
3069
|
+
- 根因:`envelopeError` 只看外层 `code`,而网页端把真实错误放在 `data.biz_code`(外层恒为 0)
|
|
3070
|
+
→ 真正的 `biz_msg` 丢失,统一降级成不可重试、无法诊断的
|
|
3071
|
+
`非流式响应(content-type: application/json)` + `MALFORMED_RESPONSE`。
|
|
3072
|
+
- 修复:识别 `data.biz_code` / `data.biz_msg`;并把「会话失效」映射成可重试的 `TRANSPORT`
|
|
3073
|
+
——**换一个新会话透明重试一次**,用户无感(本插件每次都是全新会话、不依赖服务端历史)。
|
|
3074
|
+
- **账号被服务端限制时给不出人话**(由上面那条修复才暴露出来)
|
|
3075
|
+
- 实测信封:`{"biz_code":5,"biz_msg":"user is muted","biz_data":{"is_muted":1,"mute_until":…}}`
|
|
3076
|
+
- 现在会明确报出**解除时间**,并带上 `providerRetryAfterMs`(远超重试上限)→ 重试策略直接放弃,
|
|
3077
|
+
不再空转打请求(免费网页端对高频自动化调用会静默限流,空转只会更糟)。
|
|
3078
|
+
- **上下文容量数字写错了**(用户指出)
|
|
3079
|
+
- 旧文档/代码把 `file_feature.token_limit = 890880` 当成「模型上下文窗口」,还写成「1M 扣输出预留」。
|
|
3080
|
+
实际它是**附件(file_feature)的 token 预算**;890880 = 870×1024,面板按 ÷1024 显示就成了「870K」,
|
|
3081
|
+
看起来像「说好的 1M 变成了 870K」。
|
|
3082
|
+
- 现场逐字段核对(`GET /api/v0/client/settings?scope=model`,configVersion 81)后改为:
|
|
3083
|
+
`contextWindow = 1048576`(标称 1M),`maxPromptChars` 默认 `1200000 → 1500000`
|
|
3084
|
+
(服务端单请求输入硬上限是 `input_character_limit = 2621440` 字符,留 ~43% 余量给 CJK 的字符/token 比)。
|
|
3085
|
+
- 面板文案同步改为「上下文 1M token(标称)」,并注明 890880 是附件预算、不是上下文窗口。
|
|
3086
|
+
|
|
3087
|
+
### 测试
|
|
3088
|
+
|
|
3089
|
+
- 新增 `tests/check-session-lifecycle.mjs`(10 项):删除时机、会话失效透明重试、
|
|
3090
|
+
连续失效报可重试码、`data.biz_code` 识别、`user is muted` 文案与 `providerRetryAfterMs`、
|
|
3091
|
+
以及「非会话类业务错误不得被误判」。
|
|
3092
|
+
- 断言总数 46 → **83**(logic 46 / badjson 11 / dsml 6 / echo 10 / lifecycle 10),产物核对 25 项。
|
|
3093
|
+
|
|
3094
|
+
## 0.1.3 — 2026-09-10
|
|
3095
|
+
|
|
3096
|
+
### 修复
|
|
3097
|
+
|
|
3098
|
+
- **模型漏写调用对象的闭合括号时,整段工具调用 JSON 泄漏成正文**(严重)
|
|
3099
|
+
- 现象:回复里出现一大段 `{"tool_calls":[…}`,且界面把它渲染成「一个字符一行 + 弯引号」的乱码。
|
|
3100
|
+
- 根因有两层:
|
|
3101
|
+
1. 模型侧:一次批量 3 个 `pwsh` 调用时,**每个调用对象都少写一个 `}`**(只闭合了自己的
|
|
3102
|
+
`arguments`)→ `JSON.parse` 报 `Expected double-quoted property name in JSON at position 519`;
|
|
3103
|
+
2. 适配器侧:解析失败后按「绝不静默丢内容」的旧约定把残留**原样当正文吐出**。
|
|
3104
|
+
而泄漏文本里的 `$ErrorActionPreference='…'; foreach($p in …)` 被 Web GUI 的 markdown
|
|
3105
|
+
当成 `$…$` 行内公式交给 KaTeX → 用户看到的是逐字符排版的乱码(`'` 还被渲染成 `′`)。
|
|
3106
|
+
- 修复:
|
|
3107
|
+
1. 新增**结构性修复**:按元素边界切开 `tool_calls` 数组,给每个元素补齐它自身缺的 `}`
|
|
3108
|
+
(只补括号,绝不改写内容);
|
|
3109
|
+
2. **安全闸门**:只在数组已闭合(`]` 收尾,说明模型写完了)时才补 —— 流被 60s 上限截断时
|
|
3110
|
+
补括号会造出一条被截断的命令并真的执行它,宁可拒绝;
|
|
3111
|
+
3. 解析仍失败时**不再吐成正文**:本轮没有别的正文 → 报 `EMPTY_RESPONSE`(默认可重试码,
|
|
3112
|
+
自动重发这一步);已有正文 → 补一句人话提示,原文只进日志。
|
|
3113
|
+
- 反证:真实原文 `JSON.parse` 必失败(position 519),修补后解析出 3 个调用、命令逐字不变
|
|
3114
|
+
(含 `$env:USERPROFILE\.dsh\super-injector\self-heal.log` 全程无反斜杠丢失、无 `\r`);
|
|
3115
|
+
去掉 `]` 的截断版本必须**拒绝修补**(不得执行半条命令)。
|
|
3116
|
+
- 新增 4 条断言(含 1650 字符真实原文的端到端回归)。
|
|
3117
|
+
|
|
3118
|
+
## 0.1.2 — 2026-09-10
|
|
3119
|
+
|
|
3120
|
+
### 修复
|
|
3121
|
+
|
|
3122
|
+
- **跨包工具调用标记的 hold-back 判断失效,导致合法 JSON 泄漏成正文**(严重)
|
|
3123
|
+
- 现象:模型把工具调用拆成多个分块流式输出时,完整合法的
|
|
3124
|
+
`{"tool_calls":[{"name":"find_dsh_plugin",…}]}` 直接显示在回复里,工具没有被执行。
|
|
3125
|
+
- 根因:判断「缓冲区末尾是否可能是标记前缀」时,比较串**多拼了一个引号**
|
|
3126
|
+
(`` `{"${body}` ``,而 `body` 已含前引号 → 实际比较 `{""tool`),于是永远判 `false`。
|
|
3127
|
+
正文较长(超过 hold-back 窗口)时,半截标记 `{"tool` 被当正文吐出去,后半个分块再也拼不回
|
|
3128
|
+
完整标记 —— 整个 JSON 泄漏。会话日志原文:
|
|
3129
|
+
`" GUI's capabilities.\n\n{\"tool"` + `"_calls\":[{\"name\":\"find_dsh"`
|
|
3130
|
+
- 修复:前缀拼接改为 `{` + body;并用**真实会话日志的分块序列**固化为回归断言。
|
|
3131
|
+
- 反证:`{"tool` / `{"tool_` / `{"too` / `{"tool_call` 在旧判断下全为 `false`(不保留 → 泄漏),新判断全为 `true`。
|
|
3132
|
+
- 新增 2 条断言:真实分块序列(长正文 + JSON 拆成两半)、XML 标记同样拆两半。
|
|
3133
|
+
|
|
3134
|
+
## 0.1.1 — 2026-09-10
|
|
3135
|
+
|
|
3136
|
+
### 修复
|
|
3137
|
+
|
|
3138
|
+
- **SSE 去重模型错误导致回答被大量丢失**(严重)
|
|
3139
|
+
- 现象:完整回答在 DSH 里只显示 1~3 字碎片(如「,」「不上」「了一圈」),
|
|
3140
|
+
并伴随 `EMPTY_RESPONSE` 重试(会话日志中可见同一 step 出现 `usage×2 / finish×2`)。
|
|
3141
|
+
- 根因:旧实现用「只增不减的已发射计数器」做快照去重,而网页端的**快照会把派生文本重置为更短内容**
|
|
3142
|
+
(例如只含 THINK 片段的快照)。计数器被撑大后保持高位,后续仅在文本长度超过它时才吐字 ——
|
|
3143
|
+
前面全丢、只剩余数的尾巴;余数为空则被判定为空响应,触发重试。
|
|
3144
|
+
- 修复:改为「**增量事件驱动发射,快照只做严格延伸对账**」。已发射内容只增不减,
|
|
3145
|
+
过期(更短)快照与分歧快照一律忽略,绝不重置、绝不吐碎片。
|
|
3146
|
+
- 反证:同一事件序列下旧算法只输出 `"抱歉,我把生态找没有现成插件。"`(中间丢失);
|
|
3147
|
+
连续缩水快照的极端形态下 6 段内容只活下来 6 个字符。新算法输出完整文本。
|
|
3148
|
+
- 新增 5 条回归断言覆盖:缩水快照丢字、假空回复、快照延伸补差、分歧快照忽略、直连格式穿插快照。
|
|
3149
|
+
|
|
3150
|
+
### 新增
|
|
3151
|
+
|
|
3152
|
+
- `CHANGELOG.md`(本文件)
|
|
3153
|
+
- README 徽章、架构/流程示意图、界面预览、英文版 `README.en.md`
|
|
3154
|
+
|
|
3155
|
+
## 0.1.0 — 2026-09-10
|
|
3156
|
+
|
|
3157
|
+
首个可用版本。
|
|
3158
|
+
|
|
3159
|
+
- LLM 适配器(provider `deepseek-web`):PoW(SHA3 WASM 求解)、会话创建与清理、
|
|
3160
|
+
SSE 双格式流式解析、图片上传通道(`file/upload_file` + `ref_file_ids`)
|
|
3161
|
+
- 提示词工具协议桥:JSON 协议 + XML/DSML 兜底 + 非法 JSON 宽容修复(Windows 路径逐字还原)
|
|
3162
|
+
- Electron 登录窗口旁路捕获凭证:AppKit token 解包、游客 token 持续刷新、fail-open 落盘、
|
|
3163
|
+
从已登录分区恢复
|
|
3164
|
+
- 设置面板:状态 / 浏览器登录 / 手动 token / 连通性测试
|
|
3165
|
+
- Apache-2.0(`LICENSE` / `NOTICE`)、非官方声明与风险提示
|