dsh-workbuddy-xdpool 1.2.0 → 1.4.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/README.md CHANGED
@@ -9,15 +9,25 @@
9
9
 
10
10
  [English](./README.en.md) | 中文
11
11
 
12
- 将 WorkBuddy 桌面 App 里登录过的**所有账号**自动并入一个 **DeepSeek Harness 模型池**:无需任何手动配置,你在 WorkBuddy 桌面登录的每个账号都会成为一个池成员;某个账号被限流(429)时,请求会自动切换到下一个可用账号,实现多账号自动容错轮换。
12
+ 把 WorkBuddy 桌面 App 里登录过的**所有账号**,合成一个 DSH 模型池。你在 WorkBuddy 里登过几个号,池里就有几个号——不用手动填表,不用一个个录入。
13
13
 
14
- > 与单账号连接插件(如 dsh-workbuddy-connect)的区别:**XD Pool 把多账号当成常态**——它不挑账号、不做手动导入,而是把本机 WorkBuddy 桌面 App 的所有历史登录快照全部纳入一个共享池,用一个 `workbuddy-xdpool` provider 分组对外暴露,模型请求在池内自动 failover。
14
+ 哪个号被限流了、积分用光了,请求自动换到下一个能用的号上去。你只管问,剩下的它自己处理。
15
+
16
+ **另外附送一整套「积分自动化」**:签到、领任务奖励、连登兑换、开猫猫旅行……这些每天都要手动点一遍的活儿,插件在后台替你干完了。
17
+
18
+ > **和单账号连接插件的区别**:`dsh-workbuddy-connect` 那类插件是「一次一个账号」。XD Pool 把**多账号当默认情况**——不挑号、不手动导入,本机 WorkBuddy 所有历史登录快照全部收进一个共享池,对外用一个 `workbuddy-xdpool` provider 分组,池内自动容错。
19
+
20
+ ## 先看几张图
15
21
 
16
22
  **插件配置卡片(设置 → 插件 → DSH WorkBuddy XD Pool)**
17
23
 
18
- ![WorkBuddy 池设置卡片:国内版 / 国际版切换栏、池健康状态、账号面板、积分包与合计、每账号每日签到、模型管理(启用勾选 / 图片输入 / 上下文窗口)](assets/settings-card.png)
24
+ ![WorkBuddy 池设置卡片:国内版 / 国际版切换栏、池健康状态、账号面板、积分包与合计、每账号每日签到、积分自动化面板、模型管理](assets/settings-card.png)
19
25
 
20
- **模型选择器(国内版 / 国际版 两个独立供应商分组;倍率直接拼进 model.name:DSH 0.1.2 composer 只读 name)**
26
+ **积分自动化面板**
27
+
28
+ ![积分自动化:总开关、立即运行按钮、每个任务的上次执行时间与当轮具体领取到的任务清单、连登解锁倒计时](assets/automation-panel.png)
29
+
30
+ **模型选择器(国内版 / 国际版 两个独立分组)**
21
31
 
22
32
  ![模型选择器:国内版与国际版各占一个分组,每个模型名后显示积分倍率与促销标签](assets/model-picker.png)
23
33
 
@@ -27,27 +37,55 @@
27
37
 
28
38
  ## 功能
29
39
 
30
- - **零配置开箱即用**:安装并启用后,WorkBuddy 桌面 App 里每个已登录账号都会在第一次被请求时自动发现、进入池中轮换。无需在插件里手动录入账号。
40
+ ### 一、多账号池
41
+
42
+ - **零配置**:装上、打开,就完事了。WorkBuddy 桌面 App 里每个登过的账号都会被自动发现并入池,不需要在插件里录任何东西。
31
43
 
32
- - **自动容错轮换**:池维护每个账号的 `429` 冷却状态。当某个账号触发限流进入冷却,后续请求会跳过它、落到下一个健康账号;冷却结束自动恢复。所有账号同时冷却时请求才暂停。
44
+ - **自动容错换号**:池子记着每个账号的限流状态。某个号触发 429 就冷却它,后面的请求自动落到下一个健康的号上;冷却结束自动恢复。所有号都在冷却时,请求才排队等待。
33
45
 
34
- - **账号健康一目了然**:插件设置卡片显示池健康状态(N 账号 / X 冷却、当前会轮到哪个账号)、每个账号的令牌有效期与冷却倒计时。
46
+ - **池健康一眼看清**:卡片显示「几个账号 / 几个在冷却」「下一个会轮到谁」,以及每个账号的令牌有效期和冷却倒计时。
35
47
 
36
- - **剩余积分实时可见**:卡片按账号展示积分包(`套餐名 · 剩余 / 总量`)与合计剩余(大字绿色高亮),跟随上游实时刷新。
48
+ - **剩余积分实时可见**:按账号列出积分包(`套餐名 · 剩余 / 总量`)和大字合计,跟着上游实时刷新。
37
49
 
38
- - **模型目录直接标注**:卡片列出当前在池内可用的模型,并标注积分倍率(如 `GLM-5.2 · x0.79`)、免费 / 限时免费 / 夜间折扣标签、图片输入能力与上下文窗口,倍率与标签跟随上游 `credits` / `tags` 实时更新。
50
+ - **模型目录带标注**:列出池里能用的模型,附带积分倍率(如 `GLM-5.2 · x0.79`)、免费 / 限时免费 / 夜间折扣标签、能否收图和上下文窗口大小,全部跟随上游实时更新。
39
51
 
40
- - **每日签到**:卡片在每个账号的积分区下方提供签到按钮,并显示连签天数、每日积分与里程碑额外奖励;一键领取该账号当日签到奖励。多账号可逐个领取,无需先切换账号。领取前会重新查询状态,**今日已领取的账号不会被重复领取**。CLI 亦提供 `checkin` 命令。
52
+ - **国内版 / 国际版自动适配**:按账号凭据里的登录域名自动选上游——国际版登录(`workbuddy.ai`)走 `www.workbuddy.ai`,国内版(默认)走 `copilot.tencent.com`。两种账号可以混在同一个池里。
41
53
 
42
- - **国内版 / 国际版自动适配**:按账号凭据里的登录域名自动选择上游域名——国际版登录(`workbuddy.ai`)走 `www.workbuddy.ai`;国内版(默认)走 `copilot.tencent.com` 与 `www.codebuddy.cn`。同一池内可混用两种区域的账号,各自请求各自区域的上游。
54
+ - **想手动做点啥也行**:卡片和命令行都提供「重新检测账号」「清除所有冷却」「每日签到」三个操作。
55
+
56
+ ### 二、积分自动化
57
+
58
+ 卡片上一个开关,插件在后台把你成长中心的日常动作全做掉。**默认全自动,也可以随时点「立即运行」手动跑一轮。**
59
+
60
+ 它管这五件事:
61
+
62
+ | 任务 | 默认时间 | 干什么 |
63
+ | --- | --- | --- |
64
+ | 每日签到 | 09:00 | 领取当天签到奖励 |
65
+ | 活跃上报 | 10:00 | 报个到,让成长中心知道你还在用 |
66
+ | 任务奖励 | 11:00 | 把任务中心的奖励该报名的报名、该领的领掉 |
67
+ | 连登奖励 | 12:00 | 连登档位兑换 + 抽奖 |
68
+ | 猫猫旅行 | 09:00 · 21:00 | 把猫派出去、回来领奖励 |
43
69
 
44
- - **三种人工动作**:卡片与 CLI 都提供「重新检测账号」(重新扫描桌面登录快照,把新登录的账号并入池)、「清除所有冷却」(立即解除全部 429 冷却)与「每日签到」三个操作。
70
+ **「任务奖励」这一项覆盖了 13 个任务**,包括那些看起来非得本人坐在电脑前点鼠标才能完成的:
71
+
72
+ > 探索优秀灵感 · 使用 5 个模板 · 尝鲜热门技能 · 换「和平精英」主题 · 逛资料库 · 召唤 5 次专家 · 召唤 3 次专家团 · 体验腾讯轻量云专家 · 进入任一小助手应用 · 进入企鹅教师助手 · 设计创意模式建画布 · 创建定时任务 · 猫猫对话
73
+
74
+ 实测一轮跑下来,四个账号合计领到 **3300 积分 + 135 能量**。
75
+
76
+ **卡片上会告诉你这一轮具体领到了什么**——不是只写「领了 8 个任务」,而是把任务名字列出来。连登还差几天解锁也会写清楚(比如「还差 4 天解锁 7 天档」),免得你以为是坏了。
77
+
78
+ **关于「今日收益」**:卡片按来源分四行显示——任务奖励、签到、连登、旅行。今天赚了多少一眼看到,跨天自动清零。
79
+
80
+ **关于保留积分**:可以给每个账号设一个积分下限,余额低于这个数的账号就不派活了,省着点用。
81
+
82
+ > **国际版不显示这个面板**——那边的网关压根没有积分系统。
45
83
 
46
84
  ## 安装
47
85
 
48
- 前置:已安装并登录 WorkBuddy 桌面 App(插件复用 App 的登录状态;多账号 = 在桌面 App 里逐个登录/切换账号即可,每次登录都会被自动吸收进池)。已针对 DSH Desktop host `0.1.2` 兼容。
86
+ 前置条件:装好 WorkBuddy 桌面 App 并登录(插件复用它存在本机的登录状态)。多账号 = 在桌面 App 里换着登几次,每次登录都会被自动吸进池里。已针对 DSH Desktop host `0.1.2` 适配。
49
87
 
50
- > 与 host 兼容 `0.1.1-rc.2` / `0.1.2` 系:设置节安装会按 host 能力自动选择 `settings.installSection`(0.1.2-rc.1+)或自由函数(更早)。
88
+ > 兼容 host `0.1.1-rc.2` / `0.1.2` 系列:安装设置节时会按 host 能力自动选择 `settings.installSection`(0.1.2-rc.1+)或更早的自由函数写法。
51
89
 
52
90
  **方式一:从 npm 安装(推荐)**
53
91
 
@@ -56,8 +94,8 @@
56
94
  dsh plugin --profile desktop add dsh-workbuddy-xdpool
57
95
  ```
58
96
 
59
- > 推荐 npm 安装:pnpm / npm 需要拉取的依赖只有插件自身(**约 1 个包,数秒完成**)。
60
- > 从 GitHub 源码安装会连带安装开发依赖(构建器、测试框架等数百个包),明显更慢。
97
+ > 推荐 npm:需要拉取的依赖只有插件自己(**约 1 个包,几秒搞定**)。
98
+ > 从 GitHub 源码装会连带装开发依赖(构建器、测试框架等几百个包),慢得多。
61
99
 
62
100
  **方式二:从 GitHub 源码安装**
63
101
 
@@ -68,74 +106,74 @@ dsh plugin --profile desktop add github:XDTrees/dsh-workbuddy-xdpool
68
106
  **方式三:手动注册 bundle**
69
107
 
70
108
  ```sh
71
- # 1) 安装包(npm 或 GitHub 任选其一)
109
+ # 1) 先装包(npm 或 GitHub 任选)
72
110
  dsh plugin --profile desktop add dsh-workbuddy-xdpool
73
111
 
74
112
  # 2) 注册 bundle:编辑 ~/.dsh/profiles/desktop/package.json,
75
- # 在 "dsh" → "profile" → "bundles" 数组末尾追加 "dsh-workbuddy-xdpool"
113
+ # 在 "dsh" → "profile" → "bundles" 数组末尾加上 "dsh-workbuddy-xdpool"
76
114
 
77
115
  # 3) 重启 DSH Desktop
78
116
  ```
79
117
 
80
- **本地构建**(开发者):
118
+ **本地构建(开发者)**
81
119
 
82
120
  ```sh
83
121
  pnpm install
84
- pnpm build # 产出 lib/index.js + lib/index.d.ts + lib/bin.js + lib/client.js
85
- pnpm test # 26 项测试(自动容错轮换、按(账号,模型)冷却、签到守卫)
86
- pnpm typecheck # 宿主侧
87
- pnpm typecheck:client # 客户端
122
+ pnpm build # 产出 lib/index.js + lib/index.d.ts + lib/bin.js + lib/client.js
123
+ pnpm test # 138 项测试
124
+ pnpm typecheck # 宿主侧类型检查
125
+ pnpm typecheck:client # 客户端类型检查
88
126
  ```
89
127
 
90
- > **构建产物已随仓库提交**(`lib/` 不再 gitignore)。因此从 GitHub 安装时不需要任何安装期脚本,不会触发 pnpm 的「构建脚本被拦截,请放行」提示。**改动 `src/` 后请重新 `pnpm build` 并把 `lib/` 一并提交**,否则用户拿到的是旧产物。
128
+ > **构建产物已随仓库提交**(`lib/` 不在 gitignore 里),所以从 GitHub 装不需要任何安装期脚本,也不会弹 pnpm 那句「构建脚本被拦截,请放行」。**改了 `src/` 记得重新 `pnpm build` 并把 `lib/` 一起提交**,否则用户拿到的是旧产物。
91
129
 
92
- > 注意:`pnpm install` 需用 pnpm 11(`npx pnpm@11`),必要时加 `--config.confirmModulesPurge=false --config.minimumReleaseAge=0`(pnpm 11 默认的 `minimumReleaseAge` 供应链年龄策略会拦截刚发布的 rc 包)。
130
+ > 注意:`pnpm install` 要用 pnpm 11(`npx pnpm@11`),必要时加 `--config.confirmModulesPurge=false --config.minimumReleaseAge=0`——pnpm 11 默认的 `minimumReleaseAge` 供应链年龄策略会拦下刚发布的 rc 包。
93
131
 
94
- 装好后:模型选择器里会出现 **WorkBuddy XD Pool** 分组;设置 → 插件 → **DSH WorkBuddy XD Pool** 卡片可查看池健康、各账号令牌/积分/签到/冷却,以及「重新检测账号」「清除所有冷却」按钮与每个账号的签到按钮。
132
+ 装完以后:模型选择器里会出现 **WorkBuddy XD Pool** 分组;设置 → 插件 → **DSH WorkBuddy XD Pool** 能看池健康、各账号的令牌 / 积分 / 签到 / 冷却状态,还有「重新检测账号」「清除所有冷却」按钮和每个账号的签到按钮。
95
133
 
96
- 插件在 Web / TUI profile 下同样可用(`--profile web` / `--profile dsh-tui`)。
134
+ Web / TUI profile 也能用(`--profile web` / `--profile dsh-tui`)。
97
135
 
98
136
  ## 命令行
99
137
 
100
138
  统一用 `dsh plugin --profile desktop exec dsh-workbuddy-xdpool <子命令>` 调用:
101
139
 
102
140
  ```sh
103
- dsh plugin --profile desktop exec dsh-workbuddy-xdpool status # 池账号数/冷却 + shim 状态(--credits 查积分、--json 机器可读、--rates 看倍率)
104
- dsh plugin --profile desktop exec dsh-workbuddy-xdpool accounts # 已发现账号(--json)
105
- dsh plugin --profile desktop exec dsh-workbuddy-xdpool doctor # 诊断发现/冷却/上游连通性
106
- dsh plugin --profile desktop exec dsh-workbuddy-xdpool reset # 立即清除所有 429 冷却
107
- dsh plugin --profile desktop exec dsh-workbuddy-xdpool checkin # 查询每个账号今日签到状态(--json 机器可读)
141
+ dsh plugin --profile desktop exec dsh-workbuddy-xdpool status # 池里几个号 / 几个冷却 + shim 状态(--credits 查积分、--json 机器可读、--rates 看倍率)
142
+ dsh plugin --profile desktop exec dsh-workbuddy-xdpool accounts # 已发现的账号(--json)
143
+ dsh plugin --profile desktop exec dsh-workbuddy-xdpool doctor # 诊断发现 / 冷却 / 上游连通性
144
+ dsh plugin --profile desktop exec dsh-workbuddy-xdpool reset # 立刻清掉所有 429 冷却
145
+ dsh plugin --profile desktop exec dsh-workbuddy-xdpool checkin # 查每个账号今天签到了没(--json 机器可读)
108
146
  dsh plugin --profile desktop exec dsh-workbuddy-xdpool checkin all
109
- # 领取所有账号今日签到奖励;也可传账号标签只领一个
110
- dsh plugin --profile desktop exec dsh-workbuddy-xdpool login # 引导如何在桌面再加一个账号入池
147
+ # 领掉所有账号今天的签到奖励;也可以只传一个账号标签领单个
148
+ dsh plugin --profile desktop exec dsh-workbuddy-xdpool login # 教你怎么在桌面再加一个号进池
111
149
  ```
112
150
 
113
151
  ## 池里怎么多账号?
114
152
 
115
- 池走**自动发现**:WorkBuddy 桌面 App 每次登录都会在本机留下一个带令牌的历史快照,XD Pool 扫描这些快照,把每个账号都吸收入池。因此多账号 = 在 WorkBuddy 桌面 App 里逐个登录 / 切换账号即可,之后点卡片「重新检测账号」或重启 DSH,新账号自动成为池成员。
153
+ 池子走**自动发现**:WorkBuddy 桌面 App 每次登录都会在本机留一个带令牌的历史快照,XD Pool 扫描这些快照,把每个账号都收进池。所以多账号 = 在桌面 App 里换着登几次,之后点卡片上的「重新检测账号」或者重启 DSH,新号就自动进池了。
116
154
 
117
- 若你想对桌面 App 之外的某个登录做**显式快照**(例如临时固定某个账号再验证),也可手动导入:
155
+ 如果你想给桌面 App 之外的一次登录做个**显式快照**(比如临时固定某个号做验证),也可以手动导入:
118
156
 
119
157
  ```sh
120
158
  # 在 WorkBuddy 桌面 App 登录账号后(key 自己起名):
121
159
  dsh plugin --profile desktop exec dsh-workbuddy-xdpool import myKey
122
- # 查看/删除已导入快照:
160
+ # 查看 / 删除已导入的快照:
123
161
  dsh plugin --profile desktop exec dsh-workbuddy-xdpool accounts
124
162
  dsh plugin --profile desktop exec dsh-workbuddy-xdpool remove myKey
125
163
  ```
126
164
 
127
- 导入快照以 key 的 **MD5 前 8 位**命名落在 `~/.dsh/.workbuddy-xdpool/`(key 本身记在文件里),中文、带 `/`、带空格的 key 都安全;长期使用靠 refresh token 自动续期,失效则回到桌面重新登录后 `import <key> --force` 覆盖。
165
+ 导入的快照以 key 的 **MD5 前 8 位**命名,放在 `~/.dsh/.workbuddy-xdpool/`(key 本身记在文件里),中文、带 `/`、带空格的 key 都安全。长期使用靠 refresh token 自动续期;失效了就回桌面重新登录,再 `import <key> --force` 覆盖。
128
166
 
129
167
  ## 配置
130
168
 
131
- 池的有效配置经插件设置节(`settings.workbuddy-xdpool`)读取,模型设置页可改,改动即时生效:
169
+ 池的配置走插件设置节(`settings.workbuddy-xdpool`),模型设置页里能改,改完立即生效:
132
170
 
133
171
  | 字段 | 说明 | 默认 |
134
172
  | --- | --- | --- |
135
- | `authFile` | 覆盖 WorkBuddy 桌面 auth 文件路径(跨平台探测异常时用,等价于 `WORKBUDDY_AUTH_FILE`) | 自动探测 |
173
+ | `authFile` | 覆盖 WorkBuddy 桌面 auth 文件路径(跨平台自动探测出问题时用,等同于 `WORKBUDDY_AUTH_FILE`) | 自动探测 |
136
174
  | `cooldownMs` | 单账号 429 冷却时长(毫秒) | `60000` |
137
175
 
138
- 也可直接写在 `~/.dsh/settings.yaml`:
176
+ 也可以直接写进 `~/.dsh/settings.yaml`:
139
177
 
140
178
  ```yaml
141
179
  workbuddy-xdpool:
@@ -144,20 +182,33 @@ workbuddy-xdpool:
144
182
 
145
183
  ## 架构
146
184
 
147
- - **宿主侧**(`src/`,DSH 主进程内):
148
- - `index.ts` —— 注册 `workbuddy-xdpool` provider、`workbuddy-xdpool` 设置节(`settings.installSection`)、4 条同源路由(状态 / 重新检测 / 清除冷却 / 签到)、账号发现与模型目录播种。
149
- - `accounts.ts` —— `WorkBuddyAccountPool`:读本机 WorkBuddy 桌面 auth 快照、429 冷却、round-robin failover 与 token 刷新。
150
- - `catalog.ts` / `upstream.ts` —— 上游模型目录(含每模型积分倍率、免费/图片能力标签)、积分查询与每日签到的上游客户端(按凭据域名自动切换国内 / 国际版域名)。
151
- - `web-status.ts` / `status-paths.ts` —— 卡片消费的同源状态文档与路由;签到是本插件唯一的写操作,按「POST + 回环来源 + 显式 accountId + 领取前二次确认」四重守卫。
152
- - `bin.ts` —— 上述 CLI。
153
- - **客户端**(`src/client/`,浏览器卡片,经 `dsh.client` 由宿主加载):折叠卡片外壳沿用宿主内置卡的 `dsm-plugin-card*` 样式语言(`--dsw-alias-*` 主题变量),内容用 `dsm-workbuddy-xdpool-*` 前缀,绝不污染宿主其它卡片;命名空间 `settings.workbuddy-xdpool`。
154
- - **构建**:`tsdown` 产出 `lib/index.js`(宿主入口)+ `lib/index.d.ts`(类型)+ `lib/bin.js`(CLI)+ `lib/client.js`(CJS,`window.__ModuleLoader__.load` 包裹的浏览器 bundle)。四个产物均随仓库提交,因此安装时不需要任何构建脚本。
185
+ - **宿主侧**(`src/`,跑在 DSH 主进程里)
186
+ - `index.ts` —— 注册 `workbuddy-xdpool` provider、`workbuddy-xdpool` 设置节、各条同源路由(状态 / 重新检测 / 清除冷却 / 签到 / 自动化运行 / 保留积分),以及账号发现与模型目录播种。
187
+ - `accounts.ts` —— `WorkBuddyAccountPool`:读本机 WorkBuddy 桌面 auth 快照、429 冷却、轮换与 token 刷新;账号停用与积分保留也在这里。
188
+ - `scheduler.ts` —— 积分自动化的调度器:按本地时点跑五个任务(签到 / 上报 / 任务 / 连登 / 旅行),每日收益账本落盘,手动「立即运行」走的是同一套逻辑。
189
+ - `task-events.ts` —— 13 条任务事件链的构造。每条链都是纯数据,外加它该走哪个指纹通道(桌面 / 网页);需要真实会话的任务(技能、专家)在这里先开一次真对话,拿到服务端 id。
190
+ - `catalog.ts` / `upstream.ts` —— 上游模型目录(含每模型积分倍率、免费 / 图片能力标签)、积分查询、签到、自动化各接口的上游客户端(按凭据域名自动切国内外)。
191
+ - `web-status.ts` / `status-paths.ts` —— 卡片读的同源状态文档与路由。写操作按「POST + 回环来源 + 显式 accountId」把关。
192
+ - `bin.ts` —— 上面那套 CLI。
193
+ - **客户端**(`src/client/`,浏览器卡片,经 `dsh.client` 由宿主加载)
194
+ 折叠卡片外壳沿用宿主内置卡的 `dsm-plugin-card*` 样式语言(`--dsw-alias-*` 主题变量),内容类名统一 `dsm-workbuddy-xdpool-*` 前缀,不污染宿主其它卡片;文案命名空间 `settings.workbuddy-xdpool`。
195
+ - **构建**
196
+ `tsdown` 产出 `lib/index.js`(宿主入口)+ `lib/index.d.ts`(类型)+ `lib/bin.js`(CLI)+ `lib/client.js`(CJS,用 `window.__ModuleLoader__.load` 包起来的浏览器 bundle)。四个产物都随仓库提交,所以安装时不需要任何构建脚本。
155
197
 
156
198
  ## 已知限制
157
199
 
158
- - **仅使用本机桌面 App 的账号**:池不会、也无法替你发起 WorkBuddy 的登录/扫码(token 由 WorkBuddy 桌面 App 自己的腾讯 SSO 登录铸造并设备绑定)。加池账号 = 在 WorkBuddy 桌面 App 里登录/切换,XD Pool 自动吸收。
159
- - 依赖 WorkBuddy 客户端接口(非官方开放 API),WorkBuddy 更新后插件可能需要随之调整;若某账号 refresh token 失效,回到桌面重新登录即可。
160
- - 若 Windows 与 Linux 用户名不同且 Windows 环境变量未传入 WSL,请用 `WORKBUDDY_AUTH_FILE` 或配置节的 `authFile` 指定实际位置。
200
+ - **只能用本机桌面 App 里的账号**:池不会、也没法替你发起 WorkBuddy 的登录或扫码(token 由 WorkBuddy 桌面 App 自己的腾讯 SSO 铸造并和设备绑定)。加号 = 在桌面 App 里登录 / 切换,XD Pool 自动吸收。
201
+
202
+ - **自动化里有几个任务刻意没做**
203
+ - `Expert_Philanthropy`(公益专家):需要真捐钱,绕不过去。
204
+ - `black_cat`(夜猫子):只在 23:00–08:00 计分,而且**没有积分奖励**——熬夜白熬。
205
+ - `Model_chat_GLM5.2`(指定模型对话)与 `wb_wechat_oa_subscribe_task`(关注公众号):路径已经摸清,留到下个版本。
206
+
207
+ - **`Expert_team_use_3` 偶发卡在 2/3**:有个号当天用掉两次专家团后,第三次怎么发都不涨(换个没用过的专家团也不涨)。怀疑该任务对单账号有当日额度、跨天恢复,但没验证。**新号不受影响**(实测 0/3 → 3/3)。
208
+
209
+ - **依赖 WorkBuddy 客户端接口**(不是官方开放 API),WorkBuddy 更新后插件可能得跟着调。某个账号的 refresh token 失效时,回桌面重新登录即可。
210
+
211
+ - 如果 Windows 和 Linux 用户名不同、且 Windows 环境变量没传进 WSL,请用 `WORKBUDDY_AUTH_FILE` 或配置里的 `authFile` 指定实际位置。
161
212
 
162
213
  ## 免责声明
163
214
 
@@ -172,7 +223,7 @@ workbuddy-xdpool:
172
223
 
173
224
  - [corrinehu/dsh-workbuddy-connect](https://github.com/corrinehu/dsh-workbuddy-connect)(MIT)—— 设置节注册(`settings.installSection`)与 DSH 插件结构、客户端卡片加载机制、桌面端凭据刷新与 loopback shim 加固的核心参照;本项目沿用其「宿主通过 installSection 挂卡片」的打通路径。
174
225
  - [dingminhua/dsh-connect-workbuddy](https://github.com/dingminhua/dsh-connect-workbuddy)(MIT,Copyright (c) 2026 LaoDing)—— `dsm-plugin-card*` 卡片样式语言与 `--dsw-alias-*` 主题变量的参照实现;**每日签到**(`/v2/billing/meter/checkin-activity-status` 与 `/v2/billing/meter/daily-checkin`)、积分包聚合口径(月度周期套餐 / 一次性礼包区分)与国内 / 国际版按 `domain` 选择上游域名的做法,参考了该项目已验证的接口形态。
175
- - [Sliverkiss/workbuddy2api](https://github.com/Sliverkiss/workbuddy2api)(MIT)—— WorkBuddy 上游协议(`copilot.tencent.com` 的 wire behavior)与积分接口的参照实现。
226
+ - [Sliverkiss/workbuddy2api](https://github.com/Sliverkiss/workbuddy2api)(MIT)—— WorkBuddy 上游协议(`copilot.tencent.com` 的 wire behavior)与积分接口的参照实现;**积分自动化**(成长中心签到 / 活跃上报 / 连登兑换 / 猫猫旅行状态机 / 13 条任务事件链 / 专家召唤链)整体照该项目的 Go 版逆向实现移植,每条事件链的判据与实测结论都来自它。
176
227
 
177
228
  以上项目的版权归各自作者所有。本项目采用**参考设计思路 + 独立实现**的方式,未整体复制任何参考项目的源码。若标注有遗漏或不当之处,欢迎提交 issue 指正。
178
229
 
Binary file
Binary file
package/lib/bin.js CHANGED
@@ -29,6 +29,8 @@ const MODELS_CATALOG_PATH = "/v2/enterprises/personal/models";
29
29
  const GLOBAL_CONFIG_PATH = "/v3/config";
30
30
  const JSON_TIMEOUT_MS = 3e4;
31
31
  const ERROR_BODY_LIMIT = 4096;
32
+ /** Cap on the reassembled compaction reply, guarding against a runaway stream. */
33
+ const COMPLETION_TEXT_LIMIT = 65536;
32
34
  /** Insufficient-credit markers, ASCII lowercase plus the original Chinese. */
33
35
  const HARD_CREDIT_MARKERS = [
34
36
  "insufficient credit",
@@ -47,8 +49,31 @@ const HARD_CREDIT_MARKERS = [
47
49
  "额度用尽",
48
50
  "没有积分"
49
51
  ];
50
- /** Session-invalidation markers that mean "sign in again in the WorkBuddy app". */
51
- const SESSION_DEAD_MARKERS = ["Offline user session not found", "12153"];
52
+ /** Session-invalidation markers that mean "this credential is dead; use another".
53
+ * Kept alongside the HTTP-status rule in `classifyUpstreamError`: the status is
54
+ * enough for a direct 401/403, but some failures arrive wrapped in a 200
55
+ * envelope or a 4xx the gateway words differently. Adding the English and
56
+ * Chinese phrasings the upstream actually uses keeps those recoverable too —
57
+ * an unmatched one fell through to `client`, which is terminal in the shim
58
+ * and pinned the pool to the first account (the "API 密钥无效" bug). */
59
+ const SESSION_DEAD_MARKERS = [
60
+ "Offline user session not found",
61
+ "12153",
62
+ "api key is invalid",
63
+ "invalid api key",
64
+ "invalid_api_key",
65
+ "api密钥无效",
66
+ "密钥无效",
67
+ "无效的密钥",
68
+ "unauthorized",
69
+ "token expired",
70
+ "token is invalid",
71
+ "login expired",
72
+ "please login",
73
+ "未登录",
74
+ "登录已失效",
75
+ "重新登录"
76
+ ];
52
77
  /**
53
78
  * Markers for "already checked in today".
54
79
  *
@@ -221,12 +246,82 @@ function envelopeError(status, envelope) {
221
246
  return /* @__PURE__ */ new Error(`workbuddy upstream ${kind} (http ${status}): ${envelope.msg.slice(0, 160)}`);
222
247
  }
223
248
  /**
249
+ * Read an OpenAI-style SSE chat stream and concatenate the assistant text.
250
+ *
251
+ * The upstream always streams (`stream: true` is forced on every chat body),
252
+ * so a non-streaming internal call has to reassemble the deltas itself. Only
253
+ * `choices[0].delta.content` is collected; reasoning deltas are dropped
254
+ * because a compaction summary needs the final answer, not the scratchpad.
255
+ */
256
+ async function readCompletionText(body) {
257
+ const decoder = new TextDecoder();
258
+ const reader = body.getReader();
259
+ let buffer = "";
260
+ let text = "";
261
+ try {
262
+ for (;;) {
263
+ const { done, value } = await reader.read();
264
+ if (done) break;
265
+ buffer += decoder.decode(value, { stream: true });
266
+ let split = buffer.indexOf("\n\n");
267
+ while (split !== -1) {
268
+ const frame = buffer.slice(0, split);
269
+ buffer = buffer.slice(split + 2);
270
+ text += contentOfFrame(frame);
271
+ if (text.length > COMPLETION_TEXT_LIMIT) return text.slice(0, COMPLETION_TEXT_LIMIT);
272
+ split = buffer.indexOf("\n\n");
273
+ }
274
+ }
275
+ if (buffer.trim() !== "") text += contentOfFrame(buffer);
276
+ } finally {
277
+ reader.releaseLock?.();
278
+ }
279
+ return text;
280
+ }
281
+ /** Pull `choices[0].delta.content` (or a non-streaming `message.content`) out of one SSE frame. */
282
+ function contentOfFrame(frame) {
283
+ let out = "";
284
+ for (const rawLine of frame.split(/\r?\n/u)) {
285
+ const line = rawLine.trim();
286
+ if (!line.startsWith("data:")) continue;
287
+ const payload = line.slice(5).trim();
288
+ if (payload === "" || payload === "[DONE]") continue;
289
+ let parsed;
290
+ try {
291
+ parsed = JSON.parse(payload);
292
+ } catch {
293
+ continue;
294
+ }
295
+ if (typeof parsed !== "object" || parsed === null) continue;
296
+ const choices = parsed["choices"];
297
+ if (!Array.isArray(choices) || choices.length === 0) continue;
298
+ const choice = choices[0];
299
+ const delta = choice["delta"];
300
+ if (typeof delta === "object" && delta !== null) {
301
+ const content = delta["content"];
302
+ if (typeof content === "string") out += content;
303
+ }
304
+ const message = choice["message"];
305
+ if (typeof message === "object" && message !== null) {
306
+ const content = message["content"];
307
+ if (typeof content === "string") out += content;
308
+ }
309
+ const data = parsed["data"];
310
+ if (typeof data === "object" && data !== null) {
311
+ const inner = data["content"];
312
+ if (typeof inner === "string") out += inner;
313
+ }
314
+ }
315
+ return out;
316
+ }
317
+ /**
224
318
  * Classify an upstream failure from its HTTP status and body excerpt.
225
319
  * Body markers win over status, because the upstream reuses 400/200 for
226
320
  * several distinct conditions.
227
321
  */
228
322
  function classifyUpstreamError(status, body) {
229
323
  if (status === 402) return "hard_credit";
324
+ if (status === 401 || status === 403) return "session_dead";
230
325
  const lower = body.toLowerCase();
231
326
  for (const marker of HARD_CREDIT_MARKERS) if (lower.includes(marker.toLowerCase()) || body.includes(marker)) return "hard_credit";
232
327
  for (const marker of SESSION_DEAD_MARKERS) if (body.includes(marker)) return "session_dead";
@@ -589,6 +684,51 @@ var WorkBuddyUpstreamClient = class {
589
684
  }
590
685
  return JSON.stringify(obj);
591
686
  }
687
+ /**
688
+ * Parse a raw OpenAI chat body without normalising it.
689
+ *
690
+ * The compactor needs the message array as objects, while `chatStream` only
691
+ * accepts the serialised string form.
692
+ */
693
+ parseChatBody(raw) {
694
+ let body;
695
+ try {
696
+ body = JSON.parse(raw);
697
+ } catch {
698
+ return;
699
+ }
700
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return void 0;
701
+ return body;
702
+ }
703
+ /** Re-serialise `base` with a rewritten `messages` array, still normalised. */
704
+ buildChatBody(base, messages) {
705
+ return this.prepareChatBody(JSON.stringify({
706
+ ...base,
707
+ messages
708
+ }));
709
+ }
710
+ /**
711
+ * Run one NON-streaming completion and return the assistant text.
712
+ *
713
+ * Used only for internal compaction (summarising dropped turns). The chat
714
+ * endpoint itself always streams, so this reassembles the SSE frames into a
715
+ * single string. Throws on any failure: the compactor then falls back to
716
+ * plain truncation rather than failing the user's turn.
717
+ */
718
+ async completeChat(credential, prepared, signal) {
719
+ const response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
720
+ method: "POST",
721
+ headers: chatHeaders(credential),
722
+ body: prepared,
723
+ ...signal === void 0 ? {} : { signal }
724
+ });
725
+ if (!response.ok) {
726
+ const text = (await response.text().catch(() => "")).slice(0, ERROR_BODY_LIMIT);
727
+ throw new Error(`compaction upstream http ${response.status}: ${text}`);
728
+ }
729
+ if (response.body === null) throw new Error("compaction upstream returned no body");
730
+ return await readCompletionText(response.body);
731
+ }
592
732
  /** Forward one chat completion. Never throws for upstream failures. */
593
733
  async chatStream(credential, prepared, signal) {
594
734
  let response;
@@ -2714,6 +2854,16 @@ function dayKey(date) {
2714
2854
  return `${date.getFullYear()}-${month}-${day}`;
2715
2855
  }
2716
2856
  /**
2857
+ * The scheduled SLOT `date` falls in, as `YYYY-MM-DDTHH`.
2858
+ *
2859
+ * The per-job run guard keys on this instead of the date, so a job configured
2860
+ * for several hours runs in each of them while a second tick inside the same
2861
+ * hour is still refused.
2862
+ */
2863
+ function slotKey(date) {
2864
+ return `${dayKey(date)}T${String(date.getHours()).padStart(2, "0")}`;
2865
+ }
2866
+ /**
2717
2867
  * Whether `now`'s local hour is one of `hours`.
2718
2868
  *
2719
2869
  * The reference panel computes a `nextFire` instant and sleeps until it; this
@@ -2818,7 +2968,7 @@ var WorkBuddyScheduler = class {
2818
2968
  this.reportHours = options.reportHours ?? [10];
2819
2969
  this.taskHours = options.taskHours ?? [11];
2820
2970
  this.streakHours = options.streakHours ?? [12];
2821
- this.travelHours = options.travelHours ?? [12];
2971
+ this.travelHours = options.travelHours ?? [9, 21];
2822
2972
  }
2823
2973
  /** Apply a new configuration; safe to call while running. */
2824
2974
  /**
@@ -2896,7 +3046,7 @@ var WorkBuddyScheduler = class {
2896
3046
  async runNow(kind, force = false) {
2897
3047
  const today = dayKey(this.now());
2898
3048
  const state = this.states[kind];
2899
- if (!force && state.lastRunDate === today) return { ...state };
3049
+ if (!force && state.lastRunSlot === slotKey(this.now())) return { ...state };
2900
3050
  this.busy = true;
2901
3051
  try {
2902
3052
  await this.runJob(kind, today);
@@ -2991,10 +3141,11 @@ var WorkBuddyScheduler = class {
2991
3141
  this.busy = true;
2992
3142
  try {
2993
3143
  const now = this.now();
3144
+ const slot = slotKey(now);
2994
3145
  const today = dayKey(now);
2995
3146
  for (const kind of JOB_KINDS) {
2996
3147
  if (this.stopped) return;
2997
- if (this.states[kind].lastRunDate === today) continue;
3148
+ if (this.states[kind].lastRunSlot === slot) continue;
2998
3149
  if (!isFireHour(now, this.hoursOf(kind))) continue;
2999
3150
  await this.runJob(kind, today);
3000
3151
  }
@@ -3092,6 +3243,8 @@ var WorkBuddyScheduler = class {
3092
3243
  let credit = 0;
3093
3244
  let energy = 0;
3094
3245
  let claimed = 0;
3246
+ const detail = [];
3247
+ let progressNote;
3095
3248
  const accounts = this.accountsInOrder();
3096
3249
  if (kind === "tasks") await this.sendEventChains(accounts);
3097
3250
  for (let index = 0; index < accounts.length; index++) {
@@ -3104,7 +3257,10 @@ var WorkBuddyScheduler = class {
3104
3257
  try {
3105
3258
  const claim = await this.client.claimDailyCheckin(account.credential);
3106
3259
  this.recordEarnings(account.id, today, { checkinCredit: claim.credit });
3107
- if (claim.credit > 0) this.logger.info?.(`automation checkin ${account.label}: +${claim.credit} credit`);
3260
+ if (claim.credit > 0) {
3261
+ if (claim.credit > 0) detail.push(`签到 +${claim.credit}`);
3262
+ this.logger.info?.(`automation checkin ${account.label}: +${claim.credit} credit`);
3263
+ }
3108
3264
  } catch (error) {
3109
3265
  if (!isAlreadyCheckin(error)) throw error;
3110
3266
  this.logger.info?.(`automation checkin ${account.label}: already done today`);
@@ -3120,6 +3276,7 @@ var WorkBuddyScheduler = class {
3120
3276
  credit += result.credit;
3121
3277
  energy += result.energy;
3122
3278
  claimed += result.claimed;
3279
+ detail.push(...result.titles);
3123
3280
  this.claimableSeen += result.claimableCount;
3124
3281
  this.recordEarnings(account.id, today, {
3125
3282
  credit: result.credit,
@@ -3129,9 +3286,11 @@ var WorkBuddyScheduler = class {
3129
3286
  this.logger.info?.(`automation tasks ${account.label}: ${result.claimed} claimed (+${result.credit} credit, +${result.energy} energy, ${result.claimableCount} claimable seen)`);
3130
3287
  break;
3131
3288
  }
3132
- case "streak":
3133
- await this.redeemStreak(account);
3289
+ case "streak": {
3290
+ const progress = await this.redeemStreak(account);
3291
+ if (progress !== void 0) progressNote = progress;
3134
3292
  break;
3293
+ }
3135
3294
  case "travel": await this.runTravel(account);
3136
3295
  }
3137
3296
  ok++;
@@ -3142,6 +3301,7 @@ var WorkBuddyScheduler = class {
3142
3301
  if (index < accounts.length - 1 && this.delayMs > 0 && !this.stopped) await sleep(this.delayMs);
3143
3302
  }
3144
3303
  const state = this.states[kind];
3304
+ state.lastRunSlot = slotKey(this.now());
3145
3305
  state.lastRunDate = today;
3146
3306
  state.lastRunAtMs = this.now().getTime();
3147
3307
  state.ok = ok;
@@ -3150,6 +3310,8 @@ var WorkBuddyScheduler = class {
3150
3310
  state.energy = energy;
3151
3311
  state.claimed = claimed;
3152
3312
  state.message = this.summarise(kind, ok, failed, claimed, credit, energy);
3313
+ state.detail = detail;
3314
+ state.progress = progressNote;
3153
3315
  this.logger.info?.(`automation ${accountWord}: ${state.message}`);
3154
3316
  }
3155
3317
  /** Compose the one-line summary shown on the card. */
@@ -3352,6 +3514,7 @@ var WorkBuddyScheduler = class {
3352
3514
  let credit = 0;
3353
3515
  let energy = 0;
3354
3516
  let claimed = 0;
3517
+ const titles = [];
3355
3518
  for (const task of claimable) {
3356
3519
  if (this.stopped) break;
3357
3520
  const reward = await this.client.claimTaskReward(credential, task.taskCode);
@@ -3359,6 +3522,7 @@ var WorkBuddyScheduler = class {
3359
3522
  energy += reward.energy;
3360
3523
  if (reward.credit > 0 || reward.energy > 0) {
3361
3524
  claimed++;
3525
+ titles.push(task.title);
3362
3526
  this.logger.info?.(`automation claim ${account.label}: ${task.title} +${reward.credit}c +${reward.energy}e`);
3363
3527
  } else this.logger.info?.(`automation claim ${account.label}: ${task.taskCode} already claimed`);
3364
3528
  if (this.delayMs > 0 && !this.stopped) await sleep(this.delayMs);
@@ -3367,7 +3531,8 @@ var WorkBuddyScheduler = class {
3367
3531
  claimed,
3368
3532
  credit,
3369
3533
  energy,
3370
- claimableCount: claimable.length
3534
+ claimableCount: claimable.length,
3535
+ titles
3371
3536
  };
3372
3537
  }
3373
3538
  /**
@@ -3407,6 +3572,11 @@ var WorkBuddyScheduler = class {
3407
3572
  }
3408
3573
  if (this.delayMs > 0 && !this.stopped) await sleep(this.delayMs);
3409
3574
  }
3575
+ const pendingTier = status.tiers.find((tier) => tier.status === "locked");
3576
+ if (pendingTier !== void 0) {
3577
+ const remaining = Math.max(0, pendingTier.days - status.days);
3578
+ return remaining > 0 ? `${pendingTier.tier} in ${remaining}d` : `${pendingTier.tier} ready`;
3579
+ }
3410
3580
  }
3411
3581
  /**
3412
3582
  * One trip through the buddy travel loop for an account.
@@ -3450,6 +3620,17 @@ var WorkBuddyScheduler = class {
3450
3620
  this.logger.info?.(`automation travel ${account.label}: nothing to do (state=${travel.state})`);
3451
3621
  }
3452
3622
  };
3623
+ [
3624
+ "You are compacting an ongoing conversation so it can continue without the original history.",
3625
+ "Summarise the transcript below into a dense briefing for the next assistant turn.",
3626
+ "Preserve, in this order of priority:",
3627
+ "1. explicit user requirements, constraints and corrections;",
3628
+ "2. decisions already made, and the reasoning behind them;",
3629
+ "3. concrete facts: file paths, identifiers, commands, numbers, error messages;",
3630
+ "4. unfinished work and the current blocker.",
3631
+ "Drop pleasantries, repetition and superseded attempts.",
3632
+ "Write the briefing only — no preamble, no markdown fence."
3633
+ ].join("\n");
3453
3634
  //#endregion
3454
3635
  //#region src/status.ts
3455
3636
  /** Format the per-model credit multipliers. */