@ljwei-stak/dsh-model-router 0.18.0 → 0.18.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.
@@ -2348,6 +2348,65 @@ var INSTALL_DEFAULTS = Object.freeze({
2348
2348
  toolScriptUrlsJson: "{}",
2349
2349
  toolAllowScriptInstall: true
2350
2350
  });
2351
+ function toolInstallSettingsSignature(value = {}) {
2352
+ try {
2353
+ const preferences = parseInstallPreferences({ ...value, toolInstallDir: "" }, { tools: OFFICIAL_TOOLS });
2354
+ const sorted = (map) => Object.entries(map).sort(([left], [right]) => left.localeCompare(right));
2355
+ return JSON.stringify([
2356
+ String(value.toolInstallDir ?? "").trim(),
2357
+ preferences.registry,
2358
+ sorted(preferences.methods),
2359
+ sorted(preferences.scriptUrls),
2360
+ preferences.allowScriptInstall
2361
+ ]);
2362
+ } catch {
2363
+ return JSON.stringify([
2364
+ "invalid",
2365
+ value.toolInstallDir,
2366
+ value.toolNpmRegistry,
2367
+ value.toolInstallMethodsJson,
2368
+ value.toolScriptUrlsJson,
2369
+ value.toolAllowScriptInstall
2370
+ ]);
2371
+ }
2372
+ }
2373
+ function installSummaryMatchesSettings(summary, value) {
2374
+ if (!summary) return false;
2375
+ return toolInstallSettingsSignature(value) === toolInstallSettingsSignature({
2376
+ toolInstallDir: summary.requestedInstallDir ?? summary.installDir ?? "",
2377
+ toolNpmRegistry: summary.registry || DEFAULT_NPM_REGISTRY,
2378
+ toolInstallMethodsJson: JSON.stringify(summary.methods ?? {}),
2379
+ toolScriptUrlsJson: JSON.stringify(summary.scriptUrls ?? {}),
2380
+ toolAllowScriptInstall: summary.allowScriptInstall !== false
2381
+ });
2382
+ }
2383
+ function savedToolInstallMethods(value = {}) {
2384
+ try {
2385
+ return parseInstallPreferences({ toolInstallMethodsJson: value.toolInstallMethodsJson }, { tools: OFFICIAL_TOOLS }).methods;
2386
+ } catch {
2387
+ return {};
2388
+ }
2389
+ }
2390
+ async function mutateToolInstallSettings(scope, operations, revision, { timeoutMs = 15e3, onSettled } = {}) {
2391
+ let timer;
2392
+ try {
2393
+ const writing = Promise.resolve().then(() => scope.mutate(operations, revision)).then((accepted) => {
2394
+ onSettled?.({ accepted });
2395
+ return accepted;
2396
+ }, (error) => {
2397
+ onSettled?.({ accepted: false, error });
2398
+ throw error;
2399
+ });
2400
+ return await Promise.race([
2401
+ writing,
2402
+ new Promise((_2, reject) => {
2403
+ timer = setTimeout(() => reject(new Error("\u5B89\u88C5\u8BBE\u7F6E\u4FDD\u5B58\u672A\u6536\u5230\u786E\u8BA4\uFF0C\u8BF7\u5728\u8FDE\u63A5\u6062\u590D\u540E\u91CD\u8BD5\uFF1B\u8FDF\u5230\u7ED3\u679C\u4F1A\u6309\u5B9E\u9645\u5DF2\u4FDD\u5B58\u8BBE\u7F6E\u663E\u793A\u3002")), timeoutMs);
2404
+ })
2405
+ ]);
2406
+ } finally {
2407
+ clearTimeout(timer);
2408
+ }
2409
+ }
2351
2410
  function validateToolInstallSetting(key, value) {
2352
2411
  try {
2353
2412
  parseInstallPreferences({ [key]: value }, { tools: OFFICIAL_TOOLS });
@@ -12645,7 +12704,7 @@ var xterm_default = `/**
12645
12704
  `;
12646
12705
 
12647
12706
  // .dsh-plugin/client/host-version.mjs
12648
- var ROUTER_CLIENT_VERSION = true ? "0.18.0" : "";
12707
+ var ROUTER_CLIENT_VERSION = true ? "0.18.1" : "";
12649
12708
  var STALE_HOST_MESSAGE = "\u63D2\u4EF6\u540E\u53F0\u7248\u672C\u8F83\u65E7\uFF0C\u8BF7\u5B8C\u5168\u9000\u51FA\u5E76\u91CD\u542F Harness\uFF08\u5305\u62EC\u6258\u76D8\u56FE\u6807\uFF09\u540E\u518D\u4F7F\u7528\u3002";
12650
12709
  function isMissingRemoteMethod(message) {
12651
12710
  const value = String(message ?? "");
@@ -13498,7 +13557,7 @@ var INSTALL_SETTING_FIELDS = [
13498
13557
  hint: "\u6309\u5DE5\u5177 ID \u6307\u5B9A https \u955C\u50CF\u5730\u5740\u3002\u811A\u672C\u4E0B\u8F7D\u540E\u4ECD\u4F1A\u6838\u5BF9\u5382\u5546\u6807\u8BB0\uFF0C\u518D\u6267\u884C\u672C\u5730\u526F\u672C\u3002"
13499
13558
  }
13500
13559
  ];
13501
- function InstallSettings({ settingsScope, install, onPendingChange }) {
13560
+ function InstallSettings({ settingsScope, install, onPendingChange, onMutationStart }) {
13502
13561
  const [snapshot, setSnapshot] = import_react5.default.useState(() => settingsScope?.getSnapshot?.() ?? { value: {}, writable: false });
13503
13562
  const [drafts, setDrafts] = import_react5.default.useState({});
13504
13563
  const [saving, setSaving] = import_react5.default.useState(false);
@@ -13535,7 +13594,12 @@ function InstallSettings({ settingsScope, install, onPendingChange }) {
13535
13594
  setNotice("");
13536
13595
  try {
13537
13596
  const entries = Object.entries(drafts).map(([key, value]) => ({ op: "set", path: [key], value }));
13538
- const accepted = await settingsScope.mutate(entries, snapshot.revision);
13597
+ const finishMutation = onMutationStart?.(entries);
13598
+ const onSettled = (outcome) => {
13599
+ const current = finishMutation?.(outcome);
13600
+ if (current && outcome.accepted) setNotice("\u5B89\u88C5\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u3002");
13601
+ };
13602
+ const accepted = await mutateToolInstallSettings(settingsScope, entries, snapshot.revision, { onSettled });
13539
13603
  if (!accepted) throw new Error("\u8BBE\u7F6E\u672A\u88AB\u4FDD\u5B58\uFF0C\u53EF\u80FD\u88AB\u5176\u4ED6\u9875\u9762\u4FEE\u6539\uFF1B\u8F93\u5165\u5DF2\u4FDD\u7559\uFF0C\u8BF7\u91CD\u65B0\u6838\u5BF9\u3002");
13540
13604
  const next = settingsScope.getSnapshot();
13541
13605
  setSnapshot(next);
@@ -13575,7 +13639,7 @@ function InstallSettings({ settingsScope, install, onPendingChange }) {
13575
13639
  onPendingChange?.(false);
13576
13640
  } }, "\u653E\u5F03\u66F4\u6539"), dirty && /* @__PURE__ */ import_react5.default.createElement("span", { className: "mr-caption" }, invalid ? "\u8BF7\u4FEE\u6B63\u8F93\u5165\u540E\u4FDD\u5B58\u3002" : "\u66F4\u6539\u5C1A\u672A\u4FDD\u5B58\uFF0C\u5B89\u88C5\u4E0E\u66F4\u65B0\u6682\u4E0D\u53EF\u7528\u3002")), /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-caption", style: { marginTop: 8 }, role: notice ? "status" : void 0 }, "\u5F53\u524D\u751F\u6548\uFF1A\u5B89\u88C5\u76EE\u5F55 ", install?.installDir || "\uFF08\u5404\u5382\u5546\u9ED8\u8BA4\uFF09", " \xB7 npm \u6E90 ", install?.registry || "\uFF08\u9ED8\u8BA4\uFF09", notice ? ` \xB7 ${notice}` : ""));
13577
13641
  }
13578
- function OfficialToolsCard({ listOfficialTools, installOfficialTool, uninstallOfficialTool, repairOfficialTool, cancelOfficialToolInstall, officialToolInstallStatus, onProbes, health, onRefreshHealth, settingsScope }) {
13642
+ function OfficialToolsCard({ listOfficialTools, loadOfficialInstallSettings, installOfficialTool, uninstallOfficialTool, repairOfficialTool, cancelOfficialToolInstall, officialToolInstallStatus, onProbes, health, onRefreshHealth, settingsScope }) {
13579
13643
  const healthById = Object.fromEntries((health?.tools ?? []).map((item) => [item.id, item]));
13580
13644
  const [probeState, setProbeState] = import_react5.default.useState({ status: "loading", probes: [], capabilities: [], readiness: [], install: null, error: "" });
13581
13645
  const [jobs, setJobs] = import_react5.default.useState({});
@@ -13583,12 +13647,17 @@ function OfficialToolsCard({ listOfficialTools, installOfficialTool, uninstallOf
13583
13647
  const [settingsPending, setSettingsPending] = import_react5.default.useState(false);
13584
13648
  const [methodSaving, setMethodSaving] = import_react5.default.useState(null);
13585
13649
  const [previewPending, setPreviewPending] = import_react5.default.useState(false);
13586
- const [settingsWritable, setSettingsWritable] = import_react5.default.useState(() => settingsScope?.getSnapshot?.()?.writable === true);
13650
+ const [settingsSnapshot, setSettingsSnapshot] = import_react5.default.useState(() => settingsScope?.getSnapshot?.() ?? { value: {}, writable: false });
13651
+ const [previewError, setPreviewError] = import_react5.default.useState("");
13652
+ const [probeSettingsStale, setProbeSettingsStale] = import_react5.default.useState(false);
13587
13653
  const settingsPendingRef = import_react5.default.useRef(false);
13588
13654
  const methodSavingRef = import_react5.default.useRef(null);
13589
13655
  const previewPendingRef = import_react5.default.useRef(false);
13590
13656
  const mounted = import_react5.default.useRef(false);
13591
13657
  const request = import_react5.default.useRef(0);
13658
+ const previewRequest = import_react5.default.useRef(0);
13659
+ const mutationGeneration = import_react5.default.useRef(0);
13660
+ const mutationPendingRef = import_react5.default.useRef(null);
13592
13661
  const submitting = import_react5.default.useRef(/* @__PURE__ */ new Set());
13593
13662
  const polling = import_react5.default.useRef(/* @__PURE__ */ new Set());
13594
13663
  const jobsRef = import_react5.default.useRef({});
@@ -13606,29 +13675,46 @@ function OfficialToolsCard({ listOfficialTools, installOfficialTool, uninstallOf
13606
13675
  const refresh = async () => {
13607
13676
  onRefreshHealth?.();
13608
13677
  const current = ++request.current;
13678
+ const settingsSignature = toolInstallSettingsSignature(settingsScope?.getSnapshot?.()?.value);
13609
13679
  setProbeState((previous) => ({
13610
13680
  ...previous,
13611
- status: previous.probes.length > 0 ? "refreshing" : "loading",
13681
+ status: previous.probes.length > 0 || previous.install ? "refreshing" : "loading",
13612
13682
  error: ""
13613
13683
  }));
13614
13684
  try {
13615
13685
  if (typeof listOfficialTools !== "function") throw new Error("\u5B98\u65B9\u5DE5\u5177\u5B89\u88C5\u6865\u5C1A\u672A\u52A0\u8F7D\u3002");
13616
13686
  const response = await listOfficialTools();
13617
13687
  if (!mounted.current || current !== request.current) return;
13688
+ if (settingsSignature !== toolInstallSettingsSignature(settingsScope?.getSnapshot?.()?.value)) return;
13618
13689
  if (!response?.ok) throw new Error(remoteError(response, "\u65E0\u6CD5\u68C0\u6D4B\u5B98\u65B9\u5DE5\u5177\u3002"));
13619
13690
  const probes = Array.isArray(response.value?.tools) ? response.value.tools : [];
13620
13691
  const capabilities = Array.isArray(response.value?.executionCapabilities) ? response.value.executionCapabilities : [];
13621
13692
  const readiness = Array.isArray(response.value?.executionReadiness) ? response.value.executionReadiness : [];
13622
- setProbeState({ status: "ready", probes, capabilities, readiness, install: response.value?.install ?? null, error: "" });
13623
- previewPendingRef.current = false;
13624
- setPreviewPending(false);
13693
+ const install2 = response.value?.install;
13694
+ const matches = settingsSignature === toolInstallSettingsSignature(settingsScope?.getSnapshot?.()?.value) && installSummaryMatchesSettings(install2, settingsScope?.getSnapshot?.()?.value);
13695
+ if (!matches) return;
13696
+ setProbeState((previous) => ({
13697
+ status: "ready",
13698
+ probes,
13699
+ capabilities,
13700
+ readiness,
13701
+ install: matches ? install2 : previous.install,
13702
+ error: ""
13703
+ }));
13704
+ setProbeSettingsStale(false);
13705
+ if (matches && !mutationPendingRef.current) {
13706
+ previewPendingRef.current = false;
13707
+ setPreviewPending(false);
13708
+ setPreviewError("");
13709
+ }
13625
13710
  onProbes({ probes, capabilities, readiness, hostVersion: typeof response.value?.hostVersion === "string" ? response.value.hostVersion : null });
13626
13711
  } catch (error) {
13627
13712
  if (!mounted.current || current !== request.current) return;
13713
+ if (settingsSignature !== toolInstallSettingsSignature(settingsScope?.getSnapshot?.()?.value)) return;
13628
13714
  const message = remoteErrorText(text5(error?.message), "\u65E0\u6CD5\u68C0\u6D4B\u5B98\u65B9\u5DE5\u5177\u3002");
13629
13715
  let kept = false;
13630
13716
  setProbeState((previous) => {
13631
- if (previous.probes.length > 0) {
13717
+ if (previous.probes.length > 0 || previous.install) {
13632
13718
  kept = true;
13633
13719
  return { ...previous, status: "ready", error: message };
13634
13720
  }
@@ -13637,9 +13723,38 @@ function OfficialToolsCard({ listOfficialTools, installOfficialTool, uninstallOf
13637
13723
  if (!kept) onProbes({ probes: [], capabilities: [], readiness: [] });
13638
13724
  }
13639
13725
  };
13726
+ const refreshInstallSettings = async () => {
13727
+ const current = ++previewRequest.current;
13728
+ const snapshot = settingsScope?.getSnapshot?.();
13729
+ const signature2 = toolInstallSettingsSignature(snapshot?.value);
13730
+ previewPendingRef.current = true;
13731
+ setPreviewPending(true);
13732
+ setPreviewError("");
13733
+ try {
13734
+ if (typeof loadOfficialInstallSettings !== "function") throw new Error("\u5B89\u88C5\u8BBE\u7F6E\u6865\u5C1A\u672A\u52A0\u8F7D\uFF0C\u8BF7\u66F4\u65B0 Host \u540E\u91CD\u65B0\u6253\u5F00\u5DE5\u4F5C\u53F0\u3002");
13735
+ const response = await loadOfficialInstallSettings();
13736
+ if (!mounted.current || current !== previewRequest.current || signature2 !== toolInstallSettingsSignature(settingsScope?.getSnapshot?.()?.value)) return;
13737
+ if (!response?.ok) throw new Error(remoteError(response, "\u65E0\u6CD5\u8BFB\u53D6\u5B89\u88C5\u8BBE\u7F6E\u3002"));
13738
+ if (!installSummaryMatchesSettings(response.value?.install, settingsScope?.getSnapshot?.()?.value)) {
13739
+ throw new Error("\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u4F46 Host \u5B89\u88C5\u914D\u7F6E\u5C1A\u672A\u540C\u6B65\uFF1B\u8BF7\u5237\u65B0\u5B89\u88C5\u547D\u4EE4\u540E\u518D\u5B89\u88C5\u3002");
13740
+ }
13741
+ setProbeState((previous) => ({
13742
+ ...previous,
13743
+ status: previous.status === "loading" || previous.status === "error" ? "ready" : previous.status,
13744
+ install: response.value.install
13745
+ }));
13746
+ if (!mutationPendingRef.current) {
13747
+ previewPendingRef.current = false;
13748
+ setPreviewPending(false);
13749
+ }
13750
+ } catch (error) {
13751
+ if (mounted.current && current === previewRequest.current) setPreviewError(text5(error?.message) || "\u5B89\u88C5\u547D\u4EE4\u5237\u65B0\u5931\u8D25\u3002");
13752
+ }
13753
+ };
13640
13754
  import_react5.default.useEffect(() => {
13641
13755
  mounted.current = true;
13642
13756
  void refresh();
13757
+ void refreshInstallSettings();
13643
13758
  if (typeof officialToolInstallStatus === "function") {
13644
13759
  void Promise.all(OFFICIAL_TOOLS.map(async (tool) => {
13645
13760
  try {
@@ -13664,29 +13779,40 @@ function OfficialToolsCard({ listOfficialTools, installOfficialTool, uninstallOf
13664
13779
  return () => {
13665
13780
  mounted.current = false;
13666
13781
  request.current += 1;
13782
+ previewRequest.current += 1;
13783
+ };
13784
+ }, []);
13785
+ const refreshInstallSettingsRef = import_react5.default.useRef(refreshInstallSettings);
13786
+ refreshInstallSettingsRef.current = refreshInstallSettings;
13787
+ const beginSettingsMutation = import_react5.default.useCallback((operations) => {
13788
+ const generation = ++mutationGeneration.current;
13789
+ mutationPendingRef.current = { generation, operations };
13790
+ previewPendingRef.current = true;
13791
+ setPreviewPending(true);
13792
+ return () => {
13793
+ if (!mounted.current || mutationPendingRef.current?.generation !== generation) return false;
13794
+ mutationPendingRef.current = null;
13795
+ void refreshInstallSettingsRef.current();
13796
+ return true;
13667
13797
  };
13668
13798
  }, []);
13669
- const refreshRef = import_react5.default.useRef(refresh);
13670
- refreshRef.current = refresh;
13671
13799
  import_react5.default.useEffect(() => {
13672
13800
  if (typeof settingsScope?.subscribe !== "function") return void 0;
13673
- const signature2 = (snapshot) => JSON.stringify([
13674
- snapshot?.value?.toolInstallDir,
13675
- snapshot?.value?.toolNpmRegistry,
13676
- snapshot?.value?.toolInstallMethodsJson,
13677
- snapshot?.value?.toolScriptUrlsJson,
13678
- snapshot?.value?.toolAllowScriptInstall
13679
- ]);
13801
+ const signature2 = (snapshot) => toolInstallSettingsSignature(snapshot?.value);
13680
13802
  let previous = signature2(settingsScope.getSnapshot());
13681
13803
  return settingsScope.subscribe(() => {
13682
13804
  const snapshot = settingsScope.getSnapshot();
13683
- setSettingsWritable(snapshot.writable === true);
13805
+ setSettingsSnapshot(snapshot);
13684
13806
  const next = signature2(snapshot);
13685
13807
  if (next === previous) return;
13686
13808
  previous = next;
13809
+ request.current += 1;
13810
+ setProbeSettingsStale(true);
13811
+ setProbeState((previous2) => ({ ...previous2, status: "ready", probes: [], readiness: [], error: "" }));
13812
+ onProbes({ probes: [], capabilities: [], readiness: [] });
13687
13813
  previewPendingRef.current = true;
13688
13814
  setPreviewPending(true);
13689
- void refreshRef.current();
13815
+ void refreshInstallSettingsRef.current();
13690
13816
  });
13691
13817
  }, [settingsScope]);
13692
13818
  import_react5.default.useEffect(() => {
@@ -13797,11 +13923,14 @@ function OfficialToolsCard({ listOfficialTools, installOfficialTool, uninstallOf
13797
13923
  if (methodId) map[toolId] = methodId;
13798
13924
  else delete map[toolId];
13799
13925
  try {
13800
- const accepted = await settingsScope.mutate([{ op: "set", path: ["toolInstallMethodsJson"], value: JSON.stringify(map) }], snapshot.revision);
13926
+ const operations = [{ op: "set", path: ["toolInstallMethodsJson"], value: JSON.stringify(map) }];
13927
+ const finishMutation = beginSettingsMutation(operations);
13928
+ const onSettled = (outcome) => {
13929
+ if (finishMutation(outcome) && outcome.accepted) setRowErrors((previous) => ({ ...previous, [toolId]: "" }));
13930
+ };
13931
+ const accepted = await mutateToolInstallSettings(settingsScope, operations, snapshot.revision, { onSettled });
13801
13932
  if (!accepted) throw new Error("\u5B89\u88C5\u65B9\u5F0F\u672A\u88AB\u4FDD\u5B58\uFF0C\u53EF\u80FD\u88AB\u5176\u4ED6\u9875\u9762\u4FEE\u6539\u3002");
13802
- previewPendingRef.current = true;
13803
- setPreviewPending(true);
13804
- await refresh();
13933
+ setSettingsSnapshot(settingsScope.getSnapshot());
13805
13934
  } catch (error) {
13806
13935
  if (mounted.current) setRowErrors((previous) => ({ ...previous, [toolId]: text5(error?.message) || "\u5B89\u88C5\u65B9\u5F0F\u4FDD\u5B58\u5931\u8D25\u3002" }));
13807
13936
  } finally {
@@ -13826,9 +13955,14 @@ function OfficialToolsCard({ listOfficialTools, installOfficialTool, uninstallOf
13826
13955
  const capabilitiesById = Object.fromEntries(probeState.capabilities.map((item) => [item.id, item]));
13827
13956
  const readinessById = Object.fromEntries(probeState.readiness.map((item) => [item.id, item]));
13828
13957
  const installById = Object.fromEntries((probeState.install?.tools ?? []).map((item) => [item.id, item]));
13958
+ const savedMethods = savedToolInstallMethods(settingsSnapshot.value);
13829
13959
  return /* @__PURE__ */ import_react5.default.createElement("section", { className: "mr-card", "aria-label": "\u5B98\u65B9\u5DE5\u5177" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-card-head" }, /* @__PURE__ */ import_react5.default.createElement("div", null, /* @__PURE__ */ import_react5.default.createElement("h2", { className: "mr-card-title" }, "\u5B98\u65B9\u5DE5\u5177 \xB7 \u4F53\u68C0"), /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-card-copy" }, "\u68C0\u6D4B\u672C\u673A\u5B98\u65B9\u5DE5\u5177\u7684\u5B89\u88C5\u3001\u7248\u672C\u548C\u767B\u5F55\u72B6\u6001\uFF0C\u5E76\u4ECE\u56FA\u5B9A\u6CE8\u518C\u8868\u4E00\u952E\u5B89\u88C5\u6216\u66F4\u65B0\u5230\u5404\u5382\u5546\u6700\u65B0\u7248\uFF08\u65B0\u7248\u672C\u672A\u7ECF\u63D2\u4EF6\u6D4B\u8BD5\uFF09\u3002\u53EF\u6309\u5DE5\u5177\u9009\u62E9 npm\u3001pnpm \u6216\u5382\u5546\u5B89\u88C5\u811A\u672C\u65B9\u5F0F\uFF0C\u5E76\u7EDF\u4E00\u8BBE\u7F6E\u5B89\u88C5\u76EE\u5F55\u4E0E\u4E0B\u8F7D\u6E90\uFF1B\u5378\u8F7D\u53EA\u5220\u9664\u7A0B\u5E8F\uFF0C\u914D\u7F6E\u4E0E\u767B\u5F55\u4FDD\u7559\u3002\u672A\u767B\u5F55\u7684\u5DE5\u5177\u70B9\u201C\u53BB\u767B\u5F55\u201D\u67E5\u770B\u767B\u5F55\u547D\u4EE4\u3002ZCode \u4F1A\u6253\u5F00\u5B98\u65B9\u5B89\u88C5\u7A97\u53E3\u4F9B\u4F60\u9009\u62E9\u76EE\u5F55\uFF1B\u5B8C\u6210\u540E\u91CD\u65B0\u4F53\u68C0\u3002")), /* @__PURE__ */ import_react5.default.createElement("button", { className: "mr-button mr-button-secondary", type: "button", disabled: probeState.status === "loading", onClick: () => {
13830
13960
  void refresh();
13831
- } }, "\u91CD\u65B0\u4F53\u68C0")), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-card-body" }, /* @__PURE__ */ import_react5.default.createElement(InstallSettings, { settingsScope, install: probeState.install, onPendingChange: handleSettingsPending }), previewPending && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-caption", role: "status" }, "\u5B89\u88C5\u8BBE\u7F6E\u5DF2\u53D8\u66F4\uFF0C\u6B63\u5728\u5237\u65B0\u5B9E\u9645\u5B89\u88C5\u547D\u4EE4\uFF1B\u5237\u65B0\u5B8C\u6210\u540E\u53EF\u7EE7\u7EED\u5B89\u88C5\u4E0E\u66F4\u65B0\u3002"), probeState.status === "loading" && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-empty", role: "status" }, "\u6B63\u5728\u68C0\u6D4B\u672C\u673A\u5B98\u65B9\u5DE5\u5177\u2026"), probeState.error && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-error", role: "alert" }, probeState.error), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-tools", role: "list", "aria-label": "\u5B98\u65B9\u5DE5\u5177\u6CE8\u518C\u8868" }, OFFICIAL_TOOLS.map((tool) => {
13961
+ } }, "\u91CD\u65B0\u4F53\u68C0")), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-card-body" }, /* @__PURE__ */ import_react5.default.createElement(InstallSettings, { settingsScope, install: probeState.install, onPendingChange: handleSettingsPending, onMutationStart: beginSettingsMutation }), previewPending && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-caption", role: "status" }, "\u5B89\u88C5\u8BBE\u7F6E\u5DF2\u53D8\u66F4\uFF0C\u6B63\u5728\u5237\u65B0\u5B9E\u9645\u5B89\u88C5\u547D\u4EE4\uFF1B\u4ECD\u53EF\u5207\u6362\u65B9\u5F0F\uFF0C\u5237\u65B0\u5B8C\u6210\u540E\u53EF\u5B89\u88C5\u4E0E\u66F4\u65B0\u3002", !previewError && /* @__PURE__ */ import_react5.default.createElement("button", { className: "mr-button mr-button-secondary", type: "button", onClick: () => {
13962
+ void refreshInstallSettings();
13963
+ } }, "\u5237\u65B0\u5B89\u88C5\u547D\u4EE4")), previewError && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-error", role: "alert" }, previewError, " ", /* @__PURE__ */ import_react5.default.createElement("button", { className: "mr-button mr-button-secondary", type: "button", onClick: () => {
13964
+ void refreshInstallSettings();
13965
+ } }, "\u5237\u65B0\u5B89\u88C5\u547D\u4EE4")), probeSettingsStale && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-caption", role: "status" }, "\u5B89\u88C5\u8BBE\u7F6E\u5DF2\u66F4\u6539\uFF0C\u65E7\u76EE\u6807\u7684\u7248\u672C\u4E0E\u5B89\u88C5\u72B6\u6001\u5DF2\u8FC7\u671F\u3002\u53EF\u5B89\u88C5\u6240\u9009\u76EE\u6807\uFF1B\u5378\u8F7D\u524D\u8BF7\u91CD\u65B0\u4F53\u68C0\u3002"), probeState.status === "loading" && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-empty", role: "status" }, "\u6B63\u5728\u68C0\u6D4B\u672C\u673A\u5B98\u65B9\u5DE5\u5177\u2026"), probeState.error && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-error", role: "alert" }, probeState.error), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-tools", role: "list", "aria-label": "\u5B98\u65B9\u5DE5\u5177\u6CE8\u518C\u8868" }, OFFICIAL_TOOLS.map((tool) => {
13832
13966
  const summary = installById[tool.id];
13833
13967
  const command = summary ? summary.command : installCommandLine(tool);
13834
13968
  const updateCommand = summary?.updateCommand ?? command;
@@ -13850,15 +13984,15 @@ function OfficialToolsCard({ listOfficialTools, installOfficialTool, uninstallOf
13850
13984
  removable: !(summary?.uninstallError ?? (summary?.uninstallCommand ? "" : summary?.error))
13851
13985
  }
13852
13986
  });
13853
- const verified = job?.status === "succeeded" && (job.operation === "uninstall" ? job.postInstallProbe?.installed === false : job.postInstallProbe?.installed === true);
13987
+ const verified = Boolean(probe) && job?.status === "succeeded" && (job.operation === "uninstall" ? job.postInstallProbe?.installed === false : job.postInstallProbe?.installed === true);
13854
13988
  const operation = job?.operation === "uninstall" ? "\u5378\u8F7D" : job?.operation === "repair" ? "\u4FEE\u590D" : "\u5B89\u88C5";
13855
- const status = running ? job.cancelRequested ? `\u6B63\u5728\u53D6\u6D88${operation}\u2026` : `${operation}\u4E2D\u2026` : job?.status === "installer-opened" ? "\u5B98\u65B9\u5B89\u88C5\u5668\u5DF2\u6253\u5F00\uFF0C\u8BF7\u5B8C\u6210\u5B89\u88C5\u540E\u91CD\u65B0\u68C0\u6D4B" : job?.status === "cancelled" ? `${operation}\u5DF2\u53D6\u6D88\uFF0C\u8BF7\u91CD\u65B0\u68C0\u6D4B` : verified ? `${operation}\u6210\u529F\u5E76\u9A8C\u8BC1` : probeLabel(probe);
13989
+ const status = running ? job.cancelRequested ? `\u6B63\u5728\u53D6\u6D88${operation}\u2026` : `${operation}\u4E2D\u2026` : job?.status === "installer-opened" ? "\u5B98\u65B9\u5B89\u88C5\u5668\u5DF2\u6253\u5F00\uFF0C\u8BF7\u5B8C\u6210\u5B89\u88C5\u540E\u91CD\u65B0\u68C0\u6D4B" : job?.status === "cancelled" ? `${operation}\u5DF2\u53D6\u6D88\uFF0C\u8BF7\u91CD\u65B0\u68C0\u6D4B` : verified ? `${operation}\u6210\u529F\u5E76\u9A8C\u8BC1` : probe ? probeLabel(probe) : "\u6240\u9009\u5B89\u88C5\u76EE\u6807\u5C1A\u672A\u4F53\u68C0";
13856
13990
  return /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-tool", role: "listitem", key: tool.id }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-tool-info" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-route-name", title: tool.purpose }, tool.label), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-route-provider" }, tool.vendor, " \xB7 ", tool.id), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-tool-status", role: "status" }, /* @__PURE__ */ import_react5.default.createElement("span", { className: `mr-tool-dot ${running ? "running" : probe?.installed ? "installed" : "missing"}` }), status, probe?.installed && probe.version ? ` \xB7 ${probe.version}` : "", healthById[tool.id]?.latestVersion ? ` \xB7 \u6700\u65B0 ${healthById[tool.id].latestVersion}` : ""), /* @__PURE__ */ import_react5.default.createElement(ToolLoginLine, { entry: healthById[tool.id] }), probe?.installed && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-caption mr-tool-detail" }, tool.headlessAdapter ? "\u5DF2\u53EF\u7531 model_router_execute \u4EE5\u65E0\u754C\u9762\u65B9\u5F0F\u8C03\u7528\u3002\u547D\u4EE4\u7F3A\u5931\u6216\u5931\u8D25\u65F6\u56DE\u9000\u6A21\u578B\u76EE\u5F55 API\u3002\u7B7E\u540D\u6C99\u7BB1\u5165\u53E3\u4E0D\u542F\u52A8\u6B64 CLI\u3002" : readiness?.ready ? `\u5B98\u65B9\u6267\u884C\u5165\u53E3\u5DF2\u6838\u9A8C\uFF0C\u53EF\u5728\u4F1A\u8BDD\u4E2D\u8C03\u7528 model_router_tool_run\uFF1B${capability?.modes?.includes("read-only") ? "\u652F\u6301\u53EA\u8BFB\u548C\u7ECF\u5BA1\u6279\u7684\u53EF\u7F16\u8F91\u4EFB\u52A1" : "\u4EC5\u652F\u6301\u7ECF\u5BA1\u6279\u7684\u53EF\u7F16\u8F91\u9694\u79BB\u5DE5\u4F5C\u533A\u4EFB\u52A1"}\uFF0C\u8D26\u53F7\u53CA\u6A21\u578B\u4ECD\u9700\u5B9E\u6D4B\u3002` : `\u5DF2\u5B89\u88C5\uFF0C\u4F46\u5F53\u524D\u4E0D\u53EF\u6258\u7BA1\u6267\u884C\uFF1A${readiness?.reason || capability?.reason || "\u6267\u884C\u5165\u53E3\u5C1A\u672A\u6838\u9A8C\u3002"}`), command ? /* @__PURE__ */ import_react5.default.createElement(import_react5.default.Fragment, null, /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-caption mr-tool-detail" }, "\u5B89\u88C5\u65B9\u5F0F\uFF1A", methods.find((method) => method.id === summary?.methodId)?.label ?? tool.manager, " \xB7 \u5B9E\u9645\u5B89\u88C5\u76EE\u5F55\uFF1A", summary?.installDir || "\u5404\u5382\u5546\u9ED8\u8BA4\u76EE\u5F55"), /* @__PURE__ */ import_react5.default.createElement("code", { className: "mr-tool-command", "aria-label": `${tool.label} \u5B89\u88C5\u547D\u4EE4` }, command), updateCommand !== command && /* @__PURE__ */ import_react5.default.createElement(import_react5.default.Fragment, null, /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-caption mr-tool-detail" }, "\u66F4\u65B0\u6CBF\u7528\u539F\u5B89\u88C5\u4F4D\u7F6E\uFF1A", summary?.updateInstallDir || "\u5404\u5382\u5546\u9ED8\u8BA4\u76EE\u5F55"), /* @__PURE__ */ import_react5.default.createElement("code", { className: "mr-tool-command", "aria-label": `${tool.label} \u66F4\u65B0\u547D\u4EE4` }, updateCommand))) : /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-caption", style: { margin: "6px 0 0" } }, summary?.error ?? tool.unsupportedReason), (summary?.notices ?? []).map((notice) => /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-caption mr-tool-detail", key: notice }, notice)), probe?.detail && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-caption mr-tool-detail" }, probe.detail), (rowErrors[tool.id] || job?.error) && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-error mr-tool-error", role: "alert" }, rowErrors[tool.id] || job.error), Array.isArray(job?.outputTail) && job.outputTail.length > 0 && /* @__PURE__ */ import_react5.default.createElement("details", { className: "mr-tool-log" }, /* @__PURE__ */ import_react5.default.createElement("summary", null, "\u5B89\u88C5\u65E5\u5FD7"), /* @__PURE__ */ import_react5.default.createElement("pre", null, job.outputTail.slice(-6).join("\n")))), (command || methods.length > 0) && /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-tool-actions" }, methods.length > 0 && /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-tool-methods" }, /* @__PURE__ */ import_react5.default.createElement("label", null, /* @__PURE__ */ import_react5.default.createElement("span", null, "\u5B89\u88C5\u65B9\u5F0F"), /* @__PURE__ */ import_react5.default.createElement(
13857
13991
  "select",
13858
13992
  {
13859
13993
  className: "mr-mini-select",
13860
- value: summary?.savedMethodId ?? "",
13861
- disabled: running || settingsBusy || !settingsWritable,
13994
+ value: savedMethods[tool.id] ?? "",
13995
+ disabled: running || settingsPending || Boolean(methodSaving) || settingsSnapshot.writable !== true,
13862
13996
  onChange: (event) => {
13863
13997
  void setInstallMethod(tool.id, event.target.value);
13864
13998
  }
@@ -14001,7 +14135,7 @@ ${reasons.map((entry, index) => `${index + 1}. ${entry.text}`).join("\n")}
14001
14135
  }, []);
14002
14136
  return { health, ledger, boundaries, busy, refreshHealth, refreshLedger, refreshBoundaries, finishOnboarding, rate, rerun };
14003
14137
  }
14004
- function RouterMainPage({ loadCatalog, settingsScope, listOfficialTools, installOfficialTool, uninstallOfficialTool, repairOfficialTool, cancelOfficialToolInstall, officialToolInstallStatus, toolHealth, completeOnboarding, loadLedger, rateResult, rerunStep, loadBoundaries, previewRun, startRun, terminalApi }) {
14138
+ function RouterMainPage({ loadCatalog, settingsScope, listOfficialTools, loadOfficialInstallSettings, installOfficialTool, uninstallOfficialTool, repairOfficialTool, cancelOfficialToolInstall, officialToolInstallStatus, toolHealth, completeOnboarding, loadLedger, rateResult, rerunStep, loadBoundaries, previewRun, startRun, terminalApi }) {
14005
14139
  const workbench = useWorkbenchData({ toolHealth, completeOnboarding, loadLedger, rateResult, rerunStep, loadBoundaries });
14006
14140
  const [catalogState, setCatalogState] = import_react5.default.useState({ status: "loading", catalog: null, error: "" });
14007
14141
  const [task, setTask] = import_react5.default.useState("");
@@ -14223,6 +14357,7 @@ function RouterMainPage({ loadCatalog, settingsScope, listOfficialTools, install
14223
14357
  OfficialToolsCard,
14224
14358
  {
14225
14359
  listOfficialTools,
14360
+ loadOfficialInstallSettings,
14226
14361
  installOfficialTool,
14227
14362
  uninstallOfficialTool,
14228
14363
  repairOfficialTool,
@@ -14413,6 +14548,7 @@ var toolIdParameter = Object.freeze({
14413
14548
  var jsonParameter = (name, codec) => Object.freeze({ name, wire: name, source: "json", codec });
14414
14549
  var OFFICIAL_TOOLS_REMOTE_DESCRIPTORS = Object.freeze([
14415
14550
  descriptor("list", [], listResultCodec),
14551
+ descriptor("installSettings", [], anyObjectCodec("OfficialInstallSettings")),
14416
14552
  descriptor("installTool", [jsonParameter("request", toolActionCodec)], installResultCodec),
14417
14553
  descriptor("uninstallTool", [jsonParameter("request", toolActionCodec)], installResultCodec),
14418
14554
  descriptor("repairTool", [jsonParameter("request", toolActionCodec)], installResultCodec),
@@ -14836,6 +14972,7 @@ function registerUi(ctx) {
14836
14972
  loadCatalog: () => ctx.remote.session.modelCatalog(),
14837
14973
  settingsScope,
14838
14974
  listOfficialTools: () => officialToolsRemote.list(),
14975
+ loadOfficialInstallSettings: () => officialToolsRemote.installSettings(),
14839
14976
  installOfficialTool: (request) => officialToolsRemote.installTool(request),
14840
14977
  uninstallOfficialTool: (request) => officialToolsRemote.uninstallTool(request),
14841
14978
  repairOfficialTool: (request) => officialToolsRemote.repairTool(request),
@@ -1434,11 +1434,25 @@ export function routerRemoteServices(ctx, config, { terminals = null } = {}) {
1434
1434
  /** Register model-facing tools and the human /router command. */
1435
1435
  export function apply(ctx, config = {}) {
1436
1436
  // Install addresses (directory, npm source, per-tool method and script source)
1437
- // live in volatile config, so re-apply picks up whatever the user last saved.
1438
- // A malformed value is reported here rather than at the first click.
1437
+ // are volatile refs: Loader changes them without re-applying the plugin.
1438
+ // Read them after the owning fiber's live update, before a saved view is sent.
1439
1439
  let installPreferenceError = null
1440
- try { setInstallPreferences(parseInstallPreferences(config, { tools: OFFICIAL_TOOLS, ...currentInstallOptions() })) }
1441
- catch (error) { installPreferenceError = errorText(error) }
1440
+ const syncInstallPreferences = () => {
1441
+ try {
1442
+ const next = parseInstallPreferences(config, { tools: OFFICIAL_TOOLS, ...currentInstallOptions() })
1443
+ setInstallPreferences(next)
1444
+ installPreferenceError = null
1445
+ } catch (error) { installPreferenceError = errorText(error) }
1446
+ }
1447
+ const installFields = new Set(['toolInstallDir', 'toolNpmRegistry', 'toolInstallMethodsJson', 'toolScriptUrlsJson', 'toolAllowScriptInstall'])
1448
+ syncInstallPreferences()
1449
+ // Loader scopes this event to the fiber whose refs were committed.
1450
+ ctx.on('loader/volatile-update', paths => {
1451
+ if (Array.isArray(paths) && paths.some(path => installFields.has(path?.[0]))) syncInstallPreferences()
1452
+ })
1453
+ const assertInstallPreferences = () => {
1454
+ if (installPreferenceError) throw new Error(`官方工具安装设置无效:${installPreferenceError}`)
1455
+ }
1442
1456
  // The official Host injects typert; direct lightweight uses of apply may
1443
1457
  // supply only the model/command services and do not expose the Desktop RPC.
1444
1458
  if (ctx.typert) {
@@ -1449,7 +1463,11 @@ export function apply(ctx, config = {}) {
1449
1463
  process.once('exit', onExit)
1450
1464
  return () => { process.off('exit', onExit); terminals.disposeAll('dispose') }
1451
1465
  }, 'model-router-galgame: interactive terminals')
1452
- registerOfficialToolsRemote(ctx, routerRemoteServices(ctx, config, { terminals }))
1466
+ registerOfficialToolsRemote(ctx, {
1467
+ ...routerRemoteServices(ctx, config, { terminals }),
1468
+ installSettingsError: () => installPreferenceError,
1469
+ installRequestedDirectory: () => valueOf(config, 'toolInstallDir', ''),
1470
+ })
1453
1471
  }
1454
1472
  ctx.on('tools/pre-execute', async (exec, next) => {
1455
1473
  const decision = await next()
@@ -1498,7 +1516,7 @@ export function apply(ctx, config = {}) {
1498
1516
  if (reasons.length) return combinedAsk(reasons)
1499
1517
  return decision
1500
1518
  })
1501
- registerOfficialToolModels(ctx, config)
1519
+ registerOfficialToolModels(ctx, config, assertInstallPreferences)
1502
1520
  ctx.tools.register(defineTool({
1503
1521
  name: 'model_router_routes',
1504
1522
  description: 'List provider/model routes registered in the official DeepSeek Harness model directory. Credential and network availability are not verified. No API keys or endpoints are returned.',
@@ -1611,6 +1629,7 @@ export function apply(ctx, config = {}) {
1611
1629
  const toolId = matched[2]
1612
1630
  const method = matched[3]
1613
1631
  try {
1632
+ assertInstallPreferences()
1614
1633
  const job = action === 'uninstall' ? startUninstall(toolId, { method })
1615
1634
  : action === 'repair' ? startRepair(toolId, { method })
1616
1635
  : startInstall(toolId, { method })
@@ -1649,7 +1668,7 @@ async function waitForInstall(toolId, signal) {
1649
1668
  }
1650
1669
 
1651
1670
  /** Register the model-facing official-tool probes and installer. */
1652
- function registerOfficialToolModels(ctx, config) {
1671
+ function registerOfficialToolModels(ctx, config, assertInstallPreferences) {
1653
1672
  ctx.tools.register(defineTool({
1654
1673
  name: 'model_router_tools',
1655
1674
  description: 'Probe the fixed registry of official model tools (Kimi Code, Claude Code, Codex, MiniMax Code, MiMo Code, Grok Build, Gemini CLI, OpenCode, Step Code, ZCode) and report which are installed with their versions, plus each tool\'s available install methods and the effective install directory and npm source. Never reads credentials.',
@@ -1680,6 +1699,7 @@ function registerOfficialToolModels(ctx, config) {
1680
1699
  },
1681
1700
  output: JSON_OUTPUT,
1682
1701
  async execute(args, exec) {
1702
+ assertInstallPreferences()
1683
1703
  const job = startInstall(args.tool, { method: text(args.method) || undefined })
1684
1704
  const settled = await waitForInstall(job.tool, exec.signal)
1685
1705
  const tool = getOfficialTool(args.tool)
@@ -1704,6 +1724,7 @@ function registerOfficialToolModels(ctx, config) {
1704
1724
  },
1705
1725
  output: JSON_OUTPUT,
1706
1726
  async execute(args, exec) {
1727
+ assertInstallPreferences()
1707
1728
  const job = startRepair(args.tool, { method: text(args.method) || undefined })
1708
1729
  const settled = await waitForInstall(job.tool, exec.signal)
1709
1730
  const tool = getOfficialTool(args.tool)
@@ -1726,6 +1747,7 @@ function registerOfficialToolModels(ctx, config) {
1726
1747
  },
1727
1748
  output: JSON_OUTPUT,
1728
1749
  async execute(args, exec) {
1750
+ assertInstallPreferences()
1729
1751
  const job = startUninstall(args.tool, { method: text(args.method) || undefined })
1730
1752
  const settled = await waitForInstall(job.tool, exec.signal)
1731
1753
  const tool = getOfficialTool(args.tool)
@@ -179,13 +179,25 @@ export class OfficialToolsRemoteService extends TypertRemoteService {
179
179
  executionCapabilities: officialToolExecutionCapabilities(),
180
180
  executionReadiness,
181
181
  hostVersion: HOST_PLUGIN_VERSION,
182
- install: installSummary(),
182
+ install: this.installSettings().install,
183
183
  }
184
184
  }
185
185
 
186
+ /** Refresh saved methods and commands without spawning CLIs or checking downloads. */
187
+ installSettings() {
188
+ const error = this.services?.installSettingsError?.()
189
+ if (error) throw new Error(`官方工具安装设置无效:${error}`)
190
+ const install = installSummary()
191
+ // The saved input can contain Host-only home/environment expansions. Keep
192
+ // it separate from the resolved directory used by the actual command.
193
+ install.requestedInstallDir = this.services?.installRequestedDirectory?.() ?? install.installDir
194
+ return { install, hostVersion: HOST_PLUGIN_VERSION }
195
+ }
196
+
186
197
  /** Start one serialized fixed-registry install; return immediately for UI polling. */
187
198
  async installTool(request) {
188
199
  return settled(async () => {
200
+ this.installSettings()
189
201
  await ensureNpmPrefixOnPath()
190
202
  return { accepted: true, job: startInstall(request.tool, { method: request.method }) }
191
203
  })
@@ -197,6 +209,7 @@ export class OfficialToolsRemoteService extends TypertRemoteService {
197
209
  */
198
210
  async uninstallTool(request) {
199
211
  return settled(async () => {
212
+ this.installSettings()
200
213
  await ensureNpmPrefixOnPath()
201
214
  return { accepted: true, job: startUninstall(request.tool, { method: request.method }) }
202
215
  })
@@ -205,6 +218,7 @@ export class OfficialToolsRemoteService extends TypertRemoteService {
205
218
  /** Reinstall the tool even when it already reports the latest version. */
206
219
  async repairTool(request) {
207
220
  return settled(async () => {
221
+ this.installSettings()
208
222
  await ensureNpmPrefixOnPath()
209
223
  return { accepted: true, job: startRepair(request.tool, { method: request.method }) }
210
224
  })
@@ -193,6 +193,7 @@ const jsonParameter = (name, codec) => Object.freeze({ name, wire: name, source:
193
193
 
194
194
  export const OFFICIAL_TOOLS_REMOTE_DESCRIPTORS = Object.freeze([
195
195
  descriptor('list', [], listResultCodec),
196
+ descriptor('installSettings', [], anyObjectCodec('OfficialInstallSettings')),
196
197
  descriptor('installTool', [jsonParameter('request', toolActionCodec)], installResultCodec),
197
198
  descriptor('uninstallTool', [jsonParameter('request', toolActionCodec)], installResultCodec),
198
199
  descriptor('repairTool', [jsonParameter('request', toolActionCodec)], installResultCodec),
@@ -56,7 +56,7 @@ export const CLAUDE_UPDATE_ARGS = Object.freeze(['update'])
56
56
 
57
57
  /**
58
58
  * Host-side install preferences (install directory, npm source, per-tool method
59
- * and script source). Refreshed from plugin config on every apply, so a client
59
+ * and script source). Refreshed on apply and live config updates, so a client
60
60
  * can only ever pick a method id — these addresses never arrive over the wire.
61
61
  */
62
62
  let preferences = { installDir: '', registry: '', methods: {}, scriptUrls: {}, allowScriptInstall: true }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.1 — 2026-10-12
4
+
5
+ Stable patch under the npm latest tag. Fully quit Harness, including the tray or WebUI host process, and restart after upgrading.
6
+
7
+ - Fix official-tool method selection stuck on saving: configuration persistence no longer waits for CLI inspection, and selectors reflect the acknowledged settings snapshot.
8
+ - Refresh install commands through a lightweight Host endpoint; delayed tool inspections cannot overwrite a newer install preview. Install actions wait for confirmed settings while method selection remains available.
9
+ - Synchronize volatile install settings into Host preferences without requiring plugin reload. Invalid configuration is reported and cannot start an operation with older settings.
10
+ - Add regression coverage using the real settings controller and Loader, plus a mock UI scenario with a hanging tool inspection. Release validation is recorded in the release notes.
11
+
3
12
  ## 0.18.0 — 2026-10-12
4
13
 
5
14
  Stable minor under the npm latest tag. Fully quit Harness, including the tray or WebUI host process, and restart after upgrading. Existing releases remain unchanged.
package/README.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # Model Router · DeepSeek Harness Desktop
2
2
 
3
- Version: **0.18.0**. See [routing research](docs/ROUTING_RESEARCH.zh.md), [mathematical derivation and experiments](docs/ROUTING_DERIVATION_EXPERIMENTS.zh.md), and the [LiveBench evidence audit and validation protocol](docs/ROUTING_LIVEBENCH_VALIDATION.zh.md). The latest public 2026-06-25 score/cost tables are aggregates; its paired question-quality/cost matrix has not been obtained, and no real routing gain is claimed.
3
+ Version: **0.18.1**. See [routing research](docs/ROUTING_RESEARCH.zh.md), [mathematical derivation and experiments](docs/ROUTING_DERIVATION_EXPERIMENTS.zh.md), and the [LiveBench evidence audit and validation protocol](docs/ROUTING_LIVEBENCH_VALIDATION.zh.md). The latest public 2026-06-25 score/cost tables are aggregates; its paired question-quality/cost matrix has not been obtained, and no real routing gain is claimed.
4
4
 
5
5
  A cost-aware model-routing workbench for **DeepSeek Harness Desktop**. Describe a task, compare your configured models, inspect the plan, and explicitly choose when to execute it.
6
6
 
7
+ **0.18.1:** fixes install-method selection stuck on saving. Saved volatile settings now update Host install preferences immediately; command previews refresh separately from tool inspection.
8
+
7
9
  **0.18.0:** expands official vendor installer choices alongside npm/pnpm, preserves the configured install destination during updates, and verifies the target copy instead of an older PATH copy. MiniMax Windows scripts beginning with PowerShell `<# ... #>` help comments are accepted; HTML error pages remain rejected. Save unified install settings before installing or updating.
8
10
 
9
11
  **0.16.4:** PATH probes keep command output even when no install log callback is attached. 0.16.3 captured stdout only inside `onOutput?.(...)`, so `where` and `--version` looked empty and every PATH CLI was reported not-installed. MiniMax and ZCode were unaffected because they do not use that probe.
@@ -16,7 +18,7 @@ A cost-aware model-routing workbench for **DeepSeek Harness Desktop**. Describe
16
18
 
17
19
  **0.16.0:** [dynamic data and adaptive feedback](docs/ROUTING_ADAPTIVE_LEARNING.zh.md) adds opt-in public benchmark/curated fixed-USD pricing refresh and task-specific, time-decayed, shrinkage-regularized subjective utility. Objective proxy quality floors remain separate. Public sources default off; preferences are shared within one local DSH_HOME and limited to the latest 200 runs. This is not a universal vendor-price scraper or evidence of real quality–cost gains.
18
20
 
19
- Current version: **0.18.0** (`@ljwei-stak/dsh-model-router`). Its declared host compatibility is **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2**. The router and [GAL](https://github.com/Alice-Marx/deepseek-harness-galgame) have been independent plugins since 0.12.0.
21
+ Current version: **0.18.1** (`@ljwei-stak/dsh-model-router`). Its declared host compatibility is **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2**. The router and [GAL](https://github.com/Alice-Marx/deepseek-harness-galgame) have been independent plugins since 0.12.0.
20
22
 
21
23
  **0.15.0 changes:** a four-tab workbench with routing advice directly below task planning, protection against stale execution previews, preserved spending totals across history pruning and retries, and a standalone mock UI preview for source development.
22
24
 
@@ -48,16 +50,16 @@ Generating a route recommendation is local: it does not start a vendor CLI or ma
48
50
  Open **Plugins → Add plugin** and enter:
49
51
 
50
52
  ```text
51
- @ljwei-stak/dsh-model-router@0.18.0
53
+ @ljwei-stak/dsh-model-router@0.18.1
52
54
  ```
53
55
 
54
- Use the official HTTPS npm source `https://registry.npmjs.org/` when a mirror lacks that exact version. Enable the plugin, then fully quit Harness (including its tray process) and restart it. Plugin details should show **0.18.0** and the sidebar should contain **模型路由** (Model Router). A global `npm install -g` alone does not register a Desktop plugin.
56
+ Use the official HTTPS npm source `https://registry.npmjs.org/` when a mirror lacks that exact version. Enable the plugin, then fully quit Harness (including its tray process) and restart it. Plugin details should show **0.18.1** and the sidebar should contain **模型路由** (Model Router). A global `npm install -g` alone does not register a Desktop plugin.
55
57
 
56
58
  You can instead install a local `.tgz` archive or its extracted inner `package` directory through the plugin manager. Both must contain `package.json` and `.dsh-plugin`. See [releases](https://github.com/Alice-Marx/dsh-model-router/releases) for archived builds, or build your own checkout below.
57
59
 
58
60
  ### Upgrade from the old package name
59
61
 
60
- The old name `@ljwei-stak/model-router-galgame` ended at 0.13.0. Remove that plugin entry while retaining your profile/application data, then add `@ljwei-stak/dsh-model-router@0.18.0`. Do not keep both entries installed: they share the internal plugin id `model-router-galgame` and tool names. The retained id lets existing router settings and history carry over.
62
+ The old name `@ljwei-stak/model-router-galgame` ended at 0.13.0. Remove that plugin entry while retaining your profile/application data, then add `@ljwei-stak/dsh-model-router@0.18.1`. Do not keep both entries installed: they share the internal plugin id `model-router-galgame` and tool names. The retained id lets existing router settings and history carry over.
61
63
 
62
64
  For the combined 0.11.x package, back up the profile and export the GAL saves you want to preserve before upgrading. Story playback, artwork, music and saves now belong to the independent `@ljwei-stak/dsh-galgame` package. The detailed save limitations are in [MIGRATION.md](docs/archive/MIGRATION.md).
63
65
 
package/README.zh.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # Model Router · DeepSeek Harness 模型路由插件
2
2
 
3
- 版本:**0.18.0**。选模求解器概览见[路由研究说明](docs/ROUTING_RESEARCH.zh.md);公式、条件证明与实验见[数学推导与实验记录](docs/ROUTING_DERIVATION_EXPERIMENTS.zh.md)。[LiveBench 最新证据与验证协议](docs/ROUTING_LIVEBENCH_VALIDATION.zh.md)已核查 2026-06-25 聚合评分/费用;最新逐题质量—费用矩阵尚未取得,不声称真实路由收益。
3
+ 版本:**0.18.1**。选模求解器概览见[路由研究说明](docs/ROUTING_RESEARCH.zh.md);公式、条件证明与实验见[数学推导与实验记录](docs/ROUTING_DERIVATION_EXPERIMENTS.zh.md)。[LiveBench 最新证据与验证协议](docs/ROUTING_LIVEBENCH_VALIDATION.zh.md)已核查 2026-06-25 聚合评分/费用;最新逐题质量—费用矩阵尚未取得,不声称真实路由收益。
4
4
 
5
5
  为 **DeepSeek Harness Desktop** 提供任务规划、成本感知的模型分配和官方工具执行工作台。输入任务,比较已配置模型,审阅计划,再明确选择是否执行。
6
6
 
7
+ **0.18.1**:修复安装方式切换卡在“保存方式中…”的问题。已保存设置立即同步到宿主安装配置,命令预览单独刷新;工具体检延迟时仍能继续切换方式。
8
+
7
9
  **0.18.0**:补齐厂商提供的 curl/irm 安装方式,安装与更新复用已保存的目录、方式和下载源,并核验目标目录里的版本。修复 MiniMax 官方 PowerShell 脚本以 `<# ... #>` 帮助注释开头时被误判为 HTML 的问题。统一安装设置改为明确保存后生效。
8
10
 
9
11
  **0.16.4**:PATH 探测在没有安装日志回调时也会保留命令输出。0.16.3 把 stdout 写进了 `onOutput?.(...)`,`where` 和 `--version` 因此变成空字符串,PATH 上的 CLI 全被报成未安装。MiniMax 与 ZCode 不走这条探测,所以仍能显示。
@@ -16,7 +18,7 @@
16
18
 
17
19
  **0.16.0 新增**:[动态数据与持续反馈](docs/ROUTING_ADAPTIVE_LEARNING.zh.md):可选公开价格/LiveBench快照刷新;按任务类型、时间衰减和低样本收缩调整个人偏好,质量门槛独立保留。设置在“预算与安全”,公开源默认关闭;价格源需提供按官方文档核对的固定USD单价JSON,不是通用官网抓价器。本地DSH_HOME共享、最近200运行窗口,暂无真实收益证明。
18
20
 
19
- 当前版本为 **0.18.0**,包名为 `@ljwei-stak/dsh-model-router`;声明兼容的宿主为 **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2**。自 0.12.0 起,路由与 [GAL](https://github.com/Alice-Marx/deepseek-harness-galgame) 已拆成两个独立插件。
21
+ 当前版本为 **0.18.1**,包名为 `@ljwei-stak/dsh-model-router`;声明兼容的宿主为 **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2**。自 0.12.0 起,路由与 [GAL](https://github.com/Alice-Marx/deepseek-harness-galgame) 已拆成两个独立插件。
20
22
 
21
23
  **0.15.0 更新**:工作台分为四页,路由建议紧接任务规划展示;执行输入变化时丢弃旧预览;裁剪历史或重试后仍保留费用累计;源码开发新增独立模拟 UI 预览。
22
24
 
@@ -48,16 +50,16 @@
48
50
  打开 **插件 → 添加插件**,填写:
49
51
 
50
52
  ```text
51
- @ljwei-stak/dsh-model-router@0.18.0
53
+ @ljwei-stak/dsh-model-router@0.18.1
52
54
  ```
53
55
 
54
- 镜像缺少该精确版本时使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。安装并启用后,完全退出 Harness(包括托盘进程)再启动。插件详情应显示 **0.18.0**,侧栏出现**模型路由**。仅运行全局 `npm install -g` 不会把插件注册到桌面版 profile。
56
+ 镜像缺少该精确版本时使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。安装并启用后,完全退出 Harness(包括托盘进程)再启动。插件详情应显示 **0.18.1**,侧栏出现**模型路由**。仅运行全局 `npm install -g` 不会把插件注册到桌面版 profile。
55
57
 
56
58
  也可在插件管理器填写本地 `.tgz` 文件的绝对路径,或解压后的内层 `package` 目录;其中应包含 `package.json` 与 `.dsh-plugin`。[历史发布页](https://github.com/Alice-Marx/dsh-model-router/releases)提供归档;已克隆源码的开发方式见下文。
57
59
 
58
60
  ### 从旧包名升级
59
61
 
60
- `@ljwei-stak/model-router-galgame` 的最后版本为 0.13.0。保留 profile 与应用数据,移除旧插件条目,再添加 `@ljwei-stak/dsh-model-router@0.18.0`。不要同时安装两个包:它们共用内部插件 id `model-router-galgame` 与工具名;id 保留使已有路由设置和执行历史能够延续。
62
+ `@ljwei-stak/model-router-galgame` 的最后版本为 0.13.0。保留 profile 与应用数据,移除旧插件条目,再添加 `@ljwei-stak/dsh-model-router@0.18.1`。不要同时安装两个包:它们共用内部插件 id `model-router-galgame` 与工具名;id 保留使已有路由设置和执行历史能够延续。
61
63
 
62
64
  从 0.11.x 合并版迁移时,先备份 profile 并导出希望保留的 GAL 进度。剧情、立绘、音乐、自由模式和存档属于独立 `@ljwei-stak/dsh-galgame` 插件。导出范围及同 profile 迁移的限制见 [MIGRATION.md](docs/archive/MIGRATION.md)。
63
65
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  模型路由从 **0.12.0** 起独立发布;GAL 是新的 **0.1.0** 插件。两者均声明支持官方 **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2**,可分别安装和更新。
4
4
 
5
- 当前路由正式版为 **0.18.0**。本版补齐厂商官方安装方法,修复 MiniMax PowerShell 帮助注释被误拒绝、pnpm 自定义目录和更新探测旧 PATH 副本的问题。统一安装设置需点击保存,再选择工具的安装方式。公开数据默认关闭;安装时选择下方精确版本。
5
+ 当前路由正式版为 **0.18.1**。本版修复安装方式切换卡在“保存方式中…”及宿主继续使用旧安装设置的问题。方式保存和命令预览不再等待全量工具体检。保留 0.18.0 的官方安装方法、MiniMax 脚本校验和统一目录改进。统一安装设置需点击保存,再选择工具的安装方式。公开数据默认关闭;安装时选择下方精确版本。
6
6
 
7
7
  [路由 README](../README.zh.md) · [迁移说明](archive/MIGRATION.md) · [独立 GAL README](https://github.com/Alice-Marx/deepseek-harness-galgame#readme)
8
8
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  | 需求 | 添加插件输入框 | 安装后的入口 |
12
12
  | --- | --- | --- |
13
- | 分析复杂度、按质量和费用选模型、调用官方工具 | `@ljwei-stak/dsh-model-router@0.18.0` | 模型路由 |
13
+ | 分析复杂度、按质量和费用选模型、调用官方工具 | `@ljwei-stak/dsh-model-router@0.18.1` | 模型路由 |
14
14
  | 剧情、立绘、音乐、存档、自由对话 | `@ljwei-stak/dsh-galgame@0.1.0` | Gal 模块 |
15
15
  | 两者都要 | 分别安装上面两个包 | 模型路由 + Gal 模块 |
16
16
 
@@ -24,7 +24,7 @@
24
24
  2. 在“包名、GitHub 仓库地址或本地目录路径”输入框填一个完整包名,例如:
25
25
 
26
26
  ```text
27
- @ljwei-stak/dsh-model-router@0.18.0
27
+ @ljwei-stak/dsh-model-router@0.18.1
28
28
  ```
29
29
 
30
30
  只想安装 GAL 时填写:
@@ -40,7 +40,7 @@
40
40
  ```
41
41
 
42
42
  4. 核对预览的包名、版本和宿主兼容信息,安装并启用。完全退出 Harness(包括托盘进程)再启动,确保 Host 加载新代码。
43
- 5. 检查插件详情与侧边栏:路由应为 **0.18.0 / 模型路由**;GAL 应为 **0.1.0 / Gal 模块**。仅装路由时没有 GAL 入口是预期行为。
43
+ 5. 检查插件详情与侧边栏:路由应为 **0.18.1 / 模型路由**;GAL 应为 **0.1.0 / Gal 模块**。仅装路由时没有 GAL 入口是预期行为。
44
44
 
45
45
  普通用户无需 `npm install -g`,该命令不会把插件注册到当前 Harness profile。
46
46
 
@@ -48,13 +48,13 @@
48
48
 
49
49
  进入对应 Release 下载 `.tgz`:
50
50
 
51
- - [模型路由 Releases](https://github.com/Alice-Marx/dsh-model-router/releases)(也可运行 `npm pack @ljwei-stak/dsh-model-router@0.18.0` 获取同一安装包)
51
+ - [模型路由 Releases](https://github.com/Alice-Marx/dsh-model-router/releases)(也可运行 `npm pack @ljwei-stak/dsh-model-router@0.18.1` 获取同一安装包)
52
52
  - [独立 GAL v0.1.0](https://github.com/Alice-Marx/deepseek-harness-galgame/releases/tag/v0.1.0)
53
53
 
54
54
  在“添加插件”填写下载文件的**绝对路径**,例如:
55
55
 
56
56
  ```text
57
- D:\Plugins\ljwei-stak-dsh-model-router-0.18.0.tgz
57
+ D:\Plugins\ljwei-stak-dsh-model-router-0.18.1.tgz
58
58
  ```
59
59
 
60
60
  ```text
@@ -64,7 +64,7 @@ D:\Plugins\ljwei-stak-dsh-galgame-0.1.0.tgz
64
64
  如附有 `.sha256` 文件,用 PowerShell 计算并比较摘要:
65
65
 
66
66
  ```powershell
67
- Get-FileHash -Algorithm SHA256 -LiteralPath 'D:\Plugins\ljwei-stak-dsh-model-router-0.18.0.tgz'
67
+ Get-FileHash -Algorithm SHA256 -LiteralPath 'D:\Plugins\ljwei-stak-dsh-model-router-0.18.1.tgz'
68
68
  Get-FileHash -Algorithm SHA256 -LiteralPath 'D:\Plugins\ljwei-stak-dsh-galgame-0.1.0.tgz'
69
69
  ```
70
70
 
@@ -74,7 +74,7 @@ Get-FileHash -Algorithm SHA256 -LiteralPath 'D:\Plugins\ljwei-stak-dsh-galgame-0
74
74
 
75
75
  1. 在旧合并插件的 GAL 播放器导出需要保留的剧情进度,并备份整个 Harness profile。每份 JSON 仅保存**当前剧情状态**,不含所有手动槽、设置、已读记录或本地音乐;重要槽位可依次读档再导出。
76
76
  2. 在**同一个 profile**安装独立 `@ljwei-stak/dsh-galgame@0.1.0`。
77
- 3. 0.13.0 起包名改为 `@ljwei-stak/dsh-model-router`(旧包名最后版本为 0.13.0,内容相同,之后不再更新):在插件管理器卸载原 `@ljwei-stak/model-router-galgame`(保留 profile 与应用数据),再添加 `@ljwei-stak/dsh-model-router@0.18.0`。不要两个同时安装(条目 id 与工具名相同);条目 id 未变,路由设置与执行记录会保留。完成两项更新后再玩剧情,避免同时操作旧合并入口和新入口。
77
+ 3. 0.13.0 起包名改为 `@ljwei-stak/dsh-model-router`(旧包名最后版本为 0.13.0,内容相同,之后不再更新):在插件管理器卸载原 `@ljwei-stak/model-router-galgame`(保留 profile 与应用数据),再添加 `@ljwei-stak/dsh-model-router@0.18.1`。不要两个同时安装(条目 id 与工具名相同);条目 id 未变,路由设置与执行记录会保留。完成两项更新后再玩剧情,避免同时操作旧合并入口和新入口。
78
78
  4. 现在路由与 GAL 各有一个入口。独立 GAL 沿用旧存档 localStorage 键,核对当前进度和三槽;更换 profile 或进度未恢复时,先切换到匹配剧目,再导入 JSON 备份。
79
79
  5. 后续分别更新需要的插件;只用路由可以不启用或不安装 GAL。保留 profile 与应用数据,卸载插件时不要清理故事数据。
80
80
 
@@ -82,7 +82,7 @@ GAL 核心只包含**《回声之城·正篇》**与**《旧城迁移篇:未
82
82
 
83
83
  ## 5. 模型路由第一次使用
84
84
 
85
- 工作台自 0.15.0 起分成**任务与执行、模型配置、官方工具、预算与安全**四页;0.18.0 沿用该布局。切换页面保留安装任务、终端会话和未完成的输入。
85
+ 工作台自 0.15.0 起分成**任务与执行、模型配置、官方工具、预算与安全**四页;0.18.1 沿用该布局。切换页面保留安装任务、终端会话和未完成的输入。
86
86
 
87
87
  1. 在 Harness 官方**模型**页配置供应商、模型与凭据。插件读取准确的 `provider/model` 目录,模型档案不保存 API Key。
88
88
  2. 在**模型配置**刷新目录,逐条填写自报质量评分、核对过的输入/输出单价(USD / 百万 token)和擅长方向;缺价留空。可选 `cliModel` 必须是该厂商 CLI 已验证接受的名字。可选 LiveBench 名称须准确对应版本与推理档位;它是用户确认的映射,不自动证明实际执行档位。
@@ -114,7 +114,7 @@ GAL 核心只包含**《回声之城·正篇》**与**《旧城迁移篇:未
114
114
  | 检查 | 预期 |
115
115
  | --- | --- |
116
116
  | 宿主版本 | 0.2.0-rc.1 或 0.2.0-rc.2;包括 `rc` 后缀。 |
117
- | 路由独立安装 | 0.18.0 详情、模型路由入口、目录和模型档案可用;不出现旧合并 GAL 入口。 |
117
+ | 路由独立安装 | 0.18.1 详情、模型路由入口、目录和模型档案可用;不出现旧合并 GAL 入口。 |
118
118
  | GAL 独立安装 | 0.1.0 详情、Gal 模块入口、标题与两部核心剧情可用;不要求安装路由。 |
119
119
  | 共同安装 | 两个入口各一份,路由工具和 GAL 播放器均能使用。 |
120
120
  | 存档迁移 | 同 profile 可读旧键;跨 profile 用匹配剧目 JSON 导入;旧篇目的数据不覆盖。 |
@@ -125,11 +125,11 @@ GAL 核心只包含**《回声之城·正篇》**与**《旧城迁移篇:未
125
125
  | 动态公开数据 | 默认关闭;自行启用后核对快照版本、校验时间及失败/过期状态;人工单价仍优先。 |
126
126
  | 可编辑执行 | 在干净测试仓库核对审批、隔离工作区、补丁整合、厂商实际模型与计费。 |
127
127
 
128
- 历史 0.15.0 的自动测试验证为 220 项通过、3 项 Windows 平台跳过;组件 UI 预览检查使用模拟数据。**0.18.0 的发布验证见[更新记录](CHANGELOG.md)**,不把模拟公开源/模型流和组件预览当作真实付费调用或 Harness Desktop 端到端验收;上表中的实际安装、账号和工作区检查仍需在使用环境中完成。
128
+ 历史 0.15.0 的自动测试验证为 220 项通过、3 项 Windows 平台跳过;组件 UI 预览检查使用模拟数据。**0.18.1 的发布验证见[更新记录](CHANGELOG.md)**,不把模拟公开源/模型流和组件预览当作真实付费调用或 Harness Desktop 端到端验收;上表中的实际安装、账号和工作区检查仍需在使用环境中完成。
129
129
 
130
130
  ## 8. 开发者从完整源码构建
131
131
 
132
- 路由需要 Node.js **22.19+** 与 **pnpm 10.34.6**。从完整源码仓库根目录构建;复现正式版 0.18.0 时检出对应发布标签。旧版(如 0.15.0)标签保留其当时内容。运行:
132
+ 路由需要 Node.js **22.19+** 与 **pnpm 10.34.6**。从完整源码仓库根目录构建;复现正式版 0.18.1 时检出对应发布标签。旧版(如 0.15.0)标签保留其当时内容。运行:
133
133
 
134
134
  ```powershell
135
135
  pnpm install --frozen-lockfile --strict-peer-dependencies
@@ -1,6 +1,6 @@
1
1
  # 模型路由工作台:配置、规划与实际执行
2
2
 
3
- 适用版本:正式版 `@ljwei-stak/dsh-model-router@0.18.0`;声明兼容 DeepSeek Harness Desktop `0.2.0-rc.1 / 0.2.0-rc.2`。安装与开发见[中文 README](../README.zh.md),旧包迁移见[迁移说明](archive/MIGRATION.md)。
3
+ 适用版本:正式版 `@ljwei-stak/dsh-model-router@0.18.1`;声明兼容 DeepSeek Harness Desktop `0.2.0-rc.1 / 0.2.0-rc.2`。安装与开发见[中文 README](../README.zh.md),旧包迁移见[迁移说明](archive/MIGRATION.md)。
4
4
 
5
5
  0.15.0 新增四页布局、规划后直接展示路由建议、执行输入变化时丢弃旧预览,以及历史裁剪和重试后的费用累计保留。源码中的独立 UI 预览使用模拟数据,用于组件检查,不代表真实账号调用或桌面端到端验收。
6
6
 
@@ -109,7 +109,7 @@
109
109
 
110
110
  ### 统一安装设置
111
111
 
112
- 卡片顶部**统一安装设置**一次设定所有工具的安装地址,修改后点击**保存安装设置**;保存及命令刷新完成后,下一次安装、更新、修复、卸载使用已保存配置。未保存的输入可以**放弃更改**。插件设置页里有同样的字段。
112
+ 卡片顶部**统一安装设置**一次设定所有工具的安装地址,修改后点击**保存安装设置**;保存及命令刷新完成后,下一次安装、更新、修复、卸载使用已保存配置。未保存的输入可以**放弃更改**。插件设置页里有同样的字段。方式保存与命令预览独立于工具体检,体检较慢时也能继续选择方式;命令预览确认前暂不能执行安装操作。
113
113
 
114
114
  | 设置 | 作用 | 限制 |
115
115
  | --- | --- | --- |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ljwei-stak/dsh-model-router",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "type": "module",
5
5
  "description": "Model Router for official DeepSeek Harness Desktop: cost-aware task routing, dependent work packages and official vendor CLI discovery, installation and execution. GAL is available separately as @ljwei-stak/dsh-galgame.",
6
6
  "repository": {
@@ -103,7 +103,7 @@
103
103
  "experiment:routing-summary": "node scripts/summarize-routing-search.mjs",
104
104
  "experiment:routing-adaptive": "node scripts/adaptive-routing-study.mjs",
105
105
  "audit:livebench": "node scripts/livebench-aggregate-audit.mjs",
106
- "test": "node --test tests/adaptive-*.test.mjs tests/dynamic-data.test.mjs tests/router.test.mjs tests/routing-optimizer.test.mjs tests/routing-evaluation.test.mjs tests/routing-paired-statistics.test.mjs tests/routing-selftest-tools.test.mjs tests/livebench-aggregate-audit.test.mjs tests/livebench-prior-study.test.mjs tests/routing-search-experiment.test.mjs tests/routing-regression-study.test.mjs tests/routing-search-summary.test.mjs tests/harness-*.test.mjs tests/official-tools.test.mjs tests/official-team-runtime.test.mjs tests/model-profiles.test.mjs tests/model-profile-editor.test.mjs tests/tool-install-state.test.mjs tests/vendor-adapters.test.mjs tests/desktop-compat.test.mjs tests/plugin-split.test.mjs tests/task-executors.test.mjs tests/tool-install-preferences.test.mjs tests/roadmap.test.mjs tests/insights-state.test.mjs tests/billing-team-ledger.test.mjs tests/subscription-billing.test.mjs tests/subscription-host.test.mjs tests/test-pass-regressions.test.mjs tests/roadmap-gaps.test.mjs tests/cordis-inject.test.mjs tests/cli-terminal.test.mjs tests/cli-terminal-gateway.test.mjs tests/latest-versions.test.mjs tests/vendor-installer-validation.test.mjs tests/install-entry-selection.test.mjs",
106
+ "test": "node --test tests/adaptive-*.test.mjs tests/dynamic-data.test.mjs tests/router.test.mjs tests/routing-optimizer.test.mjs tests/routing-evaluation.test.mjs tests/routing-paired-statistics.test.mjs tests/routing-selftest-tools.test.mjs tests/livebench-aggregate-audit.test.mjs tests/livebench-prior-study.test.mjs tests/routing-search-experiment.test.mjs tests/routing-regression-study.test.mjs tests/routing-search-summary.test.mjs tests/harness-*.test.mjs tests/official-tools.test.mjs tests/official-team-runtime.test.mjs tests/model-profiles.test.mjs tests/model-profile-editor.test.mjs tests/tool-install-state.test.mjs tests/tool-install-sdk-ui.test.mjs tests/vendor-adapters.test.mjs tests/desktop-compat.test.mjs tests/plugin-split.test.mjs tests/install-settings-host.test.mjs tests/task-executors.test.mjs tests/tool-install-preferences.test.mjs tests/roadmap.test.mjs tests/insights-state.test.mjs tests/billing-team-ledger.test.mjs tests/subscription-billing.test.mjs tests/subscription-host.test.mjs tests/test-pass-regressions.test.mjs tests/roadmap-gaps.test.mjs tests/cordis-inject.test.mjs tests/cli-terminal.test.mjs tests/cli-terminal-gateway.test.mjs tests/latest-versions.test.mjs tests/vendor-installer-validation.test.mjs tests/install-entry-selection.test.mjs",
107
107
  "prepack": "node scripts/prepack.mjs",
108
108
  "pack:local": "npm pack --pack-destination dist"
109
109
  },