dsh-multi-router 0.4.0 → 0.4.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,11 @@
2
2
 
3
3
  本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。版本号从 `0.1` 起以功能迭代为主,接口在 `1.0` 前可能变化。
4
4
 
5
+ ## 0.4.1
6
+
7
+ - 修复 GitHub 来源再次安装时 pnpm 复用旧锁文件:改走官方包管理更新路径刷新原分支,保留安装来源与启用状态。
8
+ - 更新完成后核验实际 Git 提交,旧提交或检查结果不一致不会误报成功;更新失败恢复原 manifest / 锁文件,支持取消与 registry 回退。
9
+
5
10
  ## 0.4.0
6
11
 
7
12
  - 首次启用自动获取并保存 WorkBuddy / Trae 本机登录,首页明确显示检测与保存结果,增加一键重试和下一步引导。
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.0。适配 DSH Desktop / Web 0.2.0-rc.2,Node.js ^22.19.0 或 >=24。
9
+ 版本:0.4.1。适配 DSH Desktop / Web 0.2.0-rc.2,Node.js ^22.19.0 或 >=24。
10
10
 
11
11
  ## 在 DSH 里管理
12
12
 
@@ -98,7 +98,7 @@ Qoder 的 `dfmodel` 在真实目录里显示为 DeepSeek-Flash,未声明具体
98
98
  在本插件配置面板打开 **插件更新**,管理当前 DSH 配置中的全部已安装插件,包括其它插件和已停用插件。首次打开自动检查,后续超过 24 小时重新打开时再检查,也可随时点击「检查全部更新」。
99
99
 
100
100
  - **npm 来源**:比较 SemVer,保留 `latest` / `next` 等频道;固定版本和版本范围在用户确认更新后升级到最新正式版本。查询沿用 DSH 的 registry、网络、认证与镜像回退设置。
101
- - **GitHub 来源**:比较原分支 / 标签的 Git 提交,版本号未变化也能发现更新。支持 `github:用户/仓库`、GitHub HTTPS / SSH 地址及锁文件中的提交;固定 Git 提交不自动解除。GitHub 查询需要能够访问仓库公开元数据,私有仓库或限流会显示原因,不会猜测最新提交。
101
+ - **GitHub 来源**:比较原分支 / 标签的 Git 提交,版本号未变化也能发现更新;安装时使用官方 `pnpm update` 路径刷新原分支,并核验实际锁文件提交,避免 `pnpm add` 复用旧锁文件。支持 `github:用户/仓库`、GitHub HTTPS / SSH 地址及锁文件中的提交;固定 Git 提交不自动解除。GitHub 查询需要能够访问仓库公开元数据,私有仓库或限流会显示原因,不会猜测最新提交。
102
102
  - 可按名称、来源和可更新状态筛选;支持单个更新、勾选批量更新、取消以及失败后重试。更新前显示目标,并保留每个插件原有启用 / 停用状态。
103
103
  - 可直接输入 npm 包名 / 指定版本,或 `github:用户/仓库` 安装新插件。实际安装由 DSH 官方插件管理服务执行兼容检查、镜像重试、配置锁及失败回滚;依赖构建脚本仍在 DSH 原安装页授权,不自动绕过。
104
104
  - 结果显示实际安装是否成功及是否需要重启。npm 镜像尚未同步、网络超时、版本不兼容会明确提示。GitHub 下载不会由 npm 镜像代替。
@@ -124,7 +124,7 @@ dsh plugin --profile desktop add dsh-multi-router
124
124
  dsh plugin --profile web add dsh-multi-router
125
125
  ```
126
126
 
127
- 包地址:[dsh-multi-router · npm](https://www.npmjs.com/package/dsh-multi-router)。正式版本由 GitHub Release 自动测试、打包并发布到 npm `latest`,包中已包含编译产物,用户无需自行构建。需要固定版本时可安装 `dsh-multi-router@0.4.0`。
127
+ 包地址:[dsh-multi-router · npm](https://www.npmjs.com/package/dsh-multi-router)。正式版本由 GitHub Release 自动测试、打包并发布到 npm `latest`,包中已包含编译产物,用户无需自行构建。需要固定版本时可安装 `dsh-multi-router@0.4.1`。
128
128
 
129
129
  已使用 npm 来源的用户更新:
130
130
 
@@ -181,7 +181,7 @@ cd dsh-multi-router
181
181
  npm ci
182
182
  npm run check
183
183
  npm run pack:release
184
- dsh plugin --profile desktop add ./dsh-multi-router-0.4.0.tgz
184
+ dsh plugin --profile desktop add ./dsh-multi-router-0.4.1.tgz
185
185
  ```
186
186
 
187
187
  ### 配置账号
@@ -34,13 +34,13 @@ import {
34
34
  import { createHash, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
35
35
 
36
36
  // package.json
37
- var version = "0.4.0";
37
+ var version = "0.4.1";
38
38
 
39
39
  // src/manager.mjs
40
- import { readFile as readFile2, 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 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 } from "@deepseek-ai/dsh-atomic-write";
43
+ import { withFileLock as withFileLock2 } 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";
@@ -234,12 +234,13 @@ async function prepareMigration(manager, raw, portable, mode) {
234
234
  }
235
235
 
236
236
  // src/plugin-updates.mjs
237
- import { readFile, stat } from "node:fs/promises";
237
+ import { readFile, stat, rm as rm2 } from "node:fs/promises";
238
238
  import { join as join3 } from "node:path";
239
239
  import { randomUUID } from "node:crypto";
240
240
  import { createRequire } from "node:module";
241
241
  import { pathToFileURL } from "node:url";
242
242
  import { parse as parseYaml } from "yaml";
243
+ import { withFileLock, writeFileAtomic as writeFileAtomic2 } from "@deepseek-ai/dsh-atomic-write";
243
244
 
244
245
  // upstream/workbuddy/src/update.ts
245
246
  var WORKBUDDY_REPOSITORY_URL = "https://github.com/corrinehu/dsh-workbuddy-connect";
@@ -360,14 +361,63 @@ async function registryVersion(service, profile, spec, signal) {
360
361
  }
361
362
  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
  }
364
+ async function refreshGitPlugin(service, profile, row, options2, signal) {
365
+ const require2 = createRequire(profile.installAnchor);
366
+ const { runProfilePnpm } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/operations")).href);
367
+ const { registryPlan, attributeFailure } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/registry")).href);
368
+ const { classifyInstallFailure } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager")).href);
369
+ const plan = registryPlan(options2.registry, await service.registries());
370
+ return withFileLock(join3(profile.dir, "package.json"), async () => {
371
+ const files = await Promise.all(["package.json", "pnpm-lock.yaml"].map(async (name) => {
372
+ const path = join3(profile.dir, name);
373
+ try {
374
+ return { path, content: await readFile(path, "utf8") };
375
+ } catch (e) {
376
+ if (e.code !== "ENOENT") throw e;
377
+ return { path };
378
+ }
379
+ }));
380
+ const restore = () => Promise.all(files.map((f) => f.content === void 0 ? rm2(f.path, { force: true }) : writeFileAtomic2(f.path, f.content, { mode: 384 })));
381
+ if (JSON.parse(files[0].content).dependencies?.[row.name] !== row.spec) throw Error("\u5B89\u88C5\u6765\u6E90\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5");
382
+ let result;
383
+ try {
384
+ for (const registry of plan) {
385
+ signal.throwIfAborted();
386
+ const run = await runProfilePnpm({ ...profile, profile: profile.name }, ["update", row.name, ...registry ? [`--registry=${registry}`] : []], {
387
+ execution: "service",
388
+ ...profile.packageManager,
389
+ signal,
390
+ outputBytes: 16384,
391
+ idleTimeoutMs: 6e5,
392
+ lookupTimeoutMs: 2e4,
393
+ activateNewBundles: false
394
+ });
395
+ if (run.exitCode === 0 && !run.timedOut && !signal.aborted) return { packageResult: run, application: "restart-required" };
396
+ const kind = classifyInstallFailure({ log: run.output, timedOut: run.timedOut });
397
+ result = {
398
+ packageResult: { ...run, kind },
399
+ application: signal.aborted ? "cancelled" : "failed",
400
+ ...run.incompatible?.length ? { error: { code: "incompatible-version" } } : {}
401
+ };
402
+ await restore();
403
+ if (signal.aborted || attributeFailure(kind, run.output, { kind: "git", host: "github.com" }) !== "registry") break;
404
+ }
405
+ return result;
406
+ } catch (e) {
407
+ await restore();
408
+ throw e;
409
+ }
410
+ }, { waitMs: 12e4 });
411
+ }
363
412
  var PluginUpdates = class {
364
- constructor({ service = () => void 0, profile = () => void 0, fetchImpl = (...args) => fetch(...args), lookup = registryVersion } = {}) {
413
+ constructor({ service = () => void 0, profile = () => void 0, fetchImpl = (...args) => fetch(...args), lookup = registryVersion, refreshGit = refreshGitPlugin } = {}) {
365
414
  this.service = service;
366
415
  this.profile = profile;
367
416
  this.fetchImpl = fetchImpl;
368
417
  this.rows = [];
369
418
  this.abort = new AbortController();
370
419
  this.lookup = lookup;
420
+ this.refreshGit = refreshGit;
371
421
  }
372
422
  state() {
373
423
  return {
@@ -475,11 +525,16 @@ var PluginUpdates = class {
475
525
  if (original?.dependencies?.[row.name] !== row.spec) throw Error("\u5B89\u88C5\u6765\u6E90\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5");
476
526
  const current = (await this.service().listBundles()).find((b) => b.name === row.name);
477
527
  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
- });
528
+ const options2 = { enabled: row.enabled, requestId: this.job.id, ...registry ? { registry } : {} };
529
+ let result;
530
+ if (row.source.kind === "github") {
531
+ this.gitAbort = new AbortController();
532
+ try {
533
+ result = await this.refreshGit(this.service(), this.profile(), row, options2, AbortSignal.any([this.abort.signal, this.gitAbort.signal]));
534
+ } finally {
535
+ this.gitAbort = void 0;
536
+ }
537
+ } else result = await this.service().installBundle(row.target, options2);
483
538
  if (result.packageResult && result.packageResult.exitCode !== 0 || ["failed", "cancelled"].includes(result.application) || result.error) {
484
539
  return {
485
540
  name: row.name,
@@ -496,6 +551,16 @@ var PluginUpdates = class {
496
551
  status: "failed",
497
552
  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
553
  };
554
+ if (row.source.kind === "github") {
555
+ const lock = await locks(this.profile().dir);
556
+ const pnpm = lock.pnpm?.importers?.["."]?.dependencies?.[row.name]?.version, npm = lock.npm?.packages?.[`node_modules/${row.name}`]?.resolved;
557
+ if (githubCommit(lock.preferNpm ? npm ?? pnpm : pnpm ?? npm) !== row.latestCommit) return {
558
+ name: row.name,
559
+ status: "failed",
560
+ restartRequired: true,
561
+ message: "\u5B9E\u9645 Git \u63D0\u4EA4\u4E0E\u68C0\u67E5\u7ED3\u679C\u4E0D\u4E00\u81F4\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5\u540E\u91CD\u8BD5\uFF1B\u4E0D\u628A\u4ECD\u505C\u7559\u5728\u65E7\u9501\u6587\u4EF6\u7684\u5B89\u88C5\u62A5\u544A\u4E3A\u6210\u529F"
562
+ };
563
+ }
499
564
  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
565
  }
501
566
  update(names, registry) {
@@ -555,7 +620,8 @@ var PluginUpdates = class {
555
620
  else if (input.action === "plugins.cancel") {
556
621
  if (this.job?.state === "running") {
557
622
  this.job.cancelRequested = true;
558
- if (this.job.kind !== "check") await this.service().cancelInstall?.(this.job.id);
623
+ if (this.gitAbort) this.gitAbort.abort();
624
+ else if (this.job.kind !== "check") await this.service().cancelInstall?.(this.job.id);
559
625
  }
560
626
  } else throw Error("\u672A\u77E5\u63D2\u4EF6\u64CD\u4F5C");
561
627
  }
@@ -668,7 +734,7 @@ var Manager = class {
668
734
  ...discovered.status.map((s) => ({ ...s, checkedAt: Date.now() }))
669
735
  ];
670
736
  if (this.closed || !discovered.accounts.length) return;
671
- await withFileLock(this.file, async () => {
737
+ await withFileLock2(this.file, async () => {
672
738
  if (this.closed) return;
673
739
  let raw = JSON.parse(await readFile2(this.file, "utf8"));
674
740
  let changed = false;
@@ -735,7 +801,7 @@ var Manager = class {
735
801
  this.catalogs.delete(id);
736
802
  this.catalogErrors.delete(id);
737
803
  } catch (e) {
738
- await rm2(staging, { recursive: true, force: true });
804
+ await rm3(staging, { recursive: true, force: true });
739
805
  this.localDiscovery = this.localDiscovery.map((s) => s.provider === item.provider ? { ...s, state: "unreadable", code: "import_failed" } : s);
740
806
  }
741
807
  }
@@ -754,7 +820,7 @@ var Manager = class {
754
820
  }
755
821
  async finishInitialSetup() {
756
822
  if (this.setup?.completedAt) return;
757
- await withFileLock(this.setupFile, async () => {
823
+ await withFileLock2(this.setupFile, async () => {
758
824
  try {
759
825
  this.setup = JSON.parse(await readFile2(this.setupFile, "utf8"));
760
826
  } catch {
@@ -938,7 +1004,7 @@ var Manager = class {
938
1004
  if (this.closed || !this.config.accounts.some((a) => a.id === account.id && a.credentialFile === account.credentialFile)) return;
939
1005
  this.catalogs.set(account.id, { credentialFile: account.credentialFile, fetchedAt: Date.now(), pricingVersion: 1, models });
940
1006
  this.catalogErrors.delete(account.id);
941
- await withFileLock(this.catalogFile, () => writePrivateJson(this.catalogFile, Object.fromEntries(this.catalogs)));
1007
+ await withFileLock2(this.catalogFile, () => writePrivateJson(this.catalogFile, Object.fromEntries(this.catalogs)));
942
1008
  } catch (error) {
943
1009
  if (this.closed || !this.config.accounts.some((a) => a.id === account.id)) return;
944
1010
  this.catalogErrors.set(account.id, { status: Number(error.status ?? error.failure?.status) || 502, code: "catalog_fetch_failed", at: Date.now() });
@@ -999,7 +1065,7 @@ var Manager = class {
999
1065
  return this.state();
1000
1066
  }
1001
1067
  if (input.action === "setup.dismiss") {
1002
- await withFileLock(this.setupFile, async () => {
1068
+ await withFileLock2(this.setupFile, async () => {
1003
1069
  this.setup = { ...this.setup, completedAt: this.setup?.completedAt ?? Date.now(), dismissed: true };
1004
1070
  await writePrivateJson(this.setupFile, this.setup);
1005
1071
  });
@@ -1075,7 +1141,7 @@ var Manager = class {
1075
1141
  await this.checkFreeModels(true);
1076
1142
  return this.state();
1077
1143
  }
1078
- return withFileLock(this.file, async () => {
1144
+ return withFileLock2(this.file, async () => {
1079
1145
  let raw = JSON.parse(await readFile2(this.file, "utf8"));
1080
1146
  if (input.revision !== revisionOf(raw)) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u518D\u4FDD\u5B58");
1081
1147
  raw.routes ??= structuredClone(this.config.routes);
@@ -1262,7 +1328,7 @@ var Manager = class {
1262
1328
  this.publish();
1263
1329
  await this.telemetry.persist();
1264
1330
  } finally {
1265
- if (staging) await rm2(staging, { recursive: true, force: true });
1331
+ if (staging) await rm3(staging, { recursive: true, force: true });
1266
1332
  }
1267
1333
  return this.state();
1268
1334
  }, { waitMs: 3e4 }).then(async (state) => {
@@ -1290,7 +1356,7 @@ var Manager = class {
1290
1356
  if (filename && String(filename) !== this.file.split(/[\\/]/).pop()) return;
1291
1357
  clearTimeout(timer);
1292
1358
  timer = setTimeout(() => {
1293
- void withFileLock(this.file, async () => {
1359
+ void withFileLock2(this.file, async () => {
1294
1360
  const raw = JSON.parse(await readFile2(this.file, "utf8"));
1295
1361
  if (this.closed || revisionOf(raw) === this.revision) return;
1296
1362
  const config = validateConfig(raw, this.file);
@@ -1358,4 +1424,4 @@ export {
1358
1424
  Manager,
1359
1425
  managementHandler
1360
1426
  };
1361
- //# sourceMappingURL=chunk-UQS7DNOQ.js.map
1427
+ //# sourceMappingURL=chunk-DZYRIZW2.js.map