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 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.5。适配 DSH Desktop / Web 0.2.0-rc.2,Node.js ^22.19.0 或 >=24。
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
+ ![WorkBuddy 短信登录面板(v0.5.6,合成数据,输入为空)](https://raw.githubusercontent.com/AndroidSix/dsh-multi-router/main/docs/screenshots/workbuddy-login.jpg)
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.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
- 这是同名插件的来源切换;账号、路由和用量保存在包目录之外,会保留。更新完成后**完全退出并重新打开 DeepSeek Harness**;关闭窗口或点击「刷新面板」不会重载后台。插件面板检测到前后台版本不一致时会显示重启步骤。
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
 
@@ -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: dsh-multi-router
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
- supportsCheckin
6
- } from "./chunk-EGLF4CWD.js";
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 randomBytes2, timingSafeEqual } from "node:crypto";
39
+ import { createHash, randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
37
40
 
38
41
  // package.json
39
- var version = "0.5.5";
42
+ var version = "0.5.7";
40
43
 
41
44
  // src/manager.mjs
42
- import { readFile as readFile2, mkdir as mkdir2, mkdtemp as mkdtemp2, rm as rm3 } from "node:fs/promises";
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 withFileLock2 } from "@deepseek-ai/dsh-atomic-write";
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
- return { name: row.name, status: "installed", application: result.application, message: 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: result.application === "restart-required" };
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 readFile2(this.file, "utf8"));
890
+ this.raw = JSON.parse(await readFile3(this.file, "utf8"));
744
891
  try {
745
- this.setup = JSON.parse(await readFile2(this.setupFile, "utf8"));
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 readFile2(this.catalogFile, "utf8"));
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 withFileLock2(this.file, async () => {
940
+ await withFileLock3(this.file, async () => {
797
941
  if (this.closed) return;
798
- let raw = JSON.parse(await readFile2(this.file, "utf8"));
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 withFileLock2(this.setupFile, async () => {
1026
+ await withFileLock3(this.setupFile, async () => {
883
1027
  try {
884
- this.setup = JSON.parse(await readFile2(this.setupFile, "utf8"));
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 withFileLock2(this.catalogFile, () => writePrivateJson(this.catalogFile, Object.fromEntries(this.catalogs)));
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 withFileLock2(this.setupFile, async () => {
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 readFile2(this.file, "utf8")))) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u63D0\u53D6\u51ED\u8BC1");
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 readFile2(this.file, "utf8")))) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u5BFC\u51FA");
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("-", "_")}_${randomBytes2(4).toString("hex")}`, capture: true };
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 withFileLock2(this.file, async () => {
1221
- let raw = JSON.parse(await readFile2(this.file, "utf8"));
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()}-${randomBytes2(3).toString("hex")}.bak`, raw);
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_${randomBytes2(8).toString("hex")}`;
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 withFileLock2(this.file, async () => {
1448
- const raw = JSON.parse(await readFile2(this.file, "utf8"));
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 = randomBytes2(32).toString("base64url");
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-PQETKUGT.js.map
1728
+ //# sourceMappingURL=chunk-JDZCWBA4.js.map