dsh-xdpool 2.4.6 → 2.5.1
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 +124 -0
- package/README.en.md +11 -2
- package/README.md +12 -3
- package/assets/community-qr.png +0 -0
- package/cordis.patch.yml +17 -41
- package/lib/bin.js +218 -21
- package/lib/client.js +277 -624
- package/lib/index.d.ts +54 -82
- package/lib/index.js +299 -386
- package/package.json +1 -4
- package/vendor/channel-pack/pack.js +27 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,130 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
本项目的所有重要变更都会记录在此文件。
|
|
3
3
|
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
|
+
## 2.5.1 — 六张 issue、一个交流群入口,外加拿掉 billion-context
|
|
5
|
+
|
|
6
|
+
> 2.5.0 之后收到的反馈集中在两类:**看得见却点不动的界面**(链不显示、单选互顶、卡片空白),和**说错话的报错**(额度已用尽、密钥无效、为预留让你调低预留)。这一版把这两类都按「根因 + 可复现断言」的方式收掉,同时按你的要求移除了 billion-context 那一页,并在「点Star」旁边加了「交流群」。
|
|
7
|
+
|
|
8
|
+
### 修掉六张 issue
|
|
9
|
+
|
|
10
|
+
**#99 路由页有链却不显示,保存还会把链删掉**
|
|
11
|
+
|
|
12
|
+
`setDrafts(prev => seeded.current ? prev : …)` 把守卫放进了函数式更新回调,紧接着一行同步 `seeded.current = true`。React 的回调在提交阶段才跑,那时标志位已经是 `true`,于是恒走 `prev` 分支 —— 初始值 `[]` 原样返回。列表恒空,而保存提交的是整张表,**第一次保存就把服务端那条链覆盖成空**。
|
|
13
|
+
|
|
14
|
+
改成同步判断再置位(与同项目 `GatewayPage.tsx` 同形)。回归用例现在连「形状」一起锁:不许再出现 `setDrafts(prev => seeded …)`。
|
|
15
|
+
|
|
16
|
+
**#101 国内版/国际版的「1M」互相顶掉**
|
|
17
|
+
|
|
18
|
+
两张区域卡同时挂载(折叠面板用 `hidden`,不是卸载),预算单选 `name` 只用了 `budget-${model.id}`,同名即同组,浏览器只保留最后一个 `checked`。后渲染的国际版把国内版的黑点顶掉,表现为「点了没保存」。用 `useId()` 给每一行实例加作用域 —— 只加 region 不够,设置页与侧栏可能同时挂载同一区域。
|
|
19
|
+
|
|
20
|
+
**#102 切回账号池就重拉全部数据(2–4 秒空白)**
|
|
21
|
+
|
|
22
|
+
页签正文是三选一表达式,切页即卸载,`useState` 随之清零;而且 `load()` 把 `account.list` / `model.list` / `credits.balances` / `credits.status` 四合一一 `await Promise.all`,让整张卡等唯一走网络的余额查询。
|
|
23
|
+
|
|
24
|
+
- 快照提到模块作用域,且**写在 `mountedRef` 判据之前**:已经发出去的结果即便在卸载后才回来,仍然落进快照
|
|
25
|
+
- 拆两波:wave1 只问内存里的三项,先把卡画全(余额格写 `…`);wave2 每渠道各自回填
|
|
26
|
+
- `balancePending` 原来被硬编成 `false`,让渲染层那条 `…` 分支成了死代码 —— 现在真正用上了
|
|
27
|
+
- 每次加载带单调序号,旧答案盖不掉新答案
|
|
28
|
+
|
|
29
|
+
**#95 扫码渠道卡整片空白,且不报错**
|
|
30
|
+
|
|
31
|
+
`if (entries.length === 0 && error === undefined) return <></>`。而 RPC 桥解析失败时 `provider.status` 回 `ok: false`、映射出空 map、`entries` 为空、`error` 仍是 `undefined` —— **「桥断了」和「没有扫码账号」渲染成同一片空气**。
|
|
32
|
+
|
|
33
|
+
改成三态:读取中一行字、失败写明原因、成功但确实没有账号才保持安静。
|
|
34
|
+
|
|
35
|
+
**#98 自动化三处**
|
|
36
|
+
|
|
37
|
+
- 「体验XX专家」任务族(`expert_use_maidehao` / `douyinyin` / `shangjia_xuanchuan` / `bowangwang`,加同族的 `WorkBuddy_Valorant`)不在 `EVENT_CHAIN_BUILDERS` 里,所以 accept 和 claim 都正常、进度永远 0/1。这批专家**不在市场列表里**,expertId 只存在于任务自己的 `jump_url` 里(percent-encoded deeplink),因此新增 `expertIdFromJumpUrl()`:先解 `deeplink` 参数、再读内层 `expertId`。use 事件保持默认 `craft` 模式 —— `LOCAL`/零成本是 lighthouse 的专属判据。
|
|
38
|
+
- `unavailableReason()` 遇到第一个低于预留的账号就 `return reserve`,后面账号根本没检查。一张 5 账号池里只要有一个长期低于预留线,任何失败都会被归因成 402「调低预留」。改为计数式:`dead`/`cooling`/`disabled` 先报(这些改数字改不好),只有**无可用账号且全部被预留挡住**才报 `reserve`。
|
|
39
|
+
- `automationEarnings` 写入是 fire-and-forget,领取完成到进程退出的窗口里丢账(实测领了 21 个任务,落盘仍是 `credit: 0`)。`persistEarnings()` 改为返回 promise,`runJob` 收尾 `await this.flushEarnings()`。
|
|
40
|
+
|
|
41
|
+
**#96 免密 lane 全失败,却报「额度已用尽」**
|
|
42
|
+
|
|
43
|
+
403 指纹拒绝被渲染成宿主自己的措辞,指向「等待」;真正该做的是「升级插件」。新增 `withFreeTierHint()`:先说指纹失配、明确「等待无用,请更新」,再保留 per-conversation 配额这一可能(两者从外部无法区分,不自作聪明)。429 仍走原有的会话配额提示。
|
|
44
|
+
|
|
45
|
+
### 新增:「交流群」
|
|
46
|
+
|
|
47
|
+
- 卡片右上角「点Star」旁边多一个「交流群」按钮,点开在原位弹层显示微信群码
|
|
48
|
+
- 底部导航网格从 `1fr auto` 扩成 `1fr auto auto`,否则第二个按钮会掉到标签栏下面
|
|
49
|
+
- 二维码以 data URI 内嵌进 `lib/client.js`:客户端是宿主加载的单个脚本,解析不了包内相对路径,而 `<img>` 加载失败是**静默**的
|
|
50
|
+
- Esc 与点击遮罩都能关;`alt` 写的是操作指引(读屏用户扫不了码)
|
|
51
|
+
- 同一张图放在 `README.md` 与 `README.en.md` 第 13 行(标题与徽章之后),过期了换 `assets/community-qr.png` 并重跑 `tools/make-community-qr.mjs`
|
|
52
|
+
|
|
53
|
+
### 移除:billion-context
|
|
54
|
+
|
|
55
|
+
那一页只读观测另一个插件的回环代理。连同 `src/billion.ts`、`src/billion-mount.ts`、`src/client/BillionPage.tsx`、`/plugins/dsh-xdpool/billion` 路由、`POOL_BILLION_PATH` 与两个文档类型、84 个 `billion.*`/`shell.tabBillion` 语言键、`BILLION_CARD_CSS`、`optionalDependencies.billion-context`,以及 `cordis.patch.yml` 里那行长期 `disabled: true` 的条目一起删掉。README 的页面表随之从七页改回六页。
|
|
56
|
+
|
|
57
|
+
### 一并发布的两个既有改动
|
|
58
|
+
|
|
59
|
+
- **CodeArts 的输出上限是上限,不是默认值** —— `listModels()` 报 `maxTokens`、`resolveModel()` 报 `defaultMaxTokens`、线上 `max_tokens` 做 `Math.min` clamp。原先 `?? 65536` 只兜底不设界,宿主把别的 provider 的 384000 原样递过来就触发 HTTP-200 的 SSE 错误帧。见 #83 / #69。
|
|
60
|
+
- **第三条内容指纹** —— 「Relative paths resolve from the workspace root」改成过去分词形式,池车道与渠道车道同步。见 #77 / #79。
|
|
61
|
+
|
|
62
|
+
### 验证
|
|
63
|
+
|
|
64
|
+
- 全套 **83 文件 1057 用例全绿**;host 侧 `tsc --noEmit` 干净
|
|
65
|
+
- 新增 6 个测试文件:`billion-removed` / `card-shell`(交流群)/ `channel-pool-loading` / `expert-task-family` / `free-lane-refusal` / `reserve-floor-scope`
|
|
66
|
+
- 交互验证不只做源码断言:`tools/probe-card-render.mjs` 用最小 hook 调度器驱动真实 `PoolShell`,完成「点击 → 重渲染 → 弹层出现 → 二维码 8666 字符 data URI → 遮罩关闭」整条链路
|
|
67
|
+
|
|
68
|
+
Refs #95 #96 #98 #99 #101 #102。
|
|
69
|
+
|
|
70
|
+
## 附:CodeArts 的输出上限是上限,不是默认值(细节,已并入 2.5.1)
|
|
71
|
+
|
|
72
|
+
> 华为这条链接 65536 能用、131072 就空流,代码里也一直写着这个数字。但它是 `options.maxTokens ?? 65536` —— 只兜底,不设界。
|
|
73
|
+
|
|
74
|
+
### 触发条件:宿主把别的 provider 的额度原样递过来
|
|
75
|
+
|
|
76
|
+
宿主从模型目录里取 `maxTokens` 填进 `options.maxTokens`,而那份目录里 384000 是 deepseek-v4-pro、fireworks、huggingface 这些**真的能到 384000** 的条目的正常值。CodeArts 自己没声明上限时,这个 384000 就原样发出去了。
|
|
77
|
+
|
|
78
|
+
后果不是 4xx:后端回 **HTTP 200**,把错误装在 SSE 帧里 ——
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
InferHub.4001.200 The request param is invalid, Please check it
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
适配器把它映射成 `INVALID_REQUEST`。用户看到的是「这个账号好好的,突然发不出话」,而新建会话发「你好」就失败,第一反应必然是密钥坏了 —— 这也是 #69「CodeArts 提示密钥无效」的一条候选根因(#69 正文只有一句话,无法确认是这条还是 403 那条,故 #83 里保留判断)。
|
|
85
|
+
|
|
86
|
+
### 实测(2026-10-25,失败会话的原文 system/user + 全部 133 个 tools 直连,只改 max_tokens)
|
|
87
|
+
|
|
88
|
+
| max_tokens | 结果 |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| 384000 | HTTP 200 + SSE 错误帧 `InferHub.4001.200` → INVALID_REQUEST |
|
|
91
|
+
| 131072 | HTTP 200,空流 |
|
|
92
|
+
| 65536 | 正常流式回复 |
|
|
93
|
+
|
|
94
|
+
### 改法:三处对齐,线上请求做 clamp
|
|
95
|
+
|
|
96
|
+
- `listModels()` 每个模型带 `maxTokens`,宿主没有理由再自己猜一个更大的
|
|
97
|
+
- `resolveModel()` 带 `defaultMaxTokens`,`catalogFactsFor()` 会把它当 `fact.maxTokens` 交给宿主 —— 这是宿主唯一能知道上限的通道
|
|
98
|
+
- 线上 `max_tokens: Math.min(options.maxTokens ?? CODEARTS_MAX_OUTPUT_TOKENS, CODEARTS_MAX_OUTPUT_TOKENS)`,调用方给更大就收回,给更小仍保留
|
|
99
|
+
|
|
100
|
+
TRAE 与 Cline 早就这么做了(`clampTraeMaxTokens` / `clampClineMaxTokens`,也都写 `resolved.defaultMaxTokens`),CodeArts 是唯一的例外。
|
|
101
|
+
|
|
102
|
+
- 回归:`tests/codearts-max-tokens.test.ts` 新增 5 例(源码断言,与 `tests/codearts-resign-headers.test.ts` 同形状 —— `pack.js` 是另一个仓库的构建产物,无法 import 驱动)
|
|
103
|
+
- 反向对照:撤掉 `pack.js` 的补丁后 5 例全红,补上全绿
|
|
104
|
+
|
|
105
|
+
Refs #83, #69。
|
|
106
|
+
|
|
107
|
+
## 附:第三条内容指纹 —— 文件引用指引里的 “resolve from the workspace root”(细节,已并入 2.5.1)
|
|
108
|
+
|
|
109
|
+
> 2.4.1/2.4.2 修好的两个指纹管了一周,10 月 28 日又整片 400/11128。这次被点名的不是产品名,而是一句普通说明。
|
|
110
|
+
|
|
111
|
+
### 触发点在 read 工具的文件引用指引
|
|
112
|
+
|
|
113
|
+
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 —— 所以这是内容指纹,不是长度规则、也不是头部规则。
|
|
114
|
+
|
|
115
|
+
这也解释了为什么它看起来「不像」前两条:前两条是识别别的客户端的文案,这一条只是被网关的字符串表收进去了,句子本身没有任何产品名。
|
|
116
|
+
|
|
117
|
+
### 改法:把陈述句改成同一句的过去分词形式
|
|
118
|
+
|
|
119
|
+
`Relative paths resolve from the workspace root` → `Relative paths are resolved against the workspace root`。语义、长度量级、换行位置都不变,被列表收录的子串消失。同一账号上整段 41k 系统提示由 400 转 200;渠道车道与池车道同步补上,`pack.js` 的三处 `[xdpool-local]` 标记保持原样。
|
|
120
|
+
|
|
121
|
+
- 池车道:`BANNED_SYSTEM_PHRASES` 增加第三行
|
|
122
|
+
- 渠道车道:`vendor/channel-pack/pack.js` 的同一个 `replace` 链增加第三段
|
|
123
|
+
- 回归:`tests/system-fingerprint.test.ts` 新增 5 例(含「三处指纹同现」整段用例);`tests/chat-identity-headers.test.ts` 对已发布 bundle 的字节断言补上第三段,pack 刷新若把它冲掉会变红
|
|
124
|
+
|
|
125
|
+
### 其他
|
|
126
|
+
- 全套 79 文件 1028 用例(1026 通过 + 2 跳过)。
|
|
127
|
+
|
|
4
128
|
## 2.4.6 — 扫码账号每次打开都空白:一份数据卡在两个细节里
|
|
5
129
|
|
|
6
130
|
> 2.4.4/2.4.5 之后侧栏能进、能滚了,但扫码过的渠道在账号池里经常整片空白,重开也一样。磁盘上账号是齐的(`state.json` 里 15 条),所以问题全在读取链。
|
package/README.en.md
CHANGED
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
<a href="https://github.com/XDTrees/dsh-xdpool/stargazers"><img src="https://img.shields.io/github/stars/XDTrees/dsh-xdpool?style=flat-square" alt="GitHub stars"></a>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
|
+
## Join the group
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="assets/community-qr.png" width="300" alt="WeChat group code: XD community group">
|
|
14
|
+
<br>
|
|
15
|
+
<sub>Scan with WeChat to join the <b>XD community group</b>. The code is valid for 7 days; if it has expired, say so in <a href="https://github.com/XDTrees/dsh-xdpool/issues">issues</a> and it will be refreshed.</sub>
|
|
16
|
+
<br>
|
|
17
|
+
<sub>The 交流群 button next to “Star” in the card's top-right corner opens this same code — no need to come back to the README.</sub>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
10
20
|
<p align="center">
|
|
11
21
|
<sub>Download stats still come from the previous package name <code>dsh-workbuddy-xdpool</code> (8,976/month as of 2026-10-12 — every download before the rename; the live shields badge kept getting rate-limited by the npm upstream, hence the snapshot figure). The new <code>dsh-xdpool</code> package accumulates its own stats, and the badge switches back to live data later.</sub>
|
|
12
22
|
</p>
|
|
@@ -17,7 +27,7 @@ English | [中文](./README.md)
|
|
|
17
27
|
|
|
18
28
|
It started as one thing: every account you've signed into the WorkBuddy desktop app, merged into one auto-failing-over pool. An account gets rate-limited or runs out of credits, and the request moves to the next one that works. No picking favourites, no manual import.
|
|
19
29
|
|
|
20
|
-
Then it grew into
|
|
30
|
+
Then it grew into six pages. Beyond the account pool: sign in to thirteen vendors' free models by web or QR, aggregate **76 vendors'** free quotas into one lane, read a usage ledger, chain models together, and expose the pool as a local OpenAI-compatible gateway.
|
|
21
31
|
|
|
22
32
|
| Page | What it does |
|
|
23
33
|
| --- | --- |
|
|
@@ -25,7 +35,6 @@ Then it grew into seven pages. Beyond the account pool: sign in to thirteen vend
|
|
|
25
35
|
| **Login channels** | Thirteen vendors that sign in by web or QR — once, and their free models join the picker |
|
|
26
36
|
| **Aggregate** | 76 vendors' free quotas into one pool (none on by default; a vendor lights up when it has a key) |
|
|
27
37
|
| **Dashboard** | Today's turns, calls, failures, failover rescues, tokens, cache hits, speed |
|
|
28
|
-
| **billion-context** | Read-only view of the local billion-context compressor |
|
|
29
38
|
| **Routing** | Chain several models, in order or round-robin |
|
|
30
39
|
| **Gateway** | Expose the pool as a local OpenAI-compatible endpoint, optionally to your LAN |
|
|
31
40
|
|
package/README.md
CHANGED
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
<a href="https://github.com/XDTrees/dsh-xdpool/stargazers"><img src="https://img.shields.io/github/stars/XDTrees/dsh-xdpool?style=flat-square" alt="GitHub stars"></a>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
|
+
## 先加群
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="assets/community-qr.png" width="300" alt="微信群二维码:XD 交流群">
|
|
14
|
+
<br>
|
|
15
|
+
<sub>微信扫一扫加入 <b>XD 交流群</b>。二维码 7 天内有效;过期了在 <a href="https://github.com/XDTrees/dsh-xdpool/issues">issues</a> 说一声会换新。</sub>
|
|
16
|
+
<br>
|
|
17
|
+
<sub>卡片右上角「点Star」旁边的「交流群」按钮打开的也是这个码 —— 不用回头找 README。</sub>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
10
20
|
<p align="center">
|
|
11
21
|
<sub>下载量统计沿用旧包名 <code>dsh-workbuddy-xdpool</code>(截至 2026-10-12 为 8,976 次/月,改名前的全部下载;shields 的实时徽章常被 npm 上游限流,所以这里用了快照数字)。新包 <code>dsh-xdpool</code> 的统计会随时间累积,之后徽章会换回实时数据。</sub>
|
|
12
22
|
</p>
|
|
@@ -17,7 +27,7 @@
|
|
|
17
27
|
|
|
18
28
|
它从一件事开始——把本机 WorkBuddy 桌面 App 里登录过的**所有账号**合成一个自动容错的池子,哪个号被限流了、积分用光了,请求自动换到下一个能用的号上去。你不用挑号,不用手动导入。
|
|
19
29
|
|
|
20
|
-
|
|
30
|
+
然后它长成了六页:账号池之外,还能扫码登录十三家厂商的免费模型、把 **76 家**厂商的免费额度聚合成一条通道、看用量账本、把几个模型串成链、把池子开成一个本地 OpenAI 兼容网关。
|
|
21
31
|
|
|
22
32
|
| 页面 | 干什么 |
|
|
23
33
|
| --- | --- |
|
|
@@ -25,7 +35,6 @@
|
|
|
25
35
|
| **扫码登录** | 十三家支持网页 / 扫码登录的厂商,登一次,它们的免费模型就进选择器 |
|
|
26
36
|
| **聚合渠道** | 76 家厂商的免费额度接进同一个池(默认一家都不开,填了 key 才亮) |
|
|
27
37
|
| **数据看板** | 今天的轮次、调用、失败、换号救回、token、缓存命中、速度 |
|
|
28
|
-
| **billion-context** | 只读观测本机 billion-context 压缩器的运行状态 |
|
|
29
38
|
| **路由** | 把多个模型串成一条链,按顺序或轮换使用 |
|
|
30
39
|
| **网关** | 把池子开成本地 OpenAI 兼容接口,可转发给局域网其它设备 |
|
|
31
40
|
|
|
@@ -38,7 +47,7 @@
|
|
|
38
47
|
|
|
39
48
|
## 先看几张图
|
|
40
49
|
|
|
41
|
-
**账号池(卡片首页:品牌栏 +
|
|
50
|
+
**账号池(卡片首页:品牌栏 + 六个页面标签,顶部就是用量)**
|
|
42
51
|
|
|
43
52
|

|
|
44
53
|
|
|
Binary file
|
package/cordis.patch.yml
CHANGED
|
@@ -4,45 +4,21 @@
|
|
|
4
4
|
- insert:
|
|
5
5
|
- id: llm-xdpool
|
|
6
6
|
name: dsh-xdpool
|
|
7
|
-
# billion-context is deliberately NOT mounted by this layer.
|
|
8
7
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# in
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# 1. Addressability. `disabled: true` keeps the row's id resolvable, so a
|
|
27
|
-
# profile that wants to flip it on can do so with one edit instead of
|
|
28
|
-
# inventing a row.
|
|
29
|
-
# 2. It states the decision where the old attempt lived, where the next person
|
|
30
|
-
# to wonder "why doesn't the pool bring up the compressor?" will look.
|
|
31
|
-
# 3. It keeps `bili-native` (billion-context's own row) as the ONLY mount of
|
|
32
|
-
# that module, which is the arrangement that actually works.
|
|
33
|
-
#
|
|
34
|
-
# CONSEQUENCE, STATED PLAINLY
|
|
35
|
-
#
|
|
36
|
-
# A profile that has not installed billion-context sees the billion card report
|
|
37
|
-
# "not installed". That is the honest reading of the facts, and it is already a
|
|
38
|
-
# first-class state on that page — the page distinguishes not-installed from
|
|
39
|
-
# no-answer from ready precisely so this case reads as information rather than
|
|
40
|
-
# as a bug. Installing billion-context remains a decision the user makes; the
|
|
41
|
-
# pool no longer makes it for them.
|
|
42
|
-
#
|
|
43
|
-
# `src/billion-mount.ts` no longer calls `create()` for the same reason: with
|
|
44
|
-
# this row disabled, a runtime create would fight the very row it sits beside.
|
|
45
|
-
# The function still runs — it is what lets the card distinguish
|
|
46
|
-
# already-installed from not-installed — it just never mounts anything.
|
|
47
|
-
- id: billion-context-xdpool
|
|
48
|
-
disabled: true
|
|
8
|
+
# billion-context is gone from this plugin, and so is the row that used to sit
|
|
9
|
+
# here (`billion-context-xdpool`, kept disabled).
|
|
10
|
+
#
|
|
11
|
+
# 2.0.0 inserted that row so a profile without the compressor would get one for
|
|
12
|
+
# free. Measured against a profile that had ALREADY installed billion-context —
|
|
13
|
+
# the normal state for anyone using it, since `bili-native` comes from that
|
|
14
|
+
# plugin's own `dsh.bundle.patch.yml` — the host did not disable the duplicate
|
|
15
|
+
# and did not resolve it to a no-op: it ran the fiber to `failed`, because the
|
|
16
|
+
# plugin registers a cordis service under its own name and a second registration
|
|
17
|
+
# is refused outright (`service "…" has been registered at <…>`). Every user who
|
|
18
|
+
# had installed it got a permanently failed entry, unexplained, pointing at us.
|
|
19
|
+
#
|
|
20
|
+
# The follow-up kept the row, disabled, "for addressability". That was the wrong
|
|
21
|
+
# trade: it left a name in this file for a feature the plugin no longer has, and
|
|
22
|
+
# the page that read it (`billion-context`) is deleted too. Installing the
|
|
23
|
+
# compressor is its own decision, made by installing it — which is the
|
|
24
|
+
# arrangement that always worked.
|
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.
|
|
@@ -964,7 +993,8 @@ function parseTask(value) {
|
|
|
964
993
|
status: typeof raw["status"] === "string" ? raw["status"] : "",
|
|
965
994
|
claimable: !claimed && target > 0 && current >= target,
|
|
966
995
|
claimed,
|
|
967
|
-
locked: raw["locked"] === true
|
|
996
|
+
locked: raw["locked"] === true,
|
|
997
|
+
jumpUrl: typeof raw["jump_url"] === "string" ? raw["jump_url"] : ""
|
|
968
998
|
};
|
|
969
999
|
}
|
|
970
1000
|
var WorkBuddyUpstreamClient = class {
|
|
@@ -3664,11 +3694,11 @@ var WorkBuddyAccountPool = class {
|
|
|
3664
3694
|
* model id the legacy account-wide check applies (callers that cannot name a
|
|
3665
3695
|
* model, e.g. CLI diagnostics).
|
|
3666
3696
|
*/
|
|
3667
|
-
available(now, modelId, region) {
|
|
3697
|
+
available(now, modelId, region, reserveExempt) {
|
|
3668
3698
|
return this.accounts.filter((account) => {
|
|
3669
3699
|
if (this.disabledIds.has(account.id)) return false;
|
|
3670
3700
|
const reserve = this.creditReserves.get(account.id);
|
|
3671
|
-
if (reserve !== void 0 && reserve > 0) {
|
|
3701
|
+
if (!reserveExempt?.has(account.id) && reserve !== void 0 && reserve > 0) {
|
|
3672
3702
|
const balance = this.creditBalances.get(account.id);
|
|
3673
3703
|
if (balance !== void 0 && balance <= reserve) return false;
|
|
3674
3704
|
}
|
|
@@ -3692,7 +3722,7 @@ var WorkBuddyAccountPool = class {
|
|
|
3692
3722
|
* Counts are over the region's accounts, since a provider only ever sees its
|
|
3693
3723
|
* own gateway.
|
|
3694
3724
|
*/
|
|
3695
|
-
unavailableReason(modelId, region) {
|
|
3725
|
+
unavailableReason(modelId, region, reserveExempt) {
|
|
3696
3726
|
const now = Date.now();
|
|
3697
3727
|
const inRegion = this.accounts.filter((account) => region === void 0 || regionOf(account.credential.domain) === region);
|
|
3698
3728
|
if (inRegion.length === 0) return {
|
|
@@ -3705,6 +3735,29 @@ var WorkBuddyAccountPool = class {
|
|
|
3705
3735
|
let cooling = 0;
|
|
3706
3736
|
let disabled = 0;
|
|
3707
3737
|
let dead = 0;
|
|
3738
|
+
/**
|
|
3739
|
+
* Accounts that survived every OTHER filter and are held back only by their
|
|
3740
|
+
* credit floor.
|
|
3741
|
+
*
|
|
3742
|
+
* Counted rather than returned on the first hit. The old `return` fired the
|
|
3743
|
+
* moment it met one reserved account, so a single account parked under its
|
|
3744
|
+
* floor reported `reserve` for the whole gateway — even while four other
|
|
3745
|
+
* accounts were healthy and had just been tried. The caller turns
|
|
3746
|
+
* `reason === 'reserve'` into a 402 telling the user to lower the reserve,
|
|
3747
|
+
* which pointed at a setting that was not the problem (#98: a five-account
|
|
3748
|
+
* pool reported `reserve_floor` because of one account that had sat below
|
|
3749
|
+
* its floor for weeks, while the actual state was "the rest are cooling").
|
|
3750
|
+
*/
|
|
3751
|
+
let reserved = 0;
|
|
3752
|
+
/**
|
|
3753
|
+
* Accounts that reached the end of the walk unblocked.
|
|
3754
|
+
*
|
|
3755
|
+
* Non-zero means the pool still has somewhere to send a request, so a
|
|
3756
|
+
* reserve is NOT what this answer is about: editing a number cannot fix a
|
|
3757
|
+
* request that some other account is already able to serve, and saying
|
|
3758
|
+
* otherwise sends the reader to the wrong setting.
|
|
3759
|
+
*/
|
|
3760
|
+
let usable = 0;
|
|
3708
3761
|
for (const account of inRegion) {
|
|
3709
3762
|
if (this.disabledIds.has(account.id)) {
|
|
3710
3763
|
disabled += 1;
|
|
@@ -3720,16 +3773,14 @@ var WorkBuddyAccountPool = class {
|
|
|
3720
3773
|
continue;
|
|
3721
3774
|
}
|
|
3722
3775
|
const reserve = this.creditReserves.get(account.id);
|
|
3723
|
-
if (reserve !== void 0 && reserve > 0) {
|
|
3776
|
+
if (!reserveExempt?.has(account.id) && reserve !== void 0 && reserve > 0) {
|
|
3724
3777
|
const balance = this.creditBalances.get(account.id);
|
|
3725
|
-
if (balance !== void 0 && balance <= reserve)
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
dead,
|
|
3730
|
-
reason: "reserve"
|
|
3731
|
-
};
|
|
3778
|
+
if (balance !== void 0 && balance <= reserve) {
|
|
3779
|
+
reserved += 1;
|
|
3780
|
+
continue;
|
|
3781
|
+
}
|
|
3732
3782
|
}
|
|
3783
|
+
usable += 1;
|
|
3733
3784
|
}
|
|
3734
3785
|
if (dead > 0) return {
|
|
3735
3786
|
total: inRegion.length,
|
|
@@ -3752,6 +3803,13 @@ var WorkBuddyAccountPool = class {
|
|
|
3752
3803
|
dead,
|
|
3753
3804
|
reason: "disabled"
|
|
3754
3805
|
};
|
|
3806
|
+
if (usable === 0 && reserved > 0) return {
|
|
3807
|
+
total: inRegion.length,
|
|
3808
|
+
cooling,
|
|
3809
|
+
disabled,
|
|
3810
|
+
dead,
|
|
3811
|
+
reason: "reserve"
|
|
3812
|
+
};
|
|
3755
3813
|
return {
|
|
3756
3814
|
total: inRegion.length,
|
|
3757
3815
|
cooling,
|
|
@@ -3928,16 +3986,18 @@ var WorkBuddyAccountPool = class {
|
|
|
3928
3986
|
* Scans on first use, and rescans when every known account is cooling down: a
|
|
3929
3987
|
* fresh desktop login is the usual way out of an exhausted pool.
|
|
3930
3988
|
*
|
|
3989
|
+
* `reserveExempt` is host-only: account ids whose own credentials freshly
|
|
3990
|
+
* confirmed a zero-credit model. It never bypasses disabled, dead credentials, cooldowns or region.
|
|
3931
3991
|
* `conversationKey` is only consulted under `sticky`; other modes ignore it.
|
|
3932
3992
|
*/
|
|
3933
|
-
async acquire(modelId, region, conversationKey) {
|
|
3993
|
+
async acquire(modelId, region, conversationKey, reserveExempt) {
|
|
3934
3994
|
if (this.accounts.length === 0) await this.scan();
|
|
3935
3995
|
const maxTries = this.accounts.length + 1;
|
|
3936
3996
|
for (let attempt = 0; attempt < maxTries; attempt += 1) {
|
|
3937
|
-
let pool = this.available(Date.now(), modelId, region);
|
|
3997
|
+
let pool = this.available(Date.now(), modelId, region, reserveExempt);
|
|
3938
3998
|
if (pool.length === 0 && attempt === 0) {
|
|
3939
3999
|
await this.scan();
|
|
3940
|
-
pool = this.available(Date.now(), modelId, region);
|
|
4000
|
+
pool = this.available(Date.now(), modelId, region, reserveExempt);
|
|
3941
4001
|
}
|
|
3942
4002
|
if (pool.length === 0) return void 0;
|
|
3943
4003
|
if (this.preferredAccountId !== void 0) {
|
|
@@ -9935,6 +9995,71 @@ function appearanceChain(themeKey = APPEARANCE_THEME_KEY) {
|
|
|
9935
9995
|
};
|
|
9936
9996
|
}
|
|
9937
9997
|
/**
|
|
9998
|
+
* Pull the `expertId` out of a task's `jump_url`.
|
|
9999
|
+
*
|
|
10000
|
+
* The "体验XX专家" tasks (#98) name an expert that is NOT in the marketplace
|
|
10001
|
+
* listing — the reporter翻遍了 marketExpertList in both `agent` and `team`
|
|
10002
|
+
* types and got zero hits — so the only place the id appears is the deeplink
|
|
10003
|
+
* embedded in the task itself:
|
|
10004
|
+
*
|
|
10005
|
+
* https://www.workbuddy.cn/work/launch/?deeplink=workbuddy%3A%2F%2Fexperts%3FexpertId%3Dex_GDl31WahyIH0&buddy_type=workbuddy
|
|
10006
|
+
*
|
|
10007
|
+
* The deeplink is percent-encoded, and it has to be: the `?` and `&` inside it
|
|
10008
|
+
* would otherwise end the outer query. So the parse is two steps — decode the
|
|
10009
|
+
* `deeplink` parameter, then read `expertId` off the scheme it decodes to — and
|
|
10010
|
+
* NOT a regex over the whole URL, which would also match the outer `expertId`
|
|
10011
|
+
* spelling in a future URL shape and silently pick the wrong one.
|
|
10012
|
+
*
|
|
10013
|
+
* Returns undefined for the tasks whose deeplink carries no id at all
|
|
10014
|
+
* (`WorkBuddy_Valorant` points at `workbuddy://expert` with no query), which is
|
|
10015
|
+
* the honest answer: those need a different source and are not guessed here.
|
|
10016
|
+
*/
|
|
10017
|
+
function expertIdFromJumpUrl(jumpUrl) {
|
|
10018
|
+
if (jumpUrl === "") return void 0;
|
|
10019
|
+
/** Read `expertId` off any URL-ish string, decoded or not. */
|
|
10020
|
+
const readId = (value) => {
|
|
10021
|
+
let query = value;
|
|
10022
|
+
try {
|
|
10023
|
+
const parsed = new URL(value);
|
|
10024
|
+
query = parsed.search === "" ? value : parsed.search;
|
|
10025
|
+
} catch {
|
|
10026
|
+
query = value;
|
|
10027
|
+
}
|
|
10028
|
+
const marker = query.indexOf("expertId=");
|
|
10029
|
+
if (marker === -1) return void 0;
|
|
10030
|
+
const rest = query.slice(marker + 9);
|
|
10031
|
+
const end = rest.search(/[&#]/u);
|
|
10032
|
+
const id = (end === -1 ? rest : rest.slice(0, end)).trim();
|
|
10033
|
+
return id === "" ? void 0 : id;
|
|
10034
|
+
};
|
|
10035
|
+
let source = jumpUrl;
|
|
10036
|
+
try {
|
|
10037
|
+
const raw = new URL(jumpUrl).searchParams.get("deeplink");
|
|
10038
|
+
if (raw !== null && raw !== "") source = raw;
|
|
10039
|
+
} catch {
|
|
10040
|
+
source = jumpUrl;
|
|
10041
|
+
}
|
|
10042
|
+
return readId(source);
|
|
10043
|
+
}
|
|
10044
|
+
/**
|
|
10045
|
+
* Build a summon record for an expert the marketplace does not list.
|
|
10046
|
+
*
|
|
10047
|
+
* Everything the chains need beyond the id is cosmetic — the scorer keys on
|
|
10048
|
+
* `expertId` — so the title from the task is reused as the display name rather
|
|
10049
|
+
* than inventing one. `version` is left empty, which every consumer already
|
|
10050
|
+
* defaults to `1.0.0`.
|
|
10051
|
+
*/
|
|
10052
|
+
function expertFromTask(expertId, title) {
|
|
10053
|
+
return {
|
|
10054
|
+
expertId,
|
|
10055
|
+
expertType: "agent",
|
|
10056
|
+
displayName: title,
|
|
10057
|
+
profession: title,
|
|
10058
|
+
version: "",
|
|
10059
|
+
categories: []
|
|
10060
|
+
};
|
|
10061
|
+
}
|
|
10062
|
+
/**
|
|
9938
10063
|
* The three "summon an expert" events (`expert_summon_click` and friends).
|
|
9939
10064
|
*
|
|
9940
10065
|
* Paid before the conversation, in the order the app emits them.
|
|
@@ -10048,7 +10173,12 @@ const EVENT_CHAIN_BUILDERS = {
|
|
|
10048
10173
|
skill_1: true,
|
|
10049
10174
|
expert_5: true,
|
|
10050
10175
|
Expert_team_use_3: true,
|
|
10051
|
-
Expert_lighthouse: true
|
|
10176
|
+
Expert_lighthouse: true,
|
|
10177
|
+
expert_use_maidehao: true,
|
|
10178
|
+
expert_use_douyinyin: true,
|
|
10179
|
+
expert_use_shangjia_xuanchuan: true,
|
|
10180
|
+
expert_use_bowangwang: true,
|
|
10181
|
+
WorkBuddy_Valorant: true
|
|
10052
10182
|
};
|
|
10053
10183
|
const AUTOMATION_TICK_MS = 6e4;
|
|
10054
10184
|
const JOB_KINDS = [
|
|
@@ -10237,6 +10367,16 @@ var WorkBuddyScheduler = class {
|
|
|
10237
10367
|
/** Host hooks that persist the ledger across restarts. */
|
|
10238
10368
|
loadEarnings;
|
|
10239
10369
|
saveEarningsFn;
|
|
10370
|
+
/**
|
|
10371
|
+
* The most recent ledger write that may still be in flight.
|
|
10372
|
+
*
|
|
10373
|
+
* `persistEarnings` is synchronous from its callers' point of view, but the
|
|
10374
|
+
* host's settings write is not: it is an async document update. Keeping the
|
|
10375
|
+
* promise here is what lets {@link flushEarnings} wait for the write that
|
|
10376
|
+
* matters — the last one before a restart — without making every counter
|
|
10377
|
+
* update await disk.
|
|
10378
|
+
*/
|
|
10379
|
+
pendingEarningsWrite = Promise.resolve();
|
|
10240
10380
|
constructor(pool, client, options = {}) {
|
|
10241
10381
|
this.pool = pool;
|
|
10242
10382
|
this.client = client;
|
|
@@ -10273,6 +10413,18 @@ var WorkBuddyScheduler = class {
|
|
|
10273
10413
|
this.saveEarningsFn = save;
|
|
10274
10414
|
}
|
|
10275
10415
|
/**
|
|
10416
|
+
* Wait for the ledger write that is (or was last) in flight.
|
|
10417
|
+
*
|
|
10418
|
+
* Called at the END of a task pass, right after the last claim, so the rewards
|
|
10419
|
+
* that were just collected are on disk before a restart can lose them. It is
|
|
10420
|
+
* deliberately NOT awaited on every counter bump: the settings document write
|
|
10421
|
+
* is the slow part, and paying it per account would slow the pass for no
|
|
10422
|
+
* benefit while the process is still alive.
|
|
10423
|
+
*/
|
|
10424
|
+
async flushEarnings() {
|
|
10425
|
+
await this.pendingEarningsWrite;
|
|
10426
|
+
}
|
|
10427
|
+
/**
|
|
10276
10428
|
* Fold a previously persisted ledger back in, when it belongs to today.
|
|
10277
10429
|
*
|
|
10278
10430
|
* Used after the settings document becomes readable, which happens after
|
|
@@ -10542,7 +10694,7 @@ var WorkBuddyScheduler = class {
|
|
|
10542
10694
|
compensationCredit: (existing?.compensationCredit ?? 0) + compensationCredit,
|
|
10543
10695
|
date: today
|
|
10544
10696
|
});
|
|
10545
|
-
this.persistEarnings();
|
|
10697
|
+
this.pendingEarningsWrite = this.persistEarnings();
|
|
10546
10698
|
}
|
|
10547
10699
|
/** Clear the per-account counters when the local day changes. */
|
|
10548
10700
|
rollEarnings(today) {
|
|
@@ -10558,9 +10710,18 @@ var WorkBuddyScheduler = class {
|
|
|
10558
10710
|
* Best effort on purpose: a failed save must never abort a run that has
|
|
10559
10711
|
* already collected rewards, and the in-memory ledger keeps serving the card
|
|
10560
10712
|
* for the rest of the session either way.
|
|
10713
|
+
*
|
|
10714
|
+
* The returned promise settles when the write has landed (or failed). Callers
|
|
10715
|
+
* that are about to END a run await it: reported in #98, a task pass that
|
|
10716
|
+
* claimed 21 tasks had its process restarted before the volatile settings
|
|
10717
|
+
* write reached disk, so `automationEarnings` still read `credit: 0` while
|
|
10718
|
+
* the upstream had really paid out. Fire-and-forget is fine for a counter the
|
|
10719
|
+
* card will re-read in this process; it is not fine for the last write before
|
|
10720
|
+
* exit.
|
|
10561
10721
|
*/
|
|
10562
10722
|
persistEarnings() {
|
|
10563
|
-
|
|
10723
|
+
const save = this.saveEarningsFn;
|
|
10724
|
+
if (save === void 0) return Promise.resolve();
|
|
10564
10725
|
const accounts = {};
|
|
10565
10726
|
for (const [id, entry] of this.earnings) accounts[id] = entry;
|
|
10566
10727
|
const ledger = {
|
|
@@ -10568,12 +10729,14 @@ var WorkBuddyScheduler = class {
|
|
|
10568
10729
|
accounts
|
|
10569
10730
|
};
|
|
10570
10731
|
try {
|
|
10571
|
-
const saved =
|
|
10572
|
-
if (saved !== void 0 && typeof saved.then === "function") saved.then(void 0, (error) => {
|
|
10732
|
+
const saved = save(ledger);
|
|
10733
|
+
if (saved !== void 0 && typeof saved.then === "function") return Promise.resolve(saved).then(void 0, (error) => {
|
|
10573
10734
|
this.logger.warn?.("dsh-xdpool: could not persist automation earnings:", error);
|
|
10574
10735
|
});
|
|
10736
|
+
return Promise.resolve();
|
|
10575
10737
|
} catch (error) {
|
|
10576
10738
|
this.logger.warn?.("dsh-xdpool: could not persist automation earnings:", error);
|
|
10739
|
+
return Promise.resolve();
|
|
10577
10740
|
}
|
|
10578
10741
|
}
|
|
10579
10742
|
/**
|
|
@@ -10682,6 +10845,7 @@ var WorkBuddyScheduler = class {
|
|
|
10682
10845
|
state.detail = detail;
|
|
10683
10846
|
state.progress = progressNote;
|
|
10684
10847
|
this.logger.info?.(`automation ${accountWord}: ${state.message}`);
|
|
10848
|
+
if (kind === "tasks" || kind === "checkin" || kind === "streak" || kind === "travel" || kind === "compensation") await this.flushEarnings();
|
|
10685
10849
|
}
|
|
10686
10850
|
/** Compose the one-line summary shown on the card. */
|
|
10687
10851
|
summarise(kind, ok, failed, claimed, credit, energy) {
|
|
@@ -10804,10 +10968,43 @@ var WorkBuddyScheduler = class {
|
|
|
10804
10968
|
case "expert_5": return this.expertChains(credential, "agent", task);
|
|
10805
10969
|
case "Expert_team_use_3": return this.expertChains(credential, "team", task);
|
|
10806
10970
|
case "Expert_lighthouse": return this.lighthouseChains(credential);
|
|
10971
|
+
case "expert_use_maidehao":
|
|
10972
|
+
case "expert_use_douyinyin":
|
|
10973
|
+
case "expert_use_shangjia_xuanchuan":
|
|
10974
|
+
case "expert_use_bowangwang":
|
|
10975
|
+
case "WorkBuddy_Valorant": return this.taskExpertChains(credential, task);
|
|
10807
10976
|
default: return [];
|
|
10808
10977
|
}
|
|
10809
10978
|
}
|
|
10810
10979
|
/**
|
|
10980
|
+
* Summon-and-use one expert NAMED BY THE TASK.
|
|
10981
|
+
*
|
|
10982
|
+
* Structurally `expertChains` minus the marketplace: the expert id is read
|
|
10983
|
+
* from the task's deeplink instead of by browsing `marketExpertList`, because
|
|
10984
|
+
* this family's experts are not listed there at all (#98 measured 500 entries
|
|
10985
|
+
* across both `agent` and `team`, zero hits).
|
|
10986
|
+
*
|
|
10987
|
+
* Returns `[]` — not a throw — when the task carries no usable id: the caller
|
|
10988
|
+
* logs "could not be built" only for exceptions, and a task shape this code
|
|
10989
|
+
* does not understand is information, not a fault.
|
|
10990
|
+
*/
|
|
10991
|
+
async taskExpertChains(credential, task) {
|
|
10992
|
+
const expertId = expertIdFromJumpUrl(task.jumpUrl);
|
|
10993
|
+
if (expertId === void 0) {
|
|
10994
|
+
this.logger.warn?.(`automation events ${task.taskCode}: no expertId in jump_url, skipping`);
|
|
10995
|
+
return [];
|
|
10996
|
+
}
|
|
10997
|
+
const expert = expertFromTask(expertId, task.title);
|
|
10998
|
+
await this.client.reportDesktopEvents(credential, expertSummonEvents(expert));
|
|
10999
|
+
if (this.delayMs > 0 && !this.stopped) await sleep(this.delayMs);
|
|
11000
|
+
const chat = await this.client.openConversation(credential, expert.expertId);
|
|
11001
|
+
if (chat === void 0) throw new Error(`${task.taskCode}: no server conversation`);
|
|
11002
|
+
return [{
|
|
11003
|
+
transport: "desktop",
|
|
11004
|
+
events: [...expertChatEvents(expert, chat.conversationId, chat.requestId), expertActualUseEvent(expert, chat.conversationId, chat.requestId)]
|
|
11005
|
+
}];
|
|
11006
|
+
}
|
|
11007
|
+
/**
|
|
10811
11008
|
* The summon-and-use chains for the expert tasks.
|
|
10812
11009
|
*
|
|
10813
11010
|
* Two steps per expert, and both are load-bearing: the summon events alone are
|