dsh-account-pool 0.1.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 ADDED
@@ -0,0 +1,362 @@
1
+ # dsh-account-pool
2
+
3
+ 把**多个 WorkBuddy / Trae 账号**汇成账号池接入 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness),并在账号之间**自动切换**。
4
+
5
+ 某个账号被限流、积分耗尽或掉线时,自动换下一个账号继续,无需手动干预。
6
+ 两家上游(腾讯 CodeBuddy 系的 WorkBuddy、字节的 Trae)共用同一套选号、
7
+ 失败换号、冷却熔断与用量统计。
8
+
9
+ ## 它解决什么问题
10
+
11
+ 单个 WorkBuddy 账号会限流、会耗尽积分,一个请求被卡住整轮对话就断。
12
+ 本项目在插件内部维护一个账号池:
13
+
14
+ - **多账号并行登录**——插件内走 OAuth 设备流,扫码/点链接即可添加,不依赖 WorkBuddy 桌面端 App
15
+ - **自动切换**——按三因子加权选号(积分比例、闲置补偿、成功率),429 限流 / 402 欠费 / 会话失效时自动换号重试
16
+ - **冷却与熔断**——限流的账号按指数退避冷却(600 秒起,封顶 2 小时),连续失败达阈值熔断
17
+ - **会话粘性**——同一会话尽量复用同一账号,多轮对话不跳号
18
+
19
+ ## 架构
20
+
21
+ 插件**自带「网关」**,不依赖任何外部服务:
22
+
23
+ ```
24
+ DSH LLM seam
25
+ → pi-ai adapter(本插件注册的 provider 路由)
26
+ → loopback shim(127.0.0.1:随机端口,本插件内置)
27
+ ├─ 选号器:三因子加权 + 冷却 + 熔断 + 会话粘性
28
+ └─ 失败自动换号重试
29
+ → copilot.tencent.com / www.workbuddy.ai
30
+ ```
31
+
32
+ 为什么需要 shim:pi-ai provider 只认「一个 baseURL + 一个 apiKey」,
33
+ 而切换必须发生在每次请求。所以在回环地址起一个 OpenAI 兼容端点,
34
+ pi-ai 指向它,由它决定本次请求用哪个账号。
35
+
36
+ **安全**:shim 只绑 `127.0.0.1`,用进程内随机 secret 做鉴权,真实的
37
+ WorkBuddy token 不交给 pi-ai。入站请求校验 Host/Origin 必须回环
38
+ (防 DNS rebinding 与跨站请求),secret 用常量时间比较。
39
+
40
+ ## 安装
41
+
42
+ ```sh
43
+ dsh plugin --profile web add dsh-account-pool
44
+ ```
45
+
46
+ 安装后**需要重启 DSH 进程**(bundle 在启动期加载)。
47
+
48
+ ## 使用
49
+
50
+ 1. 重启后打开 **设置 → WorkBuddy 账号池**
51
+ 2. 点「添加账号」,在浏览器中打开给出的链接完成登录
52
+ 3. 登录完成后账号自动加入池子,模型出现在 DSH 的模型选择器里
53
+ 4. 重复第 2 步可以添加多个账号;它们会被自动调度
54
+
55
+ 账号列表里可以看到每个账号的状态(可用 / 冷却 / 熔断)、积分、成功率和凭证到期时间。
56
+ 可以手动「解冻」某个账号,或「移除」它。
57
+
58
+ ## 导入已有 auth 文件
59
+
60
+ 已有凭证(例如 workbuddy2api 网关的 `auths/*.json`,或桌面端导出的文件)不必重新登录,
61
+ 设置页点「导入 auth」即可,支持两种格式:
62
+
63
+ **嵌套格式**(网关与官方客户端落盘的形态):
64
+
65
+ ```json
66
+ {
67
+ "auth": {
68
+ "accessToken": "eyJ...",
69
+ "refreshToken": "eyJ...",
70
+ "expiresAt": 1794467101,
71
+ "domain": "www.codebuddy.cn",
72
+ "realm": "cn"
73
+ },
74
+ "account": { "uid": "...", "nickname": "..." }
75
+ }
76
+ ```
77
+
78
+ **扁平格式**(手写方便,字段平铺在顶层):
79
+
80
+ ```json
81
+ {
82
+ "accessToken": "eyJ...",
83
+ "refreshToken": "eyJ...",
84
+ "expiresAt": 1794467101,
85
+ "domain": "www.codebuddy.cn",
86
+ "uid": "...",
87
+ "nickname": "..."
88
+ }
89
+ ```
90
+
91
+ 导入方式有两种:
92
+
93
+ - **粘贴 JSON**:把文件内容贴进对话框
94
+ - **服务器路径**:填文件路径,或填目录(会自动扫描里面所有 `.json` 逐个尝试)
95
+
96
+ 行为说明:
97
+
98
+ - `expiresAt` 兼容 **Unix 秒**(auth 文件)与**毫秒**(插件内部),自动识别
99
+ - 缺 `realm` 时按 `domain` 推断区域
100
+ - **区域不匹配的会被跳过并报明原因**,不会把国际版凭证塞进国内版池子
101
+ - 目录里损坏的文件不会中断整批导入,失败的会逐个列出
102
+
103
+ ## 每日任务
104
+
105
+ 设置页有两个页签:**账号**(账号池与运维)与**每日任务**。
106
+
107
+ ### 任务清单
108
+
109
+ 页面按任务列出,每行显示**完成数量**(`已完成账号数 / 总账号数`):
110
+
111
+ | 任务 | 判定依据 | 频率 |
112
+ |---|---|---|
113
+ | **签到** | 调签到的返回(幂等) | 每天一次 |
114
+ | **猫猫旅行** | `daily_limit_reached` 或在途 | 每天一次 |
115
+ | **连登兑换** | 上游给的档位解锁状态 | 满 7/14/28 天 |
116
+ | **抽奖** | 当前抽奖次数 | 次数来自兑换 |
117
+
118
+ 全完成显示绿色 `3/3 已完成`,部分完成显示黄色 `2/3`,没做显示灰色 `0/3`。
119
+
120
+ ### 只读与执行分离
121
+
122
+ **打开页面只做只读查询,不会替你签到或派出。** 点「全部执行」才真正执行。
123
+
124
+ 一个例外要说明:**签到没有只读状态接口**,只能靠调用签到本身来判断(幂等,不会重复扣费)。
125
+ 所以清单里的签到完成数来自**上次执行结果**,从没执行过时显示「未执行」。
126
+
127
+ ### 执行顺序
128
+
129
+ ```
130
+ 签到 → 连登兑换 → 抽奖 → 猫猫旅行
131
+ ```
132
+
133
+ 顺序有意义:
134
+ - 签到放最前——它会让积分冷却的账号恢复可用
135
+ - **兑换必须在抽奖之前**——抽奖次数只能从连登兑换获得
136
+ - 账号间串行执行,避免密集请求触发风控
137
+
138
+ ### 其他
139
+
140
+ - 国际版账号自动跳过(签到/旅行/连登都是国内版活动),不发任何上游请求
141
+ - 重复执行是幂等的:上游返回「今天已签到」,不算失败
142
+
143
+ ## 两个区域
144
+
145
+ 国内版与国际版是两个并行的 provider:
146
+
147
+ | provider | 区域 | 上游 |
148
+ |---|---|---|
149
+ | `workbuddy` | 国内版 | `copilot.tencent.com` |
150
+ | `workbuddy-global` | 国际版 | `www.workbuddy.ai` |
151
+
152
+ **两者的账号不通用。** 上游按登录域签发 token,送到另一个区域的网关会被直接拒绝
153
+ (openresty 层返回 HTML 401)。一个账号只属于一个区域,想两边都用必须分别登录。
154
+
155
+ 在 `cordis.patch.yml` 的 `regions` 里配置启用哪些区域,**默认只开国内版**。
156
+ 没有国际版账号时请不要开 `global`——否则 `workbuddy-global` 会出现在模型选择器里
157
+ 却没有任何可用账号。
158
+
159
+ 两边账号各存一份凭据文件,互不覆盖;同时启用时不同会话可以各选一边。
160
+
161
+ ## 配置
162
+
163
+ `cordis.patch.yml` 支持以下字段:
164
+
165
+ | 字段 | 默认 | 说明 |
166
+ |---|---|---|
167
+ | `regions` | `['cn']` | 启用哪些区域,可选 `cn` / `global` |
168
+ | `softCooldownSeconds` | `600` | 限流后的冷却基数,指数退避封顶 2 小时 |
169
+ | `breakerThreshold` | `3` | 连续失败多少次后熔断该账号 |
170
+
171
+ ## 用量统计
172
+
173
+ 设置页底部有「用量统计」卡片,按 **(时间片, 账号, 模型)** 分桶记录每一次请求:
174
+
175
+ | 指标 | 说明 |
176
+ |---|---|
177
+ | 请求数 / 失败数 | **失败的尝试也计入请求数**,这样「重试放大」才看得出来 |
178
+ | 输入 / 输出 / 总 tokens | 取自上游 SSE 末尾的 `usage` 帧 |
179
+ | 积分消耗 | 上游 `usage.credit` |
180
+ | 平均延迟 | 按样本数加权,不是对每桶均值再取平均 |
181
+ | 按模型明细 | 每个模型的请求、失败、token、积分 |
182
+
183
+ 保留策略:按小时分桶;当桶数超过上限(20 万)时,把**超过 90 天**的小时桶
184
+ 折叠为日桶(日桶长期保留)。折叠由**桶数上限触发**,不是按时间定时执行——
185
+ 所以在流量不大的情况下,历史小时桶会原样保留,不会自动精简。
186
+
187
+ 数据落盘到 `$DSH_HOME/.account-pool.usage.<区域>.json`,防抖 30 秒写入,重启不丢。
188
+
189
+ ## 凭证自动续期
190
+
191
+ **不需要手动管凭证,插件会自己续。**
192
+
193
+ ### 两个触发点
194
+
195
+ | 时机 | 行为 |
196
+ |---|---|
197
+ | **每次请求前** | 取凭据时若发现剩不足 5 分钟,立刻刷新 |
198
+ | **后台每 6 小时** | 检查所有账号,剩余不足 7 天的提前续上 |
199
+
200
+ 后台保活是为了应对**闲置**:只在有请求时刷新的话,长期没人用会导致
201
+ access token 过期——这还能靠 refresh token 救回;但如果闲置到 refresh token
202
+ 也过期,账号就彻底作废、只能重新登录。保活把两个到期时间都往后推。
203
+
204
+ ### 上游的两个有效期
205
+
206
+ 实测上游返回:
207
+
208
+ | 字段 | 时长 | 含义 |
209
+ |---|---|---|
210
+ | `expiresIn` | 30 天 | access token 有效期 |
211
+ | `refreshExpiresIn` | 60 天 | refresh token 有效期,即「最晚还能换新 token」的时间 |
212
+
213
+ refresh 比 access 长一倍,所以保活窗口是充足的。
214
+
215
+ ### 失败处理
216
+
217
+ - **刷新失败但 token 未过期** → 继续用旧的,不因一次网络抖动踢掉账号
218
+ - **并发刷新合并** → 同一账号的并发请求只发一次刷新,不会重复打上游
219
+ - **refresh token 轮换** → 上游返回新 refresh token 时会一并保存
220
+
221
+ ## 凭据存放
222
+
223
+ 凭证落在 `$DSH_HOME/.account-pool.<区域>.json`,权限 `0600`。
224
+ 刷新得到的 token 写回这里,**不写入 DSH settings**。
225
+
226
+ 登出(设置页「全部登出」)会删除对应区域的文件。
227
+
228
+ ## 环境要求
229
+
230
+ - DeepSeek Harness(`web` profile)
231
+ - Node `^22.19.0 || >=24.0.0`
232
+ - 能访问 `copilot.tencent.com`(国内版)或 `www.workbuddy.ai`(国际版)
233
+
234
+ ## 设置界面
235
+
236
+ 插件在 DSH 设置里注册**两个独立菜单**:
237
+
238
+ | 菜单 | 内容 |
239
+ |---|---|
240
+ | **WorkBuddy** | WorkBuddy 国内版 / 国际版的账号、每日任务(签到·旅行·兑换·抽奖)、用量统计 |
241
+ | **Trae** | Trae 账号、每日任务(签到·积分)、用量统计 |
242
+
243
+ 两者是平级的独立分区,互不混杂——各自只显示自己的区域与账号。
244
+
245
+ ## Trae 支持
246
+
247
+ 插件同时支持 **Trae(TRAE SOLO)**,与 WorkBuddy 并列成为独立的上游:
248
+
249
+ | | WorkBuddy | Trae |
250
+ |---|---|---|
251
+ | 上游 | copilot.tencent.com | trae-api-cn.mchost.guru |
252
+ | 认证 | 标准 Bearer | 自定义 `Cloud-IDE-JWT` |
253
+ | 设备标识 | 无 | 需要 machine/device id |
254
+ | 登录方式 | 浏览器授权,自动回调 | **需粘贴回调链接** |
255
+ | 每日任务 | 签到/旅行/兑换/抽奖 | 签到 + 积分 |
256
+
257
+ 两边共用同一套选号、失败换号、冷却、用量统计与安全 shim,
258
+ 只有「上游协议」这一层不同(适配器在 `lib/trae-*.js`)。
259
+
260
+ ### Trae 登录
261
+
262
+ 登录链接的参数必须**与官方客户端完全一致**——缺一个都可能走错登录流程:
263
+ 少了 `auth_type` / `login_channel` / `login_version` 等字段时,Trae 授权页
264
+ 返回的内容明显不同(实测差约 4.5KB),表现为**登录完成后不跳转回调**,
265
+ 用户永远看不到带 `refreshToken` 的地址。
266
+
267
+ 对齐后的参数:`login_version` `auth_from` `login_channel` `plugin_version`
268
+ `auth_type` `client_id` `redirect` `login_trace_id` `auth_callback_url`
269
+ `machine_id` `device_id` `x_device_id` `x_machine_id` `x_device_brand`
270
+ `x_device_type` `x_os_version` `x_app_version` `x_app_type`
271
+
272
+ ### Trae 凭证:导入 storage.json
273
+
274
+ **不做网页登录**,改为导入 Trae 桌面端已登录的凭证。
275
+
276
+ 原因是网页登录在当前部署下走不通:Trae 授权页把 token **fetch 投递到
277
+ `127.0.0.1:18080`**(不是导航跳转)。DSH 在 NAS、浏览器在另一台电脑时,
278
+ 那个 `127.0.0.1` 指**用户自己的电脑**:
279
+
280
+ - 请求被拒 → 页面提示「网络错误」
281
+ - 因为是 fetch 而非导航 → **地址栏不会出现带 token 的链接**
282
+ - 所以「复制地址栏链接」这条路也拿不到东西
283
+
284
+ ```mermaid
285
+ flowchart LR
286
+ A["Trae 授权页"] -->|"fetch 投递 token"| B["127.0.0.1:18080"]
287
+ B -->|"NAS 部署时<br/>指向用户自己的电脑"| C["无服务 → 拒绝"]
288
+ C --> D["页面报网络错误<br/>地址栏无变化 ✗"]
289
+ ```
290
+
291
+ #### 操作步骤
292
+
293
+ 1. 在**已登录 Trae 的电脑**(Windows/macOS)上找到凭证文件:
294
+
295
+ ```
296
+ Windows: %APPDATA%\Trae CN\User\globalStorage\storage.json
297
+ macOS: ~/Library/Application Support/Trae CN/User/globalStorage/storage.json
298
+ ```
299
+
300
+ 2. 插件设置里点「导入凭证」,对话框默认就是**选择文件**:
301
+
302
+ - **直接把文件拖进虚线框**,或点「选择文件」按钮选它
303
+ - 文件在**你本地被浏览器读取**,随表单一起提交——不需要先把文件传到 NAS
304
+ - 也可以切到「粘贴内容」手动粘贴 JSON,或切到「服务器路径」填 NAS 上的路径
305
+
306
+ 三种方式等价,按你的习惯选。文件读取用浏览器标准 FileReader,
307
+ 不引第三方库;超过 2MB 会提示「可能选错了」(storage.json 通常几十 KB)。
308
+
309
+ #### 为什么换个机器也能用
310
+
311
+ 解密只用**硬编码盐值 + 密文自带的随机数**:
312
+
313
+ ```
314
+ salt = SALT_A xor SALT_B
315
+ first = sha512(random) ← random 取自密文
316
+ derived = sha512(first + salt)
317
+ key = derived[0..16] iv = derived[16..32]
318
+ ```
319
+
320
+ 没有机器码、没有 DPAPI、没有系统密钥——**任何机器上都能解**。
321
+ 明文前 64 字节是 sha512 校验值,用来确认解密正确。
322
+
323
+ 区域由凭证自带的 `userRegion` 判定(`CN` / `SG`),大小写不敏感。
324
+
325
+ ## 开发自检
326
+
327
+ ```sh
328
+ npm run check
329
+ ```
330
+
331
+ 17 项检查,覆盖静态、运行时与交互三层:
332
+
333
+ | 类别 | 检查项 |
334
+ |---|---|
335
+ | 静态 | 语法、import/export 一致性、跨作用域引用、props 完整性、死代码 |
336
+ | Host | 模块加载、路由注册/注销配对、路径唯一、同源校验、定时器 unref |
337
+ | 前端 | client.js 加载、两个页签渲染、**所有按钮点击**、边界状态 |
338
+ | 行为 | 并发写入、用量统计口径、选号策略、打包内容 |
339
+
340
+ **这些检查项都是从实际出过的问题里来的**,不是凭空设计的:
341
+
342
+ | 检查项 | 对应的问题 |
343
+ |---|---|
344
+ | 跨作用域引用 | `DailyTasksView` 直接调主组件的 `setLastRun`,报 not defined |
345
+ | props 完整性 | `RegionPanel` 没收到 `onImport`,按钮点了没反应 |
346
+ | 并发写入 | 多个 `save` 共用 `.tmp` 文件名,`rename ENOENT` 丢数据 |
347
+ | 死代码 | `streamBody` 零引用 |
348
+ | 定时器 unref | 粘性清理定时器漏 `unref`,拖住宿主退出 |
349
+
350
+ 改动后跑一遍,能提前挡住这几类问题。`scripts/` 不进发布包。
351
+
352
+ ## 已知限制
353
+
354
+ - 依赖 WorkBuddy 客户端接口(非官方开放 API),上游变更后插件可能需要跟进
355
+ - 账号池状态(冷却、熔断计数)是进程内状态,重启后重置
356
+ - 本插件只负责「接入与调度」,不做自动签到、成长任务等上游活动
357
+
358
+ ## 免责声明
359
+
360
+ 本项目仅供个人学习和研究使用,仅驱动使用者自己的 WorkBuddy 账号。
361
+ 使用者需遵守 WorkBuddy 的服务条款,因使用本项目产生的任何后果由使用者自行承担。
362
+ 本项目与腾讯、WorkBuddy、DeepSeek 均无关联。
@@ -0,0 +1,26 @@
1
+ # dsh-account-pool bundle patch:把多账号池插件挂进 profile 组合层。
2
+ #
3
+ # 插件把 provider 路由注册到 DSH 的模型选择器:
4
+ # workbuddy 国内版(copilot.tencent.com)
5
+ # workbuddy-global 国际版(www.workbuddy.ai)
6
+ #
7
+ # 每个区域各持一套凭据库、选号器与 loopback shim,互不干扰。
8
+ # 账号通过插件设置页的 OAuth 设备流添加;凭证只存在本机
9
+ # $DSH_HOME/.dsh-account-pool.<region>.json(0600),不写入 DSH settings。
10
+ #
11
+ # 关于区域:国际版账号与国内版**不通用**——上游按登录域签发 token,
12
+ # 送到另一个区域的网关会被拒(openresty 层直接返回 HTML 401)。
13
+ # 所以一个账号只属于一个区域,想两边都用得分别登录。
14
+ # 没有国际版账号时请只开 cn,否则 workbuddy-global 会出现在模型选择器里
15
+ # 却没有任何可用账号。
16
+ - insert:
17
+ - id: account-pool
18
+ name: 'dsh-account-pool'
19
+ config:
20
+ # 启用的区域;当前只用国内版。要用国际版再加 'global'。
21
+ # cn=WorkBuddy 国内版 / global=WorkBuddy 国际版 / trae=Trae(独立上游)
22
+ regions: ['cn', 'trae']
23
+ # 选中账号的冷却时长(秒),限流 429 时生效,指数退避封顶 2 小时
24
+ softCooldownSeconds: 600
25
+ # 连续失败多少次后熔断该账号
26
+ breakerThreshold: 3