dsh-xdpool 2.4.6 → 2.5.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 +135 -0
- package/lib/bin.js +40 -7
- package/lib/client.js +151 -9
- package/lib/index.js +54 -8
- package/package.json +1 -1
- package/vendor/channel-pack/pack.js +151 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,141 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
本项目的所有重要变更都会记录在此文件。
|
|
3
3
|
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
|
+
|
|
5
|
+
## 2.5.0 — 十张 issue 一起收:失效账号、第三条指纹、CodeArts 上限、保存回滚、侧栏开关
|
|
6
|
+
|
|
7
|
+
> #68 到 #83 攒了十来张单子,其中三张(#72/#73/#74)是同一类毛病:代码把一个「没数据」和「数据是空」当成了同一件事。这批按这个线索统一收拾,另外把社区两个 PR 收了进来。
|
|
8
|
+
|
|
9
|
+
### 最要紧的三条
|
|
10
|
+
|
|
11
|
+
| 症状 | 真实原因 |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| 一个失效账号吃掉整次请求的 8 次重试,最后报「API 密钥无效」 | 以「刷新拿到了新 token」判定账号仍可用。有一类账号 refresh 长期有效、chat 恒回 403,判死条件因此恒假 |
|
|
14
|
+
| buddy 某一轮被中断后,之后每一轮都失败(`a message has empty content`) | 中断留下的 assistant 消息只有 reasoning、没有正文,序列化时空字符串被原样发到 wire,国内网关 400/11151 且不在重试集合里 |
|
|
15
|
+
| zcode 全部模型勾选后,每次重启又变回全禁用 | 用户全开时黑名单是空对象,与「从未选过」无法区分,启动补静音据此又把它们全关一遍 |
|
|
16
|
+
|
|
17
|
+
### #68 失效账号耗尽重试预算
|
|
18
|
+
|
|
19
|
+
池车道以「刷新成功」当「账号可用」。对 refresh 长期有效而业务侧恒 403 的账号,`recovered` 与 `accessToken !== before` 恒真 ⇒ 永不 `penalizeCredentialDead` ⇒ `available()` 不排除 ⇒ `priority` 下 `acquire()` 一直返回同一个 ⇒ 8 次预算全花在它身上。
|
|
20
|
+
|
|
21
|
+
改法:单次请求内只允许该账号换一次票(`refreshedThisRequest`),第二次仍报 `session_dead` 就直接判死并轮到下一个账号。压缩重试路径同步。取「第二次」而不是把惩罚提到刷新之前,是为了仍给真正过期的 token 一次机会。
|
|
22
|
+
|
|
23
|
+
### #70 侧栏入口开关
|
|
24
|
+
|
|
25
|
+
新增 `sidebarRow` 配置(默认开)。**只**控制左侧栏那一行:设置页的 XD Pool 卡片始终注册,模型池不受影响 —— 关掉入口不能把开关本身也锁在门外。开关写在卡片里,`apply()` 与卡片读同一份设置镜像,因此两边不会给出不同答案;宿主发布了变更通知的平台上无需重启。
|
|
26
|
+
|
|
27
|
+
### #72 订阅包余额虚高 30 倍
|
|
28
|
+
|
|
29
|
+
`fetchCredits` 只把 `CapacityType === 4` 当月包,而实际有三种:`1`=一次性包、`2`=**周期订阅包**(12 期 × 4000,`CapacityRemain` 是整期剩余、`CycleCapacityRemain` 才是当期剩余)、`4`=月包。`type=2` 落进「一次性」分支,整期额度被当余额。三账号实测:官方 3,399.8 / 插件 102,396 → 修复后 3,366;另两个只有月包的账号(3,049 / 2,771)不变,所以长期没被发现。
|
|
30
|
+
|
|
31
|
+
`expiresAtMs` 对 `type=2` 取 `DeductionEndTime`,**不能用 `CycleEndTime`** —— 它每月都到,会让「会过期」天天报警。空包只在一次性包时丢弃,月包当期耗尽要留着显示「0/500 · 下月回满」。
|
|
32
|
+
|
|
33
|
+
Refs #72。
|
|
34
|
+
|
|
35
|
+
### #73 中断回合留下的空 assistant 消息
|
|
36
|
+
|
|
37
|
+
`serializeMessages2` 出口加 `isBlankWireMessage` / `dropBlankWireMessages`,`serializeMessages4` 同步。三点取舍都用真实请求验证过:`user` 空消息不动(两网关都容忍);不伪造占位内容(会污染模型看到的对话);**有 `tool_calls` 的 assistant 消息绝不删**(它是 tool 结果的配对锚点,删了造孤儿)。
|
|
38
|
+
|
|
39
|
+
实测矩阵(国内):空 assistant 400/11151;空 system 同样 400/11151;空 user 200;`content:null` + `tool_calls` 200;删掉空 assistant 后正常 SSE。国际网关对全部形态都 200,所以故障面目前只在 buddy 车道。
|
|
40
|
+
|
|
41
|
+
Refs #73。
|
|
42
|
+
|
|
43
|
+
### #74 全开渠道每次重启被重新禁用
|
|
44
|
+
|
|
45
|
+
`setModelDisabled` 打开模型时是**删键**而不是写 `false`,于是「用户全开」与「从未选择」在磁盘上是同一个空对象。新增持久标记 `selectionTouched`:四个用户入口(`setModelDisabled` / `setModelsDisabled` / `clearModelsDisabled` / `clearDisabledModels`)都置位,启动补静音只在标记缺失时执行,且自身写黑名单时传 `{ mark: false }` 不置位。
|
|
46
|
+
|
|
47
|
+
影响面是「所有模型都打开」的渠道,目录越小越必然(zcode 2 个、gemini/minimax/raccoon 同理);部分启用的渠道免疫,这解释了为什么只有 zcode 反复被报。
|
|
48
|
+
|
|
49
|
+
Refs #74。
|
|
50
|
+
|
|
51
|
+
### #75 保存模型选择后界面回滚 30 秒
|
|
52
|
+
|
|
53
|
+
`saveModels` 收尾是 `setDraftFor(region, undefined)`,而它的语义是「退回池子最后报告的状态」,那份状态靠 30 秒轮询刷新 —— 于是保存后卡片画的是**保存前**的清单,用户在这 30 秒里再点一次保存,写入的就是回滚后的旧清单。另外 `settingsScope.set` 被声明成 `Promise<void>`,把宿主 revision 冲突时返回的 `false` 丢掉了,被拒绝也提示「已保存」。
|
|
54
|
+
|
|
55
|
+
改法:先 `refresh(region)` 再清草稿(重读失败则保留草稿);`set` 返回类型放宽到 `boolean | void`,`=== false` 时保留草稿、报「设置已在别处被修改」、不显示已保存。
|
|
56
|
+
|
|
57
|
+
Refs #75。
|
|
58
|
+
|
|
59
|
+
### #82 RPC 桥惰性解析
|
|
60
|
+
|
|
61
|
+
2.4.6 的 CHANGELOG 声称这条已修,但发布产物里 `installPackRpc` 仍只捕获一次。现已改为每次 `callPack` 前 `if (packRpcBridge === undefined) packRpcBridge = resolvePackRpcBridge()`:`connection` 就绪晚于 `apply()` 时,桥不会再永久为空、退化成裸 `fetch('/api/channel-pack')` 吃 401 而让账号池整片空白。
|
|
62
|
+
|
|
63
|
+
Refs #82。
|
|
64
|
+
|
|
65
|
+
### 同批合并的社区 PR
|
|
66
|
+
|
|
67
|
+
- **#81([@jmglsi](https://github.com/jmglsi))** 第三条内容指纹:`Relative paths resolve from the workspace root` → `are resolved against the workspace root`。用陈述句的过去分词形式,语义、长度、换行位置都不变,被网关字符串表收录的子串消失。
|
|
68
|
+
- **#84([@Refeain](https://github.com/Refeain))** CodeArts 输出上限见下节。
|
|
69
|
+
|
|
70
|
+
### #69 / #71 尚未定性
|
|
71
|
+
|
|
72
|
+
`#69 CodeArts 提示密钥无效` 只有一句话。它至少有两条来源:本条 `InferHub.4001.200`(SSE 帧里的 200,不是 4xx)与真正的 401/403。前者已随 #83 修掉;后者需要失败时的原始响应才能确认。
|
|
73
|
+
`#71 kimi k3.1 pro 没有响应` 同样只有一句话,且目录里没有 `kimi-k3.1-pro` 这个 id(有 `kimi-k3-1`)。请在 issue 里补一下:是空回复、卡住不动、还是有报错文案。
|
|
74
|
+
|
|
75
|
+
### 详情:CodeArts 的输出上限是上限,不是默认值
|
|
76
|
+
|
|
77
|
+
> 华为这条链接 65536 能用、131072 就空流,代码里也一直写着这个数字。但它是 `options.maxTokens ?? 65536` —— 只兜底,不设界。
|
|
78
|
+
|
|
79
|
+
### 触发条件:宿主把别的 provider 的额度原样递过来
|
|
80
|
+
|
|
81
|
+
宿主从模型目录里取 `maxTokens` 填进 `options.maxTokens`,而那份目录里 384000 是 deepseek-v4-pro、fireworks、huggingface 这些**真的能到 384000** 的条目的正常值。CodeArts 自己没声明上限时,这个 384000 就原样发出去了。
|
|
82
|
+
|
|
83
|
+
后果不是 4xx:后端回 **HTTP 200**,把错误装在 SSE 帧里 ——
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
InferHub.4001.200 The request param is invalid, Please check it
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
适配器把它映射成 `INVALID_REQUEST`。用户看到的是「这个账号好好的,突然发不出话」,而新建会话发「你好」就失败,第一反应必然是密钥坏了 —— 这也是 #69「CodeArts 提示密钥无效」的一条候选根因(#69 正文只有一句话,无法确认是这条还是 403 那条,故 #83 里保留判断)。
|
|
90
|
+
|
|
91
|
+
### 实测(2026-10-25,失败会话的原文 system/user + 全部 133 个 tools 直连,只改 max_tokens)
|
|
92
|
+
|
|
93
|
+
| max_tokens | 结果 |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| 384000 | HTTP 200 + SSE 错误帧 `InferHub.4001.200` → INVALID_REQUEST |
|
|
96
|
+
| 131072 | HTTP 200,空流 |
|
|
97
|
+
| 65536 | 正常流式回复 |
|
|
98
|
+
|
|
99
|
+
### 改法:三处对齐,线上请求做 clamp
|
|
100
|
+
|
|
101
|
+
- `listModels()` 每个模型带 `maxTokens`,宿主没有理由再自己猜一个更大的
|
|
102
|
+
- `resolveModel()` 带 `defaultMaxTokens`,`catalogFactsFor()` 会把它当 `fact.maxTokens` 交给宿主 —— 这是宿主唯一能知道上限的通道
|
|
103
|
+
- 线上 `max_tokens: Math.min(options.maxTokens ?? CODEARTS_MAX_OUTPUT_TOKENS, CODEARTS_MAX_OUTPUT_TOKENS)`,调用方给更大就收回,给更小仍保留
|
|
104
|
+
|
|
105
|
+
TRAE 与 Cline 早就这么做了(`clampTraeMaxTokens` / `clampClineMaxTokens`,也都写 `resolved.defaultMaxTokens`),CodeArts 是唯一的例外。
|
|
106
|
+
|
|
107
|
+
- 回归:`tests/codearts-max-tokens.test.ts` 新增 5 例(源码断言,与 `tests/codearts-resign-headers.test.ts` 同形状 —— `pack.js` 是另一个仓库的构建产物,无法 import 驱动)
|
|
108
|
+
- 反向对照:撤掉 `pack.js` 的补丁后 5 例全红,补上全绿
|
|
109
|
+
|
|
110
|
+
Refs #83, #69。
|
|
111
|
+
|
|
112
|
+
### 详情:第三条内容指纹:文件引用指引里的 “resolve from the workspace root”
|
|
113
|
+
|
|
114
|
+
> 2.4.1/2.4.2 修好的两个指纹管了一周,10 月 28 日又整片 400/11128。这次被点名的不是产品名,而是一句普通说明。
|
|
115
|
+
|
|
116
|
+
### 触发点在 read 工具的文件引用指引
|
|
117
|
+
|
|
118
|
+
DSH 在 agent 有 `read` 工具时会贡献一段文件引用指引,开头是「Tokens prefixed with @ are paths the user explicitly referenced.」,第二句是 **「Relative paths resolve from the workspace root」**。把当次真实系统提示(约 41k 字符)整段发上去稳定 400/11128,逐段二分到 204 字符命中这段,再逐句验证:两句**单独发都是 200**,两句同现才 400;改其中任意一处小改动也 200 —— 所以这是内容指纹,不是长度规则、也不是头部规则。
|
|
119
|
+
|
|
120
|
+
这也解释了为什么它看起来「不像」前两条:前两条是识别别的客户端的文案,这一条只是被网关的字符串表收进去了,句子本身没有任何产品名。
|
|
121
|
+
|
|
122
|
+
### 改法:把陈述句改成同一句的过去分词形式
|
|
123
|
+
|
|
124
|
+
`Relative paths resolve from the workspace root` → `Relative paths are resolved against the workspace root`。语义、长度量级、换行位置都不变,被列表收录的子串消失。同一账号上整段 41k 系统提示由 400 转 200;渠道车道与池车道同步补上,`pack.js` 的三处 `[xdpool-local]` 标记保持原样。
|
|
125
|
+
|
|
126
|
+
- 池车道:`BANNED_SYSTEM_PHRASES` 增加第三行
|
|
127
|
+
- 渠道车道:`vendor/channel-pack/pack.js` 的同一个 `replace` 链增加第三段
|
|
128
|
+
- 回归:`tests/system-fingerprint.test.ts` 新增 5 例(含「三处指纹同现」整段用例);`tests/chat-identity-headers.test.ts` 对已发布 bundle 的字节断言补上第三段,pack 刷新若把它冲掉会变红
|
|
129
|
+
|
|
130
|
+
### 其他
|
|
131
|
+
- 全套 79 文件 1028 用例(1026 通过 + 2 跳过)。
|
|
132
|
+
|
|
133
|
+
### 验证
|
|
134
|
+
|
|
135
|
+
- 全套 **80 文件 / 1033 用例**(含 #81 的 5 例、#84 的 5 例、本批新增)
|
|
136
|
+
- `pack.js` 每次改动后 `node --check` 通过
|
|
137
|
+
- server `tsc` 0 错;client `tsc` 的 6 条是既存问题(`accounts.ts` / `context-budget.ts` / `free-upstream.ts` 的 Node 类型不匹配),改前改后数量一致
|
|
138
|
+
|
|
4
139
|
## 2.4.6 — 扫码账号每次打开都空白:一份数据卡在两个细节里
|
|
5
140
|
|
|
6
141
|
> 2.4.4/2.4.5 之后侧栏能进、能滚了,但扫码过的渠道在账号池里经常整片空白,重开也一样。磁盘上账号是齐的(`state.json` 里 15 条),所以问题全在读取链。
|
package/lib/bin.js
CHANGED
|
@@ -339,10 +339,39 @@ const CHAT_IDENTITY_HEADERS = Object.freeze({
|
|
|
339
339
|
* rewrite is applied to assistant history too — one past reply carrying a
|
|
340
340
|
* fingerprint would otherwise fail every later request in the conversation.
|
|
341
341
|
*
|
|
342
|
+
* A THIRD FINGERPRINT (measured against the live domestic gateway 2026-10-28)
|
|
343
|
+
*
|
|
344
|
+
* The file-reference guidance DSH contributes whenever the agent has a `read`
|
|
345
|
+
* tool reintroduced the same 400/11128 after the two rewrites above shipped:
|
|
346
|
+
*
|
|
347
|
+
* Tokens prefixed with @ are paths the user explicitly referenced.
|
|
348
|
+
* Relative paths resolve from the workspace root; absolute paths identify
|
|
349
|
+
* files or directories on the host.
|
|
350
|
+
*
|
|
351
|
+
* Ordinary phrasing throughout — the blocked region names no product — so the
|
|
352
|
+
* fingerprint is not "this prompt is not WorkBuddy's" but a literal substring
|
|
353
|
+
* list that happens to include this sentence. Neither sentence is the trigger
|
|
354
|
+
* on its own: each 200s in isolation, and the pair 400s; the smallest edit
|
|
355
|
+
* anywhere in the pair clears it, which is what marks it as content and not
|
|
356
|
+
* prompt length, and the matching half is the second sentence ("Tokens
|
|
357
|
+
* prefixed with @" alone stayed clean while the "Relative paths…" line was the
|
|
358
|
+
* one whose text the gateway recognised).
|
|
359
|
+
*
|
|
360
|
+
* The rewrite turns the indicative into the same statement's participle —
|
|
361
|
+
* "Relative paths resolve from the workspace root" becomes "Relative paths are
|
|
362
|
+
* resolved against the workspace root". Meaning, length class and line breaks
|
|
363
|
+
* are preserved; the substring the gateway lists is gone. Verified at 200 with
|
|
364
|
+
* the full runtime system prompt (≈41k chars) that reproduced the 400 on the
|
|
365
|
+
* same account, and with the pair alone.
|
|
366
|
+
*
|
|
342
367
|
* The list is an ARRAY because these fingerprints are appended to over time; a
|
|
343
368
|
* future rejection should be one more row here, not a new code path.
|
|
344
369
|
*/
|
|
345
|
-
const BANNED_SYSTEM_PHRASES = Object.freeze([
|
|
370
|
+
const BANNED_SYSTEM_PHRASES = Object.freeze([
|
|
371
|
+
[/powered by DeepSeek Harness/gi, "running on DeepSeek Harness"],
|
|
372
|
+
[/DeepSeek Harness Web GUI/gi, "DeepSeek Harness web interface"],
|
|
373
|
+
[/Relative paths resolve from the workspace root/gi, "Relative paths are resolved against the workspace root"]
|
|
374
|
+
]);
|
|
346
375
|
/**
|
|
347
376
|
* Rewrite fingerprinted system/assistant content, leaving everything else
|
|
348
377
|
* byte-identical.
|
|
@@ -1325,14 +1354,17 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1325
1354
|
if (typeof raw !== "object" || raw === null) continue;
|
|
1326
1355
|
const account = raw;
|
|
1327
1356
|
const num = (key) => typeof account[key] === "number" ? account[key] : 0;
|
|
1328
|
-
const
|
|
1329
|
-
const
|
|
1330
|
-
const
|
|
1357
|
+
const capacityType = num("CapacityType");
|
|
1358
|
+
const monthly = capacityType === 4;
|
|
1359
|
+
const periodic = monthly || capacityType === 2;
|
|
1360
|
+
const size = periodic ? num("CycleCapacitySize") : num("CapacitySize");
|
|
1361
|
+
const remain = periodic ? num("CycleCapacityRemain") : num("CapacityRemain");
|
|
1331
1362
|
const capped = remain < 0 ? 0 : remain;
|
|
1332
1363
|
const cycleEndMs = parseDate(account["CycleEndTime"]);
|
|
1333
|
-
const expiresAtMs = monthly ? void 0 : parseDate(account["ExpiredTime"]) ?? cycleEndMs;
|
|
1364
|
+
const expiresAtMs = monthly ? void 0 : capacityType === 2 ? parseDate(account["DeductionEndTime"]) : parseDate(account["ExpiredTime"]) ?? cycleEndMs;
|
|
1334
1365
|
const refreshAtMs = monthly ? cycleEndMs === void 0 ? void 0 : cycleEndMs + 1e3 : void 0;
|
|
1335
|
-
if (
|
|
1366
|
+
if (expiresAtMs !== void 0 && expiresAtMs <= Date.now()) continue;
|
|
1367
|
+
if (!periodic && capped <= 0) continue;
|
|
1336
1368
|
total += capped;
|
|
1337
1369
|
if (expiresAtMs !== void 0) {
|
|
1338
1370
|
if (nearestExpiryMs === void 0 || expiresAtMs < nearestExpiryMs) nearestExpiryMs = expiresAtMs;
|
|
@@ -11274,7 +11306,8 @@ z.object({
|
|
|
11274
11306
|
modelSelectionCn: asVolatile(modelSelectionSchema.description("Model selection for the domestic gateway")),
|
|
11275
11307
|
modelSelectionGlobal: asVolatile(modelSelectionSchema.description("Model selection for the international gateway")),
|
|
11276
11308
|
automation: asVolatile(automationSchema.description("Daily points automation (activity report, task claiming, check-in)")),
|
|
11277
|
-
automationEarnings: asVolatile(z.any().description("Automation earnings ledger (written by the scheduler)"))
|
|
11309
|
+
automationEarnings: asVolatile(z.any().description("Automation earnings ledger (written by the scheduler)")),
|
|
11310
|
+
sidebarRow: asVolatile(z.boolean().default(true).description("Show the XD Pool entry in the left sidebar (the Settings page keeps working either way)"))
|
|
11278
11311
|
});
|
|
11279
11312
|
/** Assemble the runtime objects without registering anything. */
|
|
11280
11313
|
/**
|
package/lib/client.js
CHANGED
|
@@ -7,6 +7,44 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
//#endregion
|
|
8
8
|
let react = require("react");
|
|
9
9
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
//#region src/client/sidebar-row.ts
|
|
11
|
+
/**
|
|
12
|
+
* The `sidebarRow` switch, read and written from one place.
|
|
13
|
+
*
|
|
14
|
+
* Two independent owners need this flag: the page card draws the checkbox, and
|
|
15
|
+
* `apply()` decides whether to register the sidebar row before that card has ever
|
|
16
|
+
* mounted. Both therefore have to read the same source of truth — the settings
|
|
17
|
+
* mirror the host exposes — instead of one asking the server and the other
|
|
18
|
+
* guessing.
|
|
19
|
+
*
|
|
20
|
+
* The scope is typed structurally here rather than imported from `PoolCard.tsx`:
|
|
21
|
+
* this module is pulled into both tsconfigs (the server one has no JSX setting),
|
|
22
|
+
* and the only thing it needs from that component is `getSnapshot().value`.
|
|
23
|
+
*
|
|
24
|
+
* The default is ON, and the read is fail-open on purpose: "the mirror has no
|
|
25
|
+
* opinion yet" and "the user turned it off" must not be the same answer, or a
|
|
26
|
+
* host that has not populated the mirror would silently hide a feature nobody
|
|
27
|
+
* switched off. That is also why only the literal `false` disables the row.
|
|
28
|
+
*/
|
|
29
|
+
/** Settings field the switch writes (declared in the plugin Config schema). */
|
|
30
|
+
const SIDEBAR_ROW_FIELD = "sidebarRow";
|
|
31
|
+
/**
|
|
32
|
+
* Whether the sidebar entry should exist.
|
|
33
|
+
*
|
|
34
|
+
* Never throws: a locked-down host makes `getSnapshot()` unusable, and the
|
|
35
|
+
* documented degradation there is the settings page, not a missing entry.
|
|
36
|
+
*/
|
|
37
|
+
function readSidebarRow(scope) {
|
|
38
|
+
if (scope === void 0) return true;
|
|
39
|
+
try {
|
|
40
|
+
const value = scope.getSnapshot().value;
|
|
41
|
+
if (value === null || typeof value !== "object") return true;
|
|
42
|
+
return value[SIDEBAR_ROW_FIELD] !== false;
|
|
43
|
+
} catch {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
10
48
|
//#region src/status-paths.ts
|
|
11
49
|
/**
|
|
12
50
|
* Node-free constants and types shared by the Host and browser halves of the
|
|
@@ -518,6 +556,7 @@ window.__ModuleLoader__.load({
|
|
|
518
556
|
.dsm-xdpool-usage-model{font-size:12px;font-weight:600;word-break:break-all}
|
|
519
557
|
.dsm-xdpool-usage-numbers{color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:17px;word-break:break-word}
|
|
520
558
|
.dsm-xdpool-usage-note{margin:0;color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:17px}
|
|
559
|
+
.dsm-xdpool-sidebar-row-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:6px}
|
|
521
560
|
.dsm-xdpool-models-actions{display:flex;align-items:center;gap:8px;flex:none}
|
|
522
561
|
/* "Built-in list (offline)" chip: a warning tint, since the user is looking at
|
|
523
562
|
a SHORTER roster than the gateway offers and may wonder where models went. */
|
|
@@ -9008,6 +9047,7 @@ window.__ModuleLoader__.load({
|
|
|
9008
9047
|
* message.
|
|
9009
9048
|
*/
|
|
9010
9049
|
async function callPack(method, payload) {
|
|
9050
|
+
if (packRpcBridge === void 0) packRpcBridge = resolvePackRpcBridge();
|
|
9011
9051
|
if (packRpcBridge !== void 0) try {
|
|
9012
9052
|
const result = await packRpcBridge.call(method, payload);
|
|
9013
9053
|
if (result !== null && typeof result === "object") {
|
|
@@ -12368,7 +12408,7 @@ window.__ModuleLoader__.load({
|
|
|
12368
12408
|
*
|
|
12369
12409
|
* @module dsh-xdpool/version
|
|
12370
12410
|
*/
|
|
12371
|
-
const POOL_VERSION = "2.
|
|
12411
|
+
const POOL_VERSION = "2.5.0";
|
|
12372
12412
|
//#endregion
|
|
12373
12413
|
//#region src/client/PoolCard.tsx
|
|
12374
12414
|
/**
|
|
@@ -12664,6 +12704,23 @@ window.__ModuleLoader__.load({
|
|
|
12664
12704
|
/** Render pool health, per-account credits/cooldown, and the model directory. */
|
|
12665
12705
|
function PoolCard({ t, settingsScope }) {
|
|
12666
12706
|
const settingsWritable = settingsScope?.getSnapshot().writable === true;
|
|
12707
|
+
/**
|
|
12708
|
+
* Whether the plugin owns a row in the left sidebar (issue #70).
|
|
12709
|
+
*
|
|
12710
|
+
* Seeded from, and kept in step with, the settings mirror rather than from the
|
|
12711
|
+
* pool status: the row is registered by `apply()`, before any card exists, and
|
|
12712
|
+
* both sides must answer the same question the same way.
|
|
12713
|
+
*/
|
|
12714
|
+
const [sidebarRow, setSidebarRowState] = (0, react.useState)(() => readSidebarRow(settingsScope));
|
|
12715
|
+
const [sidebarRowBusy, setSidebarRowBusy] = (0, react.useState)(false);
|
|
12716
|
+
(0, react.useEffect)(() => {
|
|
12717
|
+
const unsubscribe = settingsScope?.subscribe?.(() => {
|
|
12718
|
+
setSidebarRowState(readSidebarRow(settingsScope));
|
|
12719
|
+
});
|
|
12720
|
+
return () => {
|
|
12721
|
+
unsubscribe?.();
|
|
12722
|
+
};
|
|
12723
|
+
}, [settingsScope]);
|
|
12667
12724
|
/** Which region tab is showing. A CN-only install never leaves this. */
|
|
12668
12725
|
const [activeRegion, setActiveRegion] = (0, react.useState)("cn");
|
|
12669
12726
|
/**
|
|
@@ -13324,11 +13381,18 @@ window.__ModuleLoader__.load({
|
|
|
13324
13381
|
const imageModelIds = Object.entries(local).filter(([, e]) => e.images).map(([id]) => id);
|
|
13325
13382
|
const contextBudgets = {};
|
|
13326
13383
|
for (const [id, entry] of Object.entries(local)) if (entry.budget !== void 0) contextBudgets[id] = entry.budget;
|
|
13327
|
-
await write.call(settingsScope, MODEL_SELECTION_KEY[region], {
|
|
13384
|
+
if (await write.call(settingsScope, MODEL_SELECTION_KEY[region], {
|
|
13328
13385
|
enabledModelIds,
|
|
13329
13386
|
imageModelIds,
|
|
13330
13387
|
contextBudgets
|
|
13331
|
-
})
|
|
13388
|
+
}) === false) {
|
|
13389
|
+
if (mounted.current) setError(t?.("row.modelsConflict") ?? "The settings changed elsewhere; review and save again");
|
|
13390
|
+
return;
|
|
13391
|
+
}
|
|
13392
|
+
if (await refresh(region) === void 0) {
|
|
13393
|
+
if (mounted.current) setFlash(t?.("row.modelsSaved") ?? "Saved");
|
|
13394
|
+
return;
|
|
13395
|
+
}
|
|
13332
13396
|
setDraftFor(region, void 0);
|
|
13333
13397
|
if (mounted.current) setFlash(t?.("row.modelsSaved") ?? "Saved");
|
|
13334
13398
|
} catch (cause) {
|
|
@@ -13365,6 +13429,35 @@ window.__ModuleLoader__.load({
|
|
|
13365
13429
|
}
|
|
13366
13430
|
};
|
|
13367
13431
|
/**
|
|
13432
|
+
* Turn the sidebar row on or off (issue #70).
|
|
13433
|
+
*
|
|
13434
|
+
* Written through the same settings field `apply()` reads, so the two agree
|
|
13435
|
+
* without a new endpoint. The row itself is owned by the bundle registration,
|
|
13436
|
+
* so the mirror's change notification is what makes this take effect without a
|
|
13437
|
+
* restart on hosts that publish one; everywhere else it lands on next launch,
|
|
13438
|
+
* which the hint text says rather than implies.
|
|
13439
|
+
*/
|
|
13440
|
+
const setSidebarRow = async (next) => {
|
|
13441
|
+
const write = settingsScope?.set;
|
|
13442
|
+
if (write === void 0) {
|
|
13443
|
+
setError(t?.("row.modelsSaveError", { message: "settings scope is read-only" }) ?? "settings scope is read-only");
|
|
13444
|
+
return;
|
|
13445
|
+
}
|
|
13446
|
+
setSidebarRowBusy(true);
|
|
13447
|
+
setFlash(void 0);
|
|
13448
|
+
try {
|
|
13449
|
+
if (await write.call(settingsScope, "sidebarRow", next) === false) {
|
|
13450
|
+
if (mounted.current) setError(t?.("row.modelsConflict") ?? "The settings changed elsewhere; review and save again");
|
|
13451
|
+
return;
|
|
13452
|
+
}
|
|
13453
|
+
if (mounted.current) setSidebarRowState(next);
|
|
13454
|
+
} catch (cause) {
|
|
13455
|
+
if (mounted.current) setError(String(cause));
|
|
13456
|
+
} finally {
|
|
13457
|
+
if (mounted.current) setSidebarRowBusy(false);
|
|
13458
|
+
}
|
|
13459
|
+
};
|
|
13460
|
+
/**
|
|
13368
13461
|
* Switch the daily-points automation on or off.
|
|
13369
13462
|
*
|
|
13370
13463
|
* The whole `automation` object is written as one key, because that is how the
|
|
@@ -13639,6 +13732,30 @@ window.__ModuleLoader__.load({
|
|
|
13639
13732
|
})
|
|
13640
13733
|
]
|
|
13641
13734
|
}),
|
|
13735
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
13736
|
+
className: "dsm-xdpool-card dsm-xdpool-sidebar-row",
|
|
13737
|
+
"aria-label": t?.("row.sidebarRowTitle") ?? "Sidebar entry",
|
|
13738
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
13739
|
+
className: "dsm-xdpool-sidebar-row-head",
|
|
13740
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
13741
|
+
className: "dsm-xdpool-usage-title",
|
|
13742
|
+
children: t?.("row.sidebarRowTitle") ?? "Sidebar entry"
|
|
13743
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
13744
|
+
type: "button",
|
|
13745
|
+
role: "switch",
|
|
13746
|
+
"aria-checked": sidebarRow,
|
|
13747
|
+
disabled: !settingsWritable || sidebarRowBusy,
|
|
13748
|
+
className: `dsm-xdpool-auto-switch${sidebarRow ? " dsm-xdpool-auto-switch-on" : ""}`,
|
|
13749
|
+
onClick: () => {
|
|
13750
|
+
setSidebarRow(!sidebarRow);
|
|
13751
|
+
},
|
|
13752
|
+
children: sidebarRowBusy ? t?.("row.autoBusy") ?? "Saving…" : sidebarRow ? t?.("row.autoOn") ?? "On" : t?.("row.autoOff") ?? "Off"
|
|
13753
|
+
})]
|
|
13754
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
13755
|
+
className: "dsm-xdpool-usage-note",
|
|
13756
|
+
children: sidebarRow ? t?.("row.sidebarRowOn") ?? "XD Pool has its own row in the left sidebar. Turn it off to open the pool from Settings instead." : t?.("row.sidebarRowOff") ?? "The sidebar row is hidden; the pool still opens from Settings → XD Pool, and models are unaffected."
|
|
13757
|
+
})]
|
|
13758
|
+
}),
|
|
13642
13759
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
13643
13760
|
className: "dsm-xdpool-regions",
|
|
13644
13761
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -14959,6 +15076,10 @@ window.__ModuleLoader__.load({
|
|
|
14959
15076
|
"row.modelsSaving": "Saving…",
|
|
14960
15077
|
"row.modelsDiscard": "Discard",
|
|
14961
15078
|
"row.modelsSaved": "Model selection saved",
|
|
15079
|
+
"row.sidebarRowTitle": "Sidebar entry",
|
|
15080
|
+
"row.sidebarRowOn": "XD Pool has its own row in the left sidebar. Turn it off to open the pool from Settings instead.",
|
|
15081
|
+
"row.sidebarRowOff": "The sidebar row is hidden; the pool still opens from Settings → XD Pool, and models are unaffected.",
|
|
15082
|
+
"row.modelsConflict": "The settings changed elsewhere; review and save again",
|
|
14962
15083
|
"row.modelsSaveError": "Could not save: {message}",
|
|
14963
15084
|
"row.modelsEmpty": "No model enabled — enable at least one before saving.",
|
|
14964
15085
|
"row.catalogOffline": "built-in list (offline)",
|
|
@@ -15470,6 +15591,10 @@ window.__ModuleLoader__.load({
|
|
|
15470
15591
|
"row.modelsSaving": "保存中…",
|
|
15471
15592
|
"row.modelsDiscard": "放弃修改",
|
|
15472
15593
|
"row.modelsSaved": "模型选择已保存",
|
|
15594
|
+
"row.sidebarRowTitle": "侧栏入口",
|
|
15595
|
+
"row.sidebarRowOn": "左侧栏会给 XD Pool 留一行入口。关掉后改为从「设置 → XD Pool」进入,模型池不受影响。",
|
|
15596
|
+
"row.sidebarRowOff": "侧栏入口已隐藏;仍可从「设置 → XD Pool」进入,模型池不受影响。",
|
|
15597
|
+
"row.modelsConflict": "设置已在别处被修改,请确认后重新保存",
|
|
15473
15598
|
"row.modelsSaveError": "保存失败:{message}",
|
|
15474
15599
|
"row.modelsEmpty": "至少要启用一个模型才能保存。",
|
|
15475
15600
|
"row.catalogOffline": "内置列表(离线)",
|
|
@@ -15921,12 +16046,29 @@ window.__ModuleLoader__.load({
|
|
|
15921
16046
|
}, PoolCard));
|
|
15922
16047
|
installNavIcon(ctx, () => t("row.navLabel"));
|
|
15923
16048
|
try {
|
|
15924
|
-
|
|
15925
|
-
|
|
15926
|
-
|
|
15927
|
-
|
|
15928
|
-
|
|
15929
|
-
|
|
16049
|
+
let disposeRow;
|
|
16050
|
+
const syncSidebarRow = () => {
|
|
16051
|
+
const wanted = readSidebarRow(settingsScope);
|
|
16052
|
+
if (wanted && disposeRow === void 0) disposeRow = ctx.slots.register({
|
|
16053
|
+
name: "sidebar.panellist",
|
|
16054
|
+
id: "xdpool",
|
|
16055
|
+
order: 20,
|
|
16056
|
+
label: () => t("row.navLabel")
|
|
16057
|
+
}, PanelIcon);
|
|
16058
|
+
else if (!wanted && disposeRow !== void 0) {
|
|
16059
|
+
disposeRow();
|
|
16060
|
+
disposeRow = void 0;
|
|
16061
|
+
}
|
|
16062
|
+
};
|
|
16063
|
+
ctx.slots.inject("sidebar.panellist", () => {
|
|
16064
|
+
syncSidebarRow();
|
|
16065
|
+
const unsubscribe = settingsScope?.subscribe?.(syncSidebarRow);
|
|
16066
|
+
return () => {
|
|
16067
|
+
unsubscribe?.();
|
|
16068
|
+
disposeRow?.();
|
|
16069
|
+
disposeRow = void 0;
|
|
16070
|
+
};
|
|
16071
|
+
});
|
|
15930
16072
|
ctx.slots.inject("main", () => ctx.slots.register({
|
|
15931
16073
|
name: "main",
|
|
15932
16074
|
key: "xdpool",
|
package/lib/index.js
CHANGED
|
@@ -390,10 +390,39 @@ const CHAT_IDENTITY_HEADERS = Object.freeze({
|
|
|
390
390
|
* rewrite is applied to assistant history too — one past reply carrying a
|
|
391
391
|
* fingerprint would otherwise fail every later request in the conversation.
|
|
392
392
|
*
|
|
393
|
+
* A THIRD FINGERPRINT (measured against the live domestic gateway 2026-10-28)
|
|
394
|
+
*
|
|
395
|
+
* The file-reference guidance DSH contributes whenever the agent has a `read`
|
|
396
|
+
* tool reintroduced the same 400/11128 after the two rewrites above shipped:
|
|
397
|
+
*
|
|
398
|
+
* Tokens prefixed with @ are paths the user explicitly referenced.
|
|
399
|
+
* Relative paths resolve from the workspace root; absolute paths identify
|
|
400
|
+
* files or directories on the host.
|
|
401
|
+
*
|
|
402
|
+
* Ordinary phrasing throughout — the blocked region names no product — so the
|
|
403
|
+
* fingerprint is not "this prompt is not WorkBuddy's" but a literal substring
|
|
404
|
+
* list that happens to include this sentence. Neither sentence is the trigger
|
|
405
|
+
* on its own: each 200s in isolation, and the pair 400s; the smallest edit
|
|
406
|
+
* anywhere in the pair clears it, which is what marks it as content and not
|
|
407
|
+
* prompt length, and the matching half is the second sentence ("Tokens
|
|
408
|
+
* prefixed with @" alone stayed clean while the "Relative paths…" line was the
|
|
409
|
+
* one whose text the gateway recognised).
|
|
410
|
+
*
|
|
411
|
+
* The rewrite turns the indicative into the same statement's participle —
|
|
412
|
+
* "Relative paths resolve from the workspace root" becomes "Relative paths are
|
|
413
|
+
* resolved against the workspace root". Meaning, length class and line breaks
|
|
414
|
+
* are preserved; the substring the gateway lists is gone. Verified at 200 with
|
|
415
|
+
* the full runtime system prompt (≈41k chars) that reproduced the 400 on the
|
|
416
|
+
* same account, and with the pair alone.
|
|
417
|
+
*
|
|
393
418
|
* The list is an ARRAY because these fingerprints are appended to over time; a
|
|
394
419
|
* future rejection should be one more row here, not a new code path.
|
|
395
420
|
*/
|
|
396
|
-
const BANNED_SYSTEM_PHRASES = Object.freeze([
|
|
421
|
+
const BANNED_SYSTEM_PHRASES = Object.freeze([
|
|
422
|
+
[/powered by DeepSeek Harness/gi, "running on DeepSeek Harness"],
|
|
423
|
+
[/DeepSeek Harness Web GUI/gi, "DeepSeek Harness web interface"],
|
|
424
|
+
[/Relative paths resolve from the workspace root/gi, "Relative paths are resolved against the workspace root"]
|
|
425
|
+
]);
|
|
397
426
|
/**
|
|
398
427
|
* Rewrite fingerprinted system/assistant content, leaving everything else
|
|
399
428
|
* byte-identical.
|
|
@@ -1485,14 +1514,17 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1485
1514
|
if (typeof raw !== "object" || raw === null) continue;
|
|
1486
1515
|
const account = raw;
|
|
1487
1516
|
const num = (key) => typeof account[key] === "number" ? account[key] : 0;
|
|
1488
|
-
const
|
|
1489
|
-
const
|
|
1490
|
-
const
|
|
1517
|
+
const capacityType = num("CapacityType");
|
|
1518
|
+
const monthly = capacityType === 4;
|
|
1519
|
+
const periodic = monthly || capacityType === 2;
|
|
1520
|
+
const size = periodic ? num("CycleCapacitySize") : num("CapacitySize");
|
|
1521
|
+
const remain = periodic ? num("CycleCapacityRemain") : num("CapacityRemain");
|
|
1491
1522
|
const capped = remain < 0 ? 0 : remain;
|
|
1492
1523
|
const cycleEndMs = parseDate(account["CycleEndTime"]);
|
|
1493
|
-
const expiresAtMs = monthly ? void 0 : parseDate(account["ExpiredTime"]) ?? cycleEndMs;
|
|
1524
|
+
const expiresAtMs = monthly ? void 0 : capacityType === 2 ? parseDate(account["DeductionEndTime"]) : parseDate(account["ExpiredTime"]) ?? cycleEndMs;
|
|
1494
1525
|
const refreshAtMs = monthly ? cycleEndMs === void 0 ? void 0 : cycleEndMs + 1e3 : void 0;
|
|
1495
|
-
if (
|
|
1526
|
+
if (expiresAtMs !== void 0 && expiresAtMs <= Date.now()) continue;
|
|
1527
|
+
if (!periodic && capped <= 0) continue;
|
|
1496
1528
|
total += capped;
|
|
1497
1529
|
if (expiresAtMs !== void 0) {
|
|
1498
1530
|
if (nearestExpiryMs === void 0 || expiresAtMs < nearestExpiryMs) nearestExpiryMs = expiresAtMs;
|
|
@@ -19296,6 +19328,7 @@ function createWorkBuddyShim(options) {
|
|
|
19296
19328
|
const conversationKey = conversationKeyOf(poolRaw);
|
|
19297
19329
|
const tried = [];
|
|
19298
19330
|
let last;
|
|
19331
|
+
const refreshedThisRequest = /* @__PURE__ */ new Set();
|
|
19299
19332
|
const turnId = randomBytes(8).toString("hex");
|
|
19300
19333
|
const usageContext = usageRecorder === void 0 ? void 0 : {
|
|
19301
19334
|
recorder: usageRecorder,
|
|
@@ -19350,6 +19383,12 @@ function createWorkBuddyShim(options) {
|
|
|
19350
19383
|
message: result.message
|
|
19351
19384
|
};
|
|
19352
19385
|
if (result.kind === "session_dead") {
|
|
19386
|
+
if (refreshedThisRequest.has(account.id)) {
|
|
19387
|
+
pool.penalizeCredentialDead(account.id);
|
|
19388
|
+
logger?.warn(`dsh-xdpool: ${account.label} still refused after a token refresh (attempt ${attempt + 1}/${maxAttempts}); rotating`);
|
|
19389
|
+
continue;
|
|
19390
|
+
}
|
|
19391
|
+
refreshedThisRequest.add(account.id);
|
|
19353
19392
|
logger?.warn(`dsh-xdpool: ${account.label} session dead; refreshing token and retrying`);
|
|
19354
19393
|
const before = account.credential.accessToken;
|
|
19355
19394
|
if (!await pool.refreshAccount(account.id, { force: true }) || account.credential.accessToken === before) {
|
|
@@ -19607,6 +19646,7 @@ async function recoverFromContextOverrun(options) {
|
|
|
19607
19646
|
};
|
|
19608
19647
|
const retryBody = client.buildChatBody(parsed, compacted, region ?? "cn");
|
|
19609
19648
|
const tried = [];
|
|
19649
|
+
const refreshedThisRequest = /* @__PURE__ */ new Set();
|
|
19610
19650
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
19611
19651
|
if (controller.signal.aborted) return {
|
|
19612
19652
|
ok: false,
|
|
@@ -19640,6 +19680,11 @@ async function recoverFromContextOverrun(options) {
|
|
|
19640
19680
|
detail: "prompt still exceeded the window after compaction"
|
|
19641
19681
|
};
|
|
19642
19682
|
if (result.kind === "session_dead") {
|
|
19683
|
+
if (refreshedThisRequest.has(account.id)) {
|
|
19684
|
+
pool.penalizeCredentialDead(account.id);
|
|
19685
|
+
continue;
|
|
19686
|
+
}
|
|
19687
|
+
refreshedThisRequest.add(account.id);
|
|
19643
19688
|
const before = account.credential.accessToken;
|
|
19644
19689
|
if (!await pool.refreshAccount(account.id, { force: true }) || account.credential.accessToken === before) pool.penalizeCredentialDead(account.id);
|
|
19645
19690
|
continue;
|
|
@@ -20699,7 +20744,8 @@ const Config = z.object({
|
|
|
20699
20744
|
modelSelectionCn: asVolatile(modelSelectionSchema.description("Model selection for the domestic gateway")),
|
|
20700
20745
|
modelSelectionGlobal: asVolatile(modelSelectionSchema.description("Model selection for the international gateway")),
|
|
20701
20746
|
automation: asVolatile(automationSchema.description("Daily points automation (activity report, task claiming, check-in)")),
|
|
20702
|
-
automationEarnings: asVolatile(z.any().description("Automation earnings ledger (written by the scheduler)"))
|
|
20747
|
+
automationEarnings: asVolatile(z.any().description("Automation earnings ledger (written by the scheduler)")),
|
|
20748
|
+
sidebarRow: asVolatile(z.boolean().default(true).description("Show the XD Pool entry in the left sidebar (the Settings page keeps working either way)"))
|
|
20703
20749
|
});
|
|
20704
20750
|
/** Live API, published for the CLI. */
|
|
20705
20751
|
let api;
|
|
@@ -21339,7 +21385,7 @@ function apply(ctx, config = {}) {
|
|
|
21339
21385
|
}) };
|
|
21340
21386
|
}).finally(() => {
|
|
21341
21387
|
if (stopped) return;
|
|
21342
|
-
adoptChannelPackAccounts(
|
|
21388
|
+
adoptChannelPackAccounts(scoped, core.pool, ctx.logger);
|
|
21343
21389
|
});
|
|
21344
21390
|
});
|
|
21345
21391
|
mountBillionContext(ctx, ctx.logger).then((outcome) => {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-xdpool",
|
|
3
3
|
"displayName": "DSH XD Pool",
|
|
4
4
|
"description": "One DSH model pool for every free quota you have: merge all locally signed-in WorkBuddy accounts with auto-failover, sign in to 13 vendors by QR/web, aggregate 76 vendors' free tiers, plus a usage dashboard, model routing, a local OpenAI-compatible gateway and a points-automation suite.",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.5.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "XDTrees",
|
|
8
8
|
"homepage": "https://github.com/XDTrees/dsh-xdpool#readme",
|
|
@@ -29453,6 +29453,15 @@ var channelPackSchema = Schema.object({
|
|
|
29453
29453
|
// **必须显式声明**:SettingsStore 的 `scope.replace()` 是整体覆盖,
|
|
29454
29454
|
// schema 里没声明的键会被 schemastery 剥掉,写回时就此静默丢失。
|
|
29455
29455
|
contextBudgets: Schema.dict(Schema.any()).default({}),
|
|
29456
|
+
// ── 本地扩展(dsh-xdpool,2026-10-26)────────────────────────────────
|
|
29457
|
+
// 记录「用户亲手动过这个 provider 的模型选择」,用来把「用户把模型全部
|
|
29458
|
+
// 打开」和「用户从未选择过」区分开。`disabledModels` 本身做不到这件事:
|
|
29459
|
+
// 打开模型时我们**删除**键而不是写 false(见 setModelDisabled 注释),
|
|
29460
|
+
// 所以「全开」与「从未碰过」都表现为空表。启动补静音原先拿
|
|
29461
|
+
// `disabledModelsFor(provider).size === 0` 当判据,于是把「全开」当成
|
|
29462
|
+
// 「从未选择」,每次重启又把整目录静音回去 —— 目录越小的渠道越必然中招
|
|
29463
|
+
// (zcode 只有两个模型,用户勾满之后每次重启都变回全禁用)。
|
|
29464
|
+
selectionTouched: Schema.dict(Schema.any()).default({}),
|
|
29456
29465
|
loomyPermanentLocked: Schema.boolean().default(false),
|
|
29457
29466
|
gatewayEnabled: Schema.boolean().default(true),
|
|
29458
29467
|
providerOrder: Schema.array(Schema.string()).default([])
|
|
@@ -29511,6 +29520,14 @@ function sanitizeContextBudgets(raw) {
|
|
|
29511
29520
|
}
|
|
29512
29521
|
return result;
|
|
29513
29522
|
}
|
|
29523
|
+
function sanitizeSelectionTouched(raw) {
|
|
29524
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
|
|
29525
|
+
const result = {};
|
|
29526
|
+
for (const [provider, value] of Object.entries(raw)) {
|
|
29527
|
+
if (value === true && provider.length > 0) result[provider] = true;
|
|
29528
|
+
}
|
|
29529
|
+
return result;
|
|
29530
|
+
}
|
|
29514
29531
|
function sanitizeAccounts(raw) {
|
|
29515
29532
|
if (!Array.isArray(raw)) return [];
|
|
29516
29533
|
const accounts = [];
|
|
@@ -29542,6 +29559,7 @@ var SettingsStore = class {
|
|
|
29542
29559
|
accounts: sanitizeAccounts(value.accounts),
|
|
29543
29560
|
disabledModels: sanitizeDisabledModels(value.disabledModels),
|
|
29544
29561
|
contextBudgets: sanitizeContextBudgets(value.contextBudgets),
|
|
29562
|
+
selectionTouched: sanitizeSelectionTouched(value.selectionTouched),
|
|
29545
29563
|
// 只是**镜像**(权威表在 permanent-locks.json);老文档没这个键 → false。
|
|
29546
29564
|
loomyPermanentLocked: value.loomyPermanentLocked === true,
|
|
29547
29565
|
// 老文档没这个键 → 启用,与升级前行为一致。
|
|
@@ -29557,6 +29575,9 @@ var SettingsStore = class {
|
|
|
29557
29575
|
// 预算表丢失的后果只是「下次选择器回到原生窗口」,用户重选一次即可
|
|
29558
29576
|
// (纯偏好、可逆),故接受老版本重写时丢掉本键。
|
|
29559
29577
|
contextBudgets: state.contextBudgets ?? {},
|
|
29578
|
+
// 丢了这个标记的后果是「下次启动把该渠道重新静音一次」,用户再勾一遍
|
|
29579
|
+
// 就会重新置位,属可逆,故与 contextBudgets 同样接受老版本重写时丢掉。
|
|
29580
|
+
selectionTouched: state.selectionTouched ?? {},
|
|
29560
29581
|
// 镜像字段由 AccountPool 与独立文档**同源写出**:同机上只认这个字段的
|
|
29561
29582
|
// 旧版本代码(其它 profile)读它、也会原样写回它,故两边不会脱节。
|
|
29562
29583
|
loomyPermanentLocked: state.loomyPermanentLocked === true,
|
|
@@ -29661,6 +29682,7 @@ var FileStore = class {
|
|
|
29661
29682
|
accounts: sanitizeAccounts(value.accounts),
|
|
29662
29683
|
disabledModels: sanitizeDisabledModels(value.disabledModels),
|
|
29663
29684
|
contextBudgets: sanitizeContextBudgets(value.contextBudgets),
|
|
29685
|
+
selectionTouched: sanitizeSelectionTouched(value.selectionTouched),
|
|
29664
29686
|
// 只是镜像(权威表在 permanent-locks.json);老文档没这个键 → false。
|
|
29665
29687
|
loomyPermanentLocked: value.loomyPermanentLocked === true,
|
|
29666
29688
|
// 老文档没这个键 → 启用,与升级前行为一致。
|
|
@@ -29705,6 +29727,9 @@ var FileStore = class {
|
|
|
29705
29727
|
accounts,
|
|
29706
29728
|
disabledModels: {},
|
|
29707
29729
|
contextBudgets: {},
|
|
29730
|
+
// 从凭据文件重建的文档不含任何选择历史 → 空表,让补静音照常生效
|
|
29731
|
+
// (这些账号确实是「从未选择过」,正是补静音要照顾的情形)。
|
|
29732
|
+
selectionTouched: {},
|
|
29708
29733
|
// 凭据文件里没有任何锁定信息 → 镜像写 false(权威表另有其文档)。
|
|
29709
29734
|
loomyPermanentLocked: false,
|
|
29710
29735
|
// 恢复出的文档本来就不含任何开关信息 → 启用(与全新安装一致)。
|
|
@@ -29856,6 +29881,17 @@ var AccountPool = class {
|
|
|
29856
29881
|
* 把窗口收窄到 200K 这类值」,不改变上游真实能力。
|
|
29857
29882
|
*/
|
|
29858
29883
|
contextBudgetCache = {};
|
|
29884
|
+
/**
|
|
29885
|
+
* 「用户动过该 provider 的模型选择」标记的进程内权威副本。
|
|
29886
|
+
*
|
|
29887
|
+
* 存在的唯一理由是把「用户把模型全部打开」与「用户从未选择过」区分开:
|
|
29888
|
+
* 打开模型时黑名单里**删除**键(见 {@link setModelDisabled}),所以「全开」
|
|
29889
|
+
* 与「从未碰过」的 `disabledModels` 都是空表,单看黑名单无法分辨,启动补
|
|
29890
|
+
* 静音就会把「全开」当成「从未选择」再静音一次。
|
|
29891
|
+
*
|
|
29892
|
+
* ⚠️ 只记录已置位的 provider(缺键 = 从未选择)。
|
|
29893
|
+
*/
|
|
29894
|
+
selectionTouchedCache = {};
|
|
29859
29895
|
/**
|
|
29860
29896
|
* 「锁定永久积分」开关表的**权威进程内副本**(与 {@link cache} 同理)。
|
|
29861
29897
|
*
|
|
@@ -29907,6 +29943,7 @@ var AccountPool = class {
|
|
|
29907
29943
|
this.cache = state.accounts;
|
|
29908
29944
|
this.modelCache = state.disabledModels;
|
|
29909
29945
|
this.contextBudgetCache = state.contextBudgets ?? {};
|
|
29946
|
+
this.selectionTouchedCache = state.selectionTouched ?? {};
|
|
29910
29947
|
this.gatewayEnabledCache = state.gatewayEnabled !== false;
|
|
29911
29948
|
this.providerOrderCache = state.providerOrder ?? [];
|
|
29912
29949
|
}
|
|
@@ -29984,6 +30021,7 @@ var AccountPool = class {
|
|
|
29984
30021
|
accounts: this.cache,
|
|
29985
30022
|
disabledModels: this.modelCache,
|
|
29986
30023
|
contextBudgets: this.contextBudgetCache,
|
|
30024
|
+
selectionTouched: this.selectionTouchedCache,
|
|
29987
30025
|
...this.lockFields(),
|
|
29988
30026
|
// ⚠️ 必须与账号、黑名单取自**同一时刻**的快照(见本方法的注释):
|
|
29989
30027
|
// 分两次快照会让同一份文档里的字段互相矛盾。
|
|
@@ -30105,6 +30143,9 @@ var AccountPool = class {
|
|
|
30105
30143
|
else delete perProvider[modelId];
|
|
30106
30144
|
if (Object.keys(perProvider).length === 0) delete next[provider];
|
|
30107
30145
|
else next[provider] = perProvider;
|
|
30146
|
+
// 先置位再落盘:`writeModels` 排队的快照里已带上标记,一次写入同时完成
|
|
30147
|
+
// 「改黑名单」和「记录用户动过选择」,不会出现只落了一半的状态。
|
|
30148
|
+
this.selectionTouchedCache = { ...this.selectionTouchedCache, [provider]: true };
|
|
30108
30149
|
await this.writeModels(next);
|
|
30109
30150
|
}
|
|
30110
30151
|
/**
|
|
@@ -30131,6 +30172,29 @@ var AccountPool = class {
|
|
|
30131
30172
|
this.ensureLoaded();
|
|
30132
30173
|
return { ...this.contextBudgetCache[provider] ?? {} };
|
|
30133
30174
|
}
|
|
30175
|
+
/**
|
|
30176
|
+
* 该 provider 的模型选择是否被用户动过。
|
|
30177
|
+
*
|
|
30178
|
+
* 启动补静音据此判断「要不要补」:动过就尊重用户的当前状态(哪怕是全开,
|
|
30179
|
+
* 表现为空黑名单),没动过才补静音。
|
|
30180
|
+
*/
|
|
30181
|
+
selectionTouched(provider) {
|
|
30182
|
+
this.ensureLoaded();
|
|
30183
|
+
return this.selectionTouchedCache[provider] === true;
|
|
30184
|
+
}
|
|
30185
|
+
/**
|
|
30186
|
+
* 置位「用户动过该 provider 的模型选择」并落盘。
|
|
30187
|
+
*
|
|
30188
|
+
* ⚠️ 幂等:已置位直接返回,不产生写入 —— 否则每次勾选都要重写整份文档。
|
|
30189
|
+
*/
|
|
30190
|
+
async touchSelection(provider) {
|
|
30191
|
+
this.ensureLoaded();
|
|
30192
|
+
if (this.selectionTouchedCache[provider] === true) return;
|
|
30193
|
+
const next = { ...this.selectionTouchedCache, [provider]: true };
|
|
30194
|
+
this.selectionTouchedCache = next;
|
|
30195
|
+
if (this.store.kind === "memory") return;
|
|
30196
|
+
await this.queueStoreSave();
|
|
30197
|
+
}
|
|
30134
30198
|
/**
|
|
30135
30199
|
* 设置/清除单个模型的上下文预算。
|
|
30136
30200
|
*
|
|
@@ -30160,13 +30224,18 @@ var AccountPool = class {
|
|
|
30160
30224
|
* 注意这与 {@link clearDisabledModels} **不对称** —— 后者的语义是「清空」,
|
|
30161
30225
|
* 即使传入空列表也仍有事可做(详见该方法注释)。
|
|
30162
30226
|
*/
|
|
30163
|
-
async setModelsDisabled(provider, modelIds) {
|
|
30227
|
+
async setModelsDisabled(provider, modelIds, options) {
|
|
30164
30228
|
if (modelIds.length === 0) return;
|
|
30165
30229
|
this.ensureLoaded();
|
|
30166
30230
|
const next = { ...this.modelCache };
|
|
30167
30231
|
const perProvider = { ...next[provider] ?? {} };
|
|
30168
30232
|
for (const id of modelIds) perProvider[id] = true;
|
|
30169
30233
|
next[provider] = perProvider;
|
|
30234
|
+
// @param options.mark - 默认 true(用户动作);自动静音传 false,见
|
|
30235
|
+
// touchSelection 的说明 —— 它不是「用户动过选择」。
|
|
30236
|
+
if (options?.mark !== false) {
|
|
30237
|
+
this.selectionTouchedCache = { ...this.selectionTouchedCache, [provider]: true };
|
|
30238
|
+
}
|
|
30170
30239
|
await this.writeModels(next);
|
|
30171
30240
|
}
|
|
30172
30241
|
/**
|
|
@@ -30199,6 +30268,9 @@ var AccountPool = class {
|
|
|
30199
30268
|
const next = { ...this.modelCache };
|
|
30200
30269
|
if (Object.keys(nextPerProvider).length === 0) delete next[provider];
|
|
30201
30270
|
else next[provider] = nextPerProvider;
|
|
30271
|
+
// 注意这条路径也会让黑名单变成空表(用户把该组全开),正是「全开」最
|
|
30272
|
+
// 容易发生的场景,必须同样置位,否则启动补静音会把它们再关回去。
|
|
30273
|
+
this.selectionTouchedCache = { ...this.selectionTouchedCache, [provider]: true };
|
|
30202
30274
|
await this.writeModels(next);
|
|
30203
30275
|
}
|
|
30204
30276
|
/**
|
|
@@ -30265,6 +30337,8 @@ var AccountPool = class {
|
|
|
30265
30337
|
if (this.modelCache[provider] === void 0) return;
|
|
30266
30338
|
const next = { ...this.modelCache };
|
|
30267
30339
|
delete next[provider];
|
|
30340
|
+
// 「全部打开」的最高频入口,必须置位(见 selectionTouched 注释)。
|
|
30341
|
+
this.selectionTouchedCache = { ...this.selectionTouchedCache, [provider]: true };
|
|
30268
30342
|
await this.writeModels(next);
|
|
30269
30343
|
}
|
|
30270
30344
|
/** 持久化模型黑名单(同时更新进程内权威副本)。 */
|
|
@@ -32970,6 +33044,13 @@ var DsmlContentExtractor = class {
|
|
|
32970
33044
|
return { text: remaining, reasoning: "" };
|
|
32971
33045
|
}
|
|
32972
33046
|
};
|
|
33047
|
+
// CodeArts rejects an oversized max_tokens with an HTTP-200 SSE error frame
|
|
33048
|
+
// (`InferHub.4001.200 The request param is invalid`) rather than a 4xx, so it
|
|
33049
|
+
// surfaces to the user as an opaque INVALID_REQUEST. 65536 is the measured
|
|
33050
|
+
// ceiling; 131072 already produces an empty stream. Keep the advertised default
|
|
33051
|
+
// and the final wire request on that same ceiling - TRAE and Cline do the same
|
|
33052
|
+
// with clampTraeMaxTokens / clampClineMaxTokens.
|
|
33053
|
+
var CODEARTS_MAX_OUTPUT_TOKENS = 65536;
|
|
32973
33054
|
var CodeArtsAdapter = class extends LlmAdapter {
|
|
32974
33055
|
constructor(options) {
|
|
32975
33056
|
super();
|
|
@@ -33043,6 +33124,7 @@ var CodeArtsAdapter = class extends LlmAdapter {
|
|
|
33043
33124
|
id: m.id,
|
|
33044
33125
|
name: m.name,
|
|
33045
33126
|
inputModalities: modalitiesFor(m.id),
|
|
33127
|
+
maxTokens: CODEARTS_MAX_OUTPUT_TOKENS,
|
|
33046
33128
|
...contextWindow === void 0 ? {} : { contextWindow }
|
|
33047
33129
|
};
|
|
33048
33130
|
});
|
|
@@ -33052,7 +33134,15 @@ var CodeArtsAdapter = class extends LlmAdapter {
|
|
|
33052
33134
|
const remoteModel = this.remoteModels?.find((m) => m.id === model);
|
|
33053
33135
|
const name2 = remoteModel?.name ?? model;
|
|
33054
33136
|
const contextWindow = contextWindowFor(model);
|
|
33055
|
-
const resolved = {
|
|
33137
|
+
const resolved = {
|
|
33138
|
+
provider,
|
|
33139
|
+
id: model,
|
|
33140
|
+
name: name2,
|
|
33141
|
+
inputModalities: modalitiesFor(model),
|
|
33142
|
+
// catalogFactsFor() forwards this as fact.maxTokens; without it the host
|
|
33143
|
+
// has no way to learn the ceiling and passes its own catalog value through.
|
|
33144
|
+
defaultMaxTokens: CODEARTS_MAX_OUTPUT_TOKENS
|
|
33145
|
+
};
|
|
33056
33146
|
if (contextWindow !== void 0) resolved.context = { contextWindow };
|
|
33057
33147
|
resolved.reasoning = {
|
|
33058
33148
|
efforts: [
|
|
@@ -33134,9 +33224,11 @@ var CodeArtsAdapter = class extends LlmAdapter {
|
|
|
33134
33224
|
// 大文件 write 工具参数(如 1000-2000+ 行文档)需要数万 token 的生成空间,
|
|
33135
33225
|
// 若沿用后端默认输出上限,参数 JSON 会在中途被截断成非法 JSON,harness
|
|
33136
33226
|
// 工具校验报 `invalid arguments: "arguments" must be an object`。
|
|
33137
|
-
// 参考实现 e2e 实测:65536 可用,131072
|
|
33138
|
-
//
|
|
33139
|
-
|
|
33227
|
+
// 参考实现 e2e 实测:65536 可用,131072 反而触发空流被后端拒绝。
|
|
33228
|
+
// 因此这是上限而不只是默认值:调用方传来的更大值(宿主模型目录里别的
|
|
33229
|
+
// provider 带 384000)会被原样发出并触发 INVALID_REQUEST,必须收回;
|
|
33230
|
+
// 调用方主动设的较小值保留。
|
|
33231
|
+
max_tokens: Math.min(options.maxTokens ?? CODEARTS_MAX_OUTPUT_TOKENS, CODEARTS_MAX_OUTPUT_TOKENS),
|
|
33140
33232
|
...wireTools !== void 0 && wireTools.length > 0 ? { tools: wireTools } : {}
|
|
33141
33233
|
});
|
|
33142
33234
|
const url = `${CHAT_API_BASE}/chat/completions`;
|
|
@@ -34694,6 +34786,37 @@ function contentToText2(content) {
|
|
|
34694
34786
|
return content.filter((block) => typeof block === "object" && block !== null && block.type === "text").map((block) => String(block.text)).join("");
|
|
34695
34787
|
}
|
|
34696
34788
|
var TOOL_RESULT_IMAGE_TEXT = "Attached image(s) from tool result:";
|
|
34789
|
+
/**
|
|
34790
|
+
* LOCAL ADAPTATION: a wire message that carries no payload at all.
|
|
34791
|
+
*
|
|
34792
|
+
* The domestic gateway rejects `{ role: "assistant", content: "" }` with
|
|
34793
|
+
* 400 / code 11151 "a message has empty content". That shape is reachable
|
|
34794
|
+
* without the model doing anything wrong: a turn interrupted while the model
|
|
34795
|
+
* was still thinking leaves an assistant message holding only reasoning, and
|
|
34796
|
+
* the serializers below emit it with an empty body because the domestic
|
|
34797
|
+
* gateway does not read `reasoning_content`. One such message then poisons
|
|
34798
|
+
* every later turn of that conversation, and retrying cannot help because the
|
|
34799
|
+
* offending message is history.
|
|
34800
|
+
*
|
|
34801
|
+
* `user` messages are deliberately NOT filtered: both gateways accept an empty
|
|
34802
|
+
* user message (verified against live endpoints), and dropping one would erase
|
|
34803
|
+
* a turn boundary the model is reading.
|
|
34804
|
+
*/
|
|
34805
|
+
function isBlankWireMessage(entry) {
|
|
34806
|
+
if (entry === null || typeof entry !== "object") return false;
|
|
34807
|
+
if (entry.role !== "assistant" && entry.role !== "system") return false;
|
|
34808
|
+
// A tool call IS content: it anchors the matching tool result, so removing
|
|
34809
|
+
// the message would orphan that result and fail differently.
|
|
34810
|
+
if (Array.isArray(entry.tool_calls) && entry.tool_calls.length > 0) return false;
|
|
34811
|
+
const content = entry.content;
|
|
34812
|
+
if (typeof content === "string") return content.trim().length === 0;
|
|
34813
|
+
if (Array.isArray(content)) return content.length === 0;
|
|
34814
|
+
return false;
|
|
34815
|
+
}
|
|
34816
|
+
function dropBlankWireMessages(wire) {
|
|
34817
|
+
const kept = wire.filter((entry) => !isBlankWireMessage(entry));
|
|
34818
|
+
return kept.length === wire.length ? wire : kept;
|
|
34819
|
+
}
|
|
34697
34820
|
function serializeMessages2(messages, imageUrls) {
|
|
34698
34821
|
const wire = [];
|
|
34699
34822
|
let pendingToolImages = [];
|
|
@@ -34772,7 +34895,7 @@ function serializeMessages2(messages, imageUrls) {
|
|
|
34772
34895
|
}
|
|
34773
34896
|
}
|
|
34774
34897
|
flushToolImages();
|
|
34775
|
-
return wire;
|
|
34898
|
+
return dropBlankWireMessages(wire);
|
|
34776
34899
|
}
|
|
34777
34900
|
function errorMessage2(error) {
|
|
34778
34901
|
if (error instanceof Error) return error.message;
|
|
@@ -35326,9 +35449,13 @@ var BuddyAdapter = class _BuddyAdapter extends LlmAdapter2 {
|
|
|
35326
35449
|
// pair (measured on a live account 2026-10-21; the pool's own copy of this
|
|
35327
35450
|
// list is BANNED_SYSTEM_PHRASES in src/upstream.ts). Runs AFTER the prepend
|
|
35328
35451
|
// above, because that is where the host's system text lands.
|
|
35452
|
+
// The third entry is the file-reference guidance DSH adds whenever the
|
|
35453
|
+
// agent has a `read` tool: the "Relative paths…" half is the substring the
|
|
35454
|
+
// gateway lists, and the sentence names no product, so it is rewritten for
|
|
35455
|
+
// its text rather than for claiming another client (measured 2026-10-28).
|
|
35329
35456
|
for (const message of messages) {
|
|
35330
35457
|
if ((message.role === "system" || message.role === "assistant") && typeof message.content === "string") {
|
|
35331
|
-
message.content = message.content.replace(/powered by DeepSeek Harness/gi, "running on DeepSeek Harness").replace(/DeepSeek Harness Web GUI/gi, "DeepSeek Harness web interface");
|
|
35458
|
+
message.content = message.content.replace(/powered by DeepSeek Harness/gi, "running on DeepSeek Harness").replace(/DeepSeek Harness Web GUI/gi, "DeepSeek Harness web interface").replace(/Relative paths resolve from the workspace root/gi, "Relative paths are resolved against the workspace root");
|
|
35332
35459
|
}
|
|
35333
35460
|
}
|
|
35334
35461
|
const tools = options.tools?.map((tool) => ({
|
|
@@ -38225,7 +38352,7 @@ function serializeMessages4(messages, imageUrls) {
|
|
|
38225
38352
|
}
|
|
38226
38353
|
}
|
|
38227
38354
|
flushToolImages();
|
|
38228
|
-
return wire;
|
|
38355
|
+
return dropBlankWireMessages(wire);
|
|
38229
38356
|
}
|
|
38230
38357
|
function errorMessage4(error) {
|
|
38231
38358
|
if (error instanceof Error) return error.message;
|
|
@@ -68155,12 +68282,19 @@ function apply(ctx, config = {}) {
|
|
|
68155
68282
|
all.map((account) => account.provider).filter((provider) => typeof provider === "string" && provider !== "" && !POOL_OWNED_PROVIDERS.has(provider))
|
|
68156
68283
|
);
|
|
68157
68284
|
for (const provider of providers) {
|
|
68285
|
+
// 「黑名单为空」有两种可能:用户从未选择过,或者用户把模型**全部打开**了
|
|
68286
|
+
// (打开时我们删除键,所以两者都是空表)。单看黑名单无法分辨,一律补静音
|
|
68287
|
+
// 就会把用户特意全开的渠道每次重启又关回去 —— 目录越小的渠道越必然中招。
|
|
68288
|
+
// 只有「用户没动过选择」才补。
|
|
68289
|
+
if (pool.selectionTouched(provider)) continue;
|
|
68158
68290
|
if (pool.disabledModelsFor(provider).size !== 0) continue;
|
|
68159
68291
|
const catalog = channelPackModelAdaptersRef?.[provider]?.listAllModels();
|
|
68160
68292
|
if (!Array.isArray(catalog) || catalog.length === 0) continue;
|
|
68161
|
-
|
|
68293
|
+
// mark: false —— 自动静音不是「用户动过选择」,不能让它把标记置位,
|
|
68294
|
+
// 否则此后用户全开也会因为标记已在而被永久跳过后续逻辑。
|
|
68295
|
+
await pool.setModelsDisabled(provider, catalog.map((model) => model.id), { mark: false });
|
|
68162
68296
|
ctx.logger.info(
|
|
68163
|
-
`[channel-pack] ${provider}
|
|
68297
|
+
`[channel-pack] ${provider} 启动补静音(已有账号但从未选择过模型):已默认静音 ${catalog.length} 个模型(可在账号池页勾选启用)`
|
|
68164
68298
|
);
|
|
68165
68299
|
}
|
|
68166
68300
|
} catch (error) {
|
|
@@ -68174,15 +68308,19 @@ function apply(ctx, config = {}) {
|
|
|
68174
68308
|
const isFirstForProvider = pool.listAccountsByProvider(entry.provider).length === 0;
|
|
68175
68309
|
const hadEmptyBlocklist = pool.disabledModelsFor(entry.provider).size === 0;
|
|
68176
68310
|
const result = await rawAddAccount(entry);
|
|
68177
|
-
|
|
68311
|
+
// 同上:`hadEmptyBlocklist` 分不清「从未选择」与「用户全开」,再加一道
|
|
68312
|
+
// `selectionTouched` 判据,否则给已有账号的渠道新加一个号就会把用户刚打开的
|
|
68313
|
+
// 整目录模型全部关掉。
|
|
68314
|
+
const untouched = !pool.selectionTouched(entry.provider);
|
|
68315
|
+
if ((isFirstForProvider || hadEmptyBlocklist) && untouched && !POOL_OWNED_PROVIDERS.has(entry.provider)) {
|
|
68178
68316
|
void Promise.resolve().then(async () => {
|
|
68179
68317
|
try {
|
|
68180
68318
|
const adapters = channelPackModelAdaptersRef;
|
|
68181
68319
|
const catalog = adapters?.[entry.provider]?.listAllModels();
|
|
68182
68320
|
if (!Array.isArray(catalog) || catalog.length === 0) return;
|
|
68183
|
-
await pool.setModelsDisabled(entry.provider, catalog.map((model) => model.id));
|
|
68321
|
+
await pool.setModelsDisabled(entry.provider, catalog.map((model) => model.id), { mark: false });
|
|
68184
68322
|
ctx.logger.info(
|
|
68185
|
-
`[channel-pack] ${entry.provider} ${isFirstForProvider ? "首次登录" : "
|
|
68323
|
+
`[channel-pack] ${entry.provider} ${isFirstForProvider ? "首次登录" : "补静音(从未选择过)"}:已默认静音 ${catalog.length} 个模型(可在账号池页勾选启用)`
|
|
68186
68324
|
);
|
|
68187
68325
|
} catch (error) {
|
|
68188
68326
|
ctx.logger.warn(
|