@ljwei-stak/dsh-model-router 0.16.1 → 0.16.3
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/.dsh-plugin/client.js +135 -34
- package/.dsh-plugin/official-tools-remote-service.mjs +8 -3
- package/.dsh-plugin/shared/latest-versions.mjs +3 -21
- package/.dsh-plugin/shared/official-tool-executor.mjs +43 -13
- package/.dsh-plugin/shared/official-tools-runtime.mjs +267 -62
- package/.dsh-plugin/shared/version-order.mjs +30 -0
- package/CHANGELOG.md +19 -0
- package/INSTALLATION_GUIDE.zh.md +12 -12
- package/README.md +9 -5
- package/README.zh.md +9 -5
- package/package.json +2 -1
package/.dsh-plugin/client.js
CHANGED
|
@@ -1876,33 +1876,72 @@ function ModelProfileEditor({ routes, settingsScope, onSaved }) {
|
|
|
1876
1876
|
} }, "\u5220\u9664\u6B64\u6A21\u578B\u914D\u7F6E")), notice && /* @__PURE__ */ import_react.default.createElement("p", { className: notice.tone === "error" ? "mr-error" : "mr-profile-success", role: notice.tone === "error" ? "alert" : "status" }, notice.text), /* @__PURE__ */ import_react.default.createElement("p", { className: "mr-caption" }, "\u8D28\u91CF\u8BC4\u5206\u548C\u4EF7\u683C\u90FD\u662F\u7528\u6237\u63D0\u4F9B\u7684\u4F30\u503C\u3002\u672A\u586B\u5199\u5355\u4EF7\u65F6\u663E\u793A\u201C\u4EF7\u683C\u5F85\u914D\u7F6E\u201D\uFF1B\u9884\u7B97\u53EA\u5F71\u54CD\u672C\u5730\u89C4\u5212\uFF0C\u4E0D\u9650\u5236\u5B9E\u9645\u8D26\u5355\u3002CLI \u6A21\u578B\u540D\u987B\u4E0E\u5382\u5546\u5DE5\u5177\u6838\u5BF9\u3002\u6267\u884C\u65B9\u5F0F\u51B3\u5B9A\u8BE5\u6A21\u578B\u6536\u5230\u4EFB\u52A1\u65F6\u8D70\u5B98\u65B9\u65E0\u754C\u9762\u5DE5\u5177\u8FD8\u662F\u6A21\u578B\u76EE\u5F55 API\uFF1B\u5B98\u65B9\u5DE5\u5177\u5931\u8D25\u65F6\u4ECD\u4F1A\u56DE\u9000 API\u3002\u8BA1\u8D39\u65B9\u5F0F\u9ED8\u8BA4\u8BA2\u9605\u4F18\u5148\uFF1A\u5148\u7528 CLI \u8D26\u53F7\u767B\u5F55\u6216\u7F16\u7A0B\u5957\u9910 Key \u8DEF\u7EBF\uFF0C\u8BA2\u9605\u989D\u5EA6\u7528\u5C3D\u6216\u9650\u6D41\u65F6\u540C\u4E00\u6B65\u9AA4\u81EA\u52A8\u6539\u7528 API Key\uFF0C\u5E76\u8BB0\u5165\u8FD0\u884C\u5386\u53F2\u3002"))));
|
|
1877
1877
|
}
|
|
1878
1878
|
|
|
1879
|
-
// .dsh-plugin/
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
const
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1879
|
+
// .dsh-plugin/shared/version-order.mjs
|
|
1880
|
+
var VERSION = /^(\d{1,6})\.(\d{1,6})\.(\d{1,6})(?:-[0-9A-Za-z.-]{1,64})?$/;
|
|
1881
|
+
function compareReleaseVersions(left, right) {
|
|
1882
|
+
const parse = (value) => {
|
|
1883
|
+
const match = VERSION.exec(String(value ?? ""));
|
|
1884
|
+
return match ? { parts: match.slice(1, 4).map(Number), pre: String(value).includes("-") } : null;
|
|
1885
|
+
};
|
|
1886
|
+
const a = parse(left);
|
|
1887
|
+
const b2 = parse(right);
|
|
1888
|
+
if (!a || !b2) return null;
|
|
1889
|
+
for (let index = 0; index < 3; index += 1) {
|
|
1890
|
+
if (a.parts[index] !== b2.parts[index]) return a.parts[index] > b2.parts[index] ? 1 : -1;
|
|
1888
1891
|
}
|
|
1892
|
+
if (a.pre !== b2.pre) return a.pre ? -1 : 1;
|
|
1889
1893
|
return 0;
|
|
1890
1894
|
}
|
|
1895
|
+
|
|
1896
|
+
// .dsh-plugin/client/tool-install-state.mjs
|
|
1891
1897
|
function toolInstallAction({ tool, probe, readiness, job, probeStatus, latestVersion = null }) {
|
|
1892
1898
|
const running = job?.status === "running";
|
|
1893
1899
|
const installed = probe?.installed === true;
|
|
1894
|
-
const order = installed && latestVersion ?
|
|
1895
|
-
const ready = readiness?.ready === true;
|
|
1900
|
+
const order = installed && latestVersion ? compareReleaseVersions(probe.version, latestVersion) : null;
|
|
1896
1901
|
const desktop = tool.manager === "signed-windows-installer";
|
|
1897
1902
|
const noRunner = desktop || tool.headlessAdapter === true;
|
|
1898
|
-
const repair = installed && !noRunner && readiness?.ready === false;
|
|
1903
|
+
const repair = installed && !noRunner && readiness?.ready === false && order !== 0;
|
|
1899
1904
|
const label = running ? "\u5B89\u88C5\u4E2D\u2026" : !installed ? job?.status === "failed" ? "\u91CD\u8BD5\u5B89\u88C5" : desktop ? "\u4E0B\u8F7D\u6700\u65B0\u5B89\u88C5\u5668" : "\u4E00\u952E\u5B89\u88C5\u6700\u65B0\u7248" : order === -1 ? `\u66F4\u65B0\u5230\u6700\u65B0\u7248 ${latestVersion}` : order === 1 ? "\u5DF2\u9AD8\u4E8E\u6700\u65B0\u6B63\u5F0F\u7248" : repair ? "\u4FEE\u590D\u5B98\u65B9\u6267\u884C\u5165\u53E3" : order === 0 ? "\u5DF2\u662F\u6700\u65B0\u7248\u672C" : "\u5B89\u88C5\u6700\u65B0\u7248";
|
|
1900
|
-
const current = order === 0
|
|
1905
|
+
const current = order === 0;
|
|
1906
|
+
const interactive = probeStatus === "ready" || probeStatus === "refreshing";
|
|
1901
1907
|
return {
|
|
1902
1908
|
label,
|
|
1903
|
-
disabled:
|
|
1909
|
+
disabled: !interactive || running || installed && (order === 1 || current)
|
|
1904
1910
|
};
|
|
1905
1911
|
}
|
|
1912
|
+
function installClickRefusal({ tool, submitting = false, running = false } = {}) {
|
|
1913
|
+
if (!tool) return "\u672A\u77E5\u5B98\u65B9\u5DE5\u5177\uFF0C\u65E0\u6CD5\u5F00\u59CB\u5B89\u88C5\u3002";
|
|
1914
|
+
if (tool.unsupported) return String(tool.unsupportedReason ?? "").trim() || "\u6B64\u5DE5\u5177\u6682\u4E0D\u652F\u6301\u4E00\u952E\u5B89\u88C5\u3002";
|
|
1915
|
+
if (submitting || running) return "\u8BE5\u5DE5\u5177\u6B63\u5728\u5B89\u88C5\uFF0C\u8BF7\u7B49\u5F85\u5F53\u524D\u4EFB\u52A1\u7ED3\u675F\u3002";
|
|
1916
|
+
return "";
|
|
1917
|
+
}
|
|
1918
|
+
function payloadError(payload, fallback) {
|
|
1919
|
+
if (!payload || typeof payload !== "object") return fallback;
|
|
1920
|
+
if (typeof payload.error === "string" && payload.error.trim()) return payload.error.trim();
|
|
1921
|
+
const message = payload.error?.message;
|
|
1922
|
+
if (typeof message === "string" && message.trim()) return message.trim();
|
|
1923
|
+
return fallback;
|
|
1924
|
+
}
|
|
1925
|
+
function acceptedInstallJob(response) {
|
|
1926
|
+
const fallback = "\u5B89\u88C5\u4EFB\u52A1\u672A\u88AB\u63A5\u53D7\u3002";
|
|
1927
|
+
if (!response || typeof response !== "object") return { job: null, error: fallback };
|
|
1928
|
+
let payload = response;
|
|
1929
|
+
if (payload.ok === false) return { job: null, error: payloadError(payload, fallback) };
|
|
1930
|
+
if (payload.ok === true && payload.value && typeof payload.value === "object") {
|
|
1931
|
+
payload = payload.value;
|
|
1932
|
+
if (payload.ok === false) return { job: null, error: payloadError(payload, fallback) };
|
|
1933
|
+
if (payload.ok === true && payload.value && typeof payload.value === "object") payload = payload.value;
|
|
1934
|
+
}
|
|
1935
|
+
if (payload.accepted === true && payload.job && typeof payload.job === "object") return { job: payload.job, error: "" };
|
|
1936
|
+
return { job: null, error: payloadError(payload, fallback) };
|
|
1937
|
+
}
|
|
1938
|
+
function shouldApplyInstallStatus(localJob, remoteJob) {
|
|
1939
|
+
if (!remoteJob || typeof remoteJob !== "object") return false;
|
|
1940
|
+
const localStarted = typeof localJob?.startedAt === "string" ? localJob.startedAt : "";
|
|
1941
|
+
const remoteStarted = typeof remoteJob.startedAt === "string" ? remoteJob.startedAt : "";
|
|
1942
|
+
if (!localStarted || !remoteStarted) return true;
|
|
1943
|
+
return remoteStarted >= localStarted;
|
|
1944
|
+
}
|
|
1906
1945
|
|
|
1907
1946
|
// .dsh-plugin/client/router-insights.jsx
|
|
1908
1947
|
var import_react2 = __toESM(require("react"), 1);
|
|
@@ -12115,7 +12154,7 @@ var xterm_default = `/**
|
|
|
12115
12154
|
`;
|
|
12116
12155
|
|
|
12117
12156
|
// .dsh-plugin/client/host-version.mjs
|
|
12118
|
-
var ROUTER_CLIENT_VERSION = true ? "0.16.
|
|
12157
|
+
var ROUTER_CLIENT_VERSION = true ? "0.16.3" : "";
|
|
12119
12158
|
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";
|
|
12120
12159
|
function isMissingRemoteMethod(message) {
|
|
12121
12160
|
const value = String(message ?? "");
|
|
@@ -12921,7 +12960,10 @@ function PlanResults({ plan, ledger, headingRef }) {
|
|
|
12921
12960
|
const purposeLabel = { analysis: "\u4EFB\u52A1\u5206\u6790", execution: "\u4EFB\u52A1\u5B9E\u65BD", verification: "\u72EC\u7ACB\u9A8C\u8BC1", synthesis: "\u7ED3\u679C\u6574\u5408" };
|
|
12922
12961
|
return /* @__PURE__ */ import_react5.default.createElement("section", { className: "mr-card mr-results", "aria-label": "\u8DEF\u7531\u5EFA\u8BAE" }, /* @__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", ref: headingRef, tabIndex: -1 }, "\u8DEF\u7531\u5EFA\u8BAE"), /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-card-copy" }, "\u672C\u5730\u8BA1\u7B97\u5B8C\u6210\uFF0C\u672A\u5411\u6A21\u578B\u53D1\u9001\u4EFB\u52A1\u5185\u5BB9\u3002"))), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-card-body" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-result-grid" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-metric" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-metric-label" }, "\u63A8\u8350\u8DEF\u7EBF"), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-metric-value" }, selected ? `${selected.provider}/${selected.model}` : "\u6682\u65E0\u8DEF\u7EBF")), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-metric" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-metric-label" }, "\u4EFB\u52A1\u590D\u6742\u5EA6 \xB7 \u5206\u503C\uFF080\u20131\uFF09"), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-metric-value" }, { simple: "\u7B80\u5355", balanced: "\u4E2D\u7B49", complex: "\u590D\u6742" }[plan.complexity.band] || plan.complexity.band, " \xB7 ", plan.complexity.value)), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-metric" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-metric-label" }, "\u4F30\u7B97\u603B\u6210\u672C"), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-metric-value" }, money2(plan.estimatedCost))), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-metric" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-metric-label" }, "\u8DEF\u7531\u65B9\u6848"), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-metric-value" }, ROUTING_PRESETS[plan.preset]?.label ?? "\u5747\u8861"))), budget?.exceeded && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-error", role: "alert" }, "\u6267\u884C\u524D\u9884\u7B97\u68C0\u67E5\uFF1A", budget.message, " \u5B9E\u9645\u6267\u884C\u65F6\u5C06\u6309\u8BBE\u7F6E\u81EA\u52A8\u964D\u7EA7\u6216\u6682\u505C\u8BE2\u95EE\u3002"), budget?.limited && !budget.exceeded && budget.estimateKnown && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-caption" }, "\u6267\u884C\u524D\u9884\u7B97\u68C0\u67E5\uFF1A\u672C\u6B21\u9884\u4F30 ", money2(plan.estimatedCost), "\uFF0C\u5269\u4F59\u989D\u5EA6 ", money2(budget.remainingUsd), "\u3002"), plan.loginRequired && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-caption" }, "\u63A8\u8350\u6A21\u578B\u7684\u5B98\u65B9 CLI \u672A\u767B\u5F55\uFF0C\u6267\u884C\u65F6\u76F4\u63A5\u8D70\u6A21\u578B\u76EE\u5F55 API\uFF1B\u53EF\u5728\u201C\u5B98\u65B9\u5DE5\u5177\u201D\u5361\u7247\u70B9\u201C\u53BB\u767B\u5F55\u201D\u3002"), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-channel-line" }, /* @__PURE__ */ import_react5.default.createElement("span", { className: "mr-control-label" }, "\u6267\u884C\u6E20\u9053"), /* @__PURE__ */ import_react5.default.createElement(ChannelBadge, { item: plan })), /* @__PURE__ */ import_react5.default.createElement("details", { className: "mr-plan-explanation" }, /* @__PURE__ */ import_react5.default.createElement("summary", null, "\u67E5\u770B\u89C4\u5212\u4F9D\u636E\u4E0E\u4F30\u7B97\u8BF4\u660E"), /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-caption" }, plan.reason), /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-caption" }, plan.pricingNotice, " ", plan.qualityNotice, " ", plan.availabilityNotice, " ", plan.modalityNotice || "")), plan.optimization.budgetExceeded && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-error" }, "\u6309\u5DF2\u63D0\u4F9B\u5355\u4EF7\u4F30\u7B97\uFF0C\u4EFB\u52A1\u53EF\u80FD\u8D85\u8FC7\u672C\u6B21\u9884\u7B97\u3002\u9884\u7B97\u53EA\u5F71\u54CD\u5EFA\u8BAE\uFF0C\u4E0D\u4F1A\u963B\u6B62\u5B9E\u9645\u6263\u8D39\u3002"), plan.mode === "team" && /* @__PURE__ */ import_react5.default.createElement(import_react5.default.Fragment, null, /* @__PURE__ */ import_react5.default.createElement("h3", { className: "mr-section-title" }, "\u56E2\u961F\u5DE5\u4F5C\u5305"), /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-caption" }, "\u4E0B\u65B9\u6A21\u578B\u662F\u89C4\u5212\u5EFA\u8BAE\uFF1B\u6258\u7BA1\u6267\u884C\u4F1A\u6309\u5382\u5546 CLI \u7684\u6A21\u578B\u540D\u89C4\u5219\u9009\u7528\uFF0C\u672A\u6838\u9A8C\u6620\u5C04\u65F6\u4F7F\u7528\u8BE5 CLI \u7684\u9ED8\u8BA4\u6A21\u578B\u3002"), plan.team.workPackages.length > 1 && /* @__PURE__ */ import_react5.default.createElement(DagView, { packages: plan.team.workPackages, label: "\u5DE5\u4F5C\u5305\u4F9D\u8D56\u56FE", renderNode: (item) => /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-package-route" }, item.recommendedProvider, "/", item.recommendedModel) }), plan.team.workPackages.length === 0 ? /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-empty" }, "\u5F53\u524D\u76EE\u5F55\u6CA1\u6709\u53EF\u5206\u914D\u7684\u6A21\u578B\u8DEF\u7EBF\u3002") : plan.team.workPackages.map((item, index) => /* @__PURE__ */ import_react5.default.createElement("details", { className: "mr-package mr-package-details", key: item.id }, /* @__PURE__ */ import_react5.default.createElement("summary", null, /* @__PURE__ */ import_react5.default.createElement("span", { className: "mr-package-name" }, index + 1, ". ", item.name), /* @__PURE__ */ import_react5.default.createElement("span", { className: "mr-package-route" }, item.recommendedProvider, "/", item.recommendedModel)), item.objective && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-package-copy" }, "\u5177\u4F53\u76EE\u6807\uFF1A", item.objective), /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-package-copy" }, purposeLabel[item.purpose] ?? item.purpose, item.dependsOn.length > 0 ? ` \xB7 \u4F9D\u8D56\uFF1A${item.dependsOn.map((id2) => packageNames.get(id2) ?? id2).join("\u3001")}` : ""), /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-package-copy" }, "\u96BE\u5EA6\uFF1A", { simple: "\u7B80\u5355", balanced: "\u4E2D\u7B49", complex: "\u56F0\u96BE" }[item.difficulty] || item.difficulty || "\u5F85\u8BC4\u4F30", " \xB7 \u8D39\u7528\uFF1A", money2(item.estimatedCost)), /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-package-copy" }, "\u9A8C\u6536\uFF1A", item.verificationChecklist.join("\uFF1B")), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-channel-line" }, /* @__PURE__ */ import_react5.default.createElement(ChannelBadge, { item }), item.loginRequired && /* @__PURE__ */ import_react5.default.createElement("span", { className: "mr-pill" }, "CLI \u672A\u767B\u5F55")), item.channelDetail && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-package-copy" }, "\u6E20\u9053\u8BF4\u660E\uFF1A", item.channelDetail)))), plan.routingBypassed && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-caption" }, "\u5DF2\u6307\u5B9A\u5355\u4E00\u6A21\u578B\uFF0C\u672A\u4E0E\u5176\u4ED6\u8DEF\u7EBF\u6BD4\u8F83\u3002\u5728\u5B98\u65B9\u4F1A\u8BDD\u4E2D\u8C03\u7528 ", /* @__PURE__ */ import_react5.default.createElement("code", null, "model_router_execute"), " \u5E76\u4F20\u5165\u8BE5 provider \u4E0E model \u5373\u53EF\u76F4\u63A5\u6267\u884C\uFF1B\u82E5\u8BE5\u6A21\u578B\u5141\u8BB8\u5B98\u65B9\u5DE5\u5177\uFF0C\u4F1A\u4F18\u5148\u4F7F\u7528\u5BF9\u5E94 CLI\u3002"), plan.mode === "team" && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mr-caption" }, plan.team.handoff)));
|
|
12923
12962
|
}
|
|
12924
|
-
var remoteError = (response, fallback) =>
|
|
12963
|
+
var remoteError = (response, fallback) => remoteErrorText(
|
|
12964
|
+
text5(response?.error?.message) || text5(response?.value?.error) || (typeof response?.error === "string" ? response.error : ""),
|
|
12965
|
+
fallback
|
|
12966
|
+
);
|
|
12925
12967
|
function probeLabel(probe) {
|
|
12926
12968
|
if (!probe) return "\u5C1A\u672A\u68C0\u6D4B";
|
|
12927
12969
|
if (probe.installed) return `\u5DF2\u5B89\u88C5${probe.version ? ` \xB7 ${probe.version}` : ""}`;
|
|
@@ -12939,10 +12981,22 @@ function OfficialToolsCard({ listOfficialTools, installOfficialTool, cancelOffic
|
|
|
12939
12981
|
const request = import_react5.default.useRef(0);
|
|
12940
12982
|
const submitting = import_react5.default.useRef(/* @__PURE__ */ new Set());
|
|
12941
12983
|
const polling = import_react5.default.useRef(/* @__PURE__ */ new Set());
|
|
12984
|
+
const jobsRef = import_react5.default.useRef({});
|
|
12985
|
+
const rememberJobs = (updater) => {
|
|
12986
|
+
setJobs((previous) => {
|
|
12987
|
+
const next = typeof updater === "function" ? updater(previous) : updater;
|
|
12988
|
+
jobsRef.current = next;
|
|
12989
|
+
return next;
|
|
12990
|
+
});
|
|
12991
|
+
};
|
|
12942
12992
|
const refresh = async () => {
|
|
12943
12993
|
onRefreshHealth?.();
|
|
12944
12994
|
const current = ++request.current;
|
|
12945
|
-
setProbeState((previous) => ({
|
|
12995
|
+
setProbeState((previous) => ({
|
|
12996
|
+
...previous,
|
|
12997
|
+
status: previous.probes.length > 0 ? "refreshing" : "loading",
|
|
12998
|
+
error: ""
|
|
12999
|
+
}));
|
|
12946
13000
|
try {
|
|
12947
13001
|
if (typeof listOfficialTools !== "function") throw new Error("\u5B98\u65B9\u5DE5\u5177\u5B89\u88C5\u6865\u5C1A\u672A\u52A0\u8F7D\u3002");
|
|
12948
13002
|
const response = await listOfficialTools();
|
|
@@ -12955,8 +13009,16 @@ function OfficialToolsCard({ listOfficialTools, installOfficialTool, cancelOffic
|
|
|
12955
13009
|
onProbes({ probes, capabilities, readiness, hostVersion: typeof response.value?.hostVersion === "string" ? response.value.hostVersion : null });
|
|
12956
13010
|
} catch (error) {
|
|
12957
13011
|
if (!mounted.current || current !== request.current) return;
|
|
12958
|
-
|
|
12959
|
-
|
|
13012
|
+
const message = remoteErrorText(text5(error?.message), "\u65E0\u6CD5\u68C0\u6D4B\u5B98\u65B9\u5DE5\u5177\u3002");
|
|
13013
|
+
let kept = false;
|
|
13014
|
+
setProbeState((previous) => {
|
|
13015
|
+
if (previous.probes.length > 0) {
|
|
13016
|
+
kept = true;
|
|
13017
|
+
return { ...previous, status: "ready", error: message };
|
|
13018
|
+
}
|
|
13019
|
+
return { status: "error", probes: [], capabilities: [], readiness: [], error: message };
|
|
13020
|
+
});
|
|
13021
|
+
if (!kept) onProbes({ probes: [], capabilities: [], readiness: [] });
|
|
12960
13022
|
}
|
|
12961
13023
|
};
|
|
12962
13024
|
import_react5.default.useEffect(() => {
|
|
@@ -12971,7 +13033,16 @@ function OfficialToolsCard({ listOfficialTools, installOfficialTool, cancelOffic
|
|
|
12971
13033
|
return null;
|
|
12972
13034
|
}
|
|
12973
13035
|
})).then((entries) => {
|
|
12974
|
-
if (mounted.current)
|
|
13036
|
+
if (!mounted.current) return;
|
|
13037
|
+
rememberJobs((previous) => {
|
|
13038
|
+
const next = { ...previous };
|
|
13039
|
+
for (const entry of entries) {
|
|
13040
|
+
if (!entry) continue;
|
|
13041
|
+
const [id2, job] = entry;
|
|
13042
|
+
if (shouldApplyInstallStatus(previous[id2], job)) next[id2] = job;
|
|
13043
|
+
}
|
|
13044
|
+
return next;
|
|
13045
|
+
});
|
|
12975
13046
|
});
|
|
12976
13047
|
}
|
|
12977
13048
|
return () => {
|
|
@@ -12991,12 +13062,20 @@ function OfficialToolsCard({ listOfficialTools, installOfficialTool, cancelOffic
|
|
|
12991
13062
|
if (!listening || !mounted.current) return;
|
|
12992
13063
|
if (!response?.ok) throw new Error(remoteError(response, "\u65E0\u6CD5\u83B7\u53D6\u5B89\u88C5\u8FDB\u5EA6\u3002"));
|
|
12993
13064
|
const job = response.value?.job;
|
|
12994
|
-
|
|
12995
|
-
|
|
12996
|
-
|
|
13065
|
+
const local = jobsRef.current[id2];
|
|
13066
|
+
if (!job) {
|
|
13067
|
+
if (local?.status === "running") return;
|
|
13068
|
+
throw new Error("\u5B89\u88C5\u4EFB\u52A1\u72B6\u6001\u6682\u4E0D\u53EF\u7528\u3002");
|
|
13069
|
+
}
|
|
13070
|
+
if (!shouldApplyInstallStatus(local, job)) return;
|
|
13071
|
+
rememberJobs((previous) => shouldApplyInstallStatus(previous[id2], job) ? { ...previous, [id2]: job } : previous);
|
|
13072
|
+
setRowErrors((previous) => ({
|
|
13073
|
+
...previous,
|
|
13074
|
+
[id2]: job.status === "failed" ? text5(job.error) || "\u5B89\u88C5\u5931\u8D25\u3002" : ""
|
|
13075
|
+
}));
|
|
12997
13076
|
if (job.status !== "running") void refresh();
|
|
12998
13077
|
} catch (error) {
|
|
12999
|
-
if (listening && mounted.current) setRowErrors((previous) => ({ ...previous, [id2]: text5(error?.message)
|
|
13078
|
+
if (listening && mounted.current) setRowErrors((previous) => ({ ...previous, [id2]: remoteErrorText(text5(error?.message), "\u5B89\u88C5\u72B6\u6001\u8BFB\u53D6\u5931\u8D25\uFF0C\u5C06\u7EE7\u7EED\u91CD\u8BD5\u3002") }));
|
|
13000
13079
|
} finally {
|
|
13001
13080
|
polling.current.delete(id2);
|
|
13002
13081
|
}
|
|
@@ -13011,21 +13090,43 @@ function OfficialToolsCard({ listOfficialTools, installOfficialTool, cancelOffic
|
|
|
13011
13090
|
}, [jobs, officialToolInstallStatus]);
|
|
13012
13091
|
const install = async (id2) => {
|
|
13013
13092
|
const tool = OFFICIAL_TOOLS.find((item) => item.id === id2);
|
|
13014
|
-
|
|
13093
|
+
const refusal = installClickRefusal({
|
|
13094
|
+
tool,
|
|
13095
|
+
submitting: submitting.current.has(id2),
|
|
13096
|
+
running: jobsRef.current[id2]?.status === "running"
|
|
13097
|
+
});
|
|
13098
|
+
if (refusal) {
|
|
13099
|
+
setRowErrors((previous) => ({ ...previous, [id2]: refusal }));
|
|
13100
|
+
return;
|
|
13101
|
+
}
|
|
13015
13102
|
submitting.current.add(id2);
|
|
13103
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13016
13104
|
setRowErrors((previous) => ({ ...previous, [id2]: "" }));
|
|
13017
|
-
|
|
13105
|
+
rememberJobs((previous) => ({
|
|
13106
|
+
...previous,
|
|
13107
|
+
[id2]: { tool: id2, status: "running", outputTail: [], startedAt, error: null }
|
|
13108
|
+
}));
|
|
13018
13109
|
try {
|
|
13019
13110
|
if (typeof installOfficialTool !== "function") throw new Error("\u5B98\u65B9\u5DE5\u5177\u5B89\u88C5\u6865\u5C1A\u672A\u52A0\u8F7D\u3002");
|
|
13020
13111
|
const response = await installOfficialTool(id2);
|
|
13021
13112
|
if (!mounted.current) return;
|
|
13022
|
-
|
|
13023
|
-
|
|
13113
|
+
const accepted = acceptedInstallJob(response);
|
|
13114
|
+
if (!accepted.job) throw new Error(remoteErrorText(accepted.error, "\u5B89\u88C5\u4EFB\u52A1\u672A\u88AB\u63A5\u53D7\u3002"));
|
|
13115
|
+
rememberJobs((previous) => shouldApplyInstallStatus(previous[id2], accepted.job) ? { ...previous, [id2]: accepted.job } : previous);
|
|
13024
13116
|
} catch (error) {
|
|
13025
|
-
if (mounted.current)
|
|
13026
|
-
|
|
13027
|
-
|
|
13028
|
-
|
|
13117
|
+
if (!mounted.current) return;
|
|
13118
|
+
const message = remoteErrorText(text5(error?.message), "\u5B89\u88C5\u542F\u52A8\u5931\u8D25\u3002");
|
|
13119
|
+
rememberJobs((previous) => ({
|
|
13120
|
+
...previous,
|
|
13121
|
+
[id2]: {
|
|
13122
|
+
...previous[id2] ?? {},
|
|
13123
|
+
tool: id2,
|
|
13124
|
+
status: "failed",
|
|
13125
|
+
error: message,
|
|
13126
|
+
startedAt: previous[id2]?.startedAt || startedAt
|
|
13127
|
+
}
|
|
13128
|
+
}));
|
|
13129
|
+
setRowErrors((previous) => ({ ...previous, [id2]: message }));
|
|
13029
13130
|
} finally {
|
|
13030
13131
|
submitting.current.delete(id2);
|
|
13031
13132
|
}
|
|
@@ -13037,7 +13138,7 @@ function OfficialToolsCard({ listOfficialTools, installOfficialTool, cancelOffic
|
|
|
13037
13138
|
const response = await cancelOfficialToolInstall(id2);
|
|
13038
13139
|
if (!mounted.current) return;
|
|
13039
13140
|
if (!response?.ok || !response.value?.accepted || !response.value?.job) throw new Error(remoteError(response, "\u53D6\u6D88\u8BF7\u6C42\u672A\u88AB\u63A5\u53D7\u3002"));
|
|
13040
|
-
|
|
13141
|
+
rememberJobs((previous) => ({ ...previous, [id2]: response.value.job }));
|
|
13041
13142
|
if (response.value.job.status !== "running") void refresh();
|
|
13042
13143
|
} catch (error) {
|
|
13043
13144
|
if (mounted.current) setRowErrors((previous) => ({ ...previous, [id2]: text5(error?.message) || "\u65E0\u6CD5\u53D6\u6D88\u5B89\u88C5\u3002" }));
|
|
@@ -13048,7 +13149,7 @@ function OfficialToolsCard({ listOfficialTools, installOfficialTool, cancelOffic
|
|
|
13048
13149
|
const readinessById = Object.fromEntries(probeState.readiness.map((item) => [item.id, item]));
|
|
13049
13150
|
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\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: () => {
|
|
13050
13151
|
void refresh();
|
|
13051
|
-
} }, "\u91CD\u65B0\u4F53\u68C0")), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-card-body" }, 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.
|
|
13152
|
+
} }, "\u91CD\u65B0\u4F53\u68C0")), /* @__PURE__ */ import_react5.default.createElement("div", { className: "mr-card-body" }, 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) => {
|
|
13052
13153
|
const command = installCommandLine(tool);
|
|
13053
13154
|
const probe = byId[tool.id];
|
|
13054
13155
|
const capability = capabilitiesById[tool.id];
|
|
@@ -81,9 +81,14 @@ export class OfficialToolsRemoteService extends TypertRemoteService {
|
|
|
81
81
|
/** Re-probe the local fixed registry; the caller cannot supply a command. */
|
|
82
82
|
async list() {
|
|
83
83
|
const tools = await probeAllTools({ fresh: true })
|
|
84
|
-
const executionReadiness = await Promise.all(tools.map(tool =>
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
const executionReadiness = await Promise.all(tools.map(async tool => {
|
|
85
|
+
if (!tool.installed) return { id: tool.id, ready: false, reason: 'CLI 尚未安装或版本检测失败。' }
|
|
86
|
+
try {
|
|
87
|
+
return await officialToolReadiness(tool.id)
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return { id: tool.id, ready: false, reason: `执行入口检测失败:${String(error?.message ?? error).slice(0, 200)}` }
|
|
90
|
+
}
|
|
91
|
+
}))
|
|
87
92
|
return { tools, executionCapabilities: officialToolExecutionCapabilities(), executionReadiness, hostVersion: HOST_PLUGIN_VERSION }
|
|
88
93
|
}
|
|
89
94
|
|
|
@@ -12,6 +12,9 @@ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
|
12
12
|
import { dirname, join } from 'node:path'
|
|
13
13
|
import { resolveStateHome } from './router-state.mjs'
|
|
14
14
|
import { OFFICIAL_TOOLS } from './official-tool-registry.mjs'
|
|
15
|
+
import { compareReleaseVersions, isReleaseVersion } from './version-order.mjs'
|
|
16
|
+
|
|
17
|
+
export { compareReleaseVersions, isReleaseVersion } from './version-order.mjs'
|
|
15
18
|
|
|
16
19
|
export const LATEST_CACHE_MS = 12 * 60 * 60_000
|
|
17
20
|
export const LATEST_FAILURE_RETRY_MS = 30 * 60_000
|
|
@@ -19,27 +22,6 @@ export const LATEST_TIMEOUT_MS = 5_000
|
|
|
19
22
|
export const NPM_REGISTRY = 'https://registry.npmjs.org'
|
|
20
23
|
export const ZCODE_DOWNLOAD_PAGE = 'https://zcode.z.ai/en/docs/install'
|
|
21
24
|
const MAX_DOCUMENT_BYTES = 2_000_000
|
|
22
|
-
const VERSION = /^(\d{1,6})\.(\d{1,6})\.(\d{1,6})(?:-[0-9A-Za-z.-]{1,64})?$/
|
|
23
|
-
|
|
24
|
-
export function isReleaseVersion(value) {
|
|
25
|
-
return typeof value === 'string' && VERSION.test(value)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Numeric comparison of x.y.z[-pre]; a prerelease sorts below its release. */
|
|
29
|
-
export function compareReleaseVersions(left, right) {
|
|
30
|
-
const parse = value => {
|
|
31
|
-
const match = VERSION.exec(String(value ?? ''))
|
|
32
|
-
return match ? { parts: match.slice(1, 4).map(Number), pre: String(value).includes('-') } : null
|
|
33
|
-
}
|
|
34
|
-
const a = parse(left)
|
|
35
|
-
const b = parse(right)
|
|
36
|
-
if (!a || !b) return null
|
|
37
|
-
for (let index = 0; index < 3; index += 1) {
|
|
38
|
-
if (a.parts[index] !== b.parts[index]) return a.parts[index] > b.parts[index] ? 1 : -1
|
|
39
|
-
}
|
|
40
|
-
if (a.pre !== b.pre) return a.pre ? -1 : 1
|
|
41
|
-
return 0
|
|
42
|
-
}
|
|
43
25
|
|
|
44
26
|
/** True only when both versions parse and the latest is strictly newer. */
|
|
45
27
|
export function updateAvailable(installed, latest) {
|
|
@@ -171,6 +171,42 @@ function versionAtLeast(value, minimum) {
|
|
|
171
171
|
return true
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/** Relative bin path inside a package. Reject `..` and empty segments. */
|
|
175
|
+
export function normalizeRelativeBin(value) {
|
|
176
|
+
const text = String(value ?? '').trim().replace(/\\/g, '/').replace(/^\.\//, '')
|
|
177
|
+
if (!text || text.split('/').some(part => part === '' || part === '..')) return ''
|
|
178
|
+
return text
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Official entry files to try, declared bin first. `./dist/main.mjs` matches
|
|
183
|
+
* the pinned `dist/main.mjs`. A package whose bin moved still resolves when
|
|
184
|
+
* the file stays inside that package.
|
|
185
|
+
*/
|
|
186
|
+
export function packageBinCandidates(binField, binName, expectedBin) {
|
|
187
|
+
const declared = normalizeRelativeBin(binField && typeof binField === 'object' ? binField[binName] : binField)
|
|
188
|
+
const expected = normalizeRelativeBin(expectedBin)
|
|
189
|
+
return [...new Set([declared, expected].filter(Boolean))]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function readPackageEntry(packageDirectory, packageName, binName, expectedBin, workspace, seen) {
|
|
193
|
+
try {
|
|
194
|
+
const manifest = JSON.parse(await readFile(join(packageDirectory, 'package.json'), 'utf8'))
|
|
195
|
+
if (manifest.name !== packageName) return null
|
|
196
|
+
const packageRoot = await realpath(packageDirectory)
|
|
197
|
+
if (packageRoot === workspace || inside(workspace, packageRoot) || seen.has(packageRoot)) return null
|
|
198
|
+
for (const relativeBin of packageBinCandidates(manifest.bin, binName, expectedBin)) {
|
|
199
|
+
try {
|
|
200
|
+
const entry = await realpath(join(packageRoot, relativeBin))
|
|
201
|
+
if (!inside(packageRoot, entry) || !(await stat(entry)).isFile()) continue
|
|
202
|
+
seen.add(packageRoot)
|
|
203
|
+
return { entry, packageRoot, version: manifest.version, source: 'verified-npm-package' }
|
|
204
|
+
} catch { /* try the package's other bin candidate */ }
|
|
205
|
+
}
|
|
206
|
+
} catch { /* this directory is not the requested package */ }
|
|
207
|
+
return null
|
|
208
|
+
}
|
|
209
|
+
|
|
174
210
|
/** Resolve the package's real `bin` file, never its Windows .cmd shim. */
|
|
175
211
|
async function findGlobalPackageEntries(packageName, binName, expectedBin, workspace) {
|
|
176
212
|
await ensureNpmPrefixOnPath()
|
|
@@ -181,19 +217,13 @@ async function findGlobalPackageEntries(packageName, binName, expectedBin, works
|
|
|
181
217
|
const moduleParents = [join(directory, 'node_modules')]
|
|
182
218
|
if (basename(directory).toLowerCase() === '.bin') moduleParents.push(resolve(directory, '..'))
|
|
183
219
|
if (!IS_WINDOWS) moduleParents.push(resolve(directory, '..', 'lib', 'node_modules'))
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (seen.has(packageRoot)) continue
|
|
192
|
-
const entry = await realpath(join(packageRoot, expectedBin))
|
|
193
|
-
if (!inside(packageRoot, entry) || !(await stat(entry)).isFile()) continue
|
|
194
|
-
seen.add(packageRoot)
|
|
195
|
-
found.push({ entry, packageRoot, version: manifest.version, source: 'verified-npm-package' })
|
|
196
|
-
} catch { /* this PATH entry is not the requested package */ }
|
|
220
|
+
const candidates = moduleParents.map(modules => join(modules, ...packageParts))
|
|
221
|
+
// Native installs put the exe in `<package>/bin` and that directory on PATH
|
|
222
|
+
// (for example D:\Program Files\Claude\node_modules\@anthropic-ai\claude-code\bin).
|
|
223
|
+
candidates.push(directory, resolve(directory, '..'))
|
|
224
|
+
for (const packageDirectory of candidates) {
|
|
225
|
+
const entry = await readPackageEntry(packageDirectory, packageName, binName, expectedBin, workspace, seen)
|
|
226
|
+
if (entry) found.push(entry)
|
|
197
227
|
}
|
|
198
228
|
}
|
|
199
229
|
return found
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* a package name, executable or argument. Installs are serialized (npm global
|
|
8
8
|
* state must not interleave), output is kept as a bounded tail, and success is
|
|
9
9
|
* always confirmed by a fresh probe rather than the installer's exit code.
|
|
10
|
+
* When a newer release is known, a probe that is still older fails the job
|
|
11
|
+
* instead of reporting success.
|
|
10
12
|
*/
|
|
11
13
|
import { spawn } from 'node:child_process'
|
|
12
14
|
import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'
|
|
@@ -15,6 +17,7 @@ import { getOfficialTool, installCommandLine, OFFICIAL_TOOLS } from './official-
|
|
|
15
17
|
import { discoverZCodeBundle, unverifiedZCodeInstalls } from './zcode-bundle.mjs'
|
|
16
18
|
import { openZCodeInstaller } from './zcode-installer.mjs'
|
|
17
19
|
import { compareReleaseVersions, latestVersions } from './latest-versions.mjs'
|
|
20
|
+
import { versionStillOlder } from './version-order.mjs'
|
|
18
21
|
|
|
19
22
|
const PROBE_TIMEOUT_MS = 8_000
|
|
20
23
|
const INSTALL_TIMEOUT_MS = 900_000
|
|
@@ -30,6 +33,70 @@ const probeCache = new Map()
|
|
|
30
33
|
const PROBE_CACHE_MS = 60_000
|
|
31
34
|
const installJobs = new Map()
|
|
32
35
|
let installChain = Promise.resolve()
|
|
36
|
+
const CLAUDE_UPDATE_TIMEOUT_MS = 180_000
|
|
37
|
+
|
|
38
|
+
/** Non-interactive updater for a native Claude Code install. npm does not move that copy. */
|
|
39
|
+
export const CLAUDE_UPDATE_ARGS = Object.freeze(['update'])
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Spawn shape for a fixed registry command.
|
|
43
|
+
*
|
|
44
|
+
* Windows needs `shell: true` so `npm.cmd` / `where.exe` resolve. Node itself
|
|
45
|
+
* wraps that as `cmd.exe /d /s /c`. Pre-quoting every argument (0.16.2) made
|
|
46
|
+
* cmd strip the wrong quotes and launch `node` against a missing script:
|
|
47
|
+
* `MODULE_NOT_FOUND` at `executeUserEntryPoint` with an empty requireStack.
|
|
48
|
+
* That failed `npm config get prefix`, `--version`, and `npm install -g` for
|
|
49
|
+
* every official tool. Keep the executable and argv separate.
|
|
50
|
+
*/
|
|
51
|
+
export function shellSpawnSpec(executable, args = [], { useShell = false } = {}) {
|
|
52
|
+
return { file: String(executable), args: [...(args ?? [])], shell: Boolean(useShell) }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Decode a CLI banner. Native Windows exes often write UTF-16 to a pipe. */
|
|
56
|
+
export function decodeProcessOutput(value) {
|
|
57
|
+
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value ?? '')
|
|
58
|
+
if (bytes.length >= 2 && bytes[0] === 0xFF && bytes[1] === 0xFE) {
|
|
59
|
+
return bytes.toString('utf16le').replace(/^\uFEFF/, '')
|
|
60
|
+
}
|
|
61
|
+
if (bytes.length >= 2 && bytes[0] === 0xFE && bytes[1] === 0xFF) {
|
|
62
|
+
const swapped = Buffer.from(bytes)
|
|
63
|
+
swapped.swap16()
|
|
64
|
+
return swapped.toString('utf16le').replace(/^\uFEFF/, '')
|
|
65
|
+
}
|
|
66
|
+
const sample = bytes.subarray(0, Math.min(bytes.length, 64))
|
|
67
|
+
let zeros = 0
|
|
68
|
+
for (let index = 1; index < sample.length; index += 2) if (sample[index] === 0) zeros += 1
|
|
69
|
+
const pairs = Math.floor(sample.length / 2)
|
|
70
|
+
if (pairs >= 4 && zeros >= pairs * 0.6) return bytes.toString('utf16le').replace(/\u0000/g, '')
|
|
71
|
+
return bytes.toString('utf8').replace(/^\uFEFF/, '')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Absolute paths printed by `where` / `which`, one per line. */
|
|
75
|
+
export function locatorPaths(stdout) {
|
|
76
|
+
return String(stdout ?? '').split(/\r?\n/).map(line => line.trim().replace(/^"|"$/g, '')).filter(line => {
|
|
77
|
+
if (!line || /^INFO:/i.test(line)) return false
|
|
78
|
+
return isAbsolute(line)
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Visible failure when the installer exited cleanly but the probed copy is still older. */
|
|
83
|
+
export function staleInstallMessage(label, installedVersion, latestVersion) {
|
|
84
|
+
return `${label} 的更新没有生效:检测到的版本仍是 ${installedVersion},低于最新 ${latestVersion}。PATH 上可能有另一份更早的安装。请查看本行安装日志。`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Which updater to run. `order` is compareReleaseVersions(installed, latest):
|
|
89
|
+
* -1 older, 0 same, 1 newer, null when either version is unknown.
|
|
90
|
+
*/
|
|
91
|
+
export function officialUpdatePlan({ toolId, installed, order, platform }) {
|
|
92
|
+
if (installed && order === 1) return 'refuse-downgrade'
|
|
93
|
+
if (installed && order === 0) return 'already-current'
|
|
94
|
+
if (installed && order === -1 && toolId === 'claude-code') return 'claude-update-then-registry'
|
|
95
|
+
if (installed && (order === -1 || order === null) && toolId === 'minimax-code' && platform === 'win32') {
|
|
96
|
+
return 'minimax-official-installer'
|
|
97
|
+
}
|
|
98
|
+
return 'registry-install'
|
|
99
|
+
}
|
|
33
100
|
|
|
34
101
|
async function trustedExecutionReadiness(toolId) {
|
|
35
102
|
// Loaded only during an install to avoid coupling fast version probes to
|
|
@@ -44,19 +111,28 @@ function runCapture(executable, args, { timeoutMs, onOutput, useShell = false, s
|
|
|
44
111
|
resolve({ ok: false, code: null, stdout: '', stderr: '', timedOut: false, cancelled: true })
|
|
45
112
|
return
|
|
46
113
|
}
|
|
47
|
-
|
|
48
|
-
|
|
114
|
+
const stdoutChunks = []
|
|
115
|
+
const stderrChunks = []
|
|
116
|
+
const decoded = chunks => decodeProcessOutput(Buffer.concat(chunks))
|
|
117
|
+
const pushChunk = (chunks, data) => {
|
|
118
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data)
|
|
119
|
+
chunks.push(buf)
|
|
120
|
+
let total = chunks.reduce((sum, chunk) => sum + chunk.length, 0)
|
|
121
|
+
while (total > 64_000 && chunks.length > 1) total -= chunks.shift().length
|
|
122
|
+
return decodeProcessOutput(buf)
|
|
123
|
+
}
|
|
49
124
|
let settled = false
|
|
50
125
|
let stopReason = null
|
|
51
126
|
let child
|
|
52
127
|
try {
|
|
53
|
-
|
|
128
|
+
const spec = shellSpawnSpec(executable, args, { useShell })
|
|
129
|
+
child = spawn(spec.file, spec.args, {
|
|
54
130
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
55
131
|
windowsHide: true,
|
|
56
132
|
// Windows npm CLIs install as .cmd shims; only cmd.exe can execute
|
|
57
133
|
// them. Safe here because every executable and argument comes from
|
|
58
134
|
// the fixed registry — caller input never reaches this boundary.
|
|
59
|
-
shell:
|
|
135
|
+
shell: spec.shell,
|
|
60
136
|
...(env ? { env } : {}),
|
|
61
137
|
})
|
|
62
138
|
} catch (error) {
|
|
@@ -87,7 +163,7 @@ function runCapture(executable, args, { timeoutMs, onOutput, useShell = false, s
|
|
|
87
163
|
} else {
|
|
88
164
|
try { child.kill('SIGTERM') } catch { /* already gone */ }
|
|
89
165
|
}
|
|
90
|
-
graceTimer = setTimeout(() => finish({ ok: false, code: null, stdout, stderr,
|
|
166
|
+
graceTimer = setTimeout(() => finish({ ok: false, code: null, stdout: decoded(stdoutChunks), stderr: decoded(stderrChunks),
|
|
91
167
|
timedOut: reason === 'timeout', cancelled: reason === 'cancel' }), 5_000)
|
|
92
168
|
graceTimer.unref?.()
|
|
93
169
|
}
|
|
@@ -97,20 +173,20 @@ function runCapture(executable, args, { timeoutMs, onOutput, useShell = false, s
|
|
|
97
173
|
if (signal?.aborted) onAbort()
|
|
98
174
|
child.stdout?.on('data', data => {
|
|
99
175
|
if (settled || stopReason) return
|
|
100
|
-
|
|
101
|
-
onOutput?.(String(data))
|
|
176
|
+
onOutput?.(pushChunk(stdoutChunks, data))
|
|
102
177
|
})
|
|
103
178
|
child.stderr?.on('data', data => {
|
|
104
179
|
if (settled || stopReason) return
|
|
105
|
-
|
|
106
|
-
onOutput?.(String(data))
|
|
180
|
+
onOutput?.(pushChunk(stderrChunks, data))
|
|
107
181
|
})
|
|
108
182
|
child.on('error', error => {
|
|
183
|
+
const stdout = decoded(stdoutChunks)
|
|
184
|
+
const stderr = decoded(stderrChunks)
|
|
109
185
|
finish({ ok: false, code: null, stdout, stderr: `${stderr}${error.message}`.trim(),
|
|
110
186
|
timedOut: stopReason === 'timeout', cancelled: stopReason === 'cancel' })
|
|
111
187
|
})
|
|
112
188
|
child.on('close', (code, signal) => {
|
|
113
|
-
finish({ ok: !stopReason && code === 0, code, stdout, stderr,
|
|
189
|
+
finish({ ok: !stopReason && code === 0, code, stdout: decoded(stdoutChunks), stderr: decoded(stderrChunks),
|
|
114
190
|
timedOut: stopReason === 'timeout', cancelled: stopReason === 'cancel', signal })
|
|
115
191
|
})
|
|
116
192
|
})
|
|
@@ -145,14 +221,14 @@ async function fetchOfficialMiniMaxInstaller(signal) {
|
|
|
145
221
|
return officialMiniMaxInstaller(Buffer.concat(chunks))
|
|
146
222
|
}
|
|
147
223
|
|
|
148
|
-
async function runOfficialMiniMaxInstaller(job) {
|
|
224
|
+
async function runOfficialMiniMaxInstaller(job, { reason } = {}) {
|
|
149
225
|
let staging = null
|
|
150
226
|
let prefixRoot = null
|
|
151
227
|
try {
|
|
152
228
|
job.finishedAt = null
|
|
153
229
|
job.error = null
|
|
154
230
|
job.phase = 'downloading-and-verifying'
|
|
155
|
-
pushLine(job, '— npm 原生依赖未就绪;改用 MiniMax 官方 Windows 安装脚本(安装最新版)—')
|
|
231
|
+
pushLine(job, reason || '— npm 原生依赖未就绪;改用 MiniMax 官方 Windows 安装脚本(安装最新版)—')
|
|
156
232
|
const prefixResult = await runCapture('npm', ['config', 'get', 'prefix'], {
|
|
157
233
|
timeoutMs: 5_000, useShell: true, signal: job.controller.signal,
|
|
158
234
|
})
|
|
@@ -208,6 +284,10 @@ async function runOfficialMiniMaxInstaller(job) {
|
|
|
208
284
|
if (!probe.installed || !readiness.ready) {
|
|
209
285
|
throw new Error(`官方安装器完成,但执行入口未通过 npm registry 摘要核验:${readiness.reason ?? probe.detail}`)
|
|
210
286
|
}
|
|
287
|
+
const latest = await latestVersions.lookup(job.tool).catch(() => null)
|
|
288
|
+
if (latest?.version && versionStillOlder(probe.version, latest.version)) {
|
|
289
|
+
throw new Error(staleInstallMessage(job.tool.label, probe.version, latest.version))
|
|
290
|
+
}
|
|
211
291
|
job.status = 'succeeded'
|
|
212
292
|
job.finishedAt = new Date().toISOString()
|
|
213
293
|
pushLine(job, `— 官方安装器已完成,MiniMax Code ${probe.version ?? ''} 的执行入口已核验 —`)
|
|
@@ -229,11 +309,6 @@ async function runOfficialMiniMaxInstaller(job) {
|
|
|
229
309
|
/** Tools whose ready-check failure can be repaired by reinstalling the same version. */
|
|
230
310
|
const CAPABILITY_REPAIRABLE = new Set(['claude-code', 'codex', 'kimi-code', 'minimax-code', 'mimo-code', 'grok-build'])
|
|
231
311
|
|
|
232
|
-
function appendBounded(current, addition) {
|
|
233
|
-
const merged = current + addition
|
|
234
|
-
return merged.length > 64_000 ? merged.slice(merged.length - 32_000) : merged
|
|
235
|
-
}
|
|
236
|
-
|
|
237
312
|
/** Extract the first version-looking token from a CLI banner. */
|
|
238
313
|
export function versionFromBanner(banner) {
|
|
239
314
|
const match = String(banner ?? '').match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/)
|
|
@@ -288,16 +363,28 @@ async function probeUncached(tool, runner) {
|
|
|
288
363
|
if (!located.ok || !located.stdout.trim()) {
|
|
289
364
|
continue
|
|
290
365
|
}
|
|
291
|
-
const attempt = await runner(executable, ['--version'], { timeoutMs: PROBE_TIMEOUT_MS })
|
|
366
|
+
const attempt = await runner(executable, ['--version'], { timeoutMs: PROBE_TIMEOUT_MS, useShell: IS_WINDOWS })
|
|
292
367
|
if (attempt.timedOut) {
|
|
293
368
|
return { id: tool.id, installed: false, version: null, status: 'probe-timeout', detail: `${executable} --version 超时(${PROBE_TIMEOUT_MS / 1000}s)` }
|
|
294
369
|
}
|
|
295
|
-
|
|
296
|
-
|
|
370
|
+
const attempts = [attempt]
|
|
371
|
+
if (!versionFromBanner(probeBanner(attempt))) {
|
|
372
|
+
for (const target of locatorPaths(located.stdout)) {
|
|
373
|
+
if (target === executable) continue
|
|
374
|
+
const direct = await runner(target, ['--version'], { timeoutMs: PROBE_TIMEOUT_MS, useShell: false })
|
|
375
|
+
attempts.push(direct)
|
|
376
|
+
if (direct.timedOut) break
|
|
377
|
+
if (direct.ok || versionFromBanner(probeBanner(direct))) break
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const chosen = attempts.find(item => item && (item.ok || versionFromBanner(probeBanner(item)))) ?? attempt
|
|
381
|
+
const banner = probeBanner(chosen)
|
|
382
|
+
const version = versionFromBanner(banner)
|
|
383
|
+
if (chosen.ok || version) {
|
|
297
384
|
return {
|
|
298
385
|
id: tool.id,
|
|
299
386
|
installed: true,
|
|
300
|
-
version
|
|
387
|
+
version,
|
|
301
388
|
status: 'installed',
|
|
302
389
|
detail: tool.probeNote ?? '',
|
|
303
390
|
bannerLine: banner.split('\n').find(Boolean)?.slice(0, 200) ?? '',
|
|
@@ -308,7 +395,7 @@ async function probeUncached(tool, runner) {
|
|
|
308
395
|
installed: false,
|
|
309
396
|
version: null,
|
|
310
397
|
status: 'probe-failed',
|
|
311
|
-
detail: `${executable} --version 退出码 ${
|
|
398
|
+
detail: `${executable} --version 退出码 ${chosen.code ?? '信号终止'};程序可能损坏或版本过旧。${String(chosen.stderr ?? '').split('\n').find(Boolean)?.slice(0, 120) ?? ''}`,
|
|
312
399
|
}
|
|
313
400
|
}
|
|
314
401
|
return { id: tool.id, installed: false, version: null, status: 'not-installed', detail: '未在 PATH 中找到官方命令。' }
|
|
@@ -316,7 +403,7 @@ async function probeUncached(tool, runner) {
|
|
|
316
403
|
|
|
317
404
|
/** Default runner: real spawn with the platform's shell for .cmd shims. */
|
|
318
405
|
export const defaultRunner = (executable, args, options = {}) =>
|
|
319
|
-
runCapture(executable, args, {
|
|
406
|
+
runCapture(executable, args, { useShell: IS_WINDOWS, ...options })
|
|
320
407
|
|
|
321
408
|
let prefixPromise = null
|
|
322
409
|
|
|
@@ -365,12 +452,31 @@ export async function installedToolIds() {
|
|
|
365
452
|
* Probe every registry tool in parallel (the slowest single probe bounds the
|
|
366
453
|
* wall time; the cache keeps repeat calls instant).
|
|
367
454
|
*/
|
|
455
|
+
/** One tool's probe failure must not reject detection of the rest of the page. */
|
|
456
|
+
export async function probeToolSafely(tool, runner, options) {
|
|
457
|
+
try {
|
|
458
|
+
return await probeToolWith(tool, runner, options)
|
|
459
|
+
} catch (error) {
|
|
460
|
+
return {
|
|
461
|
+
id: tool.id,
|
|
462
|
+
installed: false,
|
|
463
|
+
version: null,
|
|
464
|
+
status: 'probe-failed',
|
|
465
|
+
detail: `检测失败:${String(error?.message ?? error).slice(0, 200)}`,
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
368
470
|
export async function probeAllTools({ fresh = false } = {}) {
|
|
369
471
|
await ensureNpmPrefixOnPath()
|
|
370
|
-
return Promise.all(OFFICIAL_TOOLS.map(tool =>
|
|
472
|
+
return Promise.all(OFFICIAL_TOOLS.map(tool => probeToolSafely(tool, defaultRunner,
|
|
371
473
|
fresh ? { cacheMs: 0 } : {})))
|
|
372
474
|
}
|
|
373
475
|
|
|
476
|
+
function probeBanner(attempt) {
|
|
477
|
+
return `${attempt?.stdout ?? ''}\n${attempt?.stderr ?? ''}`.trim()
|
|
478
|
+
}
|
|
479
|
+
|
|
374
480
|
export function probeSnapshot() {
|
|
375
481
|
return [...probeCache.values()].map(entry => entry.value)
|
|
376
482
|
}
|
|
@@ -444,6 +550,86 @@ export function startInstall(toolId) {
|
|
|
444
550
|
return jobView(job)
|
|
445
551
|
}
|
|
446
552
|
|
|
553
|
+
function captureLines(job) {
|
|
554
|
+
return chunk => {
|
|
555
|
+
for (const line of String(chunk).split(/\r?\n/)) {
|
|
556
|
+
if (line.trim()) pushLine(job, line.slice(0, 500))
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Mark the job succeeded only when the fresh probe is installed, ready, and not still older. */
|
|
562
|
+
async function finishVerifiedInstall(job, probe) {
|
|
563
|
+
job.postInstallProbe = probe
|
|
564
|
+
if (job.cancelRequested) { markCancelled(job); return true }
|
|
565
|
+
if (!probe.installed) {
|
|
566
|
+
job.status = 'failed'
|
|
567
|
+
job.finishedAt = new Date().toISOString()
|
|
568
|
+
job.error = `安装命令已完成,但重探测失败:${probe.detail}`
|
|
569
|
+
return true
|
|
570
|
+
}
|
|
571
|
+
const latest = await latestVersions.lookup(job.tool).catch(() => null)
|
|
572
|
+
if (latest?.version && versionStillOlder(probe.version, latest.version)) {
|
|
573
|
+
failBecauseStillOlder(job, probe, latest.version)
|
|
574
|
+
return true
|
|
575
|
+
}
|
|
576
|
+
const readiness = await trustedExecutionReadiness(job.tool.id)
|
|
577
|
+
if (job.cancelRequested) { markCancelled(job); return true }
|
|
578
|
+
if (!readiness.ready) {
|
|
579
|
+
if (IS_WINDOWS && job.tool.id === 'minimax-code') {
|
|
580
|
+
await runOfficialMiniMaxInstaller(job)
|
|
581
|
+
return true
|
|
582
|
+
}
|
|
583
|
+
job.status = 'failed'
|
|
584
|
+
job.finishedAt = new Date().toISOString()
|
|
585
|
+
job.error = `安装命令和版本探测已成功,但官方执行入口未就绪:${readiness.reason}`
|
|
586
|
+
return true
|
|
587
|
+
}
|
|
588
|
+
job.status = 'succeeded'
|
|
589
|
+
job.finishedAt = new Date().toISOString()
|
|
590
|
+
pushLine(job, `— 已安装 ${job.tool.label} ${probe.version ?? ''},官方执行入口已核验(新版本未经插件测试)—`)
|
|
591
|
+
return true
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function failBecauseStillOlder(job, probe, latestVersion) {
|
|
595
|
+
job.postInstallProbe = probe
|
|
596
|
+
job.status = 'failed'
|
|
597
|
+
job.finishedAt = new Date().toISOString()
|
|
598
|
+
job.error = staleInstallMessage(job.tool.label, probe?.version, latestVersion)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Native Claude Code lives outside the npm prefix. `claude update` is the
|
|
603
|
+
* vendor's non-interactive updater; return true when the job is finished.
|
|
604
|
+
*/
|
|
605
|
+
async function tryClaudeUpdate(job, latestVersion) {
|
|
606
|
+
if (job.cancelRequested) { markCancelled(job); return true }
|
|
607
|
+
pushLine(job, '$ claude update')
|
|
608
|
+
job.phase = 'installing'
|
|
609
|
+
const outcome = await runCapture('claude', CLAUDE_UPDATE_ARGS, {
|
|
610
|
+
timeoutMs: CLAUDE_UPDATE_TIMEOUT_MS,
|
|
611
|
+
useShell: IS_WINDOWS,
|
|
612
|
+
signal: job.controller.signal,
|
|
613
|
+
onOutput: captureLines(job),
|
|
614
|
+
})
|
|
615
|
+
if (job.status !== 'running') return true
|
|
616
|
+
if (outcome.cancelled || job.cancelRequested) { markCancelled(job); return true }
|
|
617
|
+
if (!outcome.ok) {
|
|
618
|
+
pushLine(job, `— claude update 未完成(退出码 ${outcome.code ?? '信号终止'}),将继续尝试注册表安装 —`)
|
|
619
|
+
return false
|
|
620
|
+
}
|
|
621
|
+
await ensureNpmPrefixOnPath()
|
|
622
|
+
probeCache.delete(job.tool.id)
|
|
623
|
+
const probe = await probeToolWith(job.tool, defaultRunner)
|
|
624
|
+
if (job.cancelRequested) { markCancelled(job); return true }
|
|
625
|
+
if (latestVersion && versionStillOlder(probe.version, latestVersion)) {
|
|
626
|
+
job.postInstallProbe = probe
|
|
627
|
+
pushLine(job, `— claude update 已结束,但探测到的版本仍是 ${probe.version},低于 ${latestVersion} —`)
|
|
628
|
+
return false
|
|
629
|
+
}
|
|
630
|
+
return finishVerifiedInstall(job, probe)
|
|
631
|
+
}
|
|
632
|
+
|
|
447
633
|
async function runInstallJob(manager, args, job) {
|
|
448
634
|
if (job.status !== 'running') return
|
|
449
635
|
job.phase = 'preflight'
|
|
@@ -452,10 +638,12 @@ async function runInstallJob(manager, args, job) {
|
|
|
452
638
|
probeCache.delete(job.tool.id)
|
|
453
639
|
const before = await probeToolWith(job.tool, defaultRunner)
|
|
454
640
|
if (job.cancelRequested) { markCancelled(job); return }
|
|
641
|
+
let latest = null
|
|
642
|
+
let order = null
|
|
455
643
|
if (before.installed) {
|
|
456
|
-
|
|
644
|
+
latest = await latestVersions.lookup(job.tool, { fresh: true }).catch(() => null)
|
|
457
645
|
if (job.cancelRequested) { markCancelled(job); return }
|
|
458
|
-
|
|
646
|
+
order = latest?.version ? compareReleaseVersions(before.version, latest.version) : null
|
|
459
647
|
if (order === 1) {
|
|
460
648
|
job.status = 'failed'
|
|
461
649
|
job.finishedAt = new Date().toISOString()
|
|
@@ -476,59 +664,76 @@ async function runInstallJob(manager, args, job) {
|
|
|
476
664
|
pushLine(job, `— 已是最新版本 ${before.version},但执行入口未就绪:${readiness.reason};重新安装修复 —`)
|
|
477
665
|
}
|
|
478
666
|
}
|
|
667
|
+
const plan = officialUpdatePlan({
|
|
668
|
+
toolId: job.tool.id,
|
|
669
|
+
installed: before.installed === true,
|
|
670
|
+
order,
|
|
671
|
+
platform: process.platform,
|
|
672
|
+
})
|
|
673
|
+
let triedClaudeUpdate = false
|
|
674
|
+
if (plan === 'claude-update-then-registry') {
|
|
675
|
+
triedClaudeUpdate = true
|
|
676
|
+
const finished = await tryClaudeUpdate(job, latest?.version)
|
|
677
|
+
if (finished || job.status !== 'running') return
|
|
678
|
+
} else if (plan === 'minimax-official-installer') {
|
|
679
|
+
await runOfficialMiniMaxInstaller(job, {
|
|
680
|
+
reason: '— 已安装的 MiniMax Code 由官方安装目录接管;直接运行官方 Windows 安装脚本以更新到最新版 —',
|
|
681
|
+
})
|
|
682
|
+
return
|
|
683
|
+
}
|
|
479
684
|
job.phase = 'installing'
|
|
480
685
|
const outcome = await runCapture(manager, args, {
|
|
481
686
|
timeoutMs: INSTALL_TIMEOUT_MS,
|
|
482
687
|
useShell: IS_WINDOWS,
|
|
483
688
|
signal: job.controller.signal,
|
|
484
|
-
onOutput:
|
|
485
|
-
for (const line of chunk.split(/\r?\n/)) {
|
|
486
|
-
if (line.trim()) pushLine(job, line.slice(0, 500))
|
|
487
|
-
}
|
|
488
|
-
},
|
|
689
|
+
onOutput: captureLines(job),
|
|
489
690
|
})
|
|
490
691
|
if (job.status !== 'running') return
|
|
491
692
|
if (outcome.cancelled || job.cancelRequested) { markCancelled(job); return }
|
|
492
|
-
job.finishedAt = new Date().toISOString()
|
|
493
693
|
job.exitCode = outcome.code
|
|
494
694
|
job.timedOut = outcome.timedOut
|
|
495
695
|
if (outcome.timedOut) {
|
|
496
696
|
job.status = 'failed'
|
|
697
|
+
job.finishedAt = new Date().toISOString()
|
|
497
698
|
job.error = `安装超时(${INSTALL_TIMEOUT_MS / 1000}s),已终止。`
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
job.postInstallProbe = probe
|
|
505
|
-
if (!probe.installed) {
|
|
506
|
-
job.status = 'failed'
|
|
507
|
-
job.error = `安装命令已完成,但重探测失败:${probe.detail}`
|
|
508
|
-
} else {
|
|
509
|
-
const latest = await latestVersions.lookup(job.tool).catch(() => null)
|
|
510
|
-
if (latest?.version && compareReleaseVersions(probe.version, latest.version) === -1) {
|
|
511
|
-
pushLine(job, `— 注意:PATH 上的 ${job.tool.label} 仍是 ${probe.version},低于最新 ${latest.version};可能有另一份旧安装排在 PATH 前面 —`)
|
|
512
|
-
}
|
|
513
|
-
const readiness = await trustedExecutionReadiness(job.tool.id)
|
|
514
|
-
if (job.cancelRequested) { markCancelled(job); return }
|
|
515
|
-
if (!readiness.ready) {
|
|
516
|
-
if (IS_WINDOWS && job.tool.id === 'minimax-code') await runOfficialMiniMaxInstaller(job)
|
|
517
|
-
else {
|
|
518
|
-
job.status = 'failed'
|
|
519
|
-
job.error = `安装命令和版本探测已成功,但官方执行入口未就绪:${readiness.reason}`
|
|
520
|
-
}
|
|
521
|
-
} else {
|
|
522
|
-
job.status = 'succeeded'
|
|
523
|
-
pushLine(job, `— 已安装 ${job.tool.label} ${probe.version ?? ''},官方执行入口已核验(新版本未经插件测试)—`)
|
|
524
|
-
}
|
|
699
|
+
return
|
|
700
|
+
}
|
|
701
|
+
if (!outcome.ok) {
|
|
702
|
+
if (IS_WINDOWS && job.tool.id === 'minimax-code') {
|
|
703
|
+
await runOfficialMiniMaxInstaller(job)
|
|
704
|
+
return
|
|
525
705
|
}
|
|
526
|
-
} else if (IS_WINDOWS && job.tool.id === 'minimax-code') {
|
|
527
|
-
await runOfficialMiniMaxInstaller(job)
|
|
528
|
-
} else {
|
|
529
706
|
job.status = 'failed'
|
|
707
|
+
job.finishedAt = new Date().toISOString()
|
|
530
708
|
job.error = `安装命令失败(退出码 ${outcome.code ?? '信号终止'})。${outcome.stderr.split('\n').find(Boolean)?.slice(0, 200) ?? ''}`
|
|
709
|
+
return
|
|
710
|
+
}
|
|
711
|
+
job.phase = 'verifying'
|
|
712
|
+
await ensureNpmPrefixOnPath()
|
|
713
|
+
probeCache.delete(job.tool.id)
|
|
714
|
+
const probe = await probeToolWith(job.tool, defaultRunner)
|
|
715
|
+
if (job.cancelRequested) { markCancelled(job); return }
|
|
716
|
+
job.postInstallProbe = probe
|
|
717
|
+
const latestAfter = (await latestVersions.lookup(job.tool).catch(() => null)) ?? latest
|
|
718
|
+
const stillOlder = Boolean(latestAfter?.version && versionStillOlder(probe.version, latestAfter.version))
|
|
719
|
+
if (stillOlder && job.tool.id === 'claude-code' && !triedClaudeUpdate) {
|
|
720
|
+
triedClaudeUpdate = true
|
|
721
|
+
const finished = await tryClaudeUpdate(job, latestAfter.version)
|
|
722
|
+
if (finished || job.status !== 'running') return
|
|
723
|
+
failBecauseStillOlder(job, job.postInstallProbe ?? probe, latestAfter.version)
|
|
724
|
+
return
|
|
725
|
+
}
|
|
726
|
+
if (stillOlder && IS_WINDOWS && job.tool.id === 'minimax-code') {
|
|
727
|
+
pushLine(job, `— npm 完成后 ${job.tool.label} 仍是 ${probe.version},低于最新 ${latestAfter.version};改用官方安装脚本 —`)
|
|
728
|
+
await runOfficialMiniMaxInstaller(job)
|
|
729
|
+
return
|
|
730
|
+
}
|
|
731
|
+
if (stillOlder) {
|
|
732
|
+
pushLine(job, `— 注意:PATH 上的 ${job.tool.label} 仍是 ${probe.version},低于最新 ${latestAfter.version} —`)
|
|
733
|
+
failBecauseStillOlder(job, probe, latestAfter.version)
|
|
734
|
+
return
|
|
531
735
|
}
|
|
736
|
+
await finishVerifiedInstall(job, probe)
|
|
532
737
|
}
|
|
533
738
|
|
|
534
739
|
async function runZCodeInstallJob(job) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure x.y.z[-pre] ordering shared by the Host and the Desktop client.
|
|
3
|
+
* No Node APIs: the client bundle imports this file.
|
|
4
|
+
*/
|
|
5
|
+
const VERSION = /^(\d{1,6})\.(\d{1,6})\.(\d{1,6})(?:-[0-9A-Za-z.-]{1,64})?$/
|
|
6
|
+
|
|
7
|
+
export function isReleaseVersion(value) {
|
|
8
|
+
return typeof value === 'string' && VERSION.test(value)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Numeric comparison of x.y.z[-pre]; a prerelease sorts below its release. */
|
|
12
|
+
export function compareReleaseVersions(left, right) {
|
|
13
|
+
const parse = value => {
|
|
14
|
+
const match = VERSION.exec(String(value ?? ''))
|
|
15
|
+
return match ? { parts: match.slice(1, 4).map(Number), pre: String(value).includes('-') } : null
|
|
16
|
+
}
|
|
17
|
+
const a = parse(left)
|
|
18
|
+
const b = parse(right)
|
|
19
|
+
if (!a || !b) return null
|
|
20
|
+
for (let index = 0; index < 3; index += 1) {
|
|
21
|
+
if (a.parts[index] !== b.parts[index]) return a.parts[index] > b.parts[index] ? 1 : -1
|
|
22
|
+
}
|
|
23
|
+
if (a.pre !== b.pre) return a.pre ? -1 : 1
|
|
24
|
+
return 0
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** True when both versions parse and the installed copy is strictly older. */
|
|
28
|
+
export function versionStillOlder(installedVersion, latestVersion) {
|
|
29
|
+
return compareReleaseVersions(installedVersion, latestVersion) === -1
|
|
30
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.3 — 2026-10-09
|
|
4
|
+
|
|
5
|
+
Stable patch under the npm `latest` tag. Fully quit Harness, including a WebUI server process or the desktop tray, and restart after upgrading so the Host reloads. Existing 0.16.2 artifacts remain unchanged.
|
|
6
|
+
|
|
7
|
+
- Windows `shell: true` spawns again pass the executable and arguments separately. 0.16.2 pre-quoted that command line; cmd then started Node on a missing script (`MODULE_NOT_FOUND`, empty `requireStack`, Node 24) for every official tool's probe, `npm config get prefix`, and `npm install -g`.
|
|
8
|
+
- A `--version` banner that contains a version counts as installed, including UTF-16 output from a native Windows executable. If the name on PATH fails, the absolute path from `where`/`which` is tried without a shell. One probe or readiness failure stays on that card.
|
|
9
|
+
- Package entry lookup accepts `./dist/main.mjs` and a `bin/` directory that is itself on PATH. A CLI that is already the latest release stays on “已是最新版本” instead of “修复官方执行入口”.
|
|
10
|
+
- The 0.16.2 update-button behavior remains: the click keeps its own job, refresh does not disable a visible update, and an install that leaves the probed version older fails with a row error.
|
|
11
|
+
- Release validation on Linux: 630 tests passed, 4 conditional skips, `npm run check:client` passed. No Harness WebUI, Harness Desktop, or Windows `cmd.exe` run was available in this environment.
|
|
12
|
+
|
|
13
|
+
## 0.16.2 — 2026-10-09
|
|
14
|
+
|
|
15
|
+
Stable patch under the npm `latest` tag. Fully quit Harness, including the tray process, and restart after upgrading. Existing 0.16.1 artifacts remain unchanged.
|
|
16
|
+
|
|
17
|
+
- Official-tools update/install clicks keep the job started by that click. A status poll no longer replaces it with an older finished job or clears a failure.
|
|
18
|
+
- Refreshing an existing probe no longer disables the update button. A click that cannot start, and an install whose probed version is still older than latest, show an error on that row.
|
|
19
|
+
- Installed Claude Code runs `claude update` before the registry npm install. Installed Windows MiniMax runs the official installer. Other registry cards share the same click path and fail visibly when the probed version does not advance.
|
|
20
|
+
- Release validation: 628 tests passed, 4 conditional skips, client check passed. No paid calls or real CLI installs were part of this verification.
|
|
21
|
+
|
|
3
22
|
## 0.16.1 — 2026-10-08
|
|
4
23
|
|
|
5
24
|
Stable maturity patch under the npm `latest` tag. Fully quit Harness, including the tray process, and restart after upgrading. Existing 0.16.0 artifacts remain unchanged.
|
package/INSTALLATION_GUIDE.zh.md
CHANGED
|
@@ -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.16.1
|
|
5
|
+
当前路由正式版为 **0.16.3**。本版恢复全部官方工具卡片的检测与安装:Windows 上不再把每条探测和 `npm` 命令预先加引号,单张卡片的失败不会清空整页。0.16.2 的更新按钮修复和 0.16.1 的反馈缓存仍然保留。公开数据默认关闭;主观偏好调整不改变公共能力评分或质量门槛。源码开发可使用独立模拟 UI 预览;安装时选择下方精确版本。
|
|
6
6
|
|
|
7
7
|
[路由 README](README.zh.md) · [迁移说明](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.16.
|
|
13
|
+
| 分析复杂度、按质量和费用选模型、调用官方工具 | `@ljwei-stak/dsh-model-router@0.16.3` | 模型路由 |
|
|
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.16.
|
|
27
|
+
@ljwei-stak/dsh-model-router@0.16.3
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
只想安装 GAL 时填写:
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
4. 核对预览的包名、版本和宿主兼容信息,安装并启用。完全退出 Harness(包括托盘进程)再启动,确保 Host 加载新代码。
|
|
43
|
-
5. 检查插件详情与侧边栏:路由应为 **0.16.
|
|
43
|
+
5. 检查插件详情与侧边栏:路由应为 **0.16.3 / 模型路由**;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.16.
|
|
51
|
+
- [模型路由 Releases](https://github.com/Alice-Marx/dsh-model-router/releases)(也可运行 `npm pack @ljwei-stak/dsh-model-router@0.16.3` 获取同一安装包)
|
|
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.16.
|
|
57
|
+
D:\Plugins\ljwei-stak-dsh-model-router-0.16.3.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.16.
|
|
67
|
+
Get-FileHash -Algorithm SHA256 -LiteralPath 'D:\Plugins\ljwei-stak-dsh-model-router-0.16.3.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.16.
|
|
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.16.3`。不要两个同时安装(条目 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.16.
|
|
85
|
+
工作台自 0.15.0 起分成**任务与执行、模型配置、官方工具、预算与安全**四页;0.16.3 沿用该布局。切换页面保留安装任务、终端会话和未完成的输入。
|
|
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.16.
|
|
117
|
+
| 路由独立安装 | 0.16.3 详情、模型路由入口、目录和模型档案可用;不出现旧合并 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.16.
|
|
128
|
+
历史 0.15.0 的自动测试验证为 220 项通过、3 项 Windows 平台跳过;组件 UI 预览检查使用模拟数据。**0.16.3 的发布验证见[更新记录](CHANGELOG.md)**,不把模拟公开源/模型流和组件预览当作真实付费调用或 Harness Desktop 端到端验收;上表中的实际安装、账号和工作区检查仍需在使用环境中完成。
|
|
129
129
|
|
|
130
130
|
## 8. 开发者从完整源码构建
|
|
131
131
|
|
|
132
|
-
路由需要 Node.js **22.19+** 与 **pnpm 10.34.6**。从完整源码仓库根目录构建;复现正式版 0.16.
|
|
132
|
+
路由需要 Node.js **22.19+** 与 **pnpm 10.34.6**。从完整源码仓库根目录构建;复现正式版 0.16.3 时检出对应发布标签。旧版(如 0.15.0)标签保留其当时内容。运行:
|
|
133
133
|
|
|
134
134
|
```powershell
|
|
135
135
|
pnpm install --frozen-lockfile --strict-peer-dependencies
|
package/README.md
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# Model Router · DeepSeek Harness Desktop
|
|
2
2
|
|
|
3
|
-
Version: **0.16.
|
|
3
|
+
Version: **0.16.3**. 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.16.3:** restores official-tools detection and install for every registry card. Windows probes and `npm` are spawned with separate arguments again, so cmd no longer launches Node against a missing script. A version banner still counts as installed, one card's probe error stays on that row, and a CLI that is already current is not sent through “修复官方执行入口”.
|
|
8
|
+
|
|
9
|
+
**0.16.2:** the official-tools update/install button keeps the job started by that click, stays usable while a previous probe is refreshing, and shows a row error when the click cannot start or the probed version does not advance. Installed Claude Code runs `claude update` before the registry npm install. Installed Windows MiniMax runs the official installer.
|
|
10
|
+
|
|
7
11
|
**0.16.1:** [maturity improvements and validation](docs/ROUTING_MATURITY.zh.md) adds stable feedback cache identity, per-model/task feedback explanations, source-refresh and settings-race protection, and budget gates for unreadable or incomplete cost history. Mechanism tests do not establish real quality–cost gains.
|
|
8
12
|
|
|
9
13
|
**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.
|
|
10
14
|
|
|
11
|
-
Current version: **0.16.
|
|
15
|
+
Current version: **0.16.3** (`@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.
|
|
12
16
|
|
|
13
17
|
**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.
|
|
14
18
|
|
|
@@ -40,16 +44,16 @@ Generating a route recommendation is local: it does not start a vendor CLI or ma
|
|
|
40
44
|
Open **Plugins → Add plugin** and enter:
|
|
41
45
|
|
|
42
46
|
```text
|
|
43
|
-
@ljwei-stak/dsh-model-router@0.16.
|
|
47
|
+
@ljwei-stak/dsh-model-router@0.16.3
|
|
44
48
|
```
|
|
45
49
|
|
|
46
|
-
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.16.
|
|
50
|
+
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.16.3** and the sidebar should contain **模型路由** (Model Router). A global `npm install -g` alone does not register a Desktop plugin.
|
|
47
51
|
|
|
48
52
|
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.
|
|
49
53
|
|
|
50
54
|
### Upgrade from the old package name
|
|
51
55
|
|
|
52
|
-
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.16.
|
|
56
|
+
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.16.3`. 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.
|
|
53
57
|
|
|
54
58
|
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](MIGRATION.md).
|
|
55
59
|
|
package/README.zh.md
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# Model Router · DeepSeek Harness 模型路由插件
|
|
2
2
|
|
|
3
|
-
版本:**0.16.
|
|
3
|
+
版本:**0.16.3**。选模求解器概览见[路由研究说明](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.16.3**:恢复全部官方工具卡片的检测和安装。Windows 上的探测与 `npm` 再次按独立参数启动,避免 cmd 把 Node 指到不存在的脚本。只要输出版本号就视为已安装;单张卡片的检测错误留在该行;已经是最新版的 CLI 不再被收成「修复官方执行入口」。
|
|
8
|
+
|
|
9
|
+
**0.16.2**:官方工具的更新/安装按钮会保留本次点击启动的任务;已有探测结果时重新体检不再把按钮禁用成无响应;点击无法开始、或安装后探测版本仍低于最新版时,在该行显示错误。已安装的 Claude Code 先执行 `claude update`,再按注册表走 npm;已安装的 Windows MiniMax 走官方安装脚本。
|
|
10
|
+
|
|
7
11
|
**0.16.1 成熟度改进**:稳定反馈缓存、分模型/任务解释、刷新与设置竞态保护,以及费用历史不可读取或不完整时的预算门禁。[推导与验证](docs/ROUTING_MATURITY.zh.md)随安装包提供,机制测试不等于真实质量—成本收益。
|
|
8
12
|
|
|
9
13
|
**0.16.0 新增**:[动态数据与持续反馈](docs/ROUTING_ADAPTIVE_LEARNING.zh.md):可选公开价格/LiveBench快照刷新;按任务类型、时间衰减和低样本收缩调整个人偏好,质量门槛独立保留。设置在“预算与安全”,公开源默认关闭;价格源需提供按官方文档核对的固定USD单价JSON,不是通用官网抓价器。本地DSH_HOME共享、最近200运行窗口,暂无真实收益证明。
|
|
10
14
|
|
|
11
|
-
当前版本为 **0.16.
|
|
15
|
+
当前版本为 **0.16.3**,包名为 `@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) 已拆成两个独立插件。
|
|
12
16
|
|
|
13
17
|
**0.15.0 更新**:工作台分为四页,路由建议紧接任务规划展示;执行输入变化时丢弃旧预览;裁剪历史或重试后仍保留费用累计;源码开发新增独立模拟 UI 预览。
|
|
14
18
|
|
|
@@ -40,16 +44,16 @@
|
|
|
40
44
|
打开 **插件 → 添加插件**,填写:
|
|
41
45
|
|
|
42
46
|
```text
|
|
43
|
-
@ljwei-stak/dsh-model-router@0.16.
|
|
47
|
+
@ljwei-stak/dsh-model-router@0.16.3
|
|
44
48
|
```
|
|
45
49
|
|
|
46
|
-
镜像缺少该精确版本时使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。安装并启用后,完全退出 Harness(包括托盘进程)再启动。插件详情应显示 **0.16.
|
|
50
|
+
镜像缺少该精确版本时使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。安装并启用后,完全退出 Harness(包括托盘进程)再启动。插件详情应显示 **0.16.3**,侧栏出现**模型路由**。仅运行全局 `npm install -g` 不会把插件注册到桌面版 profile。
|
|
47
51
|
|
|
48
52
|
也可在插件管理器填写本地 `.tgz` 文件的绝对路径,或解压后的内层 `package` 目录;其中应包含 `package.json` 与 `.dsh-plugin`。[历史发布页](https://github.com/Alice-Marx/dsh-model-router/releases)提供归档;已克隆源码的开发方式见下文。
|
|
49
53
|
|
|
50
54
|
### 从旧包名升级
|
|
51
55
|
|
|
52
|
-
`@ljwei-stak/model-router-galgame` 的最后版本为 0.13.0。保留 profile 与应用数据,移除旧插件条目,再添加 `@ljwei-stak/dsh-model-router@0.16.
|
|
56
|
+
`@ljwei-stak/model-router-galgame` 的最后版本为 0.13.0。保留 profile 与应用数据,移除旧插件条目,再添加 `@ljwei-stak/dsh-model-router@0.16.3`。不要同时安装两个包:它们共用内部插件 id `model-router-galgame` 与工具名;id 保留使已有路由设置和执行历史能够延续。
|
|
53
57
|
|
|
54
58
|
从 0.11.x 合并版迁移时,先备份 profile 并导出希望保留的 GAL 进度。剧情、立绘、音乐、自由模式和存档属于独立 `@ljwei-stak/dsh-galgame` 插件。导出范围及同 profile 迁移的限制见 [MIGRATION.md](MIGRATION.md)。
|
|
55
59
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ljwei-stak/dsh-model-router",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
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": {
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
".dsh-plugin/shared/vendor-minimax-adapter.mjs",
|
|
58
58
|
".dsh-plugin/shared/vendor-mimo-grok-adapter.mjs",
|
|
59
59
|
".dsh-plugin/shared/latest-versions.mjs",
|
|
60
|
+
".dsh-plugin/shared/version-order.mjs",
|
|
60
61
|
".dsh-plugin/shared/npm-attestation.mjs",
|
|
61
62
|
".dsh-plugin/shared/zcode-bundle.mjs",
|
|
62
63
|
".dsh-plugin/shared/zcode-installer.mjs",
|