dsh-multi-router 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。版本号从 `0.1` 起以功能迭代为主,接口在 `1.0` 前可能变化。
4
4
 
5
+ ## 0.5.1
6
+
7
+ - GitHub 插件更新检查只请求提交 SHA,不再下载完整提交差异,修复构建产物较大时「更新元数据过大」导致无法检查的问题。
8
+ - 增加 SHA 响应格式与异常响应回归验证。
9
+
10
+ ## 0.5.0
11
+
12
+ - 新增「积分签到」面板:接入 Trae CN 与 Qoder 签到,显示账号结果与奖励,支持手动领取、批量领取及按账号选择每日自动签到(默认关闭)。
13
+ - 每日北京时间 10:05 后自动补查;持久化每日意图、账号身份去重、串行限频、Trae 同设备限制;结果不明不重复提交。
14
+ - 验证、授权拒绝与限流时暂停平台签到,官方客户端处理后可恢复后续签到;保留设备身份,不绕过风控。
15
+ - Qoder 账号统一复用官方 CLI / 原连接器的持久设备身份。WorkBuddy 与 Trae Global 未核验的签到接口明确标为暂未接入。
16
+ - 自动签到设置支持配置迁移,新增并发、重启、设备、失败及面板回归测试。
17
+
5
18
  ## 0.4.1
6
19
 
7
20
  - 修复 GitHub 来源再次安装时 pnpm 复用旧锁文件:改走官方包管理更新路径刷新原分支,保留安装来源与启用状态。
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.4.1。适配 DSH Desktop / Web 0.2.0-rc.2,Node.js ^22.19.0 或 >=24。
9
+ 版本:0.5.1。适配 DSH Desktop / Web 0.2.0-rc.2,Node.js ^22.19.0 或 >=24。
10
10
 
11
11
  ## 在 DSH 里管理
12
12
 
@@ -87,12 +87,23 @@ Qoder 的 `dfmodel` 在真实目录里显示为 DeepSeek-Flash,未声明具体
87
87
 
88
88
  统计保存于配置旁的 `usage.json`,0600 权限。仅保存账号/路由元数据、计数、平台额度和保留期限内的逐次调用明细,不保存提示词、回答或认证凭证;明细默认保留最近 3 天(滚动 72 小时),由 `usageRetentionDays` 设置 1–365 天,不再限定 80 条;缩短期限立即删除超期明细,延长不会恢复已删除记录。累计用量、额度轮换基线、临期冷却不清零,日趋势独立保留最近 31 天。Token 从本版本启用后开始计量,不回填历史会话。三家积分单位分别展示,不汇总为通用余额;平台余额下降可包含其他应用消费、积分包到期或延迟结算,不宣称是精确的单请求扣费。平台查询失败保留最后数据与错误、时间;没有可用积分数据的账号不参加积分阈值轮换。
89
89
 
90
- 账号获取采用官方客户端已有登录文件的只读解析,不修改客户端登录、不复制参考项目代码、不自动签到。手动凭证支持 WorkBuddy / Trae 字段表单、三家凭证 JSON 和 Qoder PAT;凭证格式、区域、Trae 设备身份会校验。已有账号可在详情中「补充 / 更新凭证」,旧副本保留。Qoder 普通网页 Cookie 无法替代 PAT;官方 PAT 指引见 https://docs.qoder.com/cli/authentication 。
90
+ 账号获取采用官方客户端已有登录文件的只读解析,不修改客户端登录、不复制参考项目代码;自动签到需要在独立的「积分签到」页按账号开启,默认关闭。手动凭证支持 WorkBuddy / Trae 字段表单、三家凭证 JSON 和 Qoder PAT;凭证格式、区域、Trae 设备身份会校验。已有账号可在详情中「补充 / 更新凭证」,旧副本保留。Qoder 普通网页 Cookie 无法替代 PAT;官方 PAT 指引见 https://docs.qoder.com/cli/authentication 。
91
91
 
92
92
  ## 首次使用与账号自动获取
93
93
 
94
94
  第一次启用插件时会执行 WorkBuddy / WorkBuddy AI 与 Trae CN / Global / SOLO 的本机登录检测,将找到的有效登录保存为独立账号副本。首页会展示「本机账号自动获取」结果,区分已保存、未登录和无法读取,并提供「一键获取并保存」及手动添加入口;无需先逐个点击添加账号。之后每分钟检测新增登录,重复检测不重复添加,已停用和已删除的账号保持用户选择。Qoder 仍需添加 PAT。
95
95
 
96
+ ## 每日积分签到
97
+
98
+ 在配置面板打开 **积分签到**。当前接入 Trae CN 与 Qoder CN / Global 的已有签到能力:先查询平台是否开放活动、账号与设备是否已领取,再提交领取;活动是否开放和奖励额度以平台响应为准。WorkBuddy / WorkBuddy AI 与 Trae Global 暂无已核验的签到接口,面板会显示需到官方客户端查看,不猜测接口或奖励。
99
+
100
+ - 每个账号可选择「立即签到」,或「签到全部可用账号」。**每日自动签到默认关闭**,按账号单独开启;已停用账号不参与。每天北京时间 10:05 后、DSH 运行期间执行,错过时在下次启动补查;10:05 前不会发起签到请求。
101
+ - 请求串行,账号间至少间隔 10 秒。先持久化当天尝试记录,再发请求;同一账号当天最多尝试一次,重复账号副本按平台身份去重。网络超时或结果不明确时不重复提交,请到官方客户端核对。
102
+ - 保留 Trae 原有设备身份;同一设备当天的领取已使用或结果不明确时,不切到同设备的其他账号继续领取。Qoder 优先复用官方 CLI 的设备身份,没有时使用连接器持久化的单一设备身份,所有账号一致,不随签到轮换。
103
+ - 验证、授权拒绝或限流会暂停该平台签到;在官方客户端处理后,可以点击「恢复后续签到」。恢复不会清除今日尝试记录与设备限制,不提供更换设备身份、代理或绕过验证码的能力。平台风控行为不可预测,这些措施不能保证不触发风控。
104
+ - 最近结果、可核验的奖励积分与自动签到开关直接展示(Qoder 不展示上游辅助函数猜测的奖励数额);成功后刷新该账号平台余额。记录保存在配置旁的 `checkins.json`,0600 权限,仅保存身份哈希、日期和结果,不保存 Token 或原始服务端错误。保留最近 30 天,记录损坏时停止签到,不自动清空后重试。
105
+ - 自动签到设置可随配置导入导出,领取记录与平台暂停状态不迁移。多台设备的本地记录不会互相同步,建议只在一台设备开启自动签到;平台领取状态仍会在提交前查询。
106
+
96
107
  ## 所有已安装插件的更新与安装
97
108
 
98
109
  在本插件配置面板打开 **插件更新**,管理当前 DSH 配置中的全部已安装插件,包括其它插件和已停用插件。首次打开自动检查,后续超过 24 小时重新打开时再检查,也可随时点击「检查全部更新」。
@@ -124,7 +135,7 @@ dsh plugin --profile desktop add dsh-multi-router
124
135
  dsh plugin --profile web add dsh-multi-router
125
136
  ```
126
137
 
127
- 包地址:[dsh-multi-router · npm](https://www.npmjs.com/package/dsh-multi-router)。正式版本由 GitHub Release 自动测试、打包并发布到 npm `latest`,包中已包含编译产物,用户无需自行构建。需要固定版本时可安装 `dsh-multi-router@0.4.1`。
138
+ 包地址:[dsh-multi-router · npm](https://www.npmjs.com/package/dsh-multi-router)。正式版本由 GitHub Release 自动测试、打包并发布到 npm `latest`,包中已包含编译产物,用户无需自行构建。需要固定版本时可安装 `dsh-multi-router@0.5.1`。
128
139
 
129
140
  已使用 npm 来源的用户更新:
130
141
 
@@ -181,7 +192,7 @@ cd dsh-multi-router
181
192
  npm ci
182
193
  npm run check
183
194
  npm run pack:release
184
- dsh plugin --profile desktop add ./dsh-multi-router-0.4.1.tgz
195
+ dsh plugin --profile desktop add ./dsh-multi-router-0.5.1.tgz
185
196
  ```
186
197
 
187
198
  ### 配置账号
package/lib/accounts.js CHANGED
@@ -3,10 +3,10 @@ import {
3
3
  importAccount,
4
4
  localAccountKey,
5
5
  readPortableCredential
6
- } from "./chunk-3VGKH7MF.js";
6
+ } from "./chunk-OMG4FZWV.js";
7
7
  import "./chunk-FTM7RHQV.js";
8
8
  import "./chunk-S5KMFAQM.js";
9
- import "./chunk-XMXFIV5G.js";
9
+ import "./chunk-GAQ77CH4.js";
10
10
  import "./chunk-TEZ2FMEO.js";
11
11
  export {
12
12
  discoverLocalAccounts,
@@ -1,20 +1,20 @@
1
1
  import {
2
2
  jsonCredential,
3
3
  qoderPluginDataDir
4
- } from "./chunk-UFS7ZJKY.js";
4
+ } from "./chunk-YUP77Q3A.js";
5
5
  import {
6
6
  discoverLocalAccounts,
7
7
  importAccount,
8
8
  localAccountKey,
9
9
  readPortableCredential
10
- } from "./chunk-3VGKH7MF.js";
10
+ } from "./chunk-OMG4FZWV.js";
11
11
  import {
12
12
  hostIsLoopback
13
13
  } from "./chunk-DNQPZMPF.js";
14
14
  import {
15
15
  Telemetry,
16
16
  matchModel
17
- } from "./chunk-GGRCX2NR.js";
17
+ } from "./chunk-XV7O2LRF.js";
18
18
  import {
19
19
  PROVIDERS,
20
20
  PROVIDER_NAMES,
@@ -25,22 +25,22 @@ import {
25
25
  routeReasoning,
26
26
  validateConfig,
27
27
  writePrivateJson
28
- } from "./chunk-XMXFIV5G.js";
28
+ } from "./chunk-GAQ77CH4.js";
29
29
  import {
30
30
  modelWithCurrentPromotion
31
31
  } from "./chunk-TEZ2FMEO.js";
32
32
 
33
33
  // src/manager.mjs
34
- import { createHash, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
34
+ import { createHash as createHash2, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
35
35
 
36
36
  // package.json
37
- var version = "0.4.1";
37
+ var version = "0.5.1";
38
38
 
39
39
  // src/manager.mjs
40
- import { readFile as readFile2, mkdir as mkdir2, mkdtemp as mkdtemp2, rm as rm3 } from "node:fs/promises";
40
+ import { readFile as readFile3, mkdir as mkdir2, mkdtemp as mkdtemp2, rm as rm3 } from "node:fs/promises";
41
41
  import { watch } from "node:fs";
42
42
  import { dirname as dirname3, join as join4, relative as relative2, resolve } from "node:path";
43
- import { withFileLock as withFileLock2 } from "@deepseek-ai/dsh-atomic-write";
43
+ import { withFileLock as withFileLock3 } from "@deepseek-ai/dsh-atomic-write";
44
44
 
45
45
  // upstream/qoder/src/status-paths.ts
46
46
  var QODER_STATUS_PATH = "/plugins/dsh-qoder-connect/status";
@@ -101,7 +101,7 @@ function settings(config) {
101
101
  return {
102
102
  ...pick(config, options),
103
103
  models: Object.fromEntries(Object.entries(config.models ?? {}).map(([p, rows]) => [p, Object.fromEntries(Object.entries(rows).map(([id, pref]) => [id, pick(pref, ["name", "visible"])]))])),
104
- accounts: config.accounts.map((a) => pick(a, ["id", "provider", "name", "enabled"])),
104
+ accounts: config.accounts.map((a) => pick(a, ["id", "provider", "name", "enabled", "autoCheckin"])),
105
105
  routes: Object.fromEntries(Object.entries(config.routes).map(([id, r]) => [id, {
106
106
  ...pick(r, ["name", "visible", "strategy", "contextWindow", "maxTokens", "preferExpiringCredits", "expiringWithinDays", "freeOnly", "freeModels"]),
107
107
  targets: r.targets.map((t) => pick(t, ["provider", "account", "model", "models", "weight", "tokenBudget", "creditBudget"]))
@@ -335,7 +335,8 @@ async function locks(dir) {
335
335
  async function publicJson(url, fetchImpl, signal) {
336
336
  const res = await fetchImpl(url, {
337
337
  signal: AbortSignal.any([signal, AbortSignal.timeout(12e3)]),
338
- headers: { Accept: "application/json", "User-Agent": "dsh-multi-router-plugin-updates" },
338
+ // Request only the SHA: the default JSON includes file patches and can be megabytes.
339
+ headers: { Accept: "application/vnd.github.sha", "User-Agent": "dsh-multi-router-plugin-updates" },
339
340
  redirect: "error"
340
341
  });
341
342
  if (!res.ok) throw Error(res.status === 403 || res.status === 429 ? "\u8FDC\u7AEF\u9650\u6D41\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" : res.status === 404 ? "\u4ED3\u5E93 / \u7248\u672C\u4E0D\u5B58\u5728\uFF0C\u6216\u9700\u8981\u79C1\u6709\u4ED3\u5E93\u6388\u6743" : `\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${res.status}\uFF09`);
@@ -345,7 +346,8 @@ async function publicJson(url, fetchImpl, signal) {
345
346
  if (size > 256 * 1024) throw Error("\u66F4\u65B0\u5143\u6570\u636E\u8FC7\u5927");
346
347
  chunks.push(Buffer.from(chunk));
347
348
  }
348
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
349
+ const text = Buffer.concat(chunks).toString("utf8").trim();
350
+ return /^[a-f0-9]{40}$/i.test(text) ? { sha: text } : JSON.parse(text);
349
351
  }
350
352
  var short = (sha) => sha?.slice(0, 8);
351
353
  async function registryVersion(service, profile, spec, signal) {
@@ -627,9 +629,172 @@ var PluginUpdates = class {
627
629
  }
628
630
  };
629
631
 
632
+ // src/checkins.mjs
633
+ import { createHash } from "node:crypto";
634
+ import { readFile as readFile2 } from "node:fs/promises";
635
+ import { setTimeout as delay } from "node:timers/promises";
636
+ import { withFileLock as withFileLock2 } from "@deepseek-ai/dsh-atomic-write";
637
+ var supportsCheckin = (provider) => ["trae", "qoder", "qoder-global"].includes(provider);
638
+ var hash = (value) => createHash("sha256").update(value).digest("hex");
639
+ var nextDay = (now) => Date.parse(`${beijingDay(now)}T00:00:00+08:00`) + 864e5;
640
+ var checkinMessages = {
641
+ claimed: "\u7B7E\u5230\u6210\u529F",
642
+ "already-claimed": "\u4ECA\u65E5\u5DF2\u9886\u53D6",
643
+ "device-used": "\u6B64\u8BBE\u5907\u4ECA\u65E5\u5DF2\u53C2\u4E0E\u7B7E\u5230\uFF0C\u4E0D\u518D\u5207\u6362\u5176\u4ED6\u8D26\u53F7\u9886\u53D6",
644
+ "no-campaign": "\u5E73\u53F0\u672A\u5F00\u653E\u53EF\u9886\u53D6\u6D3B\u52A8",
645
+ "before-window": "\u7B7E\u5230\u7A97\u53E3\u5C1A\u672A\u5F00\u542F",
646
+ paused: "\u5E73\u53F0\u9650\u5236\u6216\u9700\u8981\u9A8C\u8BC1\uFF0C\u5DF2\u6682\u505C\u6B64\u5E73\u53F0\u7B7E\u5230\uFF0C\u8BF7\u5728\u5B98\u65B9\u5BA2\u6237\u7AEF\u5904\u7406",
647
+ uncertain: "\u8BF7\u6C42\u7ED3\u679C\u5F85\u786E\u8BA4\uFF0C\u4ECA\u65E5\u4E0D\u518D\u81EA\u52A8\u63D0\u4EA4\uFF0C\u8BF7\u5728\u5B98\u65B9\u5BA2\u6237\u7AEF\u6838\u5BF9",
648
+ error: "\u7B7E\u5230\u672A\u5B8C\u6210\uFF0C\u4ECA\u65E5\u4E0D\u518D\u91CD\u590D\u5C1D\u8BD5\uFF0C\u8BF7\u5728\u5B98\u65B9\u5BA2\u6237\u7AEF\u67E5\u770B",
649
+ unsupported: "\u6682\u672A\u63A5\u5165\u53EF\u6838\u9A8C\u7684\u7B7E\u5230\u63A5\u53E3\uFF0C\u8BF7\u4F7F\u7528\u5B98\u65B9\u5BA2\u6237\u7AEF"
650
+ };
651
+ var Checkins = class {
652
+ constructor(file, manager, { now = Date.now, spacingMs = 1e4 } = {}) {
653
+ this.file = file;
654
+ this.manager = manager;
655
+ this.now = now;
656
+ this.spacingMs = spacingMs;
657
+ this.data = { records: {}, devices: {}, pauses: {} };
658
+ this.abort = new AbortController();
659
+ }
660
+ async read() {
661
+ try {
662
+ const raw = JSON.parse(await readFile2(this.file, "utf8"));
663
+ const object = (value) => value && typeof value === "object" && !Array.isArray(value);
664
+ if (!object(raw) || !["records", "devices", "pauses"].every((k) => object(raw[k])) || Object.values(raw.records).some((r) => !object(r) || !Array.isArray(r.ids) || !Number.isFinite(r.at) || typeof r.day !== "string")) throw Error("Invalid check-in journal");
665
+ this.data = raw;
666
+ } catch (e) {
667
+ if (e.code !== "ENOENT") throw e;
668
+ }
669
+ }
670
+ async load() {
671
+ try {
672
+ await this.read();
673
+ } catch {
674
+ this.failed = true;
675
+ }
676
+ }
677
+ state() {
678
+ return {
679
+ running: !!this.flight,
680
+ current: this.current,
681
+ error: this.failed ? "\u7B7E\u5230\u8BB0\u5F55\u65E0\u6CD5\u8BFB\u53D6\uFF0C\u5DF2\u505C\u6B62\u7B7E\u5230\uFF1B\u8BF7\u4FEE\u590D\u8BB0\u5F55\u6587\u4EF6\u540E\u91CD\u542F\u63D2\u4EF6\u3002" : void 0,
682
+ schedule: "\u5317\u4EAC\u65F6\u95F4\u6BCF\u5929 10:05 \u540E\uFF0CDSH \u8FD0\u884C\u65F6\u6267\u884C\uFF1B\u9519\u8FC7\u540E\u4E0B\u6B21\u542F\u52A8\u8865\u67E5",
683
+ accounts: this.manager.config.accounts.map((a) => {
684
+ const rows = Object.values(this.data.records).filter((r) => r.ids?.includes(a.id));
685
+ const last = rows.sort((a2, b) => b.at - a2.at)[0];
686
+ const pause = this.data.pauses[a.provider];
687
+ return {
688
+ id: a.id,
689
+ supported: supportsCheckin(a.provider),
690
+ auto: a.autoCheckin === true,
691
+ paused: !!pause,
692
+ last: last ? {
693
+ day: last.day,
694
+ at: last.at,
695
+ status: last.status,
696
+ amount: last.amount,
697
+ message: checkinMessages[last.status] || checkinMessages.error
698
+ } : void 0
699
+ };
700
+ })
701
+ };
702
+ }
703
+ start(ids, automatic = false) {
704
+ if (this.failed || this.abort.signal.aborted || this.flight) return;
705
+ if (!automatic && (!Array.isArray(ids) || !ids.length || ids.length > 200 || ids.some((id) => !this.manager.config.accounts.some((a) => a.id === id)))) throw Error("\u8BF7\u9009\u62E9\u6709\u6548\u7684\u7B7E\u5230\u8D26\u53F7");
706
+ const selected = this.manager.config.accounts.filter((a) => supportsCheckin(a.provider) && a.enabled !== false && (automatic ? a.autoCheckin === true : ids.includes(a.id))).map((a) => a.id);
707
+ if (!selected.length) return;
708
+ this.flight = (async () => {
709
+ let requested = false;
710
+ for (const id of selected) {
711
+ if (this.abort.signal.aborted) break;
712
+ const account = this.manager.config.accounts.find((a) => a.id === id && a.enabled !== false && (!automatic || a.autoCheckin === true));
713
+ if (!account) continue;
714
+ if (requested) {
715
+ await delay(this.spacingMs, void 0, { signal: this.abort.signal });
716
+ requested = false;
717
+ }
718
+ this.current = id;
719
+ requested = await this.run(account, automatic);
720
+ }
721
+ })().catch(() => {
722
+ if (!this.abort.signal.aborted) this.failed = true;
723
+ }).finally(() => {
724
+ this.flight = void 0;
725
+ this.current = void 0;
726
+ });
727
+ }
728
+ async resume(provider) {
729
+ if (!supportsCheckin(provider) || this.failed || this.flight) throw Error("\u8BF7\u7B49\u5F85\u7B7E\u5230\u5B8C\u6210\uFF0C\u6216\u5148\u4FEE\u590D\u7B7E\u5230\u8BB0\u5F55");
730
+ await withFileLock2(this.file, async () => {
731
+ await this.read();
732
+ delete this.data.pauses[provider];
733
+ await writePrivateJson(this.file, this.data);
734
+ });
735
+ }
736
+ async run(account, automatic = false) {
737
+ const now = this.now(), day = beijingDay(now);
738
+ if (now < Date.parse(`${day}T10:05:00+08:00`)) return false;
739
+ return withFileLock2(this.file, async () => {
740
+ await this.read();
741
+ if (this.abort.signal.aborted || this.data.pauses[account.provider]) return false;
742
+ if (Object.values(this.data.records).some((r) => r.day === day && r.ids?.includes(account.id))) return false;
743
+ if (this.data.lastRequestAt && now - this.data.lastRequestAt < this.spacingMs) return false;
744
+ this.data.lastRequestAt = now;
745
+ await writePrivateJson(this.file, this.data);
746
+ const client = await this.manager.router.client(account);
747
+ if (!client.checkin) return false;
748
+ let identity;
749
+ try {
750
+ identity = await client.checkin.identity(AbortSignal.any([this.abort.signal, AbortSignal.timeout(3e4)]));
751
+ } catch {
752
+ this.data.pauses[account.provider] = { at: now };
753
+ await writePrivateJson(this.file, this.data);
754
+ return true;
755
+ }
756
+ if (!identity?.account || account.provider === "trae" && !identity.device) throw Error("Missing check-in identity");
757
+ if (this.abort.signal.aborted || !this.manager.config.accounts.some((a) => a.id === account.id && a.enabled !== false && a.credentialFile === account.credentialFile && (!automatic || a.autoCheckin === true))) return true;
758
+ const key = hash(`${account.provider}:${identity.account}`), device = identity.device ? hash(`${account.provider}:${identity.device}`) : void 0;
759
+ const old = this.data.records[key];
760
+ if (old?.day === day) {
761
+ if (!old.ids.includes(account.id)) {
762
+ old.ids.push(account.id);
763
+ await writePrivateJson(this.file, this.data);
764
+ }
765
+ return true;
766
+ }
767
+ const record = { day, at: now, status: "uncertain", ids: [account.id] };
768
+ this.data.records[key] = record;
769
+ for (const [k, row] of Object.entries(this.data.records)) if (row.at < now - 30 * 864e5) delete this.data.records[k];
770
+ for (const [k, row] of Object.entries(this.data.devices)) if (row.until <= now) delete this.data.devices[k];
771
+ if (device && this.data.devices[device]?.until > now) {
772
+ record.status = "device-used";
773
+ await writePrivateJson(this.file, this.data);
774
+ return true;
775
+ }
776
+ if (device) this.data.devices[device] = { until: nextDay(now) };
777
+ await writePrivateJson(this.file, this.data);
778
+ let result;
779
+ try {
780
+ result = await client.checkin.claim(AbortSignal.any([this.abort.signal, AbortSignal.timeout(3e4)]));
781
+ } catch {
782
+ result = { status: "uncertain" };
783
+ }
784
+ record.status = Object.hasOwn(checkinMessages, result?.status) ? result.status : "uncertain";
785
+ if (device && ["already-claimed", "no-campaign"].includes(record.status)) delete this.data.devices[device];
786
+ if (["claimed", "already-claimed"].includes(record.status) && Number.isFinite(result.amount) && result.amount >= 0) record.amount = result.amount;
787
+ if (record.status === "paused") this.data.pauses[account.provider] = { at: now };
788
+ await writePrivateJson(this.file, this.data);
789
+ if (record.status === "claimed") await this.manager.monitorAccount(account);
790
+ return true;
791
+ }, { waitMs: 1e3 });
792
+ }
793
+ };
794
+
630
795
  // src/manager.mjs
631
796
  var MANAGEMENT_PATH = "/.well-known/dsh-multi-router";
632
- var revisionOf = (raw) => createHash("sha256").update(JSON.stringify(raw)).digest("hex");
797
+ var revisionOf = (raw) => createHash2("sha256").update(JSON.stringify(raw)).digest("hex");
633
798
  var ManagementError = class extends Error {
634
799
  constructor(status, message) {
635
800
  super(message);
@@ -679,6 +844,7 @@ var Manager = class {
679
844
  this.loginInfo = /* @__PURE__ */ new Map();
680
845
  this.telemetry = new Telemetry(join4(dirname3(file), "usage.json"));
681
846
  this.plugins = new PluginUpdates(pluginAccess);
847
+ this.checkins = new Checkins(join4(dirname3(file), "checkins.json"), this);
682
848
  this.setupFile = join4(dirname3(file), "initial-setup.json");
683
849
  router.telemetry = this.telemetry;
684
850
  router.monitor = (a) => this.monitorAccount(a);
@@ -686,15 +852,16 @@ var Manager = class {
686
852
  router.freeModels = (account) => this.freeModels(account);
687
853
  }
688
854
  async load() {
689
- this.raw = JSON.parse(await readFile2(this.file, "utf8"));
855
+ this.raw = JSON.parse(await readFile3(this.file, "utf8"));
690
856
  try {
691
- this.setup = JSON.parse(await readFile2(this.setupFile, "utf8"));
857
+ this.setup = JSON.parse(await readFile3(this.setupFile, "utf8"));
692
858
  } catch {
693
859
  this.setup = void 0;
694
860
  }
695
861
  this.config = validateConfig(this.raw, this.file);
862
+ await this.checkins.load();
696
863
  try {
697
- const saved = JSON.parse(await readFile2(this.catalogFile, "utf8"));
864
+ const saved = JSON.parse(await readFile3(this.catalogFile, "utf8"));
698
865
  for (const account of this.config.accounts) {
699
866
  const item = saved[account.id];
700
867
  if (item?.credentialFile === account.credentialFile && Array.isArray(item.models)) {
@@ -734,9 +901,9 @@ var Manager = class {
734
901
  ...discovered.status.map((s) => ({ ...s, checkedAt: Date.now() }))
735
902
  ];
736
903
  if (this.closed || !discovered.accounts.length) return;
737
- await withFileLock2(this.file, async () => {
904
+ await withFileLock3(this.file, async () => {
738
905
  if (this.closed) return;
739
- let raw = JSON.parse(await readFile2(this.file, "utf8"));
906
+ let raw = JSON.parse(await readFile3(this.file, "utf8"));
740
907
  let changed = false;
741
908
  const saved = /* @__PURE__ */ new Map();
742
909
  for (const item of discovered.accounts) {
@@ -755,7 +922,7 @@ var Manager = class {
755
922
  } catch {
756
923
  }
757
924
  }
758
- const id = existing?.id ?? `local_${createHash("sha256").update(key).digest("hex").slice(0, 20)}`;
925
+ const id = existing?.id ?? `local_${createHash2("sha256").update(key).digest("hex").slice(0, 20)}`;
759
926
  const recordSaved = () => {
760
927
  if (!saved.has(item.provider)) saved.set(item.provider, /* @__PURE__ */ new Set());
761
928
  saved.get(item.provider).add(id);
@@ -820,9 +987,9 @@ var Manager = class {
820
987
  }
821
988
  async finishInitialSetup() {
822
989
  if (this.setup?.completedAt) return;
823
- await withFileLock2(this.setupFile, async () => {
990
+ await withFileLock3(this.setupFile, async () => {
824
991
  try {
825
- this.setup = JSON.parse(await readFile2(this.setupFile, "utf8"));
992
+ this.setup = JSON.parse(await readFile3(this.setupFile, "utf8"));
826
993
  } catch {
827
994
  }
828
995
  if (this.setup?.completedAt) return;
@@ -889,7 +1056,7 @@ var Manager = class {
889
1056
  for (const account of this.config.accounts.filter((a) => a.enabled !== false)) {
890
1057
  for (const model of this.freeModels(account)) {
891
1058
  if (!row.targets.some((t) => t.provider === account.provider && (!t.account || t.account === account.id) && (!t.model || t.model === model.id) && (!t.models || t.models.includes(model.id)))) continue;
892
- const id = `free_${createHash("sha256").update(JSON.stringify([parentId, account.provider, model.id])).digest("hex").slice(0, 24)}`;
1059
+ const id = `free_${createHash2("sha256").update(JSON.stringify([parentId, account.provider, model.id])).digest("hex").slice(0, 24)}`;
893
1060
  if (!groups.has(id)) groups.set(id, { id, models: [], targets: [], accountNames: [], modelName: model.name, provider: account.provider, modelId: model.id });
894
1061
  const group = groups.get(id);
895
1062
  group.models.push({ model, provider: account.provider });
@@ -1004,7 +1171,7 @@ var Manager = class {
1004
1171
  if (this.closed || !this.config.accounts.some((a) => a.id === account.id && a.credentialFile === account.credentialFile)) return;
1005
1172
  this.catalogs.set(account.id, { credentialFile: account.credentialFile, fetchedAt: Date.now(), pricingVersion: 1, models });
1006
1173
  this.catalogErrors.delete(account.id);
1007
- await withFileLock2(this.catalogFile, () => writePrivateJson(this.catalogFile, Object.fromEntries(this.catalogs)));
1174
+ await withFileLock3(this.catalogFile, () => writePrivateJson(this.catalogFile, Object.fromEntries(this.catalogs)));
1008
1175
  } catch (error) {
1009
1176
  if (this.closed || !this.config.accounts.some((a) => a.id === account.id)) return;
1010
1177
  this.catalogErrors.set(account.id, { status: Number(error.status ?? error.failure?.status) || 502, code: "catalog_fetch_failed", at: Date.now() });
@@ -1022,6 +1189,7 @@ var Manager = class {
1022
1189
  localDiscovery: this.localDiscovery ?? [],
1023
1190
  setup: this.setup,
1024
1191
  pluginUpdates: this.plugins.state(),
1192
+ checkins: this.checkins.state(),
1025
1193
  telemetry: this.telemetry.state(),
1026
1194
  accounts: this.config.accounts.map((a) => ({
1027
1195
  id: a.id,
@@ -1056,6 +1224,22 @@ var Manager = class {
1056
1224
  }
1057
1225
  async action(input) {
1058
1226
  if (!input || typeof input !== "object" || Array.isArray(input)) bad("\u8BF7\u6C42\u683C\u5F0F\u65E0\u6548");
1227
+ if (input.action === "checkins.run") {
1228
+ try {
1229
+ this.checkins.start(input.ids);
1230
+ } catch (e) {
1231
+ bad(e.message);
1232
+ }
1233
+ return this.state();
1234
+ }
1235
+ if (input.action === "checkins.resume") {
1236
+ try {
1237
+ await this.checkins.resume(input.provider);
1238
+ } catch (e) {
1239
+ bad(e.message);
1240
+ }
1241
+ return this.state();
1242
+ }
1059
1243
  if (typeof input.action === "string" && input.action.startsWith("plugins.")) {
1060
1244
  try {
1061
1245
  await this.plugins.action(input);
@@ -1065,14 +1249,14 @@ var Manager = class {
1065
1249
  return this.state();
1066
1250
  }
1067
1251
  if (input.action === "setup.dismiss") {
1068
- await withFileLock2(this.setupFile, async () => {
1252
+ await withFileLock3(this.setupFile, async () => {
1069
1253
  this.setup = { ...this.setup, completedAt: this.setup?.completedAt ?? Date.now(), dismissed: true };
1070
1254
  await writePrivateJson(this.setupFile, this.setup);
1071
1255
  });
1072
1256
  return this.state();
1073
1257
  }
1074
1258
  if (input.action === "account.export") {
1075
- 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");
1259
+ 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");
1076
1260
  const account = this.config.accounts.find((a) => a.id === input.id);
1077
1261
  if (!account) bad("\u8D26\u53F7\u4E0D\u5B58\u5728");
1078
1262
  let credential;
@@ -1094,7 +1278,7 @@ var Manager = class {
1094
1278
  } };
1095
1279
  }
1096
1280
  if (input.action === "config.export") {
1097
- 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");
1281
+ 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");
1098
1282
  try {
1099
1283
  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) } };
1100
1284
  } catch (e) {
@@ -1141,8 +1325,8 @@ var Manager = class {
1141
1325
  await this.checkFreeModels(true);
1142
1326
  return this.state();
1143
1327
  }
1144
- return withFileLock2(this.file, async () => {
1145
- let raw = JSON.parse(await readFile2(this.file, "utf8"));
1328
+ return withFileLock3(this.file, async () => {
1329
+ let raw = JSON.parse(await readFile3(this.file, "utf8"));
1146
1330
  if (input.revision !== revisionOf(raw)) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u518D\u4FDD\u5B58");
1147
1331
  raw.routes ??= structuredClone(this.config.routes);
1148
1332
  let staging;
@@ -1235,6 +1419,10 @@ var Manager = class {
1235
1419
  if (!input.name && !replacing) added.name = imported.credential?.nickname || imported.credential?.accountName || input.id;
1236
1420
  } else if (input.action === "account.update") {
1237
1421
  if (!account) bad("\u8D26\u53F7\u4E0D\u5B58\u5728");
1422
+ if (input.autoCheckin !== void 0) {
1423
+ if (typeof input.autoCheckin !== "boolean" || !supportsCheckin(account.provider)) bad("\u6B64\u5E73\u53F0\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u7B7E\u5230");
1424
+ account.autoCheckin = input.autoCheckin;
1425
+ }
1238
1426
  if (input.name !== void 0) account.name = input.name;
1239
1427
  if (input.enabled !== void 0) account.enabled = input.enabled;
1240
1428
  } else if (input.action === "account.move") {
@@ -1356,8 +1544,8 @@ var Manager = class {
1356
1544
  if (filename && String(filename) !== this.file.split(/[\\/]/).pop()) return;
1357
1545
  clearTimeout(timer);
1358
1546
  timer = setTimeout(() => {
1359
- void withFileLock2(this.file, async () => {
1360
- const raw = JSON.parse(await readFile2(this.file, "utf8"));
1547
+ void withFileLock3(this.file, async () => {
1548
+ const raw = JSON.parse(await readFile3(this.file, "utf8"));
1361
1549
  if (this.closed || revisionOf(raw) === this.revision) return;
1362
1550
  const config = validateConfig(raw, this.file);
1363
1551
  this.raw = raw;
@@ -1424,4 +1612,4 @@ export {
1424
1612
  Manager,
1425
1613
  managementHandler
1426
1614
  };
1427
- //# sourceMappingURL=chunk-DZYRIZW2.js.map
1615
+ //# sourceMappingURL=chunk-C6JBDATB.js.map