@noob-stupid/dsh-plugin-console 0.5.0 → 0.5.2
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/lib/client.js
CHANGED
|
@@ -91,6 +91,14 @@ window.__ModuleLoader__.load({
|
|
|
91
91
|
const zh = {
|
|
92
92
|
tab: "插件管理",
|
|
93
93
|
safetyTab: "升级安全",
|
|
94
|
+
safetyModeManagedShort: "托管",
|
|
95
|
+
safetyModeObserverShort: "观察者",
|
|
96
|
+
safetyBackfillTitle: "补声明(已装未声明)",
|
|
97
|
+
safetyBackfillShort: "{n} 项待补声明",
|
|
98
|
+
safetyBackfillDesc: "有 {n} 个已装插件没写进 profile 清单 —— 官方/第三方插件页看不见它们,pnpm 操作还可能把它们还原。补声明会把磁盘上的真实版本写进清单(不改包、不重装)。",
|
|
99
|
+
safetyBackfillNone: "已装插件都在清单里声明过了(官方面板的「已安装」与 pnpm 都能看见)",
|
|
100
|
+
safetyBackfillBtn: "补声明({n} 项)",
|
|
101
|
+
safetyBackfillDone: "补声明完成",
|
|
94
102
|
safetyMode: "运行模式",
|
|
95
103
|
safetyModeManaged: "托管:框架升级与回滚由本控制台负责(默认,行为与之前一致)",
|
|
96
104
|
safetyModeManagedOn: "已切到托管模式:框架升级与回滚仍由本控制台负责",
|
|
@@ -366,6 +374,7 @@ window.__ModuleLoader__.load({
|
|
|
366
374
|
consentWaiting: "等待授权确认…",
|
|
367
375
|
dismissFailure: "关闭此条提示",
|
|
368
376
|
sourcesBtn: "软件源",
|
|
377
|
+
sourcesTimeout: "读取软件源超时(DSH 可能正在重启或繁忙)——点重试即可",
|
|
369
378
|
sourcesTitle: "软件源管理",
|
|
370
379
|
sourcesDesc: "分工:① 软件源 = 下载 npm 包(安装用);② 索引源 = 市场首页清单 JSON;③ Git 源 = 克隆仓库代码(无 npm 包时);④ 搜索源 = 搜索时查哪个平台——搜到之后仍按 ①/③ 下载",
|
|
371
380
|
sourcePrimary: "主源",
|
|
@@ -548,6 +557,14 @@ window.__ModuleLoader__.load({
|
|
|
548
557
|
const en = {
|
|
549
558
|
tab: "Plugin console",
|
|
550
559
|
safetyTab: "Upgrade safety",
|
|
560
|
+
safetyModeManagedShort: "managed",
|
|
561
|
+
safetyModeObserverShort: "observer",
|
|
562
|
+
safetyBackfillTitle: "Missing dependency declarations",
|
|
563
|
+
safetyBackfillShort: "{n} to declare",
|
|
564
|
+
safetyBackfillDesc: "{n} installed plugins are not declared in the profile manifest — third-party plugin pages cannot see them and a pnpm operation may restore them. Declaring writes the on-disk version into the manifest (no repack, no reinstall).",
|
|
565
|
+
safetyBackfillNone: "Every installed plugin is declared in the manifest (visible to plugin pages and pnpm)",
|
|
566
|
+
safetyBackfillBtn: "Declare {n}",
|
|
567
|
+
safetyBackfillDone: "Declarations written",
|
|
551
568
|
safetyMode: "Operating mode",
|
|
552
569
|
safetyModeManaged: "Managed: this console owns framework upgrade and rollback (default, unchanged behaviour)",
|
|
553
570
|
safetyModeManagedOn: "Switched to managed: framework upgrade and rollback stay with this console",
|
|
@@ -824,6 +841,7 @@ window.__ModuleLoader__.load({
|
|
|
824
841
|
dismissFailure: "Dismiss this notice",
|
|
825
842
|
sourcesBtn: "Sources",
|
|
826
843
|
sourcesTitle: "Software sources",
|
|
844
|
+
sourcesTimeout: "Timed out reading sources (the DSH service may be restarting or busy) — hit retry",
|
|
827
845
|
sourcesDesc: "Roles: ① registry = download npm packages (install); ② index sources = market list JSON; ③ git sources = clone repo code (when no npm package); ④ search sources = where to search — results are still downloaded via ①/③",
|
|
828
846
|
sourcePrimary: "Primary",
|
|
829
847
|
setPrimary: "Set primary",
|
|
@@ -1943,6 +1961,9 @@ window.__ModuleLoader__.load({
|
|
|
1943
1961
|
react.useEffect(() => {
|
|
1944
1962
|
call("/plugin-console/sources").then(
|
|
1945
1963
|
(data) => {
|
|
1964
|
+
// 2026-09-24 用户实测:软件源弹窗每次打开都重新请求 → 展开要等(右侧「功能包」里的软件源因为是复用已取到的数据,所以很快)。
|
|
1965
|
+
// 这里把挂载时取到的整份配置缓存下来,弹窗直接渲染缓存;保存/恢复默认后再刷新缓存。
|
|
1966
|
+
setSourcesData((prev) => (prev === null ? data.sources : prev));
|
|
1946
1967
|
setSearchSourcesList(data.sources.searchSources ?? []);
|
|
1947
1968
|
setGitSourcesList(data.sources.gitSources ?? []);
|
|
1948
1969
|
},
|
|
@@ -2744,8 +2765,14 @@ window.__ModuleLoader__.load({
|
|
|
2744
2765
|
/** 软件源管理:打开时拉取当前配置。 */
|
|
2745
2766
|
const openSources = () => {
|
|
2746
2767
|
setSourcesOpen(true);
|
|
2747
|
-
|
|
2768
|
+
// 已有缓存(挂载时取过 / 上次打开取过)→ 立即渲染,不再白等一次往返
|
|
2769
|
+
if (sourcesData !== null && sourcesData.error === undefined) return;
|
|
2770
|
+
// 2026-09-24(图1 复现):请求若一直不落地,弹窗体只看 sourcesData === null 就会永远显示
|
|
2771
|
+
// 「正在读取插件…」——界面把失败吞掉,用户只能干等。这里给 15s 上限,超时/失败都落到可重试错误态。
|
|
2772
|
+
const timeout = new Promise((resolve) => setTimeout(() => resolve({ __timeout: true }), 15000));
|
|
2773
|
+
Promise.race([call("/plugin-console/sources"), timeout]).then(
|
|
2748
2774
|
(data) => {
|
|
2775
|
+
if (data !== null && data.__timeout === true) { setSourcesData({ error: t("sourcesTimeout") }); return }
|
|
2749
2776
|
setSourcesData(data.sources);
|
|
2750
2777
|
setSearchSourcesList(data.sources.searchSources ?? []);
|
|
2751
2778
|
setGiteeClientId(data.sources.gitee?.clientId ?? "");
|
|
@@ -2753,7 +2780,7 @@ window.__ModuleLoader__.load({
|
|
|
2753
2780
|
// 未配置时为空表单引导首次填写
|
|
2754
2781
|
setGiteeClientSecret("");
|
|
2755
2782
|
},
|
|
2756
|
-
(error) => {
|
|
2783
|
+
(error) => { setSourcesData({ error: friendlyGithubError(error).message }); },
|
|
2757
2784
|
);
|
|
2758
2785
|
};
|
|
2759
2786
|
const saveGiteeSetup = () => {
|
|
@@ -4692,7 +4719,11 @@ setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
|
|
|
4692
4719
|
el("p", { className: styles.message }, t("sourcesDesc")),
|
|
4693
4720
|
sourcesData === null
|
|
4694
4721
|
? el("p", { className: styles.status }, t("loading"))
|
|
4695
|
-
:
|
|
4722
|
+
: sourcesData.error !== undefined
|
|
4723
|
+
? el("div", null,
|
|
4724
|
+
el("p", { className: styles.status }, t("failed") + ":" + sourcesData.error),
|
|
4725
|
+
el("button", { type: "button", className: styles.toggle, onClick: () => { setSourcesData(null); openSources(); } }, t("retry")))
|
|
4726
|
+
: el("div", { className: styles.accWrap },
|
|
4696
4727
|
accSection("registries", t("sourcesBtn"), [
|
|
4697
4728
|
...sourcesData.registries.map((src) =>
|
|
4698
4729
|
editReg !== null && editReg.id === src.id
|
|
@@ -4845,28 +4876,32 @@ setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
|
|
|
4845
4876
|
: null,
|
|
4846
4877
|
);
|
|
4847
4878
|
}
|
|
4848
|
-
//
|
|
4849
|
-
//
|
|
4850
|
-
//
|
|
4851
|
-
//
|
|
4852
|
-
//
|
|
4853
|
-
//
|
|
4879
|
+
// 「升级安全」入口(2026-09-24 按用户要求改造为**长按钮 + 抽屉**):纯加法。
|
|
4880
|
+
// 默认只渲染一个长按钮,点开右侧抽屉 —— 里面才是模式开关、状态、一键适配、补声明与各功能入口。
|
|
4881
|
+
// 为什么不直接塞进「图2」那一页:那一页是第三方插件的 tab(@linxin666 的 family-plugins),
|
|
4882
|
+
// 它没有开放任何子插槽;官方 settings.plugins.tab 的 owner props 也是空的(没有切兄弟 tab 的 API)。
|
|
4883
|
+
// 所以做法是「就地抽屉」,而不是往别人的 DOM 里注入(注入会在对方更新时碎掉)。
|
|
4884
|
+
// 原 tab(id=console)与悬浮面板一行未动。
|
|
4854
4885
|
function PluginSafetyTab({ t }) {
|
|
4886
|
+
const [open, setOpen] = react.useState(false);
|
|
4855
4887
|
const [status, setStatus] = react.useState(null);
|
|
4856
4888
|
const [busy, setBusy] = react.useState(false);
|
|
4857
4889
|
const [note, setNote] = react.useState(null);
|
|
4858
4890
|
const [section, setSection] = react.useState(null);
|
|
4891
|
+
const [backfill, setBackfill] = react.useState(null);
|
|
4859
4892
|
const load = async () => {
|
|
4860
4893
|
try { const data = await call("/plugin-console/compat-status"); if (data !== null) setStatus(data); } catch {}
|
|
4861
4894
|
};
|
|
4862
|
-
|
|
4895
|
+
const loadBackfill = async () => {
|
|
4896
|
+
try { const data = await call("/plugin-console/declare-installed", { mode: "plan" }); if (data !== null) setBackfill(data); } catch {}
|
|
4897
|
+
};
|
|
4898
|
+
react.useEffect(() => { ensureSafetyStyle(); load(); loadBackfill(); }, []);
|
|
4863
4899
|
const switchMode = async (mode) => {
|
|
4864
4900
|
if (busy) return;
|
|
4865
4901
|
setBusy(true);
|
|
4866
4902
|
try {
|
|
4867
|
-
|
|
4903
|
+
await call("/plugin-console/compat-mode", { mode: mode });
|
|
4868
4904
|
setNote({ text: mode === "observer" ? t("safetyModeObserverOn") : t("safetyModeManagedOn") });
|
|
4869
|
-
if (data !== null) setStatus(null);
|
|
4870
4905
|
await load();
|
|
4871
4906
|
} catch (error) { setNote({ text: (error && error.message) || t("safetySwitchFailed") }); }
|
|
4872
4907
|
finally { setBusy(false); }
|
|
@@ -4882,55 +4917,94 @@ setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
|
|
|
4882
4917
|
const pre = await call("/plugin-console/framework-preflight", {});
|
|
4883
4918
|
const blockers = (pre && pre.scan && pre.scan.blockers) || 0;
|
|
4884
4919
|
if (!apply || blockers === 0) {
|
|
4885
|
-
const
|
|
4886
|
-
const
|
|
4887
|
-
setNote({ text: blockers === 0 ? t("fwPreflightClean") : fill(t("safetyPreflightFound"), [["n",
|
|
4920
|
+
const planned = await call("/plugin-console/framework-preflight-patch", { mode: "plan" });
|
|
4921
|
+
const n = (planned && planned.changes && planned.changes.length) || 0;
|
|
4922
|
+
setNote({ text: blockers === 0 ? t("fwPreflightClean") : fill(t("safetyPreflightFound"), [["n", n || blockers]]) });
|
|
4888
4923
|
} else {
|
|
4889
4924
|
const done = await call("/plugin-console/framework-preflight-patch", { mode: "apply" });
|
|
4890
|
-
const
|
|
4891
|
-
setNote({ text: fill(t("safetyPreflightPatched"), [["n",
|
|
4925
|
+
const n = (done && done.changes && done.changes.length) || 0;
|
|
4926
|
+
setNote({ text: fill(t("safetyPreflightPatched"), [["n", n]]) });
|
|
4892
4927
|
}
|
|
4893
4928
|
} catch (error) { setNote({ text: (error && error.message) || t("safetySwitchFailed") }); }
|
|
4894
4929
|
finally { setBusy(false); }
|
|
4895
4930
|
};
|
|
4931
|
+
const applyBackfill = async () => {
|
|
4932
|
+
if (busy) return;
|
|
4933
|
+
setBusy(true);
|
|
4934
|
+
try {
|
|
4935
|
+
const done = await call("/plugin-console/declare-installed", { mode: "apply" });
|
|
4936
|
+
setNote({ text: (done && done.note) || t("safetyBackfillDone") });
|
|
4937
|
+
await loadBackfill();
|
|
4938
|
+
} catch (error) { setNote({ text: (error && error.message) || t("safetySwitchFailed") }); }
|
|
4939
|
+
finally { setBusy(false); }
|
|
4940
|
+
};
|
|
4896
4941
|
const fill = (text, pairs) => {
|
|
4897
4942
|
let out = String(text);
|
|
4898
4943
|
for (const pair of pairs) out = out.split("{" + pair[0] + "}").join(String(pair[1]));
|
|
4899
4944
|
return out;
|
|
4900
4945
|
};
|
|
4901
|
-
|
|
4902
|
-
|
|
4946
|
+
// 图标:16px 线条 SVG(用户反馈:emoji 当 logo 太差)
|
|
4947
|
+
const stroke = { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", style: { flex: "0 0 auto", opacity: .8, marginTop: "2px" } };
|
|
4948
|
+
const ICONS = {
|
|
4949
|
+
shield: "M12 3l7 3v6c0 4-3 7-7 9-4-2-7-5-7-9V6z",
|
|
4950
|
+
pin: "M12 17v4M7 4h10l-1 6 3 3H5l3-3z",
|
|
4951
|
+
back: "M11 19l-7-7 7-7M4 12h11a5 5 0 015 5v1",
|
|
4952
|
+
scan: "M4 8V6a2 2 0 012-2h2M20 8V6a2 2 0 00-2-2h-2M4 16v2a2 2 0 002 2h2M20 16v2a2 2 0 01-2 2h-2M9 12h6",
|
|
4953
|
+
list: "M8 6h12M8 12h12M8 18h12M4 6h.01M4 12h.01M4 18h.01",
|
|
4954
|
+
pulse: "M3 12h4l2-5 4 10 2-5h6"
|
|
4955
|
+
};
|
|
4956
|
+
const icon = (key) => el("svg", stroke, el("path", { d: ICONS[key] || ICONS.shield }));
|
|
4957
|
+
const row = (key, title, desc, control) => el("div", { style: { display: "flex", alignItems: "flex-start", gap: "10px", padding: "10px 12px" } },
|
|
4958
|
+
icon(key),
|
|
4903
4959
|
el("div", { style: { flex: "1 1 auto", minWidth: "0" } },
|
|
4904
|
-
el("div", { style: { fontSize: "
|
|
4905
|
-
el("div", { style: { fontSize: "
|
|
4906
|
-
control === undefined ? null : control);
|
|
4907
|
-
const sw = (on, onClick, title) => el("button", { type: "button", title: title, onClick: onClick, disabled: busy, style: { width: "
|
|
4908
|
-
el("span", { style: { position: "absolute", top: "3px", left: on ? "21px" : "3px", width: "
|
|
4960
|
+
el("div", { style: { fontSize: "13px", fontWeight: 500 } }, title),
|
|
4961
|
+
el("div", { style: { fontSize: "12px", opacity: .7, marginTop: "2px", wordBreak: "break-word" } }, desc)),
|
|
4962
|
+
control === undefined || control === null ? null : control);
|
|
4963
|
+
const sw = (on, onClick, title) => el("button", { type: "button", title: title, onClick: onClick, disabled: busy, style: { width: "40px", height: "22px", borderRadius: "999px", border: "none", padding: 0, cursor: busy ? "default" : "pointer", background: on ? "var(--dsw-alias-bg-accent, #4d6bfe)" : "var(--dsw-alias-bg-secondary, rgba(127,127,127,.35))", position: "relative", flex: "0 0 auto", marginTop: "2px" } },
|
|
4964
|
+
el("span", { style: { position: "absolute", top: "3px", left: on ? "21px" : "3px", width: "16px", height: "16px", borderRadius: "50%", background: "#fff", transition: "left .18s cubic-bezier(.34,1.56,.64,1)" } }));
|
|
4909
4965
|
const jump = (id, label) => el("button", { key: id, type: "button", className: styles.toggle, style: { marginRight: "8px", marginBottom: "8px" }, onClick: () => setSection(id) }, label);
|
|
4910
|
-
const mode = status !== null && status.mode === "observer" ? "observer" : "managed";
|
|
4966
|
+
const mode = status !== null && status.mode === "observer" ? "observer" : status !== null && status.mode === "managed" ? "managed" : "observer";
|
|
4911
4967
|
const fp = status !== null && status.fingerprint !== null && status.fingerprint !== undefined ? status.fingerprint : {};
|
|
4912
4968
|
const rb = status !== null && status.rollback !== null && status.rollback !== undefined ? status.rollback : {};
|
|
4913
4969
|
const fw = status !== null && typeof status.frameworkVersion === "string" && status.frameworkVersion !== "" ? status.frameworkVersion : t("safetyUnknown");
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4970
|
+
const pending = backfill !== null && typeof backfill.count === "number" ? backfill.count : 0;
|
|
4971
|
+
// 触发条:默认只看到这一行(点开在**原地**展开,功能包那种下拉,不走右侧固定抽屉)
|
|
4972
|
+
const trigger = el("button", { type: "button", "aria-expanded": open ? "true" : "false", onClick: () => setOpen((prev) => !prev), style: { width: "100%", display: "flex", alignItems: "center", gap: "10px", padding: "12px 14px", borderRadius: "12px", border: "1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,.18))", background: "var(--dsw-alias-bg-layer-4, rgba(127,127,127,.10))", cursor: "pointer", textAlign: "left", color: "inherit" } },
|
|
4973
|
+
icon("shield"),
|
|
4974
|
+
el("div", { style: { flex: "1 1 auto", minWidth: "0" } },
|
|
4975
|
+
el("div", { style: { fontSize: "13px", fontWeight: 500 } }, t("safetyTab")),
|
|
4976
|
+
el("div", { style: { fontSize: "12px", opacity: .7, marginTop: "2px" } }, (mode === "observer" ? t("safetyModeObserverShort") : t("safetyModeManagedShort")) + " · " + fw + (pending > 0 ? " · " + fill(t("safetyBackfillShort"), [["n", pending]]) : ""))),
|
|
4977
|
+
el("span", { style: { opacity: .5, flex: "0 0 auto", transform: open ? "rotate(90deg)" : "none", transition: "transform .2s cubic-bezier(.34,1.56,.64,1)" } }, el("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" }, el("path", { d: "M9 6l6 6-6 6" }))));
|
|
4978
|
+
const panel = el("div", { className: "pc-safety-panel", style: { marginTop: "8px", padding: "8px", borderRadius: "12px", border: "1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,.18))", background: "var(--dsw-alias-bg-layer-4, rgba(127,127,127,.10))", display: "flex", flexDirection: "column", gap: "4px" } },
|
|
4979
|
+
row("shield", t("safetyMode"), mode === "observer" ? t("safetyModeObserver") : t("safetyModeManaged") + (status !== null && typeof status.modeReason === "string" && status.modeReason !== "" ? "(" + status.modeReason + ")" : ""), sw(mode === "observer", () => switchMode(mode === "observer" ? "managed" : "observer"), t("safetyModeSwitch"))),
|
|
4980
|
+
row("pin", t("safetyFrameworkTitle"), fill(t("safetyFrameworkDesc"), [["v", fw]])),
|
|
4981
|
+
row("back", t("safetyRollbackTitle"), rb.applicable === true ? fill(t("safetyRollbackOk"), [["from", rb.from], ["to", rb.to]]) : fill(t("safetyRollbackNone"), [["reason", rb.reason === null || rb.reason === undefined ? "-" : rb.reason]])),
|
|
4982
|
+
row("scan", t("safetyFpTitle"), fp.changed === true ? fill(t("safetyFpChanged"), [["reasons", (fp.reasons || []).join(";")]]) : t("safetyFpStable"), fp.changed === true ? el("button", { type: "button", className: styles.toggle, onClick: ack, disabled: busy }, t("safetyFpAck")) : null),
|
|
4983
|
+
row("list", t("safetyBackfillTitle"), pending > 0 ? fill(t("safetyBackfillDesc"), [["n", pending]]) : t("safetyBackfillNone"), pending > 0 ? el("button", { type: "button", className: styles.toggle, disabled: busy, onClick: applyBackfill }, fill(t("safetyBackfillBtn"), [["n", pending]])) : null),
|
|
4984
|
+
row("pulse", t("safetyAdaptTitle"), t("safetyAdaptDesc"), el("div", { style: { display: "flex", gap: "8px", flex: "0 0 auto" } },
|
|
4920
4985
|
el("button", { type: "button", className: styles.toggle, disabled: busy, onClick: () => preflight(false) }, t("fwPreflightBtn")),
|
|
4921
4986
|
el("button", { type: "button", className: styles.toggle, disabled: busy, onClick: () => preflight(true) }, t("fwPreflightPatch")))),
|
|
4922
|
-
note === null ? null : el("div", { style: { fontSize: "
|
|
4923
|
-
el("div",
|
|
4924
|
-
el("div", { style: { fontSize: "12.5px", opacity: .6, margin: "4px 0 8px" } }, t("safetyJumpHint")),
|
|
4987
|
+
note === null ? null : el("div", { style: { fontSize: "12px", opacity: .8, padding: "0 12px 6px" } }, note.text),
|
|
4988
|
+
el("div", { style: { padding: "4px 12px 2px" } },
|
|
4925
4989
|
jump("market", t("safetyJumpMarket")),
|
|
4926
4990
|
jump("skills", t("safetyJumpSkills")),
|
|
4927
4991
|
jump("framework", t("safetyJumpFramework")),
|
|
4928
4992
|
jump("gate", t("safetyJumpGate")),
|
|
4929
4993
|
jump("sources", t("safetyJumpSources")),
|
|
4930
4994
|
jump("ai", t("safetyJumpAi"))),
|
|
4931
|
-
section === null ? null : el("div", { style: {
|
|
4932
|
-
el("div", { style: { fontSize: "12.5px", opacity: .6, marginBottom: "8px" } }, t("safetyEmbedded")),
|
|
4995
|
+
section === null ? null : el("div", { style: { margin: "4px 12px 8px", borderTop: "1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,.18))", paddingTop: "10px" } },
|
|
4933
4996
|
el(PluginConsoleTab, { t: t, section: section, key: section })));
|
|
4997
|
+
return el("div", null, trigger, open ? panel : null);
|
|
4998
|
+
}
|
|
4999
|
+
// 「功能包」那种就地展开的 Q 弹动画(一次性注入,不依赖外部 CSS)
|
|
5000
|
+
function ensureSafetyStyle() {
|
|
5001
|
+
try {
|
|
5002
|
+
if (typeof document === "undefined" || document.querySelector("style[data-pc-safety]") !== null) return;
|
|
5003
|
+
const tag = document.createElement("style");
|
|
5004
|
+
tag.dataset.pcSafety = "1";
|
|
5005
|
+
tag.textContent = "@keyframes pcSafetyPop{0%{opacity:0;transform:translateY(-6px) scaleY(.96)}60%{opacity:1;transform:translateY(0) scaleY(1.02)}100%{transform:translateY(0) scaleY(1)}}.pc-safety-panel{animation:pcSafetyPop .28s cubic-bezier(.34,1.56,.64,1);transform-origin:top center}";
|
|
5006
|
+
document.head.appendChild(tag);
|
|
5007
|
+
} catch {}
|
|
4934
5008
|
}
|
|
4935
5009
|
function apply(ctx) {
|
|
4936
5010
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "plugin-console: dictionaries");
|
|
@@ -4943,12 +5017,16 @@ setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
|
|
|
4943
5017
|
locale: NS,
|
|
4944
5018
|
inject: () => ({}),
|
|
4945
5019
|
}, PluginConsoleTab));
|
|
4946
|
-
//
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
5020
|
+
// 「升级安全」入口:注册进**官方插件管理页自己的子插槽**(2026-09-24 落点更正)。
|
|
5021
|
+
// 官方页 @deepseek-ai/dsh-client-ui-plugin-manager 占 main + sidebar.panellist,
|
|
5022
|
+
// 并声明了 children 子插槽;其中 `plugins.bundle.config` 按**包名**挂该 bundle 的配置卡片:
|
|
5023
|
+
// 渲染点 = renderSlot("plugins.bundle.config", { view: "page" }, { entryKey: pkg.name })
|
|
5024
|
+
// 且仅当 configured = ledger.bundles.has(pkg.name) 为真 —— 我们是 profile bundle,天然满足。
|
|
5025
|
+
// 所以入口长在那一页「已安装 → 我们的 bundle」详情里,用的是官方容器与官方风格,不是 DOM 注入。
|
|
5026
|
+
// 之前那个独立的「升级安全」tab 已按用户要求关闭(错误落点不占位);原 tab(id=console) 一行未动。
|
|
5027
|
+
ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
|
|
5028
|
+
name: "plugins.bundle.config",
|
|
5029
|
+
key: "@noob-stupid/dsh-plugin-console",
|
|
4952
5030
|
locale: NS,
|
|
4953
5031
|
inject: () => ({}),
|
|
4954
5032
|
}, PluginSafetyTab));
|
|
@@ -14,7 +14,8 @@ import { dshHome } from '../infra/paths.js'
|
|
|
14
14
|
|
|
15
15
|
/** 运行模式。managed = 托管(我们负责框架升级与回滚,既有行为);observer = 观察者(只守门,不碰升级)。 */
|
|
16
16
|
const COMPAT_MODES = ['managed', 'observer']
|
|
17
|
-
|
|
17
|
+
// 默认**观察者**(用户 2026-09-24 定):只守门、不接管框架升级;一旦用户走了本控制台的框架升级,服务端会自动切回托管
|
|
18
|
+
const DEFAULT_MODE = 'observer'
|
|
18
19
|
|
|
19
20
|
const stateDir = () => join(dshHome(), 'plugin-console')
|
|
20
21
|
const modeFile = () => join(stateDir(), 'compat-mode.json')
|
|
@@ -41,18 +42,18 @@ function writeJson(path, value) {
|
|
|
41
42
|
function readCompatMode() {
|
|
42
43
|
const rec = readJson(modeFile())
|
|
43
44
|
const mode = typeof rec?.mode === 'string' && COMPAT_MODES.includes(rec.mode) ? rec.mode : DEFAULT_MODE
|
|
44
|
-
return { mode, at: typeof rec?.at === 'number' ? rec.at : null, source: rec === null ? 'default' : 'file' }
|
|
45
|
+
return { mode, at: typeof rec?.at === 'number' ? rec.at : null, reason: typeof rec?.reason === 'string' ? rec.reason : null, source: rec === null ? 'default' : 'file' }
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* 写运行模式(只接受白名单值,避免拼错导致「模式静默失效」)。
|
|
49
50
|
* @returns {{ ok: boolean, mode: string, error?: string }}
|
|
50
51
|
*/
|
|
51
|
-
function writeCompatMode(mode) {
|
|
52
|
+
function writeCompatMode(mode, meta = {}) {
|
|
52
53
|
if (typeof mode !== 'string' || !COMPAT_MODES.includes(mode)) {
|
|
53
54
|
return { ok: false, mode: readCompatMode().mode, error: `未知模式 ${String(mode)}(可选:${COMPAT_MODES.join(' / ')})` }
|
|
54
55
|
}
|
|
55
|
-
writeJson(modeFile(), { mode, at: Date.now() })
|
|
56
|
+
writeJson(modeFile(), { mode, at: Date.now(), reason: typeof meta.reason === 'string' ? meta.reason : null })
|
|
56
57
|
return { ok: true, mode }
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -117,4 +117,50 @@ async function undeclareProfileDependency(profileDir, packageName) {
|
|
|
117
117
|
})
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
/**
|
|
121
|
+
* 扫描「已装但未声明」的插件(补声明用)。
|
|
122
|
+
*
|
|
123
|
+
* 判据刻意保守:只看 profile 的 `dsh.profile.bundles` 里**非默认**且**真装在磁盘上**的包。
|
|
124
|
+
* 为什么不扫整个 node_modules:那会把框架自带包、传递依赖一并卷进来(它们本来就不该出现在
|
|
125
|
+
* 用户清单里)。bundles 是「用户显式加进组合树的东西」—— 正是该被声明的那一批。
|
|
126
|
+
*
|
|
127
|
+
* 为什么值得做(2026-09-24 用户实测):第三方插件管理器(如 @linxin666 的插件页)与 pnpm
|
|
128
|
+
* 都只认清单里的依赖 —— 手铺安装的包在它们眼里不存在:看不见,还会被 pnpm 还原。
|
|
129
|
+
* @returns {{ candidates: {name: string, version: string}[], skipped: {name: string, reason: string}[] }}
|
|
130
|
+
*/
|
|
131
|
+
async function planDependencyBackfill(profileDir) {
|
|
132
|
+
const manifest = await readManifest(profileDir)
|
|
133
|
+
const bundles = Array.isArray(manifest.dsh?.profile?.bundles) ? manifest.dsh.profile.bundles : []
|
|
134
|
+
const deps = manifest.dependencies ?? {}
|
|
135
|
+
const candidates = []
|
|
136
|
+
const skipped = []
|
|
137
|
+
for (const name of bundles) {
|
|
138
|
+
if (typeof name !== 'string' || name === '') continue
|
|
139
|
+
if (DEFAULT_BUNDLES.includes(name)) continue
|
|
140
|
+
if (typeof deps[name] === 'string' && deps[name] !== '') continue
|
|
141
|
+
const version = await installedVersionOf(profileDir, name)
|
|
142
|
+
if (version === null) {
|
|
143
|
+
skipped.push({ name, reason: '磁盘上读不到已安装版本(可能已卸载)' })
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
candidates.push({ name, version })
|
|
147
|
+
}
|
|
148
|
+
return { candidates, skipped }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 执行补声明:逐条写入清单(每条独立走写队列,一条失败不影响其它)。 */
|
|
152
|
+
async function applyDependencyBackfill(profileDir) {
|
|
153
|
+
const { candidates, skipped } = await planDependencyBackfill(profileDir)
|
|
154
|
+
const declared = []
|
|
155
|
+
for (const item of candidates) {
|
|
156
|
+
const result = await declareProfileDependency(profileDir, item.name, item.version)
|
|
157
|
+
if (result.changed) declared.push({ name: item.name, version: result.version })
|
|
158
|
+
else skipped.push({ name: item.name, reason: result.reason ?? '无需改动' })
|
|
159
|
+
}
|
|
160
|
+
return { declared, skipped }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export {
|
|
164
|
+
DEFAULT_BUNDLES, addBundleToManifest, applyDependencyBackfill, declareProfileDependency,
|
|
165
|
+
installedVersionOf, planDependencyBackfill, removeBundleFromManifest, undeclareProfileDependency,
|
|
166
|
+
}
|
|
@@ -1,25 +1,44 @@
|
|
|
1
|
-
// L2 · routes —— 门控常驻化(观察者/托管模式 · 环境指纹 ·
|
|
2
|
-
// GET /plugin-console/compat-status
|
|
3
|
-
// POST /plugin-console/compat-mode
|
|
4
|
-
// POST /plugin-console/compat-stamp
|
|
1
|
+
// L2 · routes —— 门控常驻化(观察者/托管模式 · 环境指纹 · 回滚点)+ 官方互操作
|
|
2
|
+
// GET /plugin-console/compat-status 一次拿齐:模式 + 框架版本 + 指纹变化 + 回滚点可用性
|
|
3
|
+
// POST /plugin-console/compat-mode 切换运行模式(managed / observer)
|
|
4
|
+
// POST /plugin-console/compat-stamp 把当前指纹记为基线(用户确认「环境变更」后调用)
|
|
5
|
+
// POST /plugin-console/declare-installed 补声明:把「已装但未声明」的插件写进 profile 依赖
|
|
5
6
|
//
|
|
6
7
|
// 与 /framework-preflight 的分工:预检负责「扫什么」,这里负责「什么时候扫、以什么模式守门」。
|
|
7
8
|
// 触发源不再是我们的升级按钮,而是环境指纹变化 —— 官方桌面端自带升级器 / 官方只发桌面端 /
|
|
8
9
|
// 手动 pnpm 操作,都会在这里被发现(见 domain/compat-state.js 头注释)。
|
|
10
|
+
//
|
|
11
|
+
// declare-installed 存在的理由(2026-09-24 用户实测):第三方插件管理器与 pnpm 都只认清单里的
|
|
12
|
+
// 依赖,手铺安装的包在它们眼里不存在(看不见 + 会被还原)。老规矩:先 plan 再 apply。
|
|
9
13
|
|
|
10
14
|
import { dirname } from 'node:path'
|
|
11
15
|
import { compareFingerprint, computeFingerprint, readCompatMode, readFingerprint, readRollbackRecord, stampFingerprint, writeCompatMode } from '../domain/compat-state.js'
|
|
12
16
|
import { currentFrameworkVersion } from '../domain/framework.js'
|
|
17
|
+
import { applyDependencyBackfill, planDependencyBackfill } from '../domain/manifest.js'
|
|
13
18
|
import { sendError, sendJson } from '../infra/httpd.js'
|
|
14
19
|
import { baseDirOf, profileDirOf, resolvePackageJson } from '../infra/paths.js'
|
|
20
|
+
import { existsSync } from 'node:fs'
|
|
21
|
+
import { join } from 'node:path'
|
|
15
22
|
|
|
16
|
-
/** 框架运行时根(含 .pnpm 的那层):换了安装根(npx 缓存 ↔ 桌面端自带运行时)指纹必变。
|
|
23
|
+
/** 框架运行时根(含 .pnpm 的那层):换了安装根(npx 缓存 ↔ 桌面端自带运行时)指纹必变。
|
|
24
|
+
* `@deepseek-ai/dsh` 既可能解析到 `<root>/node_modules/@deepseek-ai/dsh`(顶层投影),
|
|
25
|
+
* 也可能落在 `<root>/.pnpm/@deepseek-ai+dsh@…/node_modules/…`(实体内部)—— 后者直接
|
|
26
|
+
* `dirname(dirname())` 会停在 `.pnpm` **内部**、数不到包数(门槛实测 `entities=null`)。
|
|
27
|
+
* 所以向上逐级找「含 .pnpm 的那一层」为止。 */
|
|
17
28
|
function frameworkPnpmRoot(ports, profileDir) {
|
|
18
29
|
try {
|
|
19
30
|
const baseDir = baseDirOf(ports?.baseUrl ?? 'file:///')
|
|
20
31
|
const pkgPath = resolvePackageJson('@deepseek-ai/dsh', baseDir, profileDir)
|
|
21
32
|
if (pkgPath === null) return null
|
|
22
|
-
|
|
33
|
+
const direct = dirname(dirname(pkgPath))
|
|
34
|
+
let dir = direct
|
|
35
|
+
for (let i = 0; i < 6; i += 1) {
|
|
36
|
+
if (existsSync(join(dir, '.pnpm'))) return dir
|
|
37
|
+
const parent = dirname(dir)
|
|
38
|
+
if (parent === dir) break
|
|
39
|
+
dir = parent
|
|
40
|
+
}
|
|
41
|
+
return direct
|
|
23
42
|
} catch {
|
|
24
43
|
return null
|
|
25
44
|
}
|
|
@@ -48,6 +67,7 @@ async function routeCompatStatusGet(req, res, rc) {
|
|
|
48
67
|
mode: mode.mode,
|
|
49
68
|
modeSource: mode.source,
|
|
50
69
|
modeChangedAt: mode.at,
|
|
70
|
+
modeReason: mode.reason ?? null,
|
|
51
71
|
frameworkVersion: current.frameworkVersion,
|
|
52
72
|
fingerprint: { current, previous, changed, reasons, baselineAt },
|
|
53
73
|
rollback: readRollbackRecord(),
|
|
@@ -66,4 +86,29 @@ async function routeCompatStamp(req, res, rc) {
|
|
|
66
86
|
sendJson(res, 200, { ok: true, fingerprint: stamped })
|
|
67
87
|
}
|
|
68
88
|
|
|
69
|
-
|
|
89
|
+
/** 补声明:plan 只报「有几项要补」,apply 真写清单(逐条写、逐条回报)。 */
|
|
90
|
+
async function routeDeclareInstalled(req, res, rc) {
|
|
91
|
+
const mode = rc.body?.mode === 'apply' ? 'apply' : 'plan'
|
|
92
|
+
const profileDir = profileDirOf(rc.ctx)
|
|
93
|
+
try {
|
|
94
|
+
if (mode === 'apply') {
|
|
95
|
+
const result = await applyDependencyBackfill(profileDir)
|
|
96
|
+
sendJson(res, 200, {
|
|
97
|
+
ok: true,
|
|
98
|
+
mode,
|
|
99
|
+
declared: result.declared,
|
|
100
|
+
skipped: result.skipped,
|
|
101
|
+
note: result.declared.length === 0
|
|
102
|
+
? '没有需要补声明的插件'
|
|
103
|
+
: `已补声明 ${result.declared.length} 项 —— 它们现在会出现在官方/第三方插件页的「已安装」里,也不会再被 pnpm 还原`,
|
|
104
|
+
})
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
const plan = await planDependencyBackfill(profileDir)
|
|
108
|
+
sendJson(res, 200, { ok: true, mode, candidates: plan.candidates, skipped: plan.skipped, count: plan.candidates.length })
|
|
109
|
+
} catch (error) {
|
|
110
|
+
sendError(res, 500, `补声明失败:${error instanceof Error ? error.message : String(error)}`)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export { routeCompatMode, routeCompatStamp, routeCompatStatusGet, routeDeclareInstalled }
|
|
@@ -10,6 +10,7 @@ import { dirname, join } from 'node:path'
|
|
|
10
10
|
import { tmpdir } from 'node:os'
|
|
11
11
|
import { pathToFileURL } from 'node:url'
|
|
12
12
|
import { createRequire } from 'node:module'
|
|
13
|
+
import { readCompatMode, writeCompatMode } from '../domain/compat-state.js'
|
|
13
14
|
import { FRAMEWORK_BACKUP_ROOT, backupProfileSnapshot, checkpointFrameworkTree, preflightDisableIncompatible, relaunchPrelude, resolveDshBin, resolveFrameworkRootNodeModules, resolveFrameworkUpgradePlan } from '../domain/framework.js'
|
|
14
15
|
import { fwIntegrityCheck } from '../infra/fw-integrity-check.js'
|
|
15
16
|
import { CORE_PATCH_ROW_IDS } from '../domain/patch.js'
|
|
@@ -22,6 +23,21 @@ import { sendError, sendJson } from '../infra/httpd.js'
|
|
|
22
23
|
import { dshHome, findPatchPath, pluginRoot } from '../infra/paths.js'
|
|
23
24
|
import { setFwCheckCache } from '../state.js'
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* 用户语义(2026-09-24):「运行模式」默认开启 = **观察者**(只守门、不接管升级);
|
|
28
|
+
* 但只要有人走了本控制台这条框架升级路径,就**自动切回托管** —— 此时升级确实由我们负责。
|
|
29
|
+
* 返回提示文案(无切换时返回 null)。
|
|
30
|
+
*/
|
|
31
|
+
function autoSwitchToManaged(reason) {
|
|
32
|
+
try {
|
|
33
|
+
if (readCompatMode().mode === 'managed') return null
|
|
34
|
+
writeCompatMode('managed', { reason })
|
|
35
|
+
return '检测到使用本控制台的框架升级:运行模式已自动切回「托管」'
|
|
36
|
+
} catch {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
25
41
|
async function routeFrameworkUpgrade(req, res, rc) {
|
|
26
42
|
const backupProfileSnapshot = rc.deps.backupProfileSnapshot
|
|
27
43
|
const preflightDisableIncompatible = rc.deps.preflightDisableIncompatible
|
|
@@ -103,6 +119,9 @@ async function routeFrameworkUpgrade(req, res, rc) {
|
|
|
103
119
|
// 保证新框架 boot 时不会被某行 import 失败拖垮(loader 单行失败 = 整个服务起不来)。
|
|
104
120
|
let preflight = { disabled: [], skipped: [] }
|
|
105
121
|
if (hasUpdate) {
|
|
122
|
+
// 用户语义(2026-09-24):走本控制台这条升级路径 = 升级由我们负责 → 自动从观察者切回托管
|
|
123
|
+
const switched = autoSwitchToManaged('使用本控制台执行框架升级')
|
|
124
|
+
if (switched !== null) steps.push(switched)
|
|
106
125
|
try {
|
|
107
126
|
preflight = await preflightDisableIncompatible({ ports: ctx, profileDir, patchPath: findPatchPath(ctx), targetVersion: target })
|
|
108
127
|
} catch (error) {
|
|
@@ -14,7 +14,7 @@ import { runSuiteInstallJob } from '../domain/suite.js'
|
|
|
14
14
|
import { readBody, sendError } from '../infra/httpd.js'
|
|
15
15
|
import { routeAiConsent, routeAiEmpowerCancel, routeAiEmpowerList, routeAiEmpowerPlan, routeAiEmpowerRun, routeAiEmpowerStatus } from './ai.js'
|
|
16
16
|
import { routeComponentAutostart, routeComponentStart, routeComponentStatus, routeComponentStop, routeComponents, routeRepoClone, routeRepoLandConfig, routeRepoList, routeRepoOpen, routeRepoRemove } from './components.js'
|
|
17
|
-
import { routeCompatMode, routeCompatStamp, routeCompatStatusGet } from './compat.js'
|
|
17
|
+
import { routeCompatMode, routeCompatStamp, routeCompatStatusGet, routeDeclareInstalled } from './compat.js'
|
|
18
18
|
import { routeFrameworkUpgrade } from './framework-upgrade.js'
|
|
19
19
|
import { routeFrameworkPreflight, routeFrameworkPreflightPatch } from './framework-preflight.js'
|
|
20
20
|
import { routeCheckUpdate, routeCompatGate, routeFrameworkCheck, routeFrameworkRelaunch, routeFrameworkRollback, routeFrameworkUpgradeStatusGet, routeRestart } from './framework.js'
|
|
@@ -95,6 +95,7 @@ const ROUTES = [
|
|
|
95
95
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/framework-preflight-patch`, handler: routeFrameworkPreflightPatch },
|
|
96
96
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/compat-mode`, handler: routeCompatMode },
|
|
97
97
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/compat-stamp`, handler: routeCompatStamp },
|
|
98
|
+
{ methods: ['POST'], path: `${ROUTE_PREFIX}/declare-installed`, handler: routeDeclareInstalled },
|
|
98
99
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/framework-rollback`, handler: routeFrameworkRollback },
|
|
99
100
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/restart`, handler: routeRestart },
|
|
100
101
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/toggle`, handler: routeToggle },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noob-stupid/dsh-plugin-console",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "DSH 框架升级安全与插件升级门控:一键升级、失败自动回滚、升级后回滚上版、旧插件不适配自动禁用;内置多源插件市场为发现层,插件源全部可自定义,可指向公司内网私有源 / 私有索引 / 本地 Git 仓库,纯内网离线可用 | Framework upgrade safety & plugin version gating for DSH, with a customizable multi-source plugin market: point every source at internal mirrors or a local file:// repo for intranet-only, offline installs.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|