dsh-multi-router 0.3.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +55 -5
- package/lib/{chunk-WIDXMBQ2.js → chunk-UQS7DNOQ.js} +396 -22
- package/lib/chunk-UQS7DNOQ.js.map +7 -0
- package/lib/client.js +228 -8
- package/lib/client.js.map +2 -2
- package/lib/host.js +4 -2
- package/lib/index.js +4 -2
- package/lib/index.js.map +2 -2
- package/lib/manager.js +1 -1
- package/package.json +21 -13
- package/lib/chunk-WIDXMBQ2.js.map +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。版本号从 `0.1` 起以功能迭代为主,接口在 `1.0` 前可能变化。
|
|
4
4
|
|
|
5
|
+
## 0.4.0
|
|
6
|
+
|
|
7
|
+
- 首次启用自动获取并保存 WorkBuddy / Trae 本机登录,首页明确显示检测与保存结果,增加一键重试和下一步引导。
|
|
8
|
+
- 内置所有已安装插件的更新面板:npm 版本 / 频道、GitHub 分支提交检查,支持其它插件、停用插件、搜索筛选与批量更新。
|
|
9
|
+
- 通过 DSH 官方安装服务执行更新和新插件安装,保留启用状态,提供取消、失败提示、npm 来源选择及重启引导。
|
|
10
|
+
- 同时存在 npm / pnpm 锁文件时读取最近更新的锁文件;固定 Git 提交不解除,npm 安装完成后核验实际版本。
|
|
11
|
+
|
|
12
|
+
|
|
5
13
|
## 0.3.8
|
|
6
14
|
|
|
7
15
|
- 恢复 WorkBuddy / Trae 本机登录自动获取:启动、每分钟检测,以及手动刷新均会扫描;沿用原插件的 WorkBuddy 多路径和加密格式读取器,Trae 自动识别 CN / Global / SOLO。
|
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.4.0。适配 DSH Desktop / Web 0.2.0-rc.2,Node.js ^22.19.0 或 >=24。
|
|
10
10
|
|
|
11
11
|
## 在 DSH 里管理
|
|
12
12
|
|
|
@@ -89,9 +89,59 @@ Qoder 的 `dfmodel` 在真实目录里显示为 DeepSeek-Flash,未声明具体
|
|
|
89
89
|
|
|
90
90
|
账号获取采用官方客户端已有登录文件的只读解析,不修改客户端登录、不复制参考项目代码、不自动签到。手动凭证支持 WorkBuddy / Trae 字段表单、三家凭证 JSON 和 Qoder PAT;凭证格式、区域、Trae 设备身份会校验。已有账号可在详情中「补充 / 更新凭证」,旧副本保留。Qoder 普通网页 Cookie 无法替代 PAT;官方 PAT 指引见 https://docs.qoder.com/cli/authentication 。
|
|
91
91
|
|
|
92
|
+
## 首次使用与账号自动获取
|
|
93
|
+
|
|
94
|
+
第一次启用插件时会执行 WorkBuddy / WorkBuddy AI 与 Trae CN / Global / SOLO 的本机登录检测,将找到的有效登录保存为独立账号副本。首页会展示「本机账号自动获取」结果,区分已保存、未登录和无法读取,并提供「一键获取并保存」及手动添加入口;无需先逐个点击添加账号。之后每分钟检测新增登录,重复检测不重复添加,已停用和已删除的账号保持用户选择。Qoder 仍需添加 PAT。
|
|
95
|
+
|
|
96
|
+
## 所有已安装插件的更新与安装
|
|
97
|
+
|
|
98
|
+
在本插件配置面板打开 **插件更新**,管理当前 DSH 配置中的全部已安装插件,包括其它插件和已停用插件。首次打开自动检查,后续超过 24 小时重新打开时再检查,也可随时点击「检查全部更新」。
|
|
99
|
+
|
|
100
|
+
- **npm 来源**:比较 SemVer,保留 `latest` / `next` 等频道;固定版本和版本范围在用户确认更新后升级到最新正式版本。查询沿用 DSH 的 registry、网络、认证与镜像回退设置。
|
|
101
|
+
- **GitHub 来源**:比较原分支 / 标签的 Git 提交,版本号未变化也能发现更新。支持 `github:用户/仓库`、GitHub HTTPS / SSH 地址及锁文件中的提交;固定 Git 提交不自动解除。GitHub 查询需要能够访问仓库公开元数据,私有仓库或限流会显示原因,不会猜测最新提交。
|
|
102
|
+
- 可按名称、来源和可更新状态筛选;支持单个更新、勾选批量更新、取消以及失败后重试。更新前显示目标,并保留每个插件原有启用 / 停用状态。
|
|
103
|
+
- 可直接输入 npm 包名 / 指定版本,或 `github:用户/仓库` 安装新插件。实际安装由 DSH 官方插件管理服务执行兼容检查、镜像重试、配置锁及失败回滚;依赖构建脚本仍在 DSH 原安装页授权,不自动绕过。
|
|
104
|
+
- 结果显示实际安装是否成功及是否需要重启。npm 镜像尚未同步、网络超时、版本不兼容会明确提示。GitHub 下载不会由 npm 镜像代替。
|
|
105
|
+
- DSH 自带组件也在清单中展示,标记为随 DSH 应用升级;本地文件 / 开发链接标记为原方式更新。不同 DSH 配置之间相互独立,只操作当前配置。
|
|
106
|
+
|
|
92
107
|
## 安装
|
|
93
108
|
|
|
94
|
-
### 方式一:
|
|
109
|
+
### 方式一:npm 安装(首选)
|
|
110
|
+
|
|
111
|
+
在 DSH 的 **插件页 → 安装插件** 输入包名:
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
dsh-multi-router
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
CLI 安装最新正式版:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Desktop profile
|
|
121
|
+
dsh plugin --profile desktop add dsh-multi-router
|
|
122
|
+
|
|
123
|
+
# Web profile
|
|
124
|
+
dsh plugin --profile web add dsh-multi-router
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
包地址:[dsh-multi-router · npm](https://www.npmjs.com/package/dsh-multi-router)。正式版本由 GitHub Release 自动测试、打包并发布到 npm `latest`,包中已包含编译产物,用户无需自行构建。需要固定版本时可安装 `dsh-multi-router@0.4.0`。
|
|
128
|
+
|
|
129
|
+
已使用 npm 来源的用户更新:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
dsh plugin --profile desktop update dsh-multi-router
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
原先通过 GitHub 或本地 `.tgz` 安装的用户,运行下面命令切换到 npm 正式版本:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
dsh plugin --profile desktop add dsh-multi-router
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
这是同名插件的来源切换;账号、路由和用量保存在包目录之外,会保留。更新完成后**完全退出并重新打开 DeepSeek Harness**;关闭窗口或点击「刷新面板」不会重载后台。插件面板检测到前后台版本不一致时会显示重启步骤。
|
|
142
|
+
|
|
143
|
+
### 方式二:GitHub 链接安装
|
|
144
|
+
|
|
95
145
|
|
|
96
146
|
在 DSH 的 **插件页 → 安装插件** 输入 GitHub 仓库地址:
|
|
97
147
|
|
|
@@ -123,7 +173,7 @@ Desktop 若提示 CLI 仅安装依赖、未自动启用 bundle:完全退出 De
|
|
|
123
173
|
|
|
124
174
|
> 首次安装请先在 DSH 中**关闭**原 WorkBuddy / Trae / Qoder 三个独立连接器(仅关闭,不必卸载,不影响其凭证),避免同名 provider 冲突。
|
|
125
175
|
|
|
126
|
-
###
|
|
176
|
+
### 方式三:本地构建 / 离线包
|
|
127
177
|
|
|
128
178
|
```bash
|
|
129
179
|
git clone https://github.com/AndroidSix/dsh-multi-router.git
|
|
@@ -131,12 +181,12 @@ cd dsh-multi-router
|
|
|
131
181
|
npm ci
|
|
132
182
|
npm run check
|
|
133
183
|
npm run pack:release
|
|
134
|
-
dsh plugin --profile desktop add ./dsh-multi-router-0.
|
|
184
|
+
dsh plugin --profile desktop add ./dsh-multi-router-0.4.0.tgz
|
|
135
185
|
```
|
|
136
186
|
|
|
137
187
|
### 配置账号
|
|
138
188
|
|
|
139
|
-
|
|
189
|
+
安装并启动插件后,已登录的 WorkBuddy / Trae 账号会自动进入账号池;Qoder PAT、多账号凭证和其它设备配置可在面板中添加或导入。也可用 CLI:
|
|
140
190
|
|
|
141
191
|
```bash
|
|
142
192
|
node lib/cli.js add --id wb1 --provider workbuddy --source '/path/workbuddy-desktop.info'
|
|
@@ -34,12 +34,12 @@ import {
|
|
|
34
34
|
import { createHash, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
|
|
35
35
|
|
|
36
36
|
// package.json
|
|
37
|
-
var version = "0.
|
|
37
|
+
var version = "0.4.0";
|
|
38
38
|
|
|
39
39
|
// src/manager.mjs
|
|
40
|
-
import { readFile, mkdir as mkdir2, mkdtemp as mkdtemp2, rm as rm2 } from "node:fs/promises";
|
|
40
|
+
import { readFile as readFile2, mkdir as mkdir2, mkdtemp as mkdtemp2, rm as rm2 } from "node:fs/promises";
|
|
41
41
|
import { watch } from "node:fs";
|
|
42
|
-
import { dirname as dirname3, join as
|
|
42
|
+
import { dirname as dirname3, join as join4, relative as relative2, resolve } from "node:path";
|
|
43
43
|
import { withFileLock } from "@deepseek-ai/dsh-atomic-write";
|
|
44
44
|
|
|
45
45
|
// upstream/qoder/src/status-paths.ts
|
|
@@ -233,6 +233,334 @@ async function prepareMigration(manager, raw, portable, mode) {
|
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
// src/plugin-updates.mjs
|
|
237
|
+
import { readFile, stat } from "node:fs/promises";
|
|
238
|
+
import { join as join3 } from "node:path";
|
|
239
|
+
import { randomUUID } from "node:crypto";
|
|
240
|
+
import { createRequire } from "node:module";
|
|
241
|
+
import { pathToFileURL } from "node:url";
|
|
242
|
+
import { parse as parseYaml } from "yaml";
|
|
243
|
+
|
|
244
|
+
// upstream/workbuddy/src/update.ts
|
|
245
|
+
var WORKBUDDY_REPOSITORY_URL = "https://github.com/corrinehu/dsh-workbuddy-connect";
|
|
246
|
+
var WORKBUDDY_UPDATE_PACKAGE_NAME = "dsh-workbuddy-connect";
|
|
247
|
+
var WORKBUDDY_UPDATE_NPM_METADATA_URL = `https://registry.npmjs.org/-/package/${WORKBUDDY_UPDATE_PACKAGE_NAME}/dist-tags`;
|
|
248
|
+
var WORKBUDDY_UPDATE_RELEASES_API_URL = `${WORKBUDDY_REPOSITORY_URL.replace("https://github.com", "https://api.github.com/repos")}/releases?per_page=100`;
|
|
249
|
+
var WORKBUDDY_RELEASE_PAGE_BASE = `${WORKBUDDY_REPOSITORY_URL}/releases/tag/`;
|
|
250
|
+
var WORKBUDDY_UPDATE_MAX_METADATA_BYTES = 64 * 1024;
|
|
251
|
+
var WORKBUDDY_UPDATE_MAX_RELEASES_BYTES = 1024 * 1024;
|
|
252
|
+
function parseWorkBuddyVersion(raw) {
|
|
253
|
+
if (typeof raw !== "string") return void 0;
|
|
254
|
+
const normalized = raw.startsWith("v") ? raw.slice(1) : raw;
|
|
255
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.exec(normalized);
|
|
256
|
+
if (match === null) return void 0;
|
|
257
|
+
const rawPrerelease = match[4] === void 0 ? [] : match[4].split(".");
|
|
258
|
+
if (rawPrerelease.some((identifier) => /^\d+$/u.test(identifier) && !/^(0|[1-9]\d*)$/u.test(identifier))) return void 0;
|
|
259
|
+
const prerelease = rawPrerelease.map((identifier) => /^(0|[1-9]\d*)$/u.test(identifier) ? Number(identifier) : identifier);
|
|
260
|
+
if (prerelease.some((identifier) => typeof identifier === "number" && !Number.isSafeInteger(identifier))) return void 0;
|
|
261
|
+
const parsed = {
|
|
262
|
+
major: Number(match[1]),
|
|
263
|
+
minor: Number(match[2]),
|
|
264
|
+
patch: Number(match[3]),
|
|
265
|
+
prerelease
|
|
266
|
+
};
|
|
267
|
+
return [parsed.major, parsed.minor, parsed.patch].every(Number.isSafeInteger) ? parsed : void 0;
|
|
268
|
+
}
|
|
269
|
+
function compareIdentifiers(left, right) {
|
|
270
|
+
if (typeof left === "number" && typeof right === "number") return left < right ? -1 : left > right ? 1 : 0;
|
|
271
|
+
if (typeof left === "number") return -1;
|
|
272
|
+
if (typeof right === "number") return 1;
|
|
273
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
274
|
+
}
|
|
275
|
+
function compareWorkBuddyVersions(left, right) {
|
|
276
|
+
const a = parseWorkBuddyVersion(left);
|
|
277
|
+
const b = parseWorkBuddyVersion(right);
|
|
278
|
+
if (a === void 0 || b === void 0) throw new TypeError("invalid WorkBuddy Connect version");
|
|
279
|
+
for (const [aPart, bPart] of [[a.major, b.major], [a.minor, b.minor], [a.patch, b.patch]]) {
|
|
280
|
+
if (aPart !== bPart) return aPart < bPart ? -1 : 1;
|
|
281
|
+
}
|
|
282
|
+
if (a.prerelease.length === 0 && b.prerelease.length !== 0) return 1;
|
|
283
|
+
if (a.prerelease.length !== 0 && b.prerelease.length === 0) return -1;
|
|
284
|
+
for (let index = 0; index < Math.max(a.prerelease.length, b.prerelease.length); index += 1) {
|
|
285
|
+
const aPart = a.prerelease[index];
|
|
286
|
+
const bPart = b.prerelease[index];
|
|
287
|
+
if (aPart === void 0) return -1;
|
|
288
|
+
if (bPart === void 0) return 1;
|
|
289
|
+
const comparison = compareIdentifiers(aPart, bPart);
|
|
290
|
+
if (comparison !== 0) return comparison;
|
|
291
|
+
}
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/plugin-updates.mjs
|
|
296
|
+
var packageName = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
|
|
297
|
+
var safeRef = /^[a-zA-Z0-9._/-]{1,200}$/;
|
|
298
|
+
function parsePluginSource(name, spec) {
|
|
299
|
+
if (!packageName.test(name) || typeof spec !== "string" || spec.length > 600 || spec.startsWith("-") || spec.trim() !== spec || /[\x00-\x1f]/.test(spec)) return { kind: "unsupported" };
|
|
300
|
+
const git = /^(?:github:|git\+https:\/\/github\.com\/|https:\/\/github\.com\/|git\+ssh:\/\/git@github\.com\/|git@github\.com:)?([\w.-]+)\/([\w.-]+?)(?:\.git)?(?:#(.+))?$/.exec(spec);
|
|
301
|
+
if (git && !spec.startsWith("@") && !spec.startsWith("file:") && !spec.startsWith("link:")) {
|
|
302
|
+
const [, owner, repo, ref] = git;
|
|
303
|
+
if (owner === "." || owner === ".." || repo === "." || repo === ".." || ref && (!safeRef.test(ref) || ref.includes(".."))) return { kind: "unsupported" };
|
|
304
|
+
return { kind: "github", owner, repo, ref, source: `github:${owner}/${repo}${ref ? "#" + ref : ""}` };
|
|
305
|
+
}
|
|
306
|
+
if (/^(?:file:|link:|workspace:|https?:|git|\/|\.)/.test(spec)) return { kind: "unsupported" };
|
|
307
|
+
const alias = /^npm:((?:@[^/]+\/)?[^@]+)(?:@(.+))?$/.exec(spec);
|
|
308
|
+
const actual = alias?.[1] ?? name, range = alias?.[2] ?? spec;
|
|
309
|
+
if (!packageName.test(actual) || !/^[a-zA-Z0-9.*^~<>=|+ -]{1,160}$/.test(range)) return { kind: "unsupported" };
|
|
310
|
+
const channel = /^[a-zA-Z][\w.-]*$/.test(range);
|
|
311
|
+
return { kind: "npm", package: actual, alias: !!alias, channel, tag: channel ? range : "latest", source: spec };
|
|
312
|
+
}
|
|
313
|
+
function githubCommit(value) {
|
|
314
|
+
if (typeof value !== "string") return void 0;
|
|
315
|
+
return /(?:\/tar\.gz\/|\/tarball\/|#)([a-f0-9]{40})(?:\b|\()/i.exec(value)?.[1]?.toLowerCase();
|
|
316
|
+
}
|
|
317
|
+
async function jsonFile(file) {
|
|
318
|
+
try {
|
|
319
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
320
|
+
} catch {
|
|
321
|
+
return void 0;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function locks(dir) {
|
|
325
|
+
let pnpm;
|
|
326
|
+
try {
|
|
327
|
+
pnpm = parseYaml(await readFile(join3(dir, "pnpm-lock.yaml"), "utf8"), { maxAliasCount: 20 });
|
|
328
|
+
} catch {
|
|
329
|
+
}
|
|
330
|
+
const npm = await jsonFile(join3(dir, "package-lock.json"));
|
|
331
|
+
const times = await Promise.all(["pnpm-lock.yaml", "package-lock.json"].map((name) => stat(join3(dir, name)).then((s) => s.mtimeMs).catch(() => 0)));
|
|
332
|
+
return { pnpm, npm, preferNpm: times[1] > times[0] };
|
|
333
|
+
}
|
|
334
|
+
async function publicJson(url, fetchImpl, signal) {
|
|
335
|
+
const res = await fetchImpl(url, {
|
|
336
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(12e3)]),
|
|
337
|
+
headers: { Accept: "application/json", "User-Agent": "dsh-multi-router-plugin-updates" },
|
|
338
|
+
redirect: "error"
|
|
339
|
+
});
|
|
340
|
+
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`);
|
|
341
|
+
let size = 0, chunks = [];
|
|
342
|
+
for await (const chunk of res.body) {
|
|
343
|
+
size += chunk.byteLength;
|
|
344
|
+
if (size > 256 * 1024) throw Error("\u66F4\u65B0\u5143\u6570\u636E\u8FC7\u5927");
|
|
345
|
+
chunks.push(Buffer.from(chunk));
|
|
346
|
+
}
|
|
347
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
348
|
+
}
|
|
349
|
+
var short = (sha) => sha?.slice(0, 8);
|
|
350
|
+
async function registryVersion(service, profile, spec, signal) {
|
|
351
|
+
const require2 = createRequire(profile.installAnchor);
|
|
352
|
+
const { viewProfilePackage } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/operations")).href);
|
|
353
|
+
const { registryPlan } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/registry")).href);
|
|
354
|
+
for (const registry of registryPlan(void 0, await service.registries())) {
|
|
355
|
+
const answer = await viewProfilePackage(profile.dir, spec, { ...profile.packageManager, registry, timeoutMs: 12e3, signal });
|
|
356
|
+
if (signal.aborted) throw Error("\u68C0\u67E5\u5DF2\u53D6\u6D88");
|
|
357
|
+
if (answer.exitCode !== 0) continue;
|
|
358
|
+
const parsed = JSON.parse(answer.stdout), metadata = Array.isArray(parsed) ? parsed.at(-1) : parsed;
|
|
359
|
+
if (metadata?.version) return metadata;
|
|
360
|
+
}
|
|
361
|
+
throw Error("npm \u67E5\u8BE2\u5931\u8D25\uFF1B\u8BF7\u68C0\u67E5 DSH \u7F51\u7EDC\u3001registry \u8BBE\u7F6E\u6216\u5305\u7684\u8BBF\u95EE\u6743\u9650");
|
|
362
|
+
}
|
|
363
|
+
var PluginUpdates = class {
|
|
364
|
+
constructor({ service = () => void 0, profile = () => void 0, fetchImpl = (...args) => fetch(...args), lookup = registryVersion } = {}) {
|
|
365
|
+
this.service = service;
|
|
366
|
+
this.profile = profile;
|
|
367
|
+
this.fetchImpl = fetchImpl;
|
|
368
|
+
this.rows = [];
|
|
369
|
+
this.abort = new AbortController();
|
|
370
|
+
this.lookup = lookup;
|
|
371
|
+
}
|
|
372
|
+
state() {
|
|
373
|
+
return {
|
|
374
|
+
available: !!this.service()?.listBundles && !!this.profile()?.dir,
|
|
375
|
+
checkedAt: this.checkedAt,
|
|
376
|
+
rows: this.rows.map(({ spec, ...row }) => ({ ...row, source: row.source.kind === "unsupported" ? { kind: "unsupported" } : row.source })),
|
|
377
|
+
job: this.job,
|
|
378
|
+
error: this.error
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
async list() {
|
|
382
|
+
const service = this.service(), profile = this.profile();
|
|
383
|
+
if (!service?.listBundles || !profile?.dir) {
|
|
384
|
+
this.rows = [];
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const [manifest, bundles, lock] = await Promise.all([jsonFile(join3(profile.dir, "package.json")), service.listBundles(), locks(profile.dir)]);
|
|
388
|
+
if (!manifest) throw Error("\u5F53\u524D DSH \u914D\u7F6E\u7684\u63D2\u4EF6\u6E05\u5355\u65E0\u6CD5\u8BFB\u53D6");
|
|
389
|
+
this.rows = bundles.filter((b) => b.installed || b.version).map((b) => {
|
|
390
|
+
const spec = manifest.dependencies?.[b.name];
|
|
391
|
+
const source = !spec || b.removable === false && !b.readOnlyReason ? { kind: "builtin" } : parsePluginSource(b.name, spec);
|
|
392
|
+
const npm = lock.npm?.packages?.[`node_modules/${b.name}`]?.resolved;
|
|
393
|
+
const pnpm = lock.pnpm?.importers?.["."]?.dependencies?.[b.name]?.version;
|
|
394
|
+
const installed = lock.preferNpm ? npm ?? pnpm : pnpm ?? npm;
|
|
395
|
+
const previous = this.rows.find((r) => r.name === b.name && r.spec === spec && r.version === b.version && r.commit === githubCommit(installed));
|
|
396
|
+
return {
|
|
397
|
+
...previous,
|
|
398
|
+
name: b.name,
|
|
399
|
+
version: b.version,
|
|
400
|
+
enabled: b.enabled,
|
|
401
|
+
spec,
|
|
402
|
+
source,
|
|
403
|
+
commit: githubCommit(installed),
|
|
404
|
+
status: previous?.status ?? (source.kind === "builtin" ? "builtin" : source.kind === "unsupported" ? "unsupported" : "unchecked"),
|
|
405
|
+
...source.kind === "builtin" ? { message: "DSH \u81EA\u5E26 / \u53D7\u4FDD\u62A4\u7EC4\u4EF6\uFF0C\u8BF7\u901A\u8FC7\u66F4\u65B0 DeepSeek Harness \u5347\u7EA7" } : {},
|
|
406
|
+
...source.kind === "unsupported" ? { message: "\u672C\u5730\u6587\u4EF6 / \u94FE\u63A5\u6765\u6E90\uFF0C\u8BF7\u901A\u8FC7\u539F\u5B89\u88C5\u65B9\u5F0F\u66F4\u65B0" } : {}
|
|
407
|
+
};
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
async checkRow(row) {
|
|
411
|
+
try {
|
|
412
|
+
if (row.source.kind === "npm") {
|
|
413
|
+
const spec = `${row.source.package}@${row.source.tag}`;
|
|
414
|
+
const found = await this.lookup(this.service(), this.profile(), spec, this.abort.signal);
|
|
415
|
+
if (!found.version || found.problem && found.problem !== "already-installed") throw Error(found.problem === "not-found" ? "npm \u4E2D\u672A\u627E\u5230\u8BE5\u5305\u6216\u9891\u9053" : "npm \u67E5\u8BE2\u5931\u8D25\uFF1B\u8BF7\u68C0\u67E5 DSH \u7684\u7F51\u7EDC\u4E0E registry \u8BBE\u7F6E");
|
|
416
|
+
if (!parseWorkBuddyVersion(found.version) || row.version && !parseWorkBuddyVersion(row.version)) throw Error("\u65E0\u6CD5\u6BD4\u8F83\u7248\u672C\u53F7");
|
|
417
|
+
row.latestVersion = found.version;
|
|
418
|
+
row.target = `${row.name}@${row.source.alias ? `npm:${row.source.package}@` : ""}${row.source.channel ? row.source.tag : found.version}`;
|
|
419
|
+
row.status = !row.version || compareWorkBuddyVersions(found.version, row.version) > 0 ? "available" : "current";
|
|
420
|
+
row.message = !row.version ? "\u5B89\u88C5\u6587\u4EF6\u4E0D\u53EF\u8BFB\uFF0C\u53EF\u91CD\u65B0\u5B89\u88C5\u8BE5\u6765\u6E90\u4FEE\u590D" : row.status === "current" ? "\u5F53\u524D\u7248\u672C\u5DF2\u662F\u8BE5\u9891\u9053\u6700\u65B0\uFF0C\u6216\u9AD8\u4E8E\u8BE5\u9891\u9053" : void 0;
|
|
421
|
+
} else if (row.source.kind === "github") {
|
|
422
|
+
const { owner, repo, ref } = row.source;
|
|
423
|
+
const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits/${encodeURIComponent(ref ?? "HEAD")}`;
|
|
424
|
+
const found = await publicJson(url, this.fetchImpl, this.abort.signal);
|
|
425
|
+
if (!/^[a-f0-9]{40}$/i.test(found.sha)) throw Error("GitHub \u8FD4\u56DE\u65E0\u6548\u63D0\u4EA4\u4FE1\u606F");
|
|
426
|
+
row.latestCommit = found.sha.toLowerCase();
|
|
427
|
+
row.target = `${row.name}@${row.spec}`;
|
|
428
|
+
row.status = row.commit === row.latestCommit ? "current" : "available";
|
|
429
|
+
row.message = row.commit ? row.status === "available" ? `\u63D0\u4EA4 ${short(row.commit)} \u2192 ${short(row.latestCommit)}` : "\u8BE5\u5206\u652F / \u6807\u7B7E\u7684\u63D0\u4EA4\u5DF2\u540C\u6B65" : "\u65E0\u6CD5\u8BFB\u53D6\u672C\u5730 Git \u63D0\u4EA4\uFF0C\u53EF\u91CD\u65B0\u5B89\u88C5\u5DF2\u68C0\u67E5\u7684\u6765\u6E90";
|
|
430
|
+
if (ref && /^[a-f0-9]{7,40}$/i.test(ref)) {
|
|
431
|
+
row.status = "pinned";
|
|
432
|
+
row.message = "\u5B89\u88C5\u6765\u6E90\u56FA\u5B9A\u5728 Git \u63D0\u4EA4\uFF1B\u4FDD\u7559\u6B64\u63D0\u4EA4\uFF0C\u4E0D\u81EA\u52A8\u5207\u6362\u5206\u652F";
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
row.checkedAt = Date.now();
|
|
436
|
+
row.checkedEnabled = row.enabled;
|
|
437
|
+
} catch (error) {
|
|
438
|
+
row.status = "failed";
|
|
439
|
+
row.message = error.message;
|
|
440
|
+
delete row.target;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
start(kind, task) {
|
|
444
|
+
if (this.flight) throw Error("\u5DF2\u6709\u63D2\u4EF6\u64CD\u4F5C\u6B63\u5728\u6267\u884C\uFF0C\u8BF7\u7B49\u5F85\u5B8C\u6210");
|
|
445
|
+
this.job = { id: randomUUID(), kind, state: "running", startedAt: Date.now(), results: [], total: 0 };
|
|
446
|
+
this.error = void 0;
|
|
447
|
+
this.flight = Promise.resolve().then(() => task(this.job)).then(() => {
|
|
448
|
+
this.job.state = this.job.cancelRequested ? "cancelled" : "done";
|
|
449
|
+
}, (e) => {
|
|
450
|
+
this.job.state = this.job.cancelRequested ? "cancelled" : "failed";
|
|
451
|
+
this.job.message = e.message;
|
|
452
|
+
}).finally(() => {
|
|
453
|
+
this.job.finishedAt = Date.now();
|
|
454
|
+
this.flight = void 0;
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
check() {
|
|
458
|
+
this.start("check", async (job) => {
|
|
459
|
+
await this.list();
|
|
460
|
+
const rows = this.rows.filter((r) => ["npm", "github"].includes(r.source.kind));
|
|
461
|
+
job.total = rows.length;
|
|
462
|
+
let next = 0;
|
|
463
|
+
await Promise.all(Array.from({ length: Math.min(3, rows.length) }, async () => {
|
|
464
|
+
while (next < rows.length && !job.cancelRequested) {
|
|
465
|
+
const row = rows[next++];
|
|
466
|
+
await this.checkRow(row);
|
|
467
|
+
job.results.push({ name: row.name, status: row.status, message: row.message });
|
|
468
|
+
}
|
|
469
|
+
}));
|
|
470
|
+
this.checkedAt = Date.now();
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
async installRow(row, registry) {
|
|
474
|
+
const original = await jsonFile(join3(this.profile().dir, "package.json"));
|
|
475
|
+
if (original?.dependencies?.[row.name] !== row.spec) throw Error("\u5B89\u88C5\u6765\u6E90\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5");
|
|
476
|
+
const current = (await this.service().listBundles()).find((b) => b.name === row.name);
|
|
477
|
+
if (!current || current.enabled !== row.checkedEnabled || current.version !== row.version) throw Error("\u63D2\u4EF6\u7248\u672C\u6216\u542F\u7528\u72B6\u6001\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5");
|
|
478
|
+
const result = await this.service().installBundle(row.target, {
|
|
479
|
+
enabled: row.enabled,
|
|
480
|
+
requestId: this.job.id,
|
|
481
|
+
...registry ? { registry } : {}
|
|
482
|
+
});
|
|
483
|
+
if (result.packageResult && result.packageResult.exitCode !== 0 || ["failed", "cancelled"].includes(result.application) || result.error) {
|
|
484
|
+
return {
|
|
485
|
+
name: row.name,
|
|
486
|
+
status: "failed",
|
|
487
|
+
application: result.application,
|
|
488
|
+
message: result.pendingBuilds?.length ? "\u4F9D\u8D56\u6784\u5EFA\u811A\u672C\u9700\u8981\u6388\u6743\uFF0C\u8BF7\u5230 DSH \u63D2\u4EF6\u5B89\u88C5\u9875\u5141\u8BB8\u8FD9\u4E9B\u811A\u672C\u540E\u91CD\u8BD5" : result.error?.code === "incompatible-version" ? "\u63D2\u4EF6\u4E0E\u5F53\u524D DSH \u4E0D\u517C\u5BB9\uFF0C\u8BF7\u5148\u5347\u7EA7 DSH \u6216\u9009\u62E9\u517C\u5BB9\u7248\u672C" : `\u5B89\u88C5\u672A\u5B8C\u6210\uFF08${result.error?.code ?? result.packageResult?.kind ?? "unknown"}\uFF09\uFF1B\u8BF7\u5230 DSH \u63D2\u4EF6\u9875\u67E5\u770B\u5B89\u88C5\u65E5\u5FD7`,
|
|
489
|
+
pendingBuilds: result.pendingBuilds ?? [],
|
|
490
|
+
logPath: result.packageResult?.logPath
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
const installed = (await this.service().listBundles()).find((b) => b.name === row.name);
|
|
494
|
+
if (row.source.kind === "npm" && installed?.version !== row.latestVersion) return {
|
|
495
|
+
name: row.name,
|
|
496
|
+
status: "failed",
|
|
497
|
+
message: `\u5B9E\u9645\u5B89\u88C5\u7248\u672C ${installed?.version ?? "\u672A\u77E5"} \u4E0E\u68C0\u67E5\u7248\u672C ${row.latestVersion} \u4E0D\u540C\uFF1B\u8BF7\u7A0D\u540E\u91CD\u65B0\u68C0\u67E5\uFF0C\u53EF\u80FD\u662F\u955C\u50CF\u5C1A\u672A\u540C\u6B65\u6216\u53D1\u884C\u65F6\u95F4\u9650\u5236`
|
|
498
|
+
};
|
|
499
|
+
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" };
|
|
500
|
+
}
|
|
501
|
+
update(names, registry) {
|
|
502
|
+
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");
|
|
503
|
+
const selected = names.map((name) => this.rows.find((r) => r.name === name));
|
|
504
|
+
if (selected.some((r) => !r || !["available", "failed"].includes(r.status) || !r.target || Date.now() - r.checkedAt > 6e5)) throw Error("\u8BF7\u5148\u68C0\u67E5\u66F4\u65B0\uFF0C\u518D\u9009\u62E9\u53EF\u66F4\u65B0\u7684\u63D2\u4EF6\uFF08\u68C0\u67E5\u7ED3\u679C\u6709\u6548\u671F 10 \u5206\u949F\uFF09");
|
|
505
|
+
selected.sort((a, b) => Number(a.name === "dsh-multi-router") - Number(b.name === "dsh-multi-router"));
|
|
506
|
+
this.start("update", async (job) => {
|
|
507
|
+
job.total = selected.length;
|
|
508
|
+
for (const row of selected) {
|
|
509
|
+
if (job.cancelRequested) break;
|
|
510
|
+
job.current = row.name;
|
|
511
|
+
let result;
|
|
512
|
+
try {
|
|
513
|
+
result = await this.installRow(row, registry);
|
|
514
|
+
} catch (e) {
|
|
515
|
+
result = { name: row.name, status: "failed", message: e.message };
|
|
516
|
+
}
|
|
517
|
+
job.results.push(result);
|
|
518
|
+
row.status = result.status;
|
|
519
|
+
row.message = result.message;
|
|
520
|
+
}
|
|
521
|
+
delete job.current;
|
|
522
|
+
await this.list();
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
install(spec, registry) {
|
|
526
|
+
if (typeof spec !== "string" || spec.length > 600 || spec.startsWith("-") || /[\s\x00-\x1f]/.test(spec)) throw Error("\u8BF7\u8F93\u5165\u4E00\u4E2A npm \u5305\u540D\u6216 GitHub \u5B89\u88C5\u5730\u5740");
|
|
527
|
+
const named = /^((?:@[^/]+\/)?[^@]+)(?:@(.+))?$/.exec(spec);
|
|
528
|
+
const github = parsePluginSource("placeholder", spec);
|
|
529
|
+
const npm = named && parsePluginSource(named[1], named[2] || "latest");
|
|
530
|
+
if (github.kind !== "github" && npm?.kind !== "npm") throw Error("\u4EC5\u652F\u6301 npm \u5305\u540D / \u7248\u672C\u6216 github:\u7528\u6237/\u4ED3\u5E93");
|
|
531
|
+
this.start("install", async (job) => {
|
|
532
|
+
job.total = 1;
|
|
533
|
+
job.current = spec;
|
|
534
|
+
const result = await this.service().installBundle(spec, { requestId: job.id, ...registry ? { registry } : {} });
|
|
535
|
+
job.results.push({
|
|
536
|
+
name: result.bundle ?? spec,
|
|
537
|
+
status: result.error || ["failed", "cancelled"].includes(result.application) || result.packageResult && result.packageResult.exitCode !== 0 ? "failed" : "installed",
|
|
538
|
+
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",
|
|
539
|
+
application: result.application,
|
|
540
|
+
pendingBuilds: result.pendingBuilds ?? [],
|
|
541
|
+
restartRequired: result.application === "restart-required"
|
|
542
|
+
});
|
|
543
|
+
delete job.current;
|
|
544
|
+
await this.list();
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
async action(input) {
|
|
548
|
+
if (!this.state().available) throw Error("\u5F53\u524D DSH \u672A\u63D0\u4F9B\u63D2\u4EF6\u7BA1\u7406\u63A5\u53E3\uFF0C\u8BF7\u5347\u7EA7 DSH \u540E\u91CD\u8BD5");
|
|
549
|
+
if (input.registry && !["https://registry.npmjs.org/", "https://registry.npmmirror.com/"].includes(input.registry)) throw Error("\u65E0\u6548\u7684 npm \u6765\u6E90");
|
|
550
|
+
if (input.action === "plugins.list") {
|
|
551
|
+
if (!this.flight) await this.list();
|
|
552
|
+
} else if (input.action === "plugins.check") this.check();
|
|
553
|
+
else if (input.action === "plugins.update") this.update(input.names, input.registry);
|
|
554
|
+
else if (input.action === "plugins.install") this.install(input.spec, input.registry);
|
|
555
|
+
else if (input.action === "plugins.cancel") {
|
|
556
|
+
if (this.job?.state === "running") {
|
|
557
|
+
this.job.cancelRequested = true;
|
|
558
|
+
if (this.job.kind !== "check") await this.service().cancelInstall?.(this.job.id);
|
|
559
|
+
}
|
|
560
|
+
} else throw Error("\u672A\u77E5\u63D2\u4EF6\u64CD\u4F5C");
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
|
|
236
564
|
// src/manager.mjs
|
|
237
565
|
var MANAGEMENT_PATH = "/.well-known/dsh-multi-router";
|
|
238
566
|
var revisionOf = (raw) => createHash("sha256").update(JSON.stringify(raw)).digest("hex");
|
|
@@ -272,28 +600,35 @@ function modelInfo(row) {
|
|
|
272
600
|
}
|
|
273
601
|
var Manager = class {
|
|
274
602
|
constructor(file, router, onChange = () => {
|
|
275
|
-
}) {
|
|
603
|
+
}, pluginAccess = {}) {
|
|
276
604
|
this.file = file;
|
|
277
605
|
this.router = router;
|
|
278
606
|
this.onChange = onChange;
|
|
279
|
-
this.catalogFile =
|
|
607
|
+
this.catalogFile = join4(dirname3(file), "catalogs.json");
|
|
280
608
|
this.catalogs = /* @__PURE__ */ new Map();
|
|
281
609
|
this.catalogErrors = /* @__PURE__ */ new Map();
|
|
282
610
|
this.refreshes = /* @__PURE__ */ new Map();
|
|
283
611
|
this.closed = false;
|
|
284
612
|
this.monitorFlights = /* @__PURE__ */ new Map();
|
|
285
613
|
this.loginInfo = /* @__PURE__ */ new Map();
|
|
286
|
-
this.telemetry = new Telemetry(
|
|
614
|
+
this.telemetry = new Telemetry(join4(dirname3(file), "usage.json"));
|
|
615
|
+
this.plugins = new PluginUpdates(pluginAccess);
|
|
616
|
+
this.setupFile = join4(dirname3(file), "initial-setup.json");
|
|
287
617
|
router.telemetry = this.telemetry;
|
|
288
618
|
router.monitor = (a) => this.monitorAccount(a);
|
|
289
619
|
router.prepareFree = () => this.checkFreeModels(true);
|
|
290
620
|
router.freeModels = (account) => this.freeModels(account);
|
|
291
621
|
}
|
|
292
622
|
async load() {
|
|
293
|
-
this.raw = JSON.parse(await
|
|
623
|
+
this.raw = JSON.parse(await readFile2(this.file, "utf8"));
|
|
624
|
+
try {
|
|
625
|
+
this.setup = JSON.parse(await readFile2(this.setupFile, "utf8"));
|
|
626
|
+
} catch {
|
|
627
|
+
this.setup = void 0;
|
|
628
|
+
}
|
|
294
629
|
this.config = validateConfig(this.raw, this.file);
|
|
295
630
|
try {
|
|
296
|
-
const saved = JSON.parse(await
|
|
631
|
+
const saved = JSON.parse(await readFile2(this.catalogFile, "utf8"));
|
|
297
632
|
for (const account of this.config.accounts) {
|
|
298
633
|
const item = saved[account.id];
|
|
299
634
|
if (item?.credentialFile === account.credentialFile && Array.isArray(item.models)) {
|
|
@@ -335,8 +670,9 @@ var Manager = class {
|
|
|
335
670
|
if (this.closed || !discovered.accounts.length) return;
|
|
336
671
|
await withFileLock(this.file, async () => {
|
|
337
672
|
if (this.closed) return;
|
|
338
|
-
let raw = JSON.parse(await
|
|
673
|
+
let raw = JSON.parse(await readFile2(this.file, "utf8"));
|
|
339
674
|
let changed = false;
|
|
675
|
+
const saved = /* @__PURE__ */ new Map();
|
|
340
676
|
for (const item of discovered.accounts) {
|
|
341
677
|
const key = localAccountKey(item.provider, item.document);
|
|
342
678
|
if (!force && raw.ignoredLocalAccounts?.includes(key)) continue;
|
|
@@ -353,6 +689,11 @@ var Manager = class {
|
|
|
353
689
|
} catch {
|
|
354
690
|
}
|
|
355
691
|
}
|
|
692
|
+
const id = existing?.id ?? `local_${createHash("sha256").update(key).digest("hex").slice(0, 20)}`;
|
|
693
|
+
const recordSaved = () => {
|
|
694
|
+
if (!saved.has(item.provider)) saved.set(item.provider, /* @__PURE__ */ new Set());
|
|
695
|
+
saved.get(item.provider).add(id);
|
|
696
|
+
};
|
|
356
697
|
let sameDevice = true;
|
|
357
698
|
if (existing && item.identityDocument) {
|
|
358
699
|
try {
|
|
@@ -362,13 +703,15 @@ var Manager = class {
|
|
|
362
703
|
sameDevice = false;
|
|
363
704
|
}
|
|
364
705
|
}
|
|
365
|
-
if (existing && sameDevice && current.credential.expiresAtMs >= item.document.credential.expiresAtMs)
|
|
366
|
-
|
|
367
|
-
|
|
706
|
+
if (existing && sameDevice && current.credential.expiresAtMs >= item.document.credential.expiresAtMs) {
|
|
707
|
+
recordSaved();
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
const base = join4(dirname3(this.file), "accounts");
|
|
368
711
|
await mkdir2(base, { recursive: true, mode: 448 });
|
|
369
|
-
const staging = await mkdtemp2(
|
|
712
|
+
const staging = await mkdtemp2(join4(base, `${id}-`));
|
|
370
713
|
try {
|
|
371
|
-
const credentialFile =
|
|
714
|
+
const credentialFile = join4(staging, "credential.json"), identityFile = join4(staging, "identity.json");
|
|
372
715
|
await importAccount({ ...item, credentialFile, identityFile });
|
|
373
716
|
const paths = {
|
|
374
717
|
credentialFile: relative2(dirname3(this.file), credentialFile),
|
|
@@ -387,6 +730,7 @@ var Manager = class {
|
|
|
387
730
|
await writePrivateJson(this.file, next);
|
|
388
731
|
raw = next;
|
|
389
732
|
changed = true;
|
|
733
|
+
recordSaved();
|
|
390
734
|
this.router.clients.delete(id);
|
|
391
735
|
this.catalogs.delete(id);
|
|
392
736
|
this.catalogErrors.delete(id);
|
|
@@ -401,12 +745,25 @@ var Manager = class {
|
|
|
401
745
|
await Promise.all(this.config.accounts.map((a) => this.readLogin(a)));
|
|
402
746
|
this.publish();
|
|
403
747
|
}
|
|
748
|
+
this.localDiscovery = this.localDiscovery.map((s) => discovered.status.some((d) => d.provider === s.provider) ? { ...s, savedCount: saved.get(s.provider)?.size ?? 0 } : s);
|
|
404
749
|
}, { waitMs: 3e4 });
|
|
405
750
|
})().finally(() => {
|
|
406
751
|
this.localScan = void 0;
|
|
407
752
|
});
|
|
408
753
|
return this.localScan;
|
|
409
754
|
}
|
|
755
|
+
async finishInitialSetup() {
|
|
756
|
+
if (this.setup?.completedAt) return;
|
|
757
|
+
await withFileLock(this.setupFile, async () => {
|
|
758
|
+
try {
|
|
759
|
+
this.setup = JSON.parse(await readFile2(this.setupFile, "utf8"));
|
|
760
|
+
} catch {
|
|
761
|
+
}
|
|
762
|
+
if (this.setup?.completedAt) return;
|
|
763
|
+
this.setup = { completedAt: Date.now(), dismissed: false };
|
|
764
|
+
await writePrivateJson(this.setupFile, this.setup);
|
|
765
|
+
});
|
|
766
|
+
}
|
|
410
767
|
async monitorAccount(account) {
|
|
411
768
|
if (this.monitorFlights.has(account.id)) return this.monitorFlights.get(account.id);
|
|
412
769
|
const promise = (async () => {
|
|
@@ -597,6 +954,8 @@ var Manager = class {
|
|
|
597
954
|
pluginVersion: version,
|
|
598
955
|
revision: this.revision,
|
|
599
956
|
localDiscovery: this.localDiscovery ?? [],
|
|
957
|
+
setup: this.setup,
|
|
958
|
+
pluginUpdates: this.plugins.state(),
|
|
600
959
|
telemetry: this.telemetry.state(),
|
|
601
960
|
accounts: this.config.accounts.map((a) => ({
|
|
602
961
|
id: a.id,
|
|
@@ -631,8 +990,23 @@ var Manager = class {
|
|
|
631
990
|
}
|
|
632
991
|
async action(input) {
|
|
633
992
|
if (!input || typeof input !== "object" || Array.isArray(input)) bad("\u8BF7\u6C42\u683C\u5F0F\u65E0\u6548");
|
|
993
|
+
if (typeof input.action === "string" && input.action.startsWith("plugins.")) {
|
|
994
|
+
try {
|
|
995
|
+
await this.plugins.action(input);
|
|
996
|
+
} catch (e) {
|
|
997
|
+
bad(e.message);
|
|
998
|
+
}
|
|
999
|
+
return this.state();
|
|
1000
|
+
}
|
|
1001
|
+
if (input.action === "setup.dismiss") {
|
|
1002
|
+
await withFileLock(this.setupFile, async () => {
|
|
1003
|
+
this.setup = { ...this.setup, completedAt: this.setup?.completedAt ?? Date.now(), dismissed: true };
|
|
1004
|
+
await writePrivateJson(this.setupFile, this.setup);
|
|
1005
|
+
});
|
|
1006
|
+
return this.state();
|
|
1007
|
+
}
|
|
634
1008
|
if (input.action === "account.export") {
|
|
635
|
-
if (input.revision !== revisionOf(JSON.parse(await
|
|
1009
|
+
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");
|
|
636
1010
|
const account = this.config.accounts.find((a) => a.id === input.id);
|
|
637
1011
|
if (!account) bad("\u8D26\u53F7\u4E0D\u5B58\u5728");
|
|
638
1012
|
let credential;
|
|
@@ -654,7 +1028,7 @@ var Manager = class {
|
|
|
654
1028
|
} };
|
|
655
1029
|
}
|
|
656
1030
|
if (input.action === "config.export") {
|
|
657
|
-
if (input.revision !== revisionOf(JSON.parse(await
|
|
1031
|
+
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");
|
|
658
1032
|
try {
|
|
659
1033
|
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) } };
|
|
660
1034
|
} catch (e) {
|
|
@@ -702,7 +1076,7 @@ var Manager = class {
|
|
|
702
1076
|
return this.state();
|
|
703
1077
|
}
|
|
704
1078
|
return withFileLock(this.file, async () => {
|
|
705
|
-
let raw = JSON.parse(await
|
|
1079
|
+
let raw = JSON.parse(await readFile2(this.file, "utf8"));
|
|
706
1080
|
if (input.revision !== revisionOf(raw)) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u518D\u4FDD\u5B58");
|
|
707
1081
|
raw.routes ??= structuredClone(this.config.routes);
|
|
708
1082
|
let staging;
|
|
@@ -725,10 +1099,10 @@ var Manager = class {
|
|
|
725
1099
|
}
|
|
726
1100
|
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(input.id ?? "") || !PROVIDERS.includes(input.provider)) bad("\u8D26\u53F7 ID \u6216\u4F9B\u5E94\u5546\u65E0\u6548");
|
|
727
1101
|
if (account && !replacing) bad("\u8D26\u53F7 ID \u5DF2\u5B58\u5728\uFF0C\u8BF7\u4F7F\u7528\u4E0D\u540C ID");
|
|
728
|
-
const base =
|
|
1102
|
+
const base = join4(dirname3(this.file), "accounts");
|
|
729
1103
|
await mkdir2(base, { recursive: true, mode: 448 });
|
|
730
|
-
staging = await mkdtemp2(
|
|
731
|
-
const credentialFile =
|
|
1104
|
+
staging = await mkdtemp2(join4(base, `${input.id}-`));
|
|
1105
|
+
const credentialFile = join4(staging, "credential.json"), identityFile = join4(staging, "identity.json");
|
|
732
1106
|
const added = {
|
|
733
1107
|
id: input.id,
|
|
734
1108
|
name: input.name || account?.name || input.id,
|
|
@@ -917,7 +1291,7 @@ var Manager = class {
|
|
|
917
1291
|
clearTimeout(timer);
|
|
918
1292
|
timer = setTimeout(() => {
|
|
919
1293
|
void withFileLock(this.file, async () => {
|
|
920
|
-
const raw = JSON.parse(await
|
|
1294
|
+
const raw = JSON.parse(await readFile2(this.file, "utf8"));
|
|
921
1295
|
if (this.closed || revisionOf(raw) === this.revision) return;
|
|
922
1296
|
const config = validateConfig(raw, this.file);
|
|
923
1297
|
this.raw = raw;
|
|
@@ -984,4 +1358,4 @@ export {
|
|
|
984
1358
|
Manager,
|
|
985
1359
|
managementHandler
|
|
986
1360
|
};
|
|
987
|
-
//# sourceMappingURL=chunk-
|
|
1361
|
+
//# sourceMappingURL=chunk-UQS7DNOQ.js.map
|