dsh-multi-router 0.6.11 → 0.6.12

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,12 @@
2
2
 
3
3
  本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。版本号从 `0.1` 起以功能迭代为主,接口在 `1.0` 前可能变化。
4
4
 
5
+ ## 0.6.12
6
+
7
+ - 插件更新确认框增加「允许本次指定版本立即安装」选项,默认勾选、可关闭;确认后仅保存所示 npm 包与确切版本的等待期例外,无需手动修改配置。每次更新仍需确认所示版本,其他依赖、兼容性和脚本策略保持不变。
8
+ - 校验确认版本与检查快照一致,保留原 YAML 注释及策略,合并同包的版本例外;配置无效或不可写时停止安装,避免误关全局保护。
9
+ - 兼容新版 DSH 将 `installBundle` 限为新安装的行为:收到 `already-installed` 后使用官方更新执行器,继续验证实际安装版本。
10
+
5
11
  ## 0.6.11
6
12
 
7
13
  - 修复 0.6.10 引入的回归:「积分使用均衡」在临期账号之间不再生效。0.6.10 按**精确到期日**分组,到期日不同的临期账号各自成为独立分组,降权只在组内进行,于是最早到期的账号用完额度后仍会一直霸占请求,直到积分包耗尽或到期——恰好废掉了「每用满阈值就换账号」这个功能本身。
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.6.11。适配 DSH Desktop / Web 0.2.1-rc.2(同时兼容 0.2.0-rc.2),Node.js ^22.19.0 或 >=24。
9
+ 版本:0.6.12。适配 DSH Desktop / Web 0.2.1-rc.2(同时兼容 0.2.0-rc.2),Node.js ^22.19.0 或 >=24。
10
10
 
11
11
  ## 在 DSH 里管理
12
12
 
@@ -170,11 +170,14 @@ Qoder 的 `dfmodel` 在真实目录里显示为 DeepSeek-Flash,未声明具体
170
170
 
171
171
  - **npm 来源**:比较 SemVer,保留 `latest` / `next` 等频道;固定版本和版本范围在用户确认更新后升级到最新正式版本。查询沿用 DSH 的 registry、网络、认证与镜像回退设置。
172
172
  - **GitHub 来源**:比较原分支 / 标签的 Git 提交,版本号未变化也能发现更新;安装时使用官方 `pnpm update` 路径刷新原分支,并核验实际锁文件提交,避免 `pnpm add` 复用旧锁文件。支持 `github:用户/仓库`、GitHub HTTPS / SSH 地址及锁文件中的提交;固定 Git 提交不自动解除。GitHub 查询需要能够访问仓库公开元数据,私有仓库或限流会显示原因,不会猜测最新提交。
173
+ - **立即安装新版**:在「查看并更新」的确认框默认勾选「允许本次指定版本立即安装(不等待 24 小时)」,仅为框中列出的 npm 包与确切版本保存等待期例外;点击确认后才生效,可取消勾选继续遵守等待期。其他版本、间接依赖、兼容检查及脚本授权保持原策略;若新依赖也在等待期内,仍会提示等待,不自动扩大例外。GitHub 来源仍沿用原分支更新。
173
174
  - 可按名称、来源和可更新状态筛选;支持单个更新、勾选批量更新、取消以及失败后重试。更新前显示目标,并保留每个插件原有启用 / 停用状态。
174
175
  - 可直接输入 npm 包名 / 指定版本,或 `github:用户/仓库` 安装新插件。实际安装由 DSH 官方插件管理服务执行兼容检查、镜像重试、配置锁及失败回滚;依赖构建脚本仍在 DSH 原安装页授权,不自动绕过。
175
176
  - 结果显示实际安装是否成功及是否需要重启。npm 镜像尚未同步、网络超时、版本不兼容会明确提示。GitHub 下载不会由 npm 镜像代替。
176
177
  - 本地文件 / 开发链接标记为原方式更新。不同 DSH 配置之间相互独立,只操作当前配置。
177
178
 
179
+ ![立即安装新版的确认选项(合成插件数据,默认勾选)](docs/screenshots/update-fresh-version.jpg)
180
+
178
181
  ## 侧栏用量概览
179
182
 
180
183
  左侧底部默认显示今日与累计 Token,点击打开本插件详情。可在面板版本号旁取消「侧栏用量」,设置会持久保存。今日按北京时间统计;每分钟仅读取本机统计缓存,隐藏窗口暂停,不增加平台用量请求。未返回 usage 的调用不估算 Token,悬停可查看说明。
@@ -247,7 +250,7 @@ dsh plugin --profile desktop add dsh-multi-router
247
250
  dsh plugin --profile desktop add dsh-multi-router@0.5.9 --registry=https://registry.npmjs.org/
248
251
  ```
249
252
 
250
- 先确认安装成功,再核对插件页的安装版本及面板顶部的运行版本。DSH 内置 pnpm 的新包等待策略可能选择至少 24 小时前的版本;指定刚发布的版本也可能触发 `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`。此时换镜像、卸载重装或重启不会解除策略,应等待到期,或由用户明确为所需版本配置例外;插件不会自动关闭等待策略。输入包名却得到旧版,可能涉及安装器缓存、镜像、旧锁文件或安装失败回滚,不能只凭 `operation-error` 判断。插件内更新器会验证实际版本、展示安装声明与检查频道;失败时可查看脱敏诊断。文件占用、网络失败与插件加载错误分别处理,不把失败安装归因于面板缓存。v0.5.6 旧后台首次迁移后可关闭再启用本插件一次;无需先卸载或删除账号配置。
253
+ 先确认安装成功,再核对插件页的安装版本及面板顶部的运行版本。DSH 内置 pnpm 的新包等待策略可能选择至少 24 小时前的版本;指定刚发布的版本也可能触发 `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`。此时换镜像、卸载重装或重启不会解除策略。可等待到期,或在本插件「插件更新 → 查看并更新」勾选「允许本次指定版本立即安装」并确认,自动保存所示确切版本的例外;插件不会关闭全局等待策略,未授权版本及间接依赖仍受保护。首次安装本插件时尚无此面板,仍需等待到期或手动配置确切版本例外。输入包名却得到旧版,可能涉及安装器缓存、镜像、旧锁文件或安装失败回滚,不能只凭 `operation-error` 判断。插件内更新器会验证实际版本、展示安装声明与检查频道;失败时可查看脱敏诊断。文件占用、网络失败与插件加载错误分别处理,不把失败安装归因于面板缓存。v0.5.6 旧后台首次迁移后可关闭再启用本插件一次;无需先卸载或删除账号配置。
251
254
 
252
255
  ### 方式二:GitHub 链接安装
253
256
 
package/cordis.patch.yml CHANGED
@@ -1,3 +1,3 @@
1
1
  - insert:
2
- - id: llm-multi-router-v2-0.6.11
3
- name: ./lib/host-v0.6.11.js
2
+ - id: llm-multi-router-v2-0.6.12
3
+ name: ./lib/host-v0.6.12.js
@@ -45,7 +45,7 @@ import {
45
45
  import { createHash, randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
46
46
 
47
47
  // package.json
48
- var version = "0.6.11";
48
+ var version = "0.6.12";
49
49
 
50
50
  // src/manager.mjs
51
51
  import { readFile as readFile4, mkdir as mkdir2, mkdtemp as mkdtemp2, rm as rm3 } from "node:fs/promises";
@@ -251,7 +251,7 @@ import { join as join4 } from "node:path";
251
251
  import { randomUUID } from "node:crypto";
252
252
  import { createRequire } from "node:module";
253
253
  import { pathToFileURL } from "node:url";
254
- import { parse as parseYaml } from "yaml";
254
+ import { parse as parseYaml, parseDocument, isMap, isSeq, isScalar } from "yaml";
255
255
  import { withFileLock, writeFileAtomic as writeFileAtomic2 } from "@deepseek-ai/dsh-atomic-write";
256
256
 
257
257
  // src/plugin-reload.mjs
@@ -488,6 +488,40 @@ async function publicJson(url, fetchImpl, signal) {
488
488
  return /^[a-f0-9]{40}$/i.test(text) ? { sha: text } : JSON.parse(text);
489
489
  }
490
490
  var short = (sha) => sha?.slice(0, 8);
491
+ var reviewedVersion = (row) => row.source.kind === "npm" && parseWorkBuddyVersion(row.latestVersion) ? `${row.source.package}@${row.latestVersion}` : void 0;
492
+ async function allowReviewedVersion(profile, row) {
493
+ const file = join4(profile.dir, "pnpm-workspace.yaml"), prefix = `${row.source.package}@`;
494
+ try {
495
+ await withFileLock(file, async () => {
496
+ let text;
497
+ try {
498
+ text = await readFile2(file, "utf8");
499
+ } catch (e) {
500
+ if (e.code !== "ENOENT") throw e;
501
+ text = "{}\n";
502
+ }
503
+ const doc = parseDocument(text), entries = doc.get("minimumReleaseAgeExclude");
504
+ if (doc.errors.length || !isMap(doc.contents) || entries !== void 0 && (!isSeq(entries) || entries.anchor || entries.items.some((x) => !isScalar(x) || typeof x.value !== "string" || x.anchor))) throw Error("invalid-policy");
505
+ const values = entries?.items.map((x) => x.value) ?? [];
506
+ const existing = values.filter((x) => x.startsWith(prefix));
507
+ const versions = [.../* @__PURE__ */ new Set([...existing.flatMap((x) => x.slice(prefix.length).split(/\s*\|\|\s*/)), row.latestVersion])];
508
+ const merged = prefix + versions.join(" || ");
509
+ if (existing.length === 1 && existing[0] === merged) return;
510
+ if (!entries) doc.set("minimumReleaseAgeExclude", [merged]);
511
+ else {
512
+ const first = entries.items.findIndex((x) => x.value.startsWith(prefix));
513
+ if (first < 0) entries.add(merged);
514
+ else {
515
+ entries.items[first].value = merged;
516
+ for (let i = entries.items.length - 1; i > first; i--) if (entries.items[i].value.startsWith(prefix)) entries.delete(i);
517
+ }
518
+ }
519
+ await writeFileAtomic2(file, String(doc), { mode: 384 });
520
+ }, { waitMs: 12e4 });
521
+ } catch {
522
+ throw Error("\u65E0\u6CD5\u4FDD\u5B58\u6B64\u7248\u672C\u7684\u7B49\u5F85\u671F\u4F8B\u5916\u914D\u7F6E\uFF1B\u8BF7\u68C0\u67E5\u5F53\u524D DSH \u914D\u7F6E\u76EE\u5F55\u6743\u9650\u53CA pnpm-workspace.yaml \u683C\u5F0F\u3002\u672A\u5173\u95ED\u7B49\u5F85\u671F\u4FDD\u62A4\u3002");
523
+ }
524
+ }
491
525
  var installFailed = (r) => !!(r.error || ["failed", "cancelled"].includes(r.application) || r.packageResult && r.packageResult.exitCode !== 0);
492
526
  function installFailure(result) {
493
527
  const text = [result.error?.diagnostic, result.packageResult?.output, result.packageResult?.kind].filter((v) => typeof v === "string").join("\n");
@@ -497,7 +531,7 @@ function installFailure(result) {
497
531
  message = "\u5B89\u88C5\u5DF2\u53D6\u6D88\uFF0C\u53EF\u91CD\u65B0\u68C0\u67E5\u540E\u91CD\u8BD5\u3002";
498
532
  } else if (/ERR_PNPM_(?:MINIMUM_RELEASE_AGE_VIOLATION|NO_MATURE_MATCHING_VERSION)|minimumReleaseAge/i.test(text)) {
499
533
  reason = "release-age";
500
- message = "\u65B0\u7248\u672C\u5C1A\u5728\u5B89\u88C5\u5668\u7684\u65B0\u5305\u7B49\u5F85\u671F\u5185\u3002\u8F93\u5165\u5305\u540D\u53EF\u80FD\u9009\u4E2D\u65E7\u7248\uFF0C\u6307\u5B9A\u65B0\u7248\u672C\u53EF\u80FD\u88AB\u62D2\u7EDD\uFF1B\u8BF7\u7B49\u5F85\u7B56\u7565\u89C4\u5B9A\u7684\u65F6\u95F4\u540E\u91CD\u8BD5\uFF0C\u6216\u7531\u7528\u6237\u660E\u786E\u4E3A\u8BE5\u7248\u672C\u8BBE\u7F6E\u4F8B\u5916\u3002\u6362\u955C\u50CF\u6216\u91CD\u542F\u4E0D\u4F1A\u89E3\u9664\u6B64\u9650\u5236\u3002";
534
+ message = "\u65B0\u5305\u5C1A\u5728\u5B89\u88C5\u5668\u7B49\u5F85\u671F\u5185\u3002\u53EF\u5728\u300C\u67E5\u770B\u5E76\u66F4\u65B0\u300D\u4E2D\u52FE\u9009\u300C\u5141\u8BB8\u672C\u6B21\u6307\u5B9A\u7248\u672C\u7ACB\u5373\u5B89\u88C5\u300D\u540E\u91CD\u8BD5\uFF1B\u53EA\u653E\u884C\u786E\u8BA4\u7684\u63D2\u4EF6\u7248\u672C\uFF0C\u4F9D\u8D56\u4ECD\u9700\u6EE1\u8DB3\u7B49\u5F85\u671F\u3002\u4E5F\u53EF\u7B49\u5F85\u5230\u671F\u518D\u5B89\u88C5\u3002";
501
535
  } else if (result.pendingBuilds?.length) {
502
536
  reason = "build";
503
537
  message = "\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\u3002";
@@ -536,7 +570,7 @@ async function registryVersion(service, profile, spec, signal, requestedRegistry
536
570
  }
537
571
  throw Error("npm \u67E5\u8BE2\u5931\u8D25\uFF1B\u8BF7\u68C0\u67E5 DSH \u7F51\u7EDC\u3001registry \u8BBE\u7F6E\u6216\u5305\u7684\u8BBF\u95EE\u6743\u9650");
538
572
  }
539
- async function refreshGitPlugin(service, profile, row, options2, signal) {
573
+ async function refreshProfilePlugin(service, profile, row, options2, signal) {
540
574
  const require2 = createRequire(profile.installAnchor);
541
575
  const { runProfilePnpm } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/operations")).href);
542
576
  const { registryPlan, attributeFailure } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/registry")).href);
@@ -558,7 +592,7 @@ async function refreshGitPlugin(service, profile, row, options2, signal) {
558
592
  try {
559
593
  for (const registry of plan) {
560
594
  signal.throwIfAborted();
561
- const run = await runProfilePnpm({ ...profile, profile: profile.name }, ["update", row.name, ...registry ? [`--registry=${registry}`] : []], {
595
+ const run = await runProfilePnpm({ ...profile, profile: profile.name }, ["update", row.source.kind === "github" ? row.name : row.target, ...registry ? [`--registry=${registry}`] : []], {
562
596
  execution: "service",
563
597
  ...profile.packageManager,
564
598
  signal,
@@ -575,7 +609,7 @@ async function refreshGitPlugin(service, profile, row, options2, signal) {
575
609
  ...run.incompatible?.length ? { error: { code: "incompatible-version" } } : {}
576
610
  };
577
611
  await restore();
578
- if (signal.aborted || attributeFailure(kind, run.output, { kind: "git", host: "github.com" }) !== "registry") break;
612
+ if (signal.aborted || attributeFailure(kind, run.output, row.source.kind === "github" ? { kind: "git", host: "github.com" } : { kind: "npm" }) !== "registry") break;
579
613
  }
580
614
  return result;
581
615
  } catch (e) {
@@ -585,7 +619,7 @@ async function refreshGitPlugin(service, profile, row, options2, signal) {
585
619
  }, { waitMs: 12e4 });
586
620
  }
587
621
  var PluginUpdates = class {
588
- constructor({ service = () => void 0, profile = () => void 0, hmr = () => void 0, fetchImpl = (...args) => fetch(...args), lookup = registryVersion, refreshGit = refreshGitPlugin, onInstalled, stateFile, now = Date.now } = {}) {
622
+ constructor({ service = () => void 0, profile = () => void 0, hmr = () => void 0, fetchImpl = (...args) => fetch(...args), lookup = registryVersion, refreshPackage = refreshProfilePlugin, onInstalled, stateFile, now = Date.now } = {}) {
589
623
  this.stateFile = stateFile;
590
624
  this.now = now;
591
625
  this.dismissed = {};
@@ -598,7 +632,7 @@ var PluginUpdates = class {
598
632
  this.rows = [];
599
633
  this.abort = new AbortController();
600
634
  this.lookup = lookup;
601
- this.refreshGit = refreshGit;
635
+ this.refreshPackage = refreshPackage;
602
636
  this.onInstalled = onInstalled;
603
637
  }
604
638
  async load() {
@@ -746,21 +780,27 @@ var PluginUpdates = class {
746
780
  this.checkedAt = Date.now();
747
781
  });
748
782
  }
749
- async installRow(row, registry) {
783
+ async installRow(row, registry, allowFreshVersions = []) {
750
784
  const original = await jsonFile(join4(this.profile().dir, "package.json"));
751
785
  if (original?.dependencies?.[row.name] !== row.spec) throw Error("\u5B89\u88C5\u6765\u6E90\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5");
752
786
  const current = (await this.service().listBundles()).find((b) => b.name === row.name);
753
- 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");
787
+ if (!current || current.removable === false || 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");
788
+ if (allowFreshVersions.includes(reviewedVersion(row))) await allowReviewedVersion(this.profile(), row);
754
789
  const options2 = { enabled: row.enabled, requestId: this.job.id, ...registry ? { registry } : {} };
755
790
  let result;
756
- if (row.source.kind === "github") {
791
+ const refresh = async () => {
757
792
  this.gitAbort = new AbortController();
758
793
  try {
759
- result = await this.refreshGit(this.service(), this.profile(), row, options2, AbortSignal.any([this.abort.signal, this.gitAbort.signal]));
794
+ return await this.refreshPackage(this.service(), this.profile(), row, options2, AbortSignal.any([this.abort.signal, this.gitAbort.signal]));
760
795
  } finally {
761
796
  this.gitAbort = void 0;
762
797
  }
763
- } else result = await outsideActivation(this.hmr(), () => this.service().installBundle(row.target, options2));
798
+ };
799
+ if (row.source.kind === "github") result = await refresh();
800
+ else {
801
+ result = await outsideActivation(this.hmr(), () => this.service().installBundle(row.target, options2));
802
+ if (result.error?.code === "already-installed") result = await refresh();
803
+ }
764
804
  if (installFailed(result)) return { name: row.name, ...installFailure(result), expectedVersion: row.latestVersion };
765
805
  const installed = (await this.service().listBundles()).find((b) => b.name === row.name);
766
806
  if (row.source.kind === "npm" && installed?.version !== row.latestVersion) return {
@@ -781,10 +821,12 @@ var PluginUpdates = class {
781
821
  const reloadPending = result.application === "restart-required" && this.onInstalled?.(row.name) === true;
782
822
  return { name: row.name, status: "installed", application: reloadPending ? "reload-pending" : result.application, message: reloadPending ? "\u5DF2\u5B89\u88C5\uFF0C\u7B49\u5F85\u5F53\u524D\u64CD\u4F5C\u7ED3\u675F\u540E\u81EA\u52A8\u52A0\u8F7D\uFF1B\u8FD0\u884C\u7248\u672C\u89C1\u9762\u677F\u9876\u90E8" : result.application === "restart-required" ? "\u5DF2\u5B89\u88C5\uFF0C\u5B8C\u5168\u9000\u51FA\u5E76\u91CD\u65B0\u6253\u5F00 DSH \u540E\u751F\u6548" : "\u5B89\u88C5\u5B8C\u6210", restartRequired: !reloadPending && result.application === "restart-required" };
783
823
  }
784
- update(names, registry) {
824
+ update(names, registry, allowFreshVersions = []) {
785
825
  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");
786
826
  const selected = names.map((name) => this.rows.find((r) => r.name === name));
787
827
  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");
828
+ const reviewed = selected.map(reviewedVersion).filter(Boolean);
829
+ if (!Array.isArray(allowFreshVersions) || allowFreshVersions.length > selected.length || new Set(allowFreshVersions).size !== allowFreshVersions.length || allowFreshVersions.some((v) => typeof v !== "string" || !reviewed.includes(v))) throw Error("\u786E\u8BA4\u7684\u63D2\u4EF6\u7248\u672C\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u67E5\u770B\u5E76\u786E\u8BA4\u66F4\u65B0");
788
830
  selected.sort((a, b) => Number(a.name === "dsh-multi-router") - Number(b.name === "dsh-multi-router"));
789
831
  this.start("update", async (job) => {
790
832
  job.total = selected.length;
@@ -793,7 +835,7 @@ var PluginUpdates = class {
793
835
  job.current = row.name;
794
836
  let result;
795
837
  try {
796
- result = await this.installRow(row, registry);
838
+ result = await this.installRow(row, registry, allowFreshVersions);
797
839
  } catch (e) {
798
840
  result = { name: row.name, status: "failed", message: e.message };
799
841
  }
@@ -860,7 +902,7 @@ var PluginUpdates = class {
860
902
  if (!this.flight) await this.list();
861
903
  } else if (input.action === "plugins.dismiss") await this.dismiss(input.keys);
862
904
  else if (input.action === "plugins.check") this.check();
863
- else if (input.action === "plugins.update") this.update(input.names, input.registry);
905
+ else if (input.action === "plugins.update") this.update(input.names, input.registry, input.allowFreshVersions);
864
906
  else if (input.action === "plugins.install") this.install(input.spec, input.registry);
865
907
  else if (input.action === "plugins.cancel") {
866
908
  if (this.job?.state === "running") {
@@ -2056,4 +2098,4 @@ export {
2056
2098
  Manager,
2057
2099
  managementHandler
2058
2100
  };
2059
- //# sourceMappingURL=chunk-YCAO7GCT.js.map
2101
+ //# sourceMappingURL=chunk-V37VG7MN.js.map