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 +13 -0
- package/README.md +15 -4
- package/lib/accounts.js +2 -2
- package/lib/{chunk-DZYRIZW2.js → chunk-C6JBDATB.js} +218 -30
- package/lib/chunk-C6JBDATB.js.map +7 -0
- package/lib/{chunk-XMXFIV5G.js → chunk-GAQ77CH4.js} +2 -1
- package/lib/chunk-GAQ77CH4.js.map +7 -0
- package/lib/{chunk-3VGKH7MF.js → chunk-OMG4FZWV.js} +2 -2
- package/lib/{chunk-XY2UB22K.js → chunk-U2YTBWLW.js} +3 -3
- package/lib/{chunk-GGRCX2NR.js → chunk-XV7O2LRF.js} +2 -2
- package/lib/{chunk-UFS7ZJKY.js → chunk-YUP77Q3A.js} +62 -5
- package/lib/chunk-YUP77Q3A.js.map +7 -0
- package/lib/cli.js +4 -4
- package/lib/client.js +81 -5
- package/lib/client.js.map +3 -3
- package/lib/clients.js +5 -3
- package/lib/host.js +10 -7
- package/lib/index.js +10 -7
- package/lib/index.js.map +2 -2
- package/lib/manager.js +5 -5
- package/lib/router.js +2 -2
- package/lib/shim.js +3 -3
- package/package.json +1 -1
- package/lib/chunk-DZYRIZW2.js.map +0 -7
- package/lib/chunk-UFS7ZJKY.js.map +0 -7
- package/lib/chunk-XMXFIV5G.js.map +0 -7
- /package/lib/{chunk-3VGKH7MF.js.map → chunk-OMG4FZWV.js.map} +0 -0
- /package/lib/{chunk-XY2UB22K.js.map → chunk-U2YTBWLW.js.map} +0 -0
- /package/lib/{chunk-GGRCX2NR.js.map → chunk-XV7O2LRF.js.map} +0 -0
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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-
|
|
6
|
+
} from "./chunk-OMG4FZWV.js";
|
|
7
7
|
import "./chunk-FTM7RHQV.js";
|
|
8
8
|
import "./chunk-S5KMFAQM.js";
|
|
9
|
-
import "./chunk-
|
|
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-
|
|
4
|
+
} from "./chunk-YUP77Q3A.js";
|
|
5
5
|
import {
|
|
6
6
|
discoverLocalAccounts,
|
|
7
7
|
importAccount,
|
|
8
8
|
localAccountKey,
|
|
9
9
|
readPortableCredential
|
|
10
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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.
|
|
37
|
+
var version = "0.5.1";
|
|
38
38
|
|
|
39
39
|
// src/manager.mjs
|
|
40
|
-
import { readFile as
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
855
|
+
this.raw = JSON.parse(await readFile3(this.file, "utf8"));
|
|
690
856
|
try {
|
|
691
|
-
this.setup = JSON.parse(await
|
|
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
|
|
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
|
|
904
|
+
await withFileLock3(this.file, async () => {
|
|
738
905
|
if (this.closed) return;
|
|
739
|
-
let raw = JSON.parse(await
|
|
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_${
|
|
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
|
|
990
|
+
await withFileLock3(this.setupFile, async () => {
|
|
824
991
|
try {
|
|
825
|
-
this.setup = JSON.parse(await
|
|
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_${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1145
|
-
let raw = JSON.parse(await
|
|
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
|
|
1360
|
-
const raw = JSON.parse(await
|
|
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-
|
|
1615
|
+
//# sourceMappingURL=chunk-C6JBDATB.js.map
|