dsh-multi-router 0.5.5 → 0.5.7
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 +14 -0
- package/README.md +21 -5
- package/THIRD_PARTY_NOTICES.md +31 -0
- package/cordis.patch.yml +2 -2
- package/lib/{chunk-PQETKUGT.js → chunk-JDZCWBA4.js} +248 -35
- package/lib/chunk-JDZCWBA4.js.map +7 -0
- package/lib/{chunk-CT6ZYIAF.js → chunk-P43AWJ4Q.js} +9 -5
- package/lib/chunk-P43AWJ4Q.js.map +7 -0
- package/lib/{chunk-EGLF4CWD.js → chunk-U2MU2M5C.js} +10 -3
- package/lib/chunk-U2MU2M5C.js.map +7 -0
- package/lib/cli.js +1 -1
- package/lib/client.js +153 -42
- package/lib/client.js.map +3 -3
- package/lib/clients.js +5 -3
- package/lib/host-v0.5.7.js +509 -0
- package/lib/host.js +151 -25
- package/lib/index.js +151 -25
- package/lib/index.js.map +4 -4
- package/lib/manager.js +2 -2
- package/lib/shim.js +1 -1
- package/package.json +1 -1
- package/lib/chunk-CT6ZYIAF.js.map +0 -7
- package/lib/chunk-EGLF4CWD.js.map +0 -7
- package/lib/chunk-PQETKUGT.js.map +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。版本号从 `0.1` 起以功能迭代为主,接口在 `1.0` 前可能变化。
|
|
4
4
|
|
|
5
|
+
## 0.5.7
|
|
6
|
+
|
|
7
|
+
- 修复更新后仍使用旧模块:每次发布生成独立的宿主入口和组件 ID,使用 DSH 官方配置重载流程同时替换后台与前端模块,避免复用旧代码缓存。
|
|
8
|
+
- 检测插件页、内置更新器与外部 CLI 的安装变化,空闲后自动重载本插件;等待流式调用、签到、登录与安装任务结束,正在输出的请求正常完成。关闭了插件的用户不会被自动重新启用;失败重载有退避和明确提示。
|
|
9
|
+
- 启动不再等待本机账号扫描、插件清单查询或网络请求。先注册管理接口与模型入口,账号与用量后台更新;初次连接自动重试,显示扫描状态和实际运行版本,新增「重新加载插件」恢复入口。
|
|
10
|
+
- 从 v0.5.6 及更早版本首次升级,关闭再启用插件一次即可迁移,无需退出 DSH;后续更新自动加载。未提供热加载接口的 DSH 保留明确的重启回退说明。
|
|
11
|
+
|
|
12
|
+
## 0.5.6
|
|
13
|
+
|
|
14
|
+
- 修复 WorkBuddy 中国版 `workbuddy.cn` 签发的有效令牌被本地误判失效,同时兼容原 `codebuddy.cn` 签发地址;保留 RS256、账号身份与中国版精确签发地址校验。
|
|
15
|
+
- 添加账号 / 更新凭证面板新增 WorkBuddy 手机短信登录,自动填写 UID 与有效期、保存 AT / RT,可验证刷新令牌并保存轮换后的新凭证;无需 Python。同一登录身份更新已有账号,不自动开启签到。
|
|
16
|
+
- 增加「重新验证登录」只读恢复入口,官方状态验证通过后才解除旧认证失败记录,不重置已领取、结果不明、设备限制或平台暂停记录。
|
|
17
|
+
- 短信发送冷却跨重启保存,验证码会话短期有效、提交次数有限;请求不自动重试,平台限制时暂停。只使用官方 HTTPS 接口并启用证书校验,不向面板返回令牌或上游私有错误。
|
|
18
|
+
|
|
5
19
|
## 0.5.5
|
|
6
20
|
|
|
7
21
|
- Token 明细与总览增加模型用时、工具调用用时、首 Token 平均(TTFT)与输出速度(TPS);支持单次展开与筛选内成功调用汇总,TPS 按输出 Token / 解码秒数加权,失败与取消不混入成功性能。DSH 步骤沿用会话统计口径,直接上游请求保留独立计时,工具用时不猜测;旧记录不补算。
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
一个插件内置 WorkBuddy、Trae、Qoder 的认证、模型发现和聊天传输,提供三家的多账号池及集中路由。运行不依赖原来三个 DSH 插件;三家代码随安装包一起打包。基于 MIT 上游,固定版本见 `upstream-lock.json`,许可见 `THIRD_PARTY_NOTICES.md`。
|
|
8
8
|
|
|
9
|
-
版本:0.5.
|
|
9
|
+
版本:0.5.7。适配 DSH Desktop / Web 0.2.0-rc.2,Node.js ^22.19.0 或 >=24。
|
|
10
10
|
|
|
11
11
|
## 在 DSH 里管理
|
|
12
12
|
|
|
@@ -24,13 +24,15 @@
|
|
|
24
24
|
|
|
25
25
|
保存即时生效;CLI / 手工修改 router.json 也会自动加载。无效配置保留最后一份有效运行配置,写入冲突返回 409,避免覆盖其他页面刚保存的修改。
|
|
26
26
|
|
|
27
|
+
面板与模型入口先启用,本机登录扫描、平台用量、免费模型发现和插件检查在后台进行。扫描时显示进度提示,已有账号和缓存路由可以继续使用;安装 / 更新中的短暂断连会自动重新连接。
|
|
28
|
+
|
|
27
29
|
原始模型 ID 用于上游协议。面板新建路由只填写名称,内部 ID 自动生成;编辑名称保留 ID,不影响已有会话引用。已有路由和预配置的内部 ID 保持兼容,面板不再提供 ID 配置。
|
|
28
30
|
|
|
29
31
|
隐藏只影响模型选择器,已有会话及路由仍可使用隐藏模型;停用账号会将其从新请求的候选池中排除。移出账号池保留独立凭证文件,且禁止移除仍被路由明确绑定的账号。
|
|
30
32
|
|
|
31
33
|
### 界面预览
|
|
32
34
|
|
|
33
|
-
以下为 **v0.5.5 实际面板 + 合成演示数据**。账号名称已替换,不包含真实手机号、邮箱、身份 ID、设备标识、登录凭证或会话内容;余额、用量和性能均为演示数值。
|
|
35
|
+
以下为 **v0.5.5–v0.5.7 实际面板 + 合成演示数据**。账号名称已替换,不包含真实手机号、邮箱、身份 ID、设备标识、登录凭证或会话内容;余额、用量和性能均为演示数值。
|
|
34
36
|
|
|
35
37
|
| 总览与用量 | Token 明细与调用性能 |
|
|
36
38
|
| --- | --- |
|
|
@@ -106,6 +108,16 @@ Qoder 的 `dfmodel` 在真实目录里显示为 DeepSeek-Flash,未声明具体
|
|
|
106
108
|
|
|
107
109
|
总览、签到记录和插件更新使用一致的表格布局;详细执行规则按需展开,路由继续通过二级弹窗编辑。沿用 DSH 明暗主题,窄窗口的宽表格可在表内横向滚动。
|
|
108
110
|
|
|
111
|
+
### WorkBuddy 面板短信登录
|
|
112
|
+
|
|
113
|
+
打开 **账号与登录 → 添加账号 → WorkBuddy → 短信登录并保存凭证**。输入已有账号的手机号,点击发送验证码,输入短信验证码后点击「登录并保存到账号池」。UID、有效期、AT / RT 自动识别保存,无需安装 Python;默认额外验证一次刷新令牌,并保存轮换后的新 AT / RT。同一登录身份更新已有账号,自动签到开关保持原设置。更新已有凭证也可使用此方式。
|
|
114
|
+
|
|
115
|
+
手机号和验证码仅提交给 WorkBuddy 中国版官方 HTTPS 接口;凭证仅在本机后台保存,不在普通面板响应中返回。短信发送至少间隔 60 秒,冷却跨重启保存;验证码最长 5 分钟有效,最多提交 5 次,登录请求不自动重试。遇到平台限制 / 图形验证时暂停请求,请使用官方客户端登录后读取凭证。仅用于已有账号,注册请在官方客户端完成。流程参考 [WorkBuddy-Daily 登录工具](https://github.com/L0NE-6/WorkBuddy-Daily/blob/main/workbuddy_login.py),保留 HTTPS 证书验证。
|
|
116
|
+
|
|
117
|
+

|
|
118
|
+
|
|
119
|
+
签到显示「令牌失效」时,可点击 **重新验证登录**:只查询官方签到状态,验证通过才解除旧认证失败记录,不提交领取、不清空已完成或结果不明的领取记录。该操作全局间隔至少 30 秒。`workbuddy.cn` 与 `codebuddy.cn` 两个中国版官方签发地址均支持;自动签到默认关闭,需在签到页单独开启。
|
|
120
|
+
|
|
109
121
|
## 每日积分签到
|
|
110
122
|
|
|
111
123
|
在配置面板打开 **积分签到**。当前接入 WorkBuddy 国内版、Trae CN 与 Qoder CN / Global 的签到能力:先查询平台是否开放活动、账号与设备是否已领取,再提交领取;活动是否开放和奖励额度以平台响应为准。WorkBuddy AI 与 Trae Global 暂无已核验的签到接口,面板会显示需到官方客户端查看,不猜测接口或奖励。
|
|
@@ -113,7 +125,7 @@ Qoder 的 `dfmodel` 在真实目录里显示为 DeepSeek-Flash,未声明具体
|
|
|
113
125
|
- 每个账号可选择「立即签到」,或「签到全部可用账号」。**每日自动签到默认关闭**,按账号单独开启;已停用账号不参与。可选择「每日启动时签到」或「每天定时签到」,定时时间按北京时间设置,默认保留 10:05。启动模式在当天首次运行时执行,持续运行则新一天自动执行;定时模式错过时间后在下次启动补查。Qoder 最早 10:05 查询当日领取窗口,WorkBuddy / Trae 启动模式不受此窗口限制。20:05 后仅补查确认未提交领取的查询失败或尚未开放窗口,每个账号最多补查一次;DSH 关闭期间不执行。
|
|
114
126
|
- Qoder 先用 PAT 交换临时 Job Token,活动请求使用 `Cosy-ClientType: 10`;仅领取有效期内 `CLAIM_BENEFIT` 且 `CLAIMABLE` 的活动。宣传、锁定或未开放给此账号的活动显示「无活动」,不会误报令牌失效;奖励仅显示领取响应实际返回的金额。TRAE 沿用客户端登录的 Refresh Token 和真实设备参数续期,不依赖短信登录,也不按账号数组位置保存状态。
|
|
115
127
|
- WorkBuddy 国内版沿用已保存的客户端 AT / RT,按 JWT 中的账号身份校验请求;续期后保存新 AT 和轮换的 RT。先查今日签到状态及活动结束时间,再提交一次领取,已签或活动结束时跳过;面板展示返回的连签天数、活动累计积分和本次奖励。网络或 5xx 仅对领取前的只读状态查询最多重试两次(2 秒、4 秒),领取请求不做快速重试。接口契约参考 [WorkBuddy-Daily](https://github.com/L0NE-6/WorkBuddy-Daily/blob/main/workbuddy_daily.py),未采用成长任务、抽奖或客户端活跃模拟。
|
|
116
|
-
-
|
|
128
|
+
- 启动、定时、手动按钮共用持久化记录;改签到计划、刷新面板或重启不重置记录。当天已处理的账号不会重复进入请求队列,令牌失效后等待显式更新凭证或 WorkBuddy 只读登录验证通过再恢复。
|
|
117
129
|
- 请求串行,账号间至少间隔 10 秒。先持久化当天意图再发请求,重复账号副本按平台身份去重,调整账号顺序不影响记录。每天最多提交一次领取;提交后网络超时或结果不明确时不重复提交,请到官方客户端核对。成功、今日已领、无活动和结果不明的账号不会加入晚间重试队列。
|
|
118
130
|
- 保留 Trae 原有设备身份;同一设备当天的领取已使用或结果不明确时,不切到同设备的其他账号继续领取。Qoder 优先复用官方 CLI 的设备身份,没有时使用连接器持久化的单一设备身份,所有账号一致,不随签到轮换。
|
|
119
131
|
- 验证、403 拒绝或限流会暂停该平台签到;在官方客户端处理后,可以点击「恢复后续签到」。恢复不会清除今日尝试记录与设备限制。401 或明确过期且无法续期的凭证单独显示「令牌失效」,请在「账号与登录」更新凭证;替换凭证只解除认证失败记录,已提交或结果不明的领取记录仍保留。普通网络查询失败不会要求重交令牌。不提供更换设备身份、代理或绕过验证码的能力;不能保证不触发平台风控。
|
|
@@ -151,7 +163,7 @@ dsh plugin --profile desktop add dsh-multi-router
|
|
|
151
163
|
dsh plugin --profile web add dsh-multi-router
|
|
152
164
|
```
|
|
153
165
|
|
|
154
|
-
包地址:[dsh-multi-router · npm](https://www.npmjs.com/package/dsh-multi-router)。正式版本由 GitHub Release 自动测试、打包并发布到 npm `latest`,包中已包含编译产物,用户无需自行构建。需要固定版本时可安装 `dsh-multi-router@0.5.
|
|
166
|
+
包地址:[dsh-multi-router · npm](https://www.npmjs.com/package/dsh-multi-router)。正式版本由 GitHub Release 自动测试、打包并发布到 npm `latest`,包中已包含编译产物,用户无需自行构建。需要固定版本时可安装 `dsh-multi-router@0.5.7`。
|
|
155
167
|
|
|
156
168
|
已使用 npm 来源的用户更新:
|
|
157
169
|
|
|
@@ -165,7 +177,11 @@ dsh plugin --profile desktop update dsh-multi-router
|
|
|
165
177
|
dsh plugin --profile desktop add dsh-multi-router
|
|
166
178
|
```
|
|
167
179
|
|
|
168
|
-
|
|
180
|
+
这是同名插件的来源切换;账号、路由和用量保存在包目录之外,会保留。
|
|
181
|
+
|
|
182
|
+
**从 v0.5.7 起,本插件支持运行中更新。** 更新完成后会等待当前模型调用、签到、账号操作与安装任务结束,调用 DSH 官方接口重新加载本插件,前端面板也同步切换。面板顶部显示实际运行版本;安装版本尚未生效时单独提示,不把磁盘版本冒充运行版本。通过插件页、插件内更新器或外部 CLI 更新均可检测;外部更新可能最多延迟一分钟。
|
|
183
|
+
|
|
184
|
+
**首次从 v0.5.6 或更早版本升级:** 旧后台没有自动重载逻辑,在 DSH 插件页关闭再重新启用一次 `dsh-multi-router` 即可加载新入口,无需退出应用。之后升级自动生效。若遇到连接或版本同步异常,面板提供「重新加载插件」与「重新连接」;只有不提供热加载接口的 DSH 才需要升级 DSH 或重启服务。其他插件是否能即时更新仍以 DSH 返回的结果为准。
|
|
169
185
|
|
|
170
186
|
### 方式二:GitHub 链接安装
|
|
171
187
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -133,3 +133,34 @@ SOFTWARE.
|
|
|
133
133
|
引入上述代码不构成对任何平台的授权。本项目**仅供个人学习研究**,解析客户端本地登录态或使用未公开接口可能违反对应平台服务条款,详见 [`DISCLAIMER.md`](./DISCLAIMER.md)。
|
|
134
134
|
|
|
135
135
|
若你对本文件的完整性有疑问或发现遗漏,请提交 issue 或 PR。
|
|
136
|
+
|
|
137
|
+
## WorkBuddy-Daily 登录流程参考
|
|
138
|
+
|
|
139
|
+
- 项目:https://github.com/L0NE-6/WorkBuddy-Daily
|
|
140
|
+
- 文件:`workbuddy_login.py`;MIT;Copyright (c) 2026 L0NE-6
|
|
141
|
+
- 本插件以 JavaScript 重写其短信登录接口流程,并增加本机冷却、短期会话、凭证轮换保存;不运行 Python 脚本,不采用关闭 TLS 证书验证、成长任务、活跃模拟或抽奖逻辑。
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
MIT License
|
|
145
|
+
|
|
146
|
+
Copyright (c) 2026 L0NE-6
|
|
147
|
+
|
|
148
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
149
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
150
|
+
in the Software without restriction, including without limitation the rights
|
|
151
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
152
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
153
|
+
furnished to do so, subject to the following conditions:
|
|
154
|
+
|
|
155
|
+
The above copyright notice and this permission notice shall be included in all
|
|
156
|
+
copies or substantial portions of the Software.
|
|
157
|
+
|
|
158
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
159
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
160
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
161
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
162
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
163
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
164
|
+
SOFTWARE.
|
|
165
|
+
|
|
166
|
+
```
|
package/cordis.patch.yml
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
- insert:
|
|
2
|
-
- id: llm-multi-router-v2
|
|
3
|
-
name:
|
|
2
|
+
- id: llm-multi-router-v2-0.5.7
|
|
3
|
+
name: ./lib/host-v0.5.7.js
|
|
@@ -2,8 +2,11 @@ import {
|
|
|
2
2
|
Checkins,
|
|
3
3
|
jsonCredential,
|
|
4
4
|
qoderPluginDataDir,
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
readLimitedText,
|
|
6
|
+
refreshWorkBuddyCheckinCredential,
|
|
7
|
+
supportsCheckin,
|
|
8
|
+
workbuddyToken
|
|
9
|
+
} from "./chunk-U2MU2M5C.js";
|
|
7
10
|
import {
|
|
8
11
|
discoverLocalAccounts,
|
|
9
12
|
importAccount,
|
|
@@ -33,16 +36,16 @@ import {
|
|
|
33
36
|
} from "./chunk-TEZ2FMEO.js";
|
|
34
37
|
|
|
35
38
|
// src/manager.mjs
|
|
36
|
-
import { createHash, randomBytes as
|
|
39
|
+
import { createHash, randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
|
|
37
40
|
|
|
38
41
|
// package.json
|
|
39
|
-
var version = "0.5.
|
|
42
|
+
var version = "0.5.7";
|
|
40
43
|
|
|
41
44
|
// src/manager.mjs
|
|
42
|
-
import { readFile as
|
|
45
|
+
import { readFile as readFile3, mkdir as mkdir2, mkdtemp as mkdtemp2, rm as rm3 } from "node:fs/promises";
|
|
43
46
|
import { watch } from "node:fs";
|
|
44
47
|
import { dirname as dirname3, join as join4, relative as relative2, resolve } from "node:path";
|
|
45
|
-
import { withFileLock as
|
|
48
|
+
import { withFileLock as withFileLock3 } from "@deepseek-ai/dsh-atomic-write";
|
|
46
49
|
|
|
47
50
|
// upstream/qoder/src/status-paths.ts
|
|
48
51
|
var QODER_STATUS_PATH = "/plugins/dsh-qoder-connect/status";
|
|
@@ -414,7 +417,7 @@ async function refreshGitPlugin(service, profile, row, options2, signal) {
|
|
|
414
417
|
}, { waitMs: 12e4 });
|
|
415
418
|
}
|
|
416
419
|
var PluginUpdates = class {
|
|
417
|
-
constructor({ service = () => void 0, profile = () => void 0, fetchImpl = (...args) => fetch(...args), lookup = registryVersion, refreshGit = refreshGitPlugin, stateFile, now = Date.now } = {}) {
|
|
420
|
+
constructor({ service = () => void 0, profile = () => void 0, fetchImpl = (...args) => fetch(...args), lookup = registryVersion, refreshGit = refreshGitPlugin, onInstalled, stateFile, now = Date.now } = {}) {
|
|
418
421
|
this.stateFile = stateFile;
|
|
419
422
|
this.now = now;
|
|
420
423
|
this.dismissed = {};
|
|
@@ -427,6 +430,7 @@ var PluginUpdates = class {
|
|
|
427
430
|
this.abort = new AbortController();
|
|
428
431
|
this.lookup = lookup;
|
|
429
432
|
this.refreshGit = refreshGit;
|
|
433
|
+
this.onInstalled = onInstalled;
|
|
430
434
|
}
|
|
431
435
|
async load() {
|
|
432
436
|
const saved = this.stateFile && await jsonFile(this.stateFile);
|
|
@@ -458,6 +462,7 @@ var PluginUpdates = class {
|
|
|
458
462
|
async tick() {
|
|
459
463
|
if (this.flight || !this.state().available) return;
|
|
460
464
|
await this.list();
|
|
465
|
+
if (this.abort.signal.aborted) return;
|
|
461
466
|
if (this.flight || this.now() >= this.lastAttempt && this.now() - this.lastAttempt < 864e5) return;
|
|
462
467
|
this.lastAttempt = this.now();
|
|
463
468
|
await this.persist();
|
|
@@ -613,7 +618,8 @@ var PluginUpdates = class {
|
|
|
613
618
|
message: "\u5B9E\u9645 Git \u63D0\u4EA4\u4E0E\u68C0\u67E5\u7ED3\u679C\u4E0D\u4E00\u81F4\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5\u540E\u91CD\u8BD5\uFF1B\u4E0D\u628A\u4ECD\u505C\u7559\u5728\u65E7\u9501\u6587\u4EF6\u7684\u5B89\u88C5\u62A5\u544A\u4E3A\u6210\u529F"
|
|
614
619
|
};
|
|
615
620
|
}
|
|
616
|
-
|
|
621
|
+
const reloadPending = result.application === "restart-required" && this.onInstalled?.(row.name) === true;
|
|
622
|
+
return { name: row.name, status: "installed", application: reloadPending ? "reload-pending" : result.application, message: reloadPending ? "\u5DF2\u5B89\u88C5\uFF0C\u7B49\u5F85\u5F53\u524D\u64CD\u4F5C\u7ED3\u675F\u540E\u81EA\u52A8\u52A0\u8F7D\uFF1B\u8FD0\u884C\u7248\u672C\u89C1\u9762\u677F\u9876\u90E8" : result.application === "restart-required" ? "\u5DF2\u5B89\u88C5\uFF0C\u5B8C\u5168\u9000\u51FA\u5E76\u91CD\u65B0\u6253\u5F00 DSH \u540E\u751F\u6548" : "\u5B89\u88C5\u5B8C\u6210", restartRequired: !reloadPending && result.application === "restart-required" };
|
|
617
623
|
}
|
|
618
624
|
update(names, registry) {
|
|
619
625
|
if (!Array.isArray(names) || !names.length || names.length > 200 || names.some((n) => typeof n !== "string") || new Set(names).size !== names.length) throw Error("\u8BF7\u9009\u62E9\u8981\u66F4\u65B0\u7684\u63D2\u4EF6");
|
|
@@ -649,13 +655,14 @@ var PluginUpdates = class {
|
|
|
649
655
|
job.total = 1;
|
|
650
656
|
job.current = spec;
|
|
651
657
|
const result = await this.service().installBundle(spec, { requestId: job.id, ...registry ? { registry } : {} });
|
|
658
|
+
const reloadPending = !result.error && result.packageResult?.exitCode === 0 && result.application === "restart-required" && this.onInstalled?.(result.bundle) === true;
|
|
652
659
|
job.results.push({
|
|
653
660
|
name: result.bundle ?? spec,
|
|
654
661
|
status: result.error || ["failed", "cancelled"].includes(result.application) || result.packageResult && result.packageResult.exitCode !== 0 ? "failed" : "installed",
|
|
655
|
-
message: result.error ? `\u5B89\u88C5\u672A\u5B8C\u6210\uFF08${result.error.code}\uFF09\uFF0C\u8BF7\u67E5\u770B DSH \u63D2\u4EF6\u5B89\u88C5\u65E5\u5FD7` : result.application === "restart-required" ? "\u5DF2\u5B89\u88C5\uFF0C\u8BF7\u5B8C\u5168\u9000\u51FA\u5E76\u91CD\u542F DSH" : "\u5B89\u88C5\u5B8C\u6210",
|
|
656
|
-
application: result.application,
|
|
662
|
+
message: result.error ? `\u5B89\u88C5\u672A\u5B8C\u6210\uFF08${result.error.code}\uFF09\uFF0C\u8BF7\u67E5\u770B DSH \u63D2\u4EF6\u5B89\u88C5\u65E5\u5FD7` : reloadPending ? "\u5DF2\u5B89\u88C5\uFF0C\u7B49\u5F85\u5F53\u524D\u64CD\u4F5C\u7ED3\u675F\u540E\u81EA\u52A8\u52A0\u8F7D" : result.application === "restart-required" ? "\u5DF2\u5B89\u88C5\uFF0C\u8BF7\u5B8C\u5168\u9000\u51FA\u5E76\u91CD\u542F DSH" : "\u5B89\u88C5\u5B8C\u6210",
|
|
663
|
+
application: reloadPending ? "reload-pending" : result.application,
|
|
657
664
|
pendingBuilds: result.pendingBuilds ?? [],
|
|
658
|
-
restartRequired: result.application === "restart-required"
|
|
665
|
+
restartRequired: !reloadPending && result.application === "restart-required"
|
|
659
666
|
});
|
|
660
667
|
delete job.current;
|
|
661
668
|
await this.list();
|
|
@@ -680,6 +687,141 @@ var PluginUpdates = class {
|
|
|
680
687
|
}
|
|
681
688
|
};
|
|
682
689
|
|
|
690
|
+
// src/workbuddy-login.mjs
|
|
691
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
692
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
693
|
+
import { withFileLock as withFileLock2 } from "@deepseek-ai/dsh-atomic-write";
|
|
694
|
+
var WorkBuddyLogin = class {
|
|
695
|
+
constructor(file, { fetchImpl = (...args) => fetch(...args), now = Date.now } = {}) {
|
|
696
|
+
this.file = file;
|
|
697
|
+
this.fetchImpl = fetchImpl;
|
|
698
|
+
this.now = now;
|
|
699
|
+
this.abort = new AbortController();
|
|
700
|
+
this.abort.signal.addEventListener("abort", () => this.finish(), { once: true });
|
|
701
|
+
}
|
|
702
|
+
async reserve(field, ms) {
|
|
703
|
+
await withFileLock2(this.file, async () => {
|
|
704
|
+
let state = {};
|
|
705
|
+
try {
|
|
706
|
+
state = JSON.parse(await readFile2(this.file, "utf8"));
|
|
707
|
+
} catch (e) {
|
|
708
|
+
if (e.code !== "ENOENT") throw Error("\u767B\u5F55\u51B7\u5374\u8BB0\u5F55\u4E0D\u53EF\u8BFB\uFF0C\u8BF7\u68C0\u67E5\u672C\u673A\u6587\u4EF6");
|
|
709
|
+
}
|
|
710
|
+
if (state.pausedUntil > this.now() || state[field] > this.now()) throw Error("\u8BF7\u6C42\u51B7\u5374\u4E2D\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\uFF1B\u5E73\u53F0\u9650\u5236\u8BF7\u5728\u5B98\u65B9\u5BA2\u6237\u7AEF\u5904\u7406");
|
|
711
|
+
state[field] = this.now() + ms;
|
|
712
|
+
await writePrivateJson(this.file, state);
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
async pause() {
|
|
716
|
+
this.pending = void 0;
|
|
717
|
+
await withFileLock2(this.file, async () => {
|
|
718
|
+
const state = JSON.parse(await readFile2(this.file, "utf8"));
|
|
719
|
+
state.pausedUntil = this.now() + 15 * 6e4;
|
|
720
|
+
await writePrivateJson(this.file, state);
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
async post(path, body) {
|
|
724
|
+
let response;
|
|
725
|
+
try {
|
|
726
|
+
response = await this.fetchImpl(`https://www.workbuddy.cn/v2/plugin/login/${path}`, {
|
|
727
|
+
method: "POST",
|
|
728
|
+
headers: { "Content-Type": "application/json", Accept: "application/json", Origin: "https://www.workbuddy.cn", Referer: "https://www.workbuddy.cn/" },
|
|
729
|
+
body: JSON.stringify(body),
|
|
730
|
+
signal: AbortSignal.any([this.abort.signal, AbortSignal.timeout(3e4)]),
|
|
731
|
+
redirect: "error"
|
|
732
|
+
});
|
|
733
|
+
} catch {
|
|
734
|
+
throw Error("\u767B\u5F55\u8BF7\u6C42\u672A\u5B8C\u6210\uFF0C\u8BF7\u7A0D\u540E\u624B\u52A8\u91CD\u8BD5\uFF1B\u4E0D\u4F1A\u81EA\u52A8\u91CD\u53D1\u77ED\u4FE1");
|
|
735
|
+
}
|
|
736
|
+
if ([403, 429].includes(response.status)) {
|
|
737
|
+
await this.pause();
|
|
738
|
+
throw Error("\u5E73\u53F0\u9650\u5236\u6216\u9700\u8981\u9A8C\u8BC1\uFF0C\u5DF2\u6682\u505C\u767B\u5F55\u8BF7\u6C42\uFF0C\u8BF7\u4F7F\u7528\u5B98\u65B9\u5BA2\u6237\u7AEF");
|
|
739
|
+
}
|
|
740
|
+
if (!response.ok) throw Error("\u5B98\u65B9\u767B\u5F55\u670D\u52A1\u6682\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
|
741
|
+
let result;
|
|
742
|
+
try {
|
|
743
|
+
result = JSON.parse(await readLimitedText(response, 256 * 1024, "Login"));
|
|
744
|
+
} catch {
|
|
745
|
+
throw Error("\u767B\u5F55\u54CD\u5E94\u65E0\u6CD5\u8BC6\u522B\uFF0C\u8BF7\u4F7F\u7528\u5B98\u65B9\u5BA2\u6237\u7AEF");
|
|
746
|
+
}
|
|
747
|
+
if (result?.code !== 0) {
|
|
748
|
+
if ([403, 429].includes(result?.code) || /频繁|图形验证码|滑块|安全验证|captcha|risk|too many|frequent|验证失败次数/i.test(result?.msg || "")) {
|
|
749
|
+
await this.pause();
|
|
750
|
+
throw Error("\u5E73\u53F0\u9650\u5236\u6216\u9700\u8981\u9A8C\u8BC1\uFF0C\u5DF2\u6682\u505C\u767B\u5F55\u8BF7\u6C42\uFF0C\u8BF7\u4F7F\u7528\u5B98\u65B9\u5BA2\u6237\u7AEF");
|
|
751
|
+
}
|
|
752
|
+
throw Error(path === "token" ? "\u767B\u5F55\u672A\u6210\u529F\uFF0C\u8BF7\u68C0\u67E5\u9A8C\u8BC1\u7801\u662F\u5426\u6B63\u786E\u3001\u8FC7\u671F\uFF1B\u4EC5\u652F\u6301\u5DF2\u6709\u8D26\u53F7" : "\u77ED\u4FE1\u672A\u53D1\u9001\u6210\u529F\uFF0C\u8BF7\u786E\u8BA4\u5DF2\u6709\u8D26\u53F7\u53CA\u624B\u673A\u53F7\uFF0C\u6216\u4F7F\u7528\u5B98\u65B9\u5BA2\u6237\u7AEF");
|
|
753
|
+
}
|
|
754
|
+
return result.data ?? {};
|
|
755
|
+
}
|
|
756
|
+
async send(phone) {
|
|
757
|
+
if (typeof phone !== "string" || !/^1[3-9]\d{9}$/.test(phone)) throw Error("\u8BF7\u8F93\u5165 11 \u4F4D\u4E2D\u56FD\u5927\u9646\u624B\u673A\u53F7");
|
|
758
|
+
if (this.flight) throw Error("\u767B\u5F55\u8BF7\u6C42\u6B63\u5728\u5904\u7406\u4E2D");
|
|
759
|
+
this.flight = (async () => {
|
|
760
|
+
await this.reserve("smsAt", 6e4);
|
|
761
|
+
this.pending = void 0;
|
|
762
|
+
const data = await this.post("send-sms", { phone });
|
|
763
|
+
const expires = Math.min(300, Math.max(1, Number(data.expires_in) || 300)) * 1e3;
|
|
764
|
+
this.pending = { phone, challenge: randomBytes2(24).toString("hex"), expiresAt: this.now() + expires, attempts: 0 };
|
|
765
|
+
clearTimeout(this.expiryTimer);
|
|
766
|
+
this.expiryTimer = setTimeout(() => this.finish(), expires);
|
|
767
|
+
this.expiryTimer.unref();
|
|
768
|
+
return { challenge: this.pending.challenge, expiresAt: this.pending.expiresAt, resendAt: this.now() + 6e4 };
|
|
769
|
+
})();
|
|
770
|
+
try {
|
|
771
|
+
return await this.flight;
|
|
772
|
+
} finally {
|
|
773
|
+
this.flight = void 0;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
async complete(challenge, code, verify = true) {
|
|
777
|
+
if (typeof challenge !== "string" || !this.pending || challenge !== this.pending.challenge || this.pending.expiresAt <= this.now()) throw Error("\u672C\u6B21\u767B\u5F55\u5DF2\u8FC7\u671F\u6216\u63D2\u4EF6\u5DF2\u91CD\u542F\uFF0C\u8BF7\u91CD\u65B0\u83B7\u53D6\u9A8C\u8BC1\u7801");
|
|
778
|
+
if (this.flight) throw Error("\u767B\u5F55\u8BF7\u6C42\u6B63\u5728\u5904\u7406\u4E2D");
|
|
779
|
+
if (this.pending.document) return this.pending;
|
|
780
|
+
if (typeof code !== "string" || !/^\d{4,8}$/.test(code) || typeof verify !== "boolean") throw Error("\u8BF7\u8F93\u5165\u6536\u5230\u7684\u77ED\u4FE1\u9A8C\u8BC1\u7801");
|
|
781
|
+
if (this.pending.attempts >= 5) throw Error("\u672C\u6B21\u9A8C\u8BC1\u7801\u5C1D\u8BD5\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u8BF7\u7A0D\u540E\u91CD\u65B0\u83B7\u53D6");
|
|
782
|
+
this.flight = (async () => {
|
|
783
|
+
await this.reserve("loginAt", 5e3);
|
|
784
|
+
const pending = this.pending;
|
|
785
|
+
pending.attempts++;
|
|
786
|
+
const data = await this.post("token", { login_method: "phone", phone: pending.phone, sms_code: code });
|
|
787
|
+
const accessToken = data.accessToken || data.access_token, refreshToken = data.refreshToken || data.refresh_token;
|
|
788
|
+
let token;
|
|
789
|
+
try {
|
|
790
|
+
token = workbuddyToken(accessToken);
|
|
791
|
+
} catch {
|
|
792
|
+
throw Error("\u767B\u5F55\u8FD4\u56DE\u7684\u4EE4\u724C\u4E0D\u5C5E\u4E8E WorkBuddy \u4E2D\u56FD\u7248\uFF0C\u8BF7\u4F7F\u7528\u5B98\u65B9\u5BA2\u6237\u7AEF");
|
|
793
|
+
}
|
|
794
|
+
if (typeof refreshToken !== "string" || !refreshToken || !Number.isFinite(token.exp) || token.exp * 1e3 <= this.now()) throw Error("\u767B\u5F55\u54CD\u5E94\u7F3A\u5C11\u6709\u6548\u51ED\u8BC1\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55");
|
|
795
|
+
let credential = { accessToken, refreshToken, uid: token.sub, expiresAtMs: token.exp * 1e3, domain: "www.workbuddy.cn", source: "dsh" };
|
|
796
|
+
if (verify) {
|
|
797
|
+
try {
|
|
798
|
+
const rotated = await refreshWorkBuddyCheckinCredential(credential, { fetchImpl: this.fetchImpl, signal: AbortSignal.any([this.abort.signal, AbortSignal.timeout(3e4)]) });
|
|
799
|
+
credential = {
|
|
800
|
+
...credential,
|
|
801
|
+
accessToken: rotated.accessToken,
|
|
802
|
+
refreshToken: rotated.refreshToken || credential.refreshToken,
|
|
803
|
+
expiresAtMs: Date.now() + rotated.expiresInSec * 1e3
|
|
804
|
+
};
|
|
805
|
+
} catch {
|
|
806
|
+
pending.warning = "\u767B\u5F55\u51ED\u8BC1\u5DF2\u4FDD\u5B58\uFF0C\u4F46\u5237\u65B0\u4EE4\u724C\u9A8C\u8BC1\u672A\u901A\u8FC7\uFF1B\u8BF7\u7A0D\u540E\u5728\u8D26\u53F7\u8BE6\u60C5\u68C0\u67E5\u767B\u5F55\u6709\u6548\u671F\u3002";
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
delete pending.phone;
|
|
810
|
+
pending.document = { version: 1, credential };
|
|
811
|
+
return pending;
|
|
812
|
+
})();
|
|
813
|
+
try {
|
|
814
|
+
return await this.flight;
|
|
815
|
+
} finally {
|
|
816
|
+
this.flight = void 0;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
finish() {
|
|
820
|
+
clearTimeout(this.expiryTimer);
|
|
821
|
+
this.pending = void 0;
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
|
|
683
825
|
// src/manager.mjs
|
|
684
826
|
var MANAGEMENT_PATH = "/.well-known/dsh-multi-router";
|
|
685
827
|
var revisionOf = (raw) => createHash("sha256").update(JSON.stringify(raw)).digest("hex");
|
|
@@ -731,8 +873,13 @@ var Manager = class {
|
|
|
731
873
|
this.monitorFlights = /* @__PURE__ */ new Map();
|
|
732
874
|
this.loginInfo = /* @__PURE__ */ new Map();
|
|
733
875
|
this.telemetry = new Telemetry(join4(dirname3(file), "usage.json"));
|
|
734
|
-
this.plugins = new PluginUpdates({ ...pluginAccess, stateFile: join4(dirname3(file), "plugin-updates.json")
|
|
876
|
+
this.plugins = new PluginUpdates({ ...pluginAccess, stateFile: join4(dirname3(file), "plugin-updates.json"), onInstalled: (name) => {
|
|
877
|
+
if (name !== "dsh-multi-router" || !this.runtime?.canReload()) return false;
|
|
878
|
+
this.runtime.schedule();
|
|
879
|
+
return true;
|
|
880
|
+
} });
|
|
735
881
|
this.checkins = new Checkins(join4(dirname3(file), "checkins.json"), this);
|
|
882
|
+
this.workbuddyLogin = new WorkBuddyLogin(join4(dirname3(file), "workbuddy-login.json"), { fetchImpl: pluginAccess.fetchImpl });
|
|
736
883
|
this.setupFile = join4(dirname3(file), "initial-setup.json");
|
|
737
884
|
router.telemetry = this.telemetry;
|
|
738
885
|
router.monitor = (a) => this.monitorAccount(a);
|
|
@@ -740,20 +887,17 @@ var Manager = class {
|
|
|
740
887
|
router.freeModels = (account) => this.freeModels(account);
|
|
741
888
|
}
|
|
742
889
|
async load() {
|
|
743
|
-
this.raw = JSON.parse(await
|
|
890
|
+
this.raw = JSON.parse(await readFile3(this.file, "utf8"));
|
|
744
891
|
try {
|
|
745
|
-
this.setup = JSON.parse(await
|
|
892
|
+
this.setup = JSON.parse(await readFile3(this.setupFile, "utf8"));
|
|
746
893
|
} catch {
|
|
747
894
|
this.setup = void 0;
|
|
748
895
|
}
|
|
749
896
|
this.config = validateConfig(this.raw, this.file);
|
|
750
897
|
await this.checkins.load();
|
|
751
898
|
await this.plugins.load();
|
|
752
|
-
if (this.plugins.state().available) await this.plugins.list().catch(() => {
|
|
753
|
-
this.plugins.rows = [];
|
|
754
|
-
});
|
|
755
899
|
try {
|
|
756
|
-
const saved = JSON.parse(await
|
|
900
|
+
const saved = JSON.parse(await readFile3(this.catalogFile, "utf8"));
|
|
757
901
|
for (const account of this.config.accounts) {
|
|
758
902
|
const item = saved[account.id];
|
|
759
903
|
if (item?.credentialFile === account.credentialFile && Array.isArray(item.models)) {
|
|
@@ -793,9 +937,9 @@ var Manager = class {
|
|
|
793
937
|
...discovered.status.map((s) => ({ ...s, checkedAt: Date.now() }))
|
|
794
938
|
];
|
|
795
939
|
if (this.closed || !discovered.accounts.length) return;
|
|
796
|
-
await
|
|
940
|
+
await withFileLock3(this.file, async () => {
|
|
797
941
|
if (this.closed) return;
|
|
798
|
-
let raw = JSON.parse(await
|
|
942
|
+
let raw = JSON.parse(await readFile3(this.file, "utf8"));
|
|
799
943
|
let changed = false;
|
|
800
944
|
const saved = /* @__PURE__ */ new Map();
|
|
801
945
|
for (const item of discovered.accounts) {
|
|
@@ -879,9 +1023,9 @@ var Manager = class {
|
|
|
879
1023
|
}
|
|
880
1024
|
async finishInitialSetup() {
|
|
881
1025
|
if (this.setup?.completedAt) return;
|
|
882
|
-
await
|
|
1026
|
+
await withFileLock3(this.setupFile, async () => {
|
|
883
1027
|
try {
|
|
884
|
-
this.setup = JSON.parse(await
|
|
1028
|
+
this.setup = JSON.parse(await readFile3(this.setupFile, "utf8"));
|
|
885
1029
|
} catch {
|
|
886
1030
|
}
|
|
887
1031
|
if (this.setup?.completedAt) return;
|
|
@@ -1063,7 +1207,7 @@ var Manager = class {
|
|
|
1063
1207
|
if (this.closed || !this.config.accounts.some((a) => a.id === account.id && a.credentialFile === account.credentialFile)) return;
|
|
1064
1208
|
this.catalogs.set(account.id, { credentialFile: account.credentialFile, fetchedAt: Date.now(), pricingVersion: 1, models });
|
|
1065
1209
|
this.catalogErrors.delete(account.id);
|
|
1066
|
-
await
|
|
1210
|
+
await withFileLock3(this.catalogFile, () => writePrivateJson(this.catalogFile, Object.fromEntries(this.catalogs)));
|
|
1067
1211
|
} catch (error) {
|
|
1068
1212
|
if (this.closed || !this.config.accounts.some((a) => a.id === account.id)) return;
|
|
1069
1213
|
this.catalogErrors.set(account.id, { status: Number(error.status ?? error.failure?.status) || 502, code: "catalog_fetch_failed", at: Date.now() });
|
|
@@ -1077,6 +1221,8 @@ var Manager = class {
|
|
|
1077
1221
|
const status = this.router.status();
|
|
1078
1222
|
return {
|
|
1079
1223
|
pluginVersion: version,
|
|
1224
|
+
startup: this.startup,
|
|
1225
|
+
runtime: this.runtime?.state(),
|
|
1080
1226
|
revision: this.revision,
|
|
1081
1227
|
localDiscovery: this.localDiscovery ?? [],
|
|
1082
1228
|
setup: this.setup,
|
|
@@ -1116,6 +1262,69 @@ var Manager = class {
|
|
|
1116
1262
|
}
|
|
1117
1263
|
async action(input) {
|
|
1118
1264
|
if (!input || typeof input !== "object" || Array.isArray(input)) bad("\u8BF7\u6C42\u683C\u5F0F\u65E0\u6548");
|
|
1265
|
+
if (input.action === "plugin.reload") {
|
|
1266
|
+
if (!this.runtime) bad("\u5F53\u524D\u540E\u53F0\u4E0D\u652F\u6301\u91CD\u65B0\u52A0\u8F7D");
|
|
1267
|
+
this.runtime.retryAt = 0;
|
|
1268
|
+
this.runtime.schedule(0);
|
|
1269
|
+
return this.state();
|
|
1270
|
+
}
|
|
1271
|
+
if (input.action === "workbuddy.sms") {
|
|
1272
|
+
try {
|
|
1273
|
+
const workbuddyLogin = await this.workbuddyLogin.send(input.phone);
|
|
1274
|
+
return { ...this.state(), workbuddyLogin };
|
|
1275
|
+
} catch (e) {
|
|
1276
|
+
bad(e.message);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
if (input.action === "workbuddy.login") {
|
|
1280
|
+
if (input.revision !== revisionOf(JSON.parse(await readFile3(this.file, "utf8")))) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u65B0\u63D0\u4EA4\u767B\u5F55");
|
|
1281
|
+
let login;
|
|
1282
|
+
try {
|
|
1283
|
+
login = await this.workbuddyLogin.complete(input.challenge, input.code, input.verify ?? true);
|
|
1284
|
+
} catch (e) {
|
|
1285
|
+
bad(e.message);
|
|
1286
|
+
}
|
|
1287
|
+
let existing;
|
|
1288
|
+
for (const a of this.config.accounts.filter((a2) => a2.provider === "workbuddy")) {
|
|
1289
|
+
try {
|
|
1290
|
+
if ((await jsonCredential(a.credentialFile)).credential.uid === login.document.credential.uid) {
|
|
1291
|
+
existing = a;
|
|
1292
|
+
break;
|
|
1293
|
+
}
|
|
1294
|
+
} catch {
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
if (input.id && input.id !== existing?.id) bad("\u767B\u5F55\u8EAB\u4EFD\u4E0E\u6B63\u5728\u7F16\u8F91\u7684\u8D26\u53F7\u4E0D\u540C\uFF0C\u8BF7\u4ECE\u6DFB\u52A0\u8D26\u53F7\u5165\u53E3\u4FDD\u5B58");
|
|
1298
|
+
const state = await this.action({
|
|
1299
|
+
action: existing ? "account.credentials" : "account.add",
|
|
1300
|
+
revision: input.revision,
|
|
1301
|
+
id: existing?.id ?? `workbuddy_${randomBytes3(8).toString("hex")}`,
|
|
1302
|
+
provider: "workbuddy",
|
|
1303
|
+
name: input.name,
|
|
1304
|
+
document: login.document
|
|
1305
|
+
});
|
|
1306
|
+
this.workbuddyLogin.finish();
|
|
1307
|
+
return { ...state, loginResult: { warning: login.warning, saved: true } };
|
|
1308
|
+
}
|
|
1309
|
+
if (input.action === "checkins.verify") {
|
|
1310
|
+
const account = this.config.accounts.find((a) => a.id === input.id && a.provider === "workbuddy");
|
|
1311
|
+
if (!account) bad("\u8BF7\u9009\u62E9 WorkBuddy \u4E2D\u56FD\u7248\u8D26\u53F7");
|
|
1312
|
+
if (this.checkins.flight || this.checkins.failed) bad("\u8BF7\u7B49\u5F85\u7B7E\u5230\u5B8C\u6210\uFF0C\u6216\u5148\u4FEE\u590D\u7B7E\u5230\u8BB0\u5F55");
|
|
1313
|
+
try {
|
|
1314
|
+
await this.workbuddyLogin.reserve("verifyAt", 3e4);
|
|
1315
|
+
} catch (e) {
|
|
1316
|
+
bad(e.message);
|
|
1317
|
+
}
|
|
1318
|
+
try {
|
|
1319
|
+
const client = await this.router.client(account);
|
|
1320
|
+
await client.checkin.validate(AbortSignal.timeout(3e4));
|
|
1321
|
+
await this.checkins.credentialsUpdated(account.id);
|
|
1322
|
+
await this.readLogin(account);
|
|
1323
|
+
} catch {
|
|
1324
|
+
bad("\u767B\u5F55\u9A8C\u8BC1\u672A\u901A\u8FC7\uFF1A\u8BF7\u5728\u8D26\u53F7\u4E0E\u767B\u5F55\u4E2D\u66F4\u65B0\u51ED\u8BC1\uFF1B\u4E0D\u4F1A\u81EA\u52A8\u91CD\u8BD5\u6216\u9886\u53D6");
|
|
1325
|
+
}
|
|
1326
|
+
return this.state();
|
|
1327
|
+
}
|
|
1119
1328
|
if (input.action === "checkins.run") {
|
|
1120
1329
|
try {
|
|
1121
1330
|
this.checkins.start(input.ids);
|
|
@@ -1141,14 +1350,14 @@ var Manager = class {
|
|
|
1141
1350
|
return this.state();
|
|
1142
1351
|
}
|
|
1143
1352
|
if (input.action === "setup.dismiss") {
|
|
1144
|
-
await
|
|
1353
|
+
await withFileLock3(this.setupFile, async () => {
|
|
1145
1354
|
this.setup = { ...this.setup, completedAt: this.setup?.completedAt ?? Date.now(), dismissed: true };
|
|
1146
1355
|
await writePrivateJson(this.setupFile, this.setup);
|
|
1147
1356
|
});
|
|
1148
1357
|
return this.state();
|
|
1149
1358
|
}
|
|
1150
1359
|
if (input.action === "account.export") {
|
|
1151
|
-
if (input.revision !== revisionOf(JSON.parse(await
|
|
1360
|
+
if (input.revision !== revisionOf(JSON.parse(await readFile3(this.file, "utf8")))) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u63D0\u53D6\u51ED\u8BC1");
|
|
1152
1361
|
const account = this.config.accounts.find((a) => a.id === input.id);
|
|
1153
1362
|
if (!account) bad("\u8D26\u53F7\u4E0D\u5B58\u5728");
|
|
1154
1363
|
let credential;
|
|
@@ -1170,7 +1379,7 @@ var Manager = class {
|
|
|
1170
1379
|
} };
|
|
1171
1380
|
}
|
|
1172
1381
|
if (input.action === "config.export") {
|
|
1173
|
-
if (input.revision !== revisionOf(JSON.parse(await
|
|
1382
|
+
if (input.revision !== revisionOf(JSON.parse(await readFile3(this.file, "utf8")))) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u5BFC\u51FA");
|
|
1174
1383
|
try {
|
|
1175
1384
|
return { ...this.state(), exported: { filename: `dsh-multi-router-${input.includeCredentials ? "encrypted" : "settings"}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`, document: await exportConfiguration(this, input) } };
|
|
1176
1385
|
} catch (e) {
|
|
@@ -1206,7 +1415,7 @@ var Manager = class {
|
|
|
1206
1415
|
}
|
|
1207
1416
|
if (input.action === "account.capture") {
|
|
1208
1417
|
if (!PROVIDERS.includes(input.provider)) bad("\u672A\u77E5\u4F9B\u5E94\u5546");
|
|
1209
|
-
input = { ...input, action: "account.add", id: `${input.provider.replace("-", "_")}_${
|
|
1418
|
+
input = { ...input, action: "account.add", id: `${input.provider.replace("-", "_")}_${randomBytes3(4).toString("hex")}`, capture: true };
|
|
1210
1419
|
}
|
|
1211
1420
|
if (input.action === "refresh") {
|
|
1212
1421
|
if (input.provider && !PROVIDERS.includes(input.provider)) bad("\u672A\u77E5\u4F9B\u5E94\u5546");
|
|
@@ -1217,8 +1426,8 @@ var Manager = class {
|
|
|
1217
1426
|
await this.checkFreeModels(true);
|
|
1218
1427
|
return this.state();
|
|
1219
1428
|
}
|
|
1220
|
-
return
|
|
1221
|
-
let raw = JSON.parse(await
|
|
1429
|
+
return withFileLock3(this.file, async () => {
|
|
1430
|
+
let raw = JSON.parse(await readFile3(this.file, "utf8"));
|
|
1222
1431
|
if (input.revision !== revisionOf(raw)) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u518D\u4FDD\u5B58");
|
|
1223
1432
|
raw.routes ??= structuredClone(this.config.routes);
|
|
1224
1433
|
let staging;
|
|
@@ -1228,7 +1437,7 @@ var Manager = class {
|
|
|
1228
1437
|
try {
|
|
1229
1438
|
const prepared = await prepareMigration(this, raw, await decodeConfiguration(input), input.mode);
|
|
1230
1439
|
staging = prepared.staging;
|
|
1231
|
-
await writePrivateJson(`${this.file}.before-import-${Date.now()}-${
|
|
1440
|
+
await writePrivateJson(`${this.file}.before-import-${Date.now()}-${randomBytes3(3).toString("hex")}.bak`, raw);
|
|
1232
1441
|
raw = prepared.raw;
|
|
1233
1442
|
} catch (e) {
|
|
1234
1443
|
bad(e.message);
|
|
@@ -1367,7 +1576,7 @@ var Manager = class {
|
|
|
1367
1576
|
} };
|
|
1368
1577
|
} else if (input.action === "route.save") {
|
|
1369
1578
|
if (input.id === void 0 || input.id === "") {
|
|
1370
|
-
input.id = input.previousId || `route_${
|
|
1579
|
+
input.id = input.previousId || `route_${randomBytes3(8).toString("hex")}`;
|
|
1371
1580
|
}
|
|
1372
1581
|
if (!/^[a-zA-Z0-9_.-]{1,128}$/.test(input.id ?? "")) bad("\u8DEF\u7531 ID \u4EC5\u652F\u6301\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u4E0B\u5212\u7EBF\u548C\u77ED\u6A2A\u7EBF");
|
|
1373
1582
|
if (input.previousId && !Object.hasOwn(raw.routes, input.previousId)) bad("\u539F\u8DEF\u7531\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u9875\u9762");
|
|
@@ -1444,8 +1653,8 @@ var Manager = class {
|
|
|
1444
1653
|
if (filename && String(filename) !== this.file.split(/[\\/]/).pop()) return;
|
|
1445
1654
|
clearTimeout(timer);
|
|
1446
1655
|
timer = setTimeout(() => {
|
|
1447
|
-
void
|
|
1448
|
-
const raw = JSON.parse(await
|
|
1656
|
+
void withFileLock3(this.file, async () => {
|
|
1657
|
+
const raw = JSON.parse(await readFile3(this.file, "utf8"));
|
|
1449
1658
|
if (this.closed || revisionOf(raw) === this.revision) return;
|
|
1450
1659
|
const config = validateConfig(raw, this.file);
|
|
1451
1660
|
this.raw = raw;
|
|
@@ -1463,12 +1672,13 @@ var Manager = class {
|
|
|
1463
1672
|
}
|
|
1464
1673
|
};
|
|
1465
1674
|
function managementHandler(manager) {
|
|
1466
|
-
const key =
|
|
1675
|
+
const key = randomBytes3(32).toString("base64url");
|
|
1467
1676
|
const json = (res, status, value) => {
|
|
1468
1677
|
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
|
|
1469
1678
|
res.end(JSON.stringify(value));
|
|
1470
1679
|
};
|
|
1471
1680
|
return async (req, res) => {
|
|
1681
|
+
manager.requests = (manager.requests ?? 0) + 1;
|
|
1472
1682
|
try {
|
|
1473
1683
|
const remote = req.socket?.remoteAddress;
|
|
1474
1684
|
if (!hostIsLoopback(req.headers.host) || remote && !["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(remote)) throw new ManagementError(403, "\u4EC5\u5141\u8BB8\u672C\u673A\u8BF7\u6C42");
|
|
@@ -1501,15 +1711,18 @@ function managementHandler(manager) {
|
|
|
1501
1711
|
error instanceof ManagementError ? error.status : 500,
|
|
1502
1712
|
{ error: error instanceof ManagementError ? error.message : "\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u914D\u7F6E\u548C\u6765\u6E90\u6587\u4EF6" }
|
|
1503
1713
|
);
|
|
1714
|
+
} finally {
|
|
1715
|
+
manager.requests--;
|
|
1504
1716
|
}
|
|
1505
1717
|
};
|
|
1506
1718
|
}
|
|
1507
1719
|
|
|
1508
1720
|
export {
|
|
1721
|
+
version,
|
|
1509
1722
|
MANAGEMENT_PATH,
|
|
1510
1723
|
ManagementError,
|
|
1511
1724
|
modelInfo,
|
|
1512
1725
|
Manager,
|
|
1513
1726
|
managementHandler
|
|
1514
1727
|
};
|
|
1515
|
-
//# sourceMappingURL=chunk-
|
|
1728
|
+
//# sourceMappingURL=chunk-JDZCWBA4.js.map
|