@noob-stupid/dsh-plugin-console 0.3.65 → 0.3.67
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/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +91 -4
- package/lib/index.js +198 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -336,4 +336,4 @@ The layered-refactor preview lives in its **own repository**: [**`Noob-stupid/ds
|
|
|
336
336
|
- **What it is**: the monolithic `lib/index.js` (8547 lines) split into layers — `lib/index.js` **141 lines** + `lib/server/**` **38 modules**; **feature-equivalent to `0.3.57`**, maintainability only;
|
|
337
337
|
- **Verified**: 19 test suites green · 8 architecture-guard assertions · **route inventory identical** (`status` + response fields) against the stable build;
|
|
338
338
|
- **Not yet exercised**: real framework upgrade / rollback, restart guardian, component start/stop, real AI run, Gitee OAuth (~10–25% long-tail risk, subjective);
|
|
339
|
-
- **For everyday use** stick to npm `@noob-stupid/dsh-plugin-console@0.3.
|
|
339
|
+
- **For everyday use** stick to npm `@noob-stupid/dsh-plugin-console@0.3.67`, or `main` of this repo.
|
package/README.zh.md
CHANGED
|
@@ -300,5 +300,5 @@ MIT
|
|
|
300
300
|
- **已验证**:19 套测试全绿 · 8 条架构守卫断言 · 与稳定版逐条对打**路由清单一致**(`status` + 响应字段);
|
|
301
301
|
- **已实测**(2026-09-20 真装真卸演练):普通插件 / bundle 插件 / 无 npm 仓库 / 套装 / 技能 / 聚合仓库子包 / 仓库落地 / 服务器组件启停;
|
|
302
302
|
- **尚未实测**:真框架升级 / 真回滚、重启守护链路、AI 真跑、Gitee OAuth 回调(长尾风险主观估计 **10%~25%**);
|
|
303
|
-
- **日常使用请继续用**:npm `@noob-stupid/dsh-plugin-console@0.3.
|
|
303
|
+
- **日常使用请继续用**:npm `@noob-stupid/dsh-plugin-console@0.3.67`,或本仓库 `main`。
|
|
304
304
|
- 请多反馈问题
|
package/lib/client.js
CHANGED
|
@@ -143,6 +143,15 @@ window.__ModuleLoader__.load({
|
|
|
143
143
|
gateModalTitle: "门控(兼容门总开关)",
|
|
144
144
|
gatePendingInfo: "当前待适配:{n} 行(升级预扫或启动失败隔离记下的,更新插件后可一键解锁)",
|
|
145
145
|
gatePendingNone: "当前没有待适配的行",
|
|
146
|
+
gateListPending: "待适配明细({n} 行)",
|
|
147
|
+
gateListAdopted: "已适配解锁({n} 行,历史上被拦过、现已放行)",
|
|
148
|
+
gateListMore: "展开全部 {n} 行",
|
|
149
|
+
gateListFold: "收起列表",
|
|
150
|
+
gateRowReason: "原因:{reason}",
|
|
151
|
+
gateRowFromBoot: "来源:启动失败隔离",
|
|
152
|
+
gateRowFromUpgrade: "来源:框架升级预扫",
|
|
153
|
+
gateRowAt: " · {t}",
|
|
154
|
+
gateRowAdoptable: " · 已通过源码扫描,可解锁",
|
|
146
155
|
fwFailedButUpgraded: "框架本体其实已经升到 {v} 并已生效——失败的只是脚本最后「重启服务」那一步。可点「重启服务」确认,或必要时回滚。",
|
|
147
156
|
fwPanelBtn: "框架",
|
|
148
157
|
fwPanelTitle: "框架升级 / 回滚",
|
|
@@ -463,6 +472,12 @@ window.__ModuleLoader__.load({
|
|
|
463
472
|
frameworkPatchSkipped: "无需应用",
|
|
464
473
|
frameworkUpgradeBtn: "框架升级",
|
|
465
474
|
frameworkUpgradeTitle: "框架升级:自动打包备份现有配置 → 停止并重启 DSH 服务 → npm 升级框架与配套包(失败自动回滚)→ 拉起服务并适配",
|
|
475
|
+
frameworkUpgradeTarget: "升级目标",
|
|
476
|
+
frameworkUpgradePick: "选择要升级到的版本(列出所有比当前新的版本,含预发布;latest 为稳定版)",
|
|
477
|
+
frameworkUpgradeMore: "展开全部可选版本({n})",
|
|
478
|
+
frameworkUpgradeFold: "收起版本列表",
|
|
479
|
+
frameworkUpgradeChannelStable: "稳定版",
|
|
480
|
+
frameworkUpgradeChannelPre: "预发布",
|
|
466
481
|
frameworkUpgradeConfirm: "确认升级?(服务将自动重启)",
|
|
467
482
|
frameworkUpgradeDone: "框架升级流程已执行",
|
|
468
483
|
frameworkRollbackBtn: "回滚到上一版",
|
|
@@ -540,6 +555,15 @@ window.__ModuleLoader__.load({
|
|
|
540
555
|
gateModalTitle: "Gating (compat gate master switches)",
|
|
541
556
|
gatePendingInfo: "Pending adaptation: {n} row(s) — recorded by the upgrade pre-scan or boot quarantine; unlock them after updating the plugin",
|
|
542
557
|
gatePendingNone: "No rows pending adaptation",
|
|
558
|
+
gateListPending: "Pending rows ({n})",
|
|
559
|
+
gateListAdopted: "Adapted & unlocked ({n} — blocked at some point, now allowed)",
|
|
560
|
+
gateListMore: "Show all {n} rows",
|
|
561
|
+
gateListFold: "Collapse list",
|
|
562
|
+
gateRowReason: "Reason: {reason}",
|
|
563
|
+
gateRowFromBoot: "Source: boot-failure quarantine",
|
|
564
|
+
gateRowFromUpgrade: "Source: framework upgrade pre-scan",
|
|
565
|
+
gateRowAt: " · {t}",
|
|
566
|
+
gateRowAdoptable: " · passed the source scan, can be unlocked",
|
|
543
567
|
fwFailedButUpgraded: "The framework itself is already on {v} and active — only the final \"restart service\" step failed. Use Restart service to confirm, or roll back if needed.",
|
|
544
568
|
fwPanelBtn: "Framework",
|
|
545
569
|
fwPanelTitle: "Framework upgrade / rollback",
|
|
@@ -860,6 +884,12 @@ window.__ModuleLoader__.load({
|
|
|
860
884
|
frameworkPatchSkipped: "not needed",
|
|
861
885
|
frameworkUpgradeBtn: "Framework upgrade",
|
|
862
886
|
frameworkUpgradeTitle: "Framework upgrade: backup config → stop & restart DSH service → npm upgrade framework + official packages (auto rollback on failure) → relaunch and adapt",
|
|
887
|
+
frameworkUpgradeTarget: "Upgrade target",
|
|
888
|
+
frameworkUpgradePick: "Pick a version to upgrade to (every version newer than the current one, pre-releases included; latest = stable channel)",
|
|
889
|
+
frameworkUpgradeMore: "Show all available versions ({n})",
|
|
890
|
+
frameworkUpgradeFold: "Collapse version list",
|
|
891
|
+
frameworkUpgradeChannelStable: "stable",
|
|
892
|
+
frameworkUpgradeChannelPre: "pre-release",
|
|
863
893
|
frameworkUpgradeConfirm: "Confirm upgrade? (service will restart)",
|
|
864
894
|
frameworkUpgradeDone: "Framework upgrade flow executed",
|
|
865
895
|
frameworkUpgradeUpToDate: " (already latest)",
|
|
@@ -1353,6 +1383,15 @@ window.__ModuleLoader__.load({
|
|
|
1353
1383
|
const [fwCheck, setFwCheck] = react.useState(null);
|
|
1354
1384
|
const [fwCheckBusy, setFwCheckBusy] = react.useState(false);
|
|
1355
1385
|
const [fwConfirmPanel, setFwConfirmPanel] = react.useState(false);
|
|
1386
|
+
// 可选升级版本列表(用户 2026-09-23 要求):默认收起;选中项优先用户选择 → latest → 原 target
|
|
1387
|
+
const [fwPicking, setFwPicking] = react.useState(false);
|
|
1388
|
+
const [fwPickAll, setFwPickAll] = react.useState(false);
|
|
1389
|
+
const [fwSelectedVersion, setFwSelectedVersion] = react.useState(null);
|
|
1390
|
+
const fwVersions = Array.isArray(fwCheck && fwCheck.versions) ? fwCheck.versions : [];
|
|
1391
|
+
const fwSelected = fwSelectedVersion !== null && fwVersions.some((v) => v.version === fwSelectedVersion)
|
|
1392
|
+
? fwSelectedVersion
|
|
1393
|
+
: (fwCheck && fwCheck.tagDefault) || (fwCheck && fwCheck.target) || (fwVersions[0] ? fwVersions[0].version : null);
|
|
1394
|
+
const fwChannelLabel = (v) => (v && v.isLatest === true ? t("frameworkUpgradeChannelStable") + " · latest" : (v && typeof v.channel === "string" && v.channel !== "" ? t("frameworkUpgradeChannelPre") + " · " + v.channel : t("frameworkUpgradeChannelPre")));
|
|
1356
1395
|
// 面板里的状态刷新:**无视**「终态已关闭」标记——卡片可以被永久关掉,面板必须永远能查到真相
|
|
1357
1396
|
const fwStatusRefresh = () => {
|
|
1358
1397
|
call("/plugin-console/framework-upgrade-status").then(
|
|
@@ -2840,7 +2879,8 @@ window.__ModuleLoader__.load({
|
|
|
2840
2879
|
/** 框架升级:Hub 打包备份现有配置 + 版本检测 + 升级脚本(脚本会自行停止/拉起服务,无需手动重启)。 */
|
|
2841
2880
|
const doFrameworkUpgrade = () => {
|
|
2842
2881
|
setFrameworkUpgrading(true);
|
|
2843
|
-
|
|
2882
|
+
// 用户选了版本就带上(宿主端按候选列表校验:只接受比当前新的真实版本)
|
|
2883
|
+
call("/plugin-console/framework-upgrade", fwSelected ? { version: fwSelected } : {}).then(
|
|
2844
2884
|
(data) => {
|
|
2845
2885
|
setFrameworkUpgrading(false);
|
|
2846
2886
|
setConfirmFrameworkUpgrade(false);
|
|
@@ -3515,6 +3555,10 @@ window.__ModuleLoader__.load({
|
|
|
3515
3555
|
const gateAutoDisable = !gateReady || (state.data.compatGate ?? {}).autoDisable !== false;
|
|
3516
3556
|
const gateAutoDetect = !gateReady || (state.data.compatGate ?? {}).autoDetect !== false;
|
|
3517
3557
|
const gatePendingCount = gateReady ? (state.data.compatPending?.pending ?? []).length : 0;
|
|
3558
|
+
// 门控明细清单(用户 2026-09-24 要求:面板要显示"是哪些插件",不只是数量)
|
|
3559
|
+
const [gateShowAll, setGateShowAll] = react.useState(false);
|
|
3560
|
+
const gateRows = gateReady ? (state.data.gating ?? null) : null;
|
|
3561
|
+
const gateTime = (ms) => (typeof ms === "number" && ms > 0 ? new Date(ms).toLocaleString() : "—");
|
|
3518
3562
|
// 「功能包 → 框架」按钮角标:升级进行中 ⟳ / 上次失败 ✕ / 有可用更新 ↑(优先级从高到低)
|
|
3519
3563
|
const fwRunning = frameworkStatus !== null
|
|
3520
3564
|
&& frameworkStatus.status !== "idle" && frameworkStatus.status !== "done" && frameworkStatus.status !== "failed";
|
|
@@ -3584,10 +3628,25 @@ window.__ModuleLoader__.load({
|
|
|
3584
3628
|
fwCheck === null
|
|
3585
3629
|
? null
|
|
3586
3630
|
: fwCheck.target
|
|
3587
|
-
? el("p", { className: styles.status, "data-pending": "true" }, t("fwPanelAvailable").replace("{v}", fwCheck.target) + "(latest " + (fwCheck.latest ?? "—") + " · next " + (fwCheck.next ?? "—") + ")")
|
|
3631
|
+
? el("p", { className: styles.status, "data-pending": "true" }, t("fwPanelAvailable").replace("{v}", fwSelected ?? fwCheck.target) + "(latest " + (fwCheck.latest ?? "—") + " · next " + (fwCheck.next ?? "—") + (fwCheck.alpha ? " · alpha " + fwCheck.alpha : "") + ")")
|
|
3588
3632
|
: fwCheck.registryError
|
|
3589
3633
|
? el("p", { className: styles.message, "data-error": "true" }, t("fwPanelCheckFailed").replace("{err}", fwCheck.registryError))
|
|
3590
3634
|
: el("p", { className: styles.status }, t("fwPanelUpToDate").replace("{latest}", fwCheck.latest ?? "—").replace("{next}", fwCheck.next ?? "—")),
|
|
3635
|
+
el("p", { className: styles.status }, t("frameworkUpgradePick")),
|
|
3636
|
+
fwPicking
|
|
3637
|
+
? el("div", { style: { flexDirection: "column", gap: 6, display: "flex", flexBasis: "100%" } },
|
|
3638
|
+
(fwPickAll ? fwVersions : fwVersions.slice(0, 6)).map((v) => el("button", {
|
|
3639
|
+
key: v.version, type: "button", className: styles.toggle, disabled: frameworkUpgrading,
|
|
3640
|
+
title: v.version + "(" + fwChannelLabel(v) + ")",
|
|
3641
|
+
style: { justifyContent: "space-between", display: "flex", width: "100%" },
|
|
3642
|
+
"data-active": v.version === fwSelected ? "true" : undefined,
|
|
3643
|
+
onClick: () => { setFwSelectedVersion(v.version); setFwPicking(false); },
|
|
3644
|
+
}, el("span", null, (v.version === fwSelected ? "● " : "○ ") + v.version), el("span", { className: styles.status }, fwChannelLabel(v)))),
|
|
3645
|
+
fwVersions.length > 6
|
|
3646
|
+
? el("button", { type: "button", className: styles.toggle, onClick: () => setFwPickAll((x) => !x) }, fwPickAll ? t("frameworkUpgradeFold") : t("frameworkUpgradeMore").replace("{n}", String(fwVersions.length)))
|
|
3647
|
+
: null)
|
|
3648
|
+
: null,
|
|
3649
|
+
el("button", { type: "button", className: styles.toggle, disabled: frameworkUpgrading || fwVersions.length === 0, title: t("frameworkUpgradePick"), "data-active": fwPicking ? "true" : undefined, onClick: () => setFwPicking((x) => !x) }, t("frameworkUpgradeTarget") + ":" + (fwSelected ?? "—") + (fwPicking ? " ▴" : " ▾")),
|
|
3591
3650
|
// ② 上次/当前升级记录(卡片可被永久关闭,这里是常驻的真相入口)
|
|
3592
3651
|
frameworkStatus !== null && frameworkStatus.status !== "idle"
|
|
3593
3652
|
? el("div", { style: { flexDirection: "column", gap: 8, display: "flex" } },
|
|
@@ -3613,7 +3672,7 @@ window.__ModuleLoader__.load({
|
|
|
3613
3672
|
el("div", { className: styles.rowTop },
|
|
3614
3673
|
el("button", { type: "button", className: styles.toggle, disabled: frameworkUpgrading, onClick: () => { setFwConfirmPanel(false); doFrameworkUpgrade(); } }, frameworkUpgrading ? t("installingLocal") : t("frameworkUpgradeConfirm")),
|
|
3615
3674
|
el("button", { type: "button", className: styles.toggle, onClick: () => setFwConfirmPanel(false) }, t("closeModal"))))
|
|
3616
|
-
: el("button", { type: "button", className: styles.toggle, disabled: frameworkUpgrading, title: t("frameworkUpgradeTitle"), onClick: () => setFwConfirmPanel(true) }, t("frameworkUpgradeBtn") + " → v" + fwCheck.target))
|
|
3675
|
+
: el("button", { type: "button", className: styles.toggle, disabled: frameworkUpgrading, title: t("frameworkUpgradeTitle"), onClick: () => setFwConfirmPanel(true) }, t("frameworkUpgradeBtn") + " → v" + (fwSelected ?? fwCheck.target)))
|
|
3617
3676
|
: null,
|
|
3618
3677
|
rollbackUsable
|
|
3619
3678
|
? el("button", { type: "button", className: styles.toggle, title: t("frameworkRollbackTitle"), disabled: frameworkUpgrading, onClick: doFrameworkRollback }, t("frameworkRollbackBtn") + " → " + (rollbackInfo.from ?? "?"))
|
|
@@ -3656,7 +3715,35 @@ window.__ModuleLoader__.load({
|
|
|
3656
3715
|
}, el("div", { className: styles.pcSwitchKnob }))),
|
|
3657
3716
|
el("p", { className: styles.message }, t("compatGateHint")),
|
|
3658
3717
|
el("p", { className: styles.status, "data-pending": gatePendingCount > 0 ? "true" : undefined },
|
|
3659
|
-
gatePendingCount > 0 ? t("gatePendingInfo").replace("{n}", String(gatePendingCount)) : t("gatePendingNone"))
|
|
3718
|
+
gatePendingCount > 0 ? t("gatePendingInfo").replace("{n}", String(gatePendingCount)) : t("gatePendingNone")),
|
|
3719
|
+
// 明细清单(2026-09-24 用户要求:面板要能看到是哪些插件,有列表数据)
|
|
3720
|
+
gateRows !== null && gateRows.pendingCount + gateRows.adoptedCount > 0
|
|
3721
|
+
? el("div", { style: { flexDirection: "column", gap: 6, display: "flex" } },
|
|
3722
|
+
el("strong", { className: styles.name }, t("gateListPending").replace("{n}", String(gateRows.pendingCount))),
|
|
3723
|
+
gateRows.pending.length === 0
|
|
3724
|
+
? el("p", { className: styles.status }, t("gatePendingNone"))
|
|
3725
|
+
: null,
|
|
3726
|
+
(gateShowAll ? gateRows.pending : gateRows.pending.slice(0, 8)).map((r) => el("div", {
|
|
3727
|
+
key: "p-" + String(r.rowId),
|
|
3728
|
+
style: { flexDirection: "column", gap: 2, display: "flex", padding: "4px 0" },
|
|
3729
|
+
},
|
|
3730
|
+
el("span", { className: styles.name }, (r.enabled === false ? "⛔ " : "• ") + String(r.name) + (r.version ? " v" + r.version : "")),
|
|
3731
|
+
el("span", { className: styles.status }, t("gateRowReason").replace("{reason}", String(r.note ?? "—"))),
|
|
3732
|
+
el("span", { className: styles.status },
|
|
3733
|
+
(r.source === "boot-quarantine" ? t("gateRowFromBoot") : t("gateRowFromUpgrade"))
|
|
3734
|
+
+ (r.forcedAt ? t("gateRowAt").replace("{t}", gateTime(r.forcedAt)) : "")
|
|
3735
|
+
+ (r.adoptable ? t("gateRowAdoptable") : "")))),
|
|
3736
|
+
gateRows.pendingCount > 8
|
|
3737
|
+
? el("button", { type: "button", className: styles.toggle, onClick: () => setGateShowAll((v) => !v) },
|
|
3738
|
+
gateShowAll ? t("gateListFold") : t("gateListMore").replace("{n}", String(gateRows.pendingCount)))
|
|
3739
|
+
: null,
|
|
3740
|
+
gateRows.adoptedCount > 0
|
|
3741
|
+
? el("div", { style: { flexDirection: "column", gap: 2, display: "flex" } },
|
|
3742
|
+
el("strong", { className: styles.name }, t("gateListAdopted").replace("{n}", String(gateRows.adoptedCount))),
|
|
3743
|
+
gateRows.adopted.slice(0, 8).map((r) => el("span", { key: "a-" + String(r.rowId), className: styles.status },
|
|
3744
|
+
"✓ " + String(r.name) + (r.version ? " v" + r.version : "") + (r.adoptedAt ? " · " + gateTime(r.adoptedAt) : ""))))
|
|
3745
|
+
: null)
|
|
3746
|
+
: null)))
|
|
3660
3747
|
: null,
|
|
3661
3748
|
aiOpen
|
|
3662
3749
|
? el("div", { className: styles.modalBackdrop },
|
package/lib/index.js
CHANGED
|
@@ -1180,6 +1180,34 @@ function queuedWrite(fn) {
|
|
|
1180
1180
|
return run
|
|
1181
1181
|
}
|
|
1182
1182
|
|
|
1183
|
+
/**
|
|
1184
|
+
* 解析"这次要升到哪个版本"(2026-09-23 抽出:原先 framework-check 与 framework-upgrade 各写了一份,
|
|
1185
|
+
* 加了「用户自选版本」后两份逻辑必须完全一致,索性收成一处)。
|
|
1186
|
+
*
|
|
1187
|
+
* @param {object} p
|
|
1188
|
+
* @param {string|null} p.current 当前框架版本
|
|
1189
|
+
* @param {string|null} p.latest dist-tags.latest
|
|
1190
|
+
* @param {string|null} p.next dist-tags.next
|
|
1191
|
+
* @param {string} [p.wanted] 用户在面板里显式选的版本(空串 = 没选)
|
|
1192
|
+
* @param {Array<{version: string}>} [p.candidates] 可选候选列表(有 wanted 时必须提供,用作白名单校验)
|
|
1193
|
+
* @returns {{ target: string|null, rejected: string|null }} rejected 非空 = 用户选的版本非法
|
|
1194
|
+
*/
|
|
1195
|
+
function pickFrameworkTarget({ current, latest, next, wanted = '', candidates = [] }) {
|
|
1196
|
+
const want = typeof wanted === 'string' ? wanted.trim() : ''
|
|
1197
|
+
if (want !== '') {
|
|
1198
|
+
const hit = candidates.find((v) => v.version === want)
|
|
1199
|
+
return hit === undefined ? { target: null, rejected: want } : { target: hit.version, rejected: null }
|
|
1200
|
+
}
|
|
1201
|
+
return {
|
|
1202
|
+
// 稳定版 latest 优先;latest 不高于当前而 next(预发布渠道)确实更新时,目标取 next。
|
|
1203
|
+
// 必须用版本号比较而不是字符串不等(避免 current=0.1.1 稳定版时被 next=0.1.1-rc.3 反向降级)。
|
|
1204
|
+
target: latest !== null && current !== null && isFrameworkVersionNewer(latest, current)
|
|
1205
|
+
? latest
|
|
1206
|
+
: (next !== null && current !== null && isFrameworkVersionNewer(next, current) ? next : null),
|
|
1207
|
+
rejected: null,
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1183
1211
|
/** 解析 DSH 框架版本号为可比较对象;正式版(无预发布段)视为 rc.∞。 */
|
|
1184
1212
|
function parseFrameworkVersion(value) {
|
|
1185
1213
|
const m = String(value ?? '').match(/^(\d+)\.(\d+)\.(\d+)(?:-(?:[a-z]+\.)?(\d+))?$/iu)
|
|
@@ -1204,6 +1232,49 @@ function isFrameworkVersionNewer(candidate, current) {
|
|
|
1204
1232
|
return a.rc > b.rc
|
|
1205
1233
|
}
|
|
1206
1234
|
|
|
1235
|
+
/**
|
|
1236
|
+
* 框架升级候选列表(2026-09-23 用户要求:「有的版本都加上,测试版也可以有列表」)。
|
|
1237
|
+
*
|
|
1238
|
+
* 为什么需要它:升级面板原先只有**一个**目标(`latest` 优先、否则 `next`),用户看到
|
|
1239
|
+
* 「可升级到 0.1.5-rc.3(latest 0.1.5-rc.3 · next 0.1.7-rc.1)」却**没法自己选**——
|
|
1240
|
+
* 想上 0.1.7-rc.1 或某个 alpha 只能手敲 pnpm 命令。
|
|
1241
|
+
*
|
|
1242
|
+
* 规则(安全边界,都在这里钉死):
|
|
1243
|
+
* · 只收 **严格比 current 新** 的版本(用 `compareSemverText`,保留完整 prerelease 语义:
|
|
1244
|
+
* 0.1.5-rc.3 < 0.1.6-alpha.1 < 0.1.7-alpha.1 < 0.1.7-rc.1 < 0.1.7 正式版);
|
|
1245
|
+
* 等于或低于当前的**一律不进列表**——降级另有「回滚到上一版」那条路,不从这里走。
|
|
1246
|
+
* · 注册表顺序(按发布时间)不可信,这里按语义版本**降序**排(最新在最前);
|
|
1247
|
+
* · `latest` / `next` / `beta` / `alpha` 等 dist-tag 命中的版本带 `channel` 标注,
|
|
1248
|
+
* 供前端打「稳定版 / 预发布」标签;`latest` 同时作为 `tagDefault`(默认选中项,保持原行为)。
|
|
1249
|
+
*
|
|
1250
|
+
* @param {object|null|undefined} meta registry 元数据({ 'dist-tags', versions })
|
|
1251
|
+
* @param {string|null} current 当前已装框架版本
|
|
1252
|
+
* @returns {{ versions: Array<{version: string, channel: string|null, isLatest: boolean}>, tagDefault: string|null }}
|
|
1253
|
+
*/
|
|
1254
|
+
function frameworkUpgradeCandidates(meta, current) {
|
|
1255
|
+
const tags = meta !== null && typeof meta === 'object' && meta['dist-tags'] !== null && typeof meta['dist-tags'] === 'object'
|
|
1256
|
+
? meta['dist-tags']
|
|
1257
|
+
: {}
|
|
1258
|
+
const cur = parseSemverText(current)
|
|
1259
|
+
const all = meta !== null && typeof meta === 'object' && meta.versions !== null && typeof meta.versions === 'object'
|
|
1260
|
+
? Object.keys(meta.versions)
|
|
1261
|
+
: []
|
|
1262
|
+
const newer = []
|
|
1263
|
+
for (const v of all) {
|
|
1264
|
+
const parsed = parseSemverText(v)
|
|
1265
|
+
if (parsed === null) continue
|
|
1266
|
+
if (cur !== null && compareSemverText(parsed, cur) <= 0) continue
|
|
1267
|
+
newer.push(v)
|
|
1268
|
+
}
|
|
1269
|
+
newer.sort((a, b) => compareSemverText(parseSemverText(b), parseSemverText(a)))
|
|
1270
|
+
const tagDefault = typeof tags.latest === 'string' && newer.includes(tags.latest) ? tags.latest : null
|
|
1271
|
+
const versions = newer.map((v) => {
|
|
1272
|
+
const channel = Object.keys(tags).find((name) => tags[name] === v) ?? null
|
|
1273
|
+
return { version: v, channel, isLatest: channel === 'latest' }
|
|
1274
|
+
})
|
|
1275
|
+
return { versions, tagDefault }
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1207
1278
|
/** 默认 profile 用户补丁层路径(无 include 条目可推导时的兜底)。 */
|
|
1208
1279
|
function defaultPatchPath() {
|
|
1209
1280
|
const home = dshHome()
|
|
@@ -1477,6 +1548,8 @@ function logQuarantineMergeError(error) {
|
|
|
1477
1548
|
* 顺序很关键(v0.3.44):**先写清单并校验成功,再归档移除记录**。反过来的话,
|
|
1478
1549
|
* 任何解析/写入意外都会"记录没了、清单也没进",用户只看到一个没有理由的【停用】。
|
|
1479
1550
|
* 导出仅供测试直接驱动(避免测试为了跑它而 apply 整个插件)。 */
|
|
1551
|
+
export { frameworkUpgradeCandidates, pickFrameworkTarget }
|
|
1552
|
+
|
|
1480
1553
|
export function mergeQuarantineRecord(ctx) {
|
|
1481
1554
|
const file = join(dshHome(), 'plugin-console', 'fw-quarantine.json')
|
|
1482
1555
|
if (!existsSync(file)) return null
|
|
@@ -1570,6 +1643,72 @@ function markPendingAdopted(pending, rowId, by, meta) {
|
|
|
1570
1643
|
* ② 记录还是 pending、但这一行**当前已经启用**(用户手动启用过 / 补丁被清过)→ 转 adopted。
|
|
1571
1644
|
* 不这么做就会出现用户实测的那种矛盾:**已启用的行,重启后仍挂着【待适配】**。
|
|
1572
1645
|
* 返回 { backfilled, adopted } 供日志/测试核对;无变化则不写盘。 */
|
|
1646
|
+
/**
|
|
1647
|
+
* 门控面板要的「明细清单」(2026-09-24 用户要求:门控面板应该能显示是哪些插件,有列表数据)。
|
|
1648
|
+
*
|
|
1649
|
+
* 此前面板只有两个总开关 + 一句「当前待适配:N 行」——用户看得到数量,看不到**是谁**:
|
|
1650
|
+
* 待适配清单 compat-pending.json 里有 rowId / moduleName / version / checkNote / 来源 / 时间,
|
|
1651
|
+
* 但只有 pending 那部分被下发,且没有「已适配(adopted)」那半边,也没把 rowId 补成可读模块名。
|
|
1652
|
+
*
|
|
1653
|
+
* 这里产出面板直接可渲染的两组明细:
|
|
1654
|
+
* pending —— 当前被拦下/待解锁的行(含原因、来源、是否已通过源码扫描可解锁)
|
|
1655
|
+
* adopted —— 曾经被拦、现已适配解锁的行(留着让用户能核对"到底动过哪些行")
|
|
1656
|
+
* 身份补全:清单里很多行只有 rowId(隔离脚本写的),按当前 loader 反查 moduleName/version。
|
|
1657
|
+
*/
|
|
1658
|
+
function gatingSummary(ctx, compatPending, ctxEntries = null) {
|
|
1659
|
+
const rec = compatPending ?? { frameworkVersion: null, upgradeFrom: null, pending: [] }
|
|
1660
|
+
const all = Array.isArray(rec.pending) ? rec.pending : []
|
|
1661
|
+
let info = new Map()
|
|
1662
|
+
try {
|
|
1663
|
+
info = rowIdModuleMap(ctx)
|
|
1664
|
+
} catch {}
|
|
1665
|
+
const byRow = new Map()
|
|
1666
|
+
for (const e of Array.isArray(ctxEntries) ? ctxEntries : []) {
|
|
1667
|
+
if (typeof e?.rowId === 'string' && e.rowId !== '') byRow.set(e.rowId, e)
|
|
1668
|
+
}
|
|
1669
|
+
const row = (p) => {
|
|
1670
|
+
const meta = info.get(p.rowId) ?? null
|
|
1671
|
+
const entry = byRow.get(p.rowId) ?? null
|
|
1672
|
+
const moduleName = p.moduleName ?? meta?.moduleName ?? entry?.moduleName ?? null
|
|
1673
|
+
// 界面上的"名字":优先可读模块名,其次是 loader 里的条目,最后才是 raw rowId
|
|
1674
|
+
const name = moduleName ?? (entry === null ? null : entry.moduleName) ?? p.rowId ?? '(未知行)'
|
|
1675
|
+
return {
|
|
1676
|
+
rowId: p.rowId ?? null,
|
|
1677
|
+
name,
|
|
1678
|
+
shortName: typeof name === 'string' && name.includes('/') ? name.slice(name.lastIndexOf('/') + 1) : name,
|
|
1679
|
+
moduleName,
|
|
1680
|
+
version: p.version ?? meta?.version ?? entry?.version ?? null,
|
|
1681
|
+
status: p.status ?? 'pending',
|
|
1682
|
+
check: p.check ?? null,
|
|
1683
|
+
note: p.checkNote ?? null,
|
|
1684
|
+
source: p.source ?? null,
|
|
1685
|
+
adoptedAt: p.adoptedAt ?? null,
|
|
1686
|
+
forcedAt: p.forcedAt ?? null,
|
|
1687
|
+
adoptedBy: p.adoptedBy ?? null,
|
|
1688
|
+
enabled: entry === null ? null : entry.enabled === true,
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
const isPending = (p) => (p.status ?? 'pending') === 'pending'
|
|
1692
|
+
const pendingRows = all.filter(isPending).map(row)
|
|
1693
|
+
const adoptedRows = all.filter((p) => !isPending(p)).map(row)
|
|
1694
|
+
// 可解锁提示:仅对 pending 行算(detectAdoptablePending 是只读检测,不写盘)
|
|
1695
|
+
let adoptable = new Map()
|
|
1696
|
+
try {
|
|
1697
|
+
adoptable = detectAdoptablePending(ctx)
|
|
1698
|
+
} catch {}
|
|
1699
|
+
for (const r of pendingRows) r.adoptable = adoptable.get(r.rowId) ?? null
|
|
1700
|
+
return {
|
|
1701
|
+
version: rec.frameworkVersion ?? null,
|
|
1702
|
+
upgradeFrom: rec.upgradeFrom ?? null,
|
|
1703
|
+
quarantineAt: rec.quarantineAt ?? null,
|
|
1704
|
+
quarantineLines: Array.isArray(rec.quarantineLines) ? rec.quarantineLines.slice(0, 5) : [],
|
|
1705
|
+
pendingCount: pendingRows.length,
|
|
1706
|
+
adoptedCount: adoptedRows.length,
|
|
1707
|
+
pending: pendingRows,
|
|
1708
|
+
adopted: adoptedRows,
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1573
1712
|
export function reconcileCompatPending(ctx) {
|
|
1574
1713
|
const pending = readCompatPending()
|
|
1575
1714
|
if (pending === null || !Array.isArray(pending.pending)) return { backfilled: 0, adopted: 0 }
|
|
@@ -6666,7 +6805,7 @@ async function handle(ctx, req, res) {
|
|
|
6666
6805
|
const selfPkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'))
|
|
6667
6806
|
selfVersion = typeof selfPkg.version === 'string' ? selfPkg.version : null
|
|
6668
6807
|
} catch {}
|
|
6669
|
-
sendJson(res, 200, { ok: true, entries, patchPath, compat, installJobs: jobs, recentFailures, pendingRestart, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, patchHeal: patchHeal === null ? null : { healed: patchHeal.healed ?? [], autoDisabled: patchHeal.autoDisabled ?? [], healedAt: patchHeal.healedAt ?? 0 }, compatPending: compatPending === null ? null : { frameworkVersion: compatPending.frameworkVersion ?? null, upgradeFrom: compatPending.upgradeFrom ?? null, pending: (compatPending.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => ({ rowId: p.rowId, moduleName: p.moduleName, version: p.version ?? null, checkNote: p.checkNote ?? null, check: p.check ?? null, riskyApprovedAt: p.riskyApprovedAt ?? null, adoptable: adoptable.get(p.rowId) ?? null })) }, compatGate, rollback: rollbackRec, selfVersion, components: findComponents().map((c) => ({ id: c.id, name: c.name, kind: c.kind ?? 'server', pid: c.pid ?? null, port: c.port ?? null, healthUrl: c.healthUrl ?? null, uiUrl: compUiUrl(c), autoStart: c.autoStart === true })) })
|
|
6808
|
+
sendJson(res, 200, { ok: true, entries, patchPath, compat, installJobs: jobs, recentFailures, pendingRestart, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, patchHeal: patchHeal === null ? null : { healed: patchHeal.healed ?? [], autoDisabled: patchHeal.autoDisabled ?? [], healedAt: patchHeal.healedAt ?? 0 }, gating: gatingSummary(ctx, compatPending, entries), compatPending: compatPending === null ? null : { frameworkVersion: compatPending.frameworkVersion ?? null, upgradeFrom: compatPending.upgradeFrom ?? null, pending: (compatPending.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => ({ rowId: p.rowId, moduleName: p.moduleName, version: p.version ?? null, checkNote: p.checkNote ?? null, check: p.check ?? null, riskyApprovedAt: p.riskyApprovedAt ?? null, adoptable: adoptable.get(p.rowId) ?? null })) }, compatGate, rollback: rollbackRec, selfVersion, components: findComponents().map((c) => ({ id: c.id, name: c.name, kind: c.kind ?? 'server', pid: c.pid ?? null, port: c.port ?? null, healthUrl: c.healthUrl ?? null, uiUrl: compUiUrl(c), autoStart: c.autoStart === true })) })
|
|
6670
6809
|
return
|
|
6671
6810
|
}
|
|
6672
6811
|
|
|
@@ -7868,26 +8007,33 @@ async function handle(ctx, req, res) {
|
|
|
7868
8007
|
// body.refresh === true 强制重查——面板是常驻入口,不能每次打开都打 registry。
|
|
7869
8008
|
const now = Date.now()
|
|
7870
8009
|
if (body.refresh === true || fwCheckCache === null || now - fwCheckCache.at > 300000) {
|
|
8010
|
+
// 当前版本先从本地包读出来——候选列表要拿它做「只收更新的」过滤
|
|
8011
|
+
let current = null
|
|
8012
|
+
try {
|
|
8013
|
+
const localRequire = createRequire(ctx.baseUrl ?? 'file:///')
|
|
8014
|
+
const pkg = JSON.parse(readFileSync(localRequire.resolve('@deepseek-ai/dsh/package.json'), 'utf8'))
|
|
8015
|
+
current = typeof pkg.version === 'string' ? pkg.version : null
|
|
8016
|
+
} catch {}
|
|
7871
8017
|
let latest = null
|
|
7872
8018
|
let next = null
|
|
8019
|
+
let alpha = null
|
|
8020
|
+
let versions = []
|
|
8021
|
+
let tagDefault = null
|
|
7873
8022
|
let registryError = null
|
|
7874
8023
|
try {
|
|
7875
8024
|
const data = await fetchJsonUrl('https://registry.npmmirror.com/@deepseek-ai%2fdsh')
|
|
7876
8025
|
latest = data?.['dist-tags']?.latest ?? null
|
|
7877
8026
|
next = data?.['dist-tags']?.next ?? null
|
|
8027
|
+
alpha = data?.['dist-tags']?.alpha ?? null
|
|
8028
|
+
// 可选升级目标列表(用户 2026-09-23 要求:所有比当前新的版本都列出来,测试版也列)
|
|
8029
|
+
const cand = frameworkUpgradeCandidates(data, current)
|
|
8030
|
+
versions = cand.versions
|
|
8031
|
+
tagDefault = cand.tagDefault
|
|
7878
8032
|
} catch (error) {
|
|
7879
8033
|
registryError = error instanceof Error ? error.message : String(error)
|
|
7880
8034
|
}
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
const localRequire = createRequire(ctx.baseUrl ?? 'file:///')
|
|
7884
|
-
const pkg = JSON.parse(readFileSync(localRequire.resolve('@deepseek-ai/dsh/package.json'), 'utf8'))
|
|
7885
|
-
current = typeof pkg.version === 'string' ? pkg.version : null
|
|
7886
|
-
} catch {}
|
|
7887
|
-
const target = latest !== null && current !== null && isFrameworkVersionNewer(latest, current)
|
|
7888
|
-
? latest
|
|
7889
|
-
: (next !== null && current !== null && isFrameworkVersionNewer(next, current) ? next : null)
|
|
7890
|
-
fwCheckCache = { at: now, data: { current, latest, next, target, registryError } }
|
|
8035
|
+
const target = pickFrameworkTarget({ current, latest, next }).target
|
|
8036
|
+
fwCheckCache = { at: now, data: { current, latest, next, alpha, target, versions, tagDefault, registryError } }
|
|
7891
8037
|
}
|
|
7892
8038
|
sendJson(res, 200, { ok: true, ...fwCheckCache.data, checkedAt: fwCheckCache.at })
|
|
7893
8039
|
return
|
|
@@ -8187,20 +8333,31 @@ async function handle(ctx, req, res) {
|
|
|
8187
8333
|
}
|
|
8188
8334
|
let latest = null
|
|
8189
8335
|
let next = null
|
|
8336
|
+
let alpha = null
|
|
8337
|
+
let versions = []
|
|
8338
|
+
let tagDefault = null
|
|
8190
8339
|
let registryError = null
|
|
8191
8340
|
try {
|
|
8192
8341
|
const data = await fetchJsonUrl('https://registry.npmmirror.com/@deepseek-ai%2fdsh')
|
|
8193
8342
|
latest = data?.['dist-tags']?.latest ?? null
|
|
8194
8343
|
next = data?.['dist-tags']?.next ?? null
|
|
8344
|
+
alpha = data?.['dist-tags']?.alpha ?? null
|
|
8345
|
+
const cand = frameworkUpgradeCandidates(data, current)
|
|
8346
|
+
versions = cand.versions
|
|
8347
|
+
tagDefault = cand.tagDefault
|
|
8195
8348
|
} catch (error) {
|
|
8196
8349
|
// 网络黑洞/超时:区分「检测失败」与「无更新」,避免误导用户以为已是最新
|
|
8197
8350
|
registryError = error instanceof Error ? error.message : String(error)
|
|
8198
8351
|
}
|
|
8199
|
-
//
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8352
|
+
// 升级目标:① 用户在面板里显式选了版本(body.version)→ 以候选列表为准校验
|
|
8353
|
+
//(只接受"比当前新"的真实发布版本;乱填/降级/不存在的版本一律 400,不生成升级脚本);
|
|
8354
|
+
// ② 没选 → 沿用原行为(helper 内注释说明:latest 优先,否则 next)。
|
|
8355
|
+
const picked = pickFrameworkTarget({ current, latest, next, wanted: body.version, candidates: versions })
|
|
8356
|
+
if (picked.rejected !== null) {
|
|
8357
|
+
sendJson(res, 400, { ok: false, error: `升级目标 ${picked.rejected} 不在可选列表内(只接受比当前 ${current} 更新的已发布版本;降级请用「回滚到上一版」)` })
|
|
8358
|
+
return
|
|
8359
|
+
}
|
|
8360
|
+
const target = picked.target
|
|
8204
8361
|
const profileDir = dirname(findPatchPath(ctx))
|
|
8205
8362
|
// 1) 升级前打包备份现有配置(patch / profile package.json / 插件清单)
|
|
8206
8363
|
let backupDir = null
|
|
@@ -8265,7 +8422,7 @@ async function handle(ctx, req, res) {
|
|
|
8265
8422
|
// 避免"kill 后升级/拉起双双失败,服务永久 down"
|
|
8266
8423
|
if (!existsSync(corepackJs) || binPath === null || !existsSync(binPath)) {
|
|
8267
8424
|
steps.push(`升级已取消:无法定位 ${!existsSync(corepackJs) ? 'corepack(pnpm 通道)' : 'dsh bin.js'}(框架安装可能不完整),服务保持运行;请先修复框架再升级`)
|
|
8268
|
-
sendJson(res, 200, { ok: true, current, latest, next, target, hasUpdate, upgraded: false, backupDir, configMigrations, preflight, steps, hints: steps })
|
|
8425
|
+
sendJson(res, 200, { ok: true, current, latest, next, alpha, target, versions, tagDefault, hasUpdate, upgraded: false, backupDir, configMigrations, preflight, steps, hints: steps })
|
|
8269
8426
|
return
|
|
8270
8427
|
}
|
|
8271
8428
|
const profileDir2 = profileDir
|
|
@@ -8458,6 +8615,29 @@ async function handle(ctx, req, res) {
|
|
|
8458
8615
|
` if ($v -ne '${target}') { Log ('pnpm 退出码 0 但顶层可见版本未更新(顶层 ' + $v + ',目标 ${target}),按失败处理'); $code = 1 }`,
|
|
8459
8616
|
` } catch { Log 'pnpm 退出码 0 但无法读取顶层版本,按失败处理'; $code = 1 }`,
|
|
8460
8617
|
` }`,
|
|
8618
|
+
// 安装后「结构完整性」校验(2026-09-24 事故后补):版本号对 ≠ 装完整。
|
|
8619
|
+
// 当天真实事故:一次 0.1.7-rc.1 升级的 pnpm 安装被中断,顶层 @deepseek-ai\dsh 整个目录消失、
|
|
8620
|
+
// .pnpm 实体只剩 lib 里几个硬链接、package.json 没了。更隐蔽的形态(package.json 写成功但
|
|
8621
|
+
// lib 文件没落全)会骗过"只对版本号"的校验,等下次重启才发现服务起不来 —— 所以逐项验结构:
|
|
8622
|
+
// package.json / lib/bin.js 都在 + CLI 自报版本等于目标;任一不过 = 本次升级失败 → 走回滚。
|
|
8623
|
+
` if ($code -eq 0) {`,
|
|
8624
|
+
` try {`,
|
|
8625
|
+
` $fwDsh = ${ps(join(fwRoot, '@deepseek-ai', 'dsh'))}`,
|
|
8626
|
+
` $binA = Join-Path $fwDsh 'lib\\bin.js'`,
|
|
8627
|
+
` $binB = $null`,
|
|
8628
|
+
` $ent = Get-ChildItem -Path (Join-Path ${ps(fwRoot)} '.pnpm') -Directory -Filter '@deepseek-ai+dsh@${target}*' -ErrorAction SilentlyContinue | Select-Object -First 1`,
|
|
8629
|
+
` if ($ent) { $binB = Join-Path $ent.FullName 'node_modules\\@deepseek-ai\\dsh\\lib\\bin.js' }`,
|
|
8630
|
+
` $bin = $null`,
|
|
8631
|
+
` if (Test-Path -LiteralPath $binA) { $bin = $binA } elseif ($binB -and (Test-Path -LiteralPath $binB)) { $bin = $binB }`,
|
|
8632
|
+
` if (-not (Test-Path -LiteralPath (Join-Path $fwDsh 'package.json'))) { Log '结构校验失败:顶层 @deepseek-ai\\dsh\\package.json 不存在(安装不完整)'; $code = 1 }`,
|
|
8633
|
+
` elseif (-not $bin) { Log '结构校验失败:找不到 dsh 的 lib/bin.js(顶层与 .pnpm 都没有)——装完也拉不起来,按失败处理'; $code = 1 }`,
|
|
8634
|
+
` else {`,
|
|
8635
|
+
` $probe = (& ${ps(nodePath)} $bin --version 2>&1 | Out-String).Trim()`,
|
|
8636
|
+
` if ($probe -ne '${target}') { Log ('结构校验失败:CLI 自报版本 ' + $probe + ',目标 ${target}(安装不完整或被其它副本顶替),按失败处理'); $code = 1 }`,
|
|
8637
|
+
` else { Log ('结构校验通过:package.json + bin.js 就位,CLI 自报 ' + $probe) }`,
|
|
8638
|
+
` }`,
|
|
8639
|
+
` } catch { Log ('结构校验异常,按失败处理:' + $_.Exception.Message); $code = 1 }`,
|
|
8640
|
+
` }`,
|
|
8461
8641
|
` return $code`,
|
|
8462
8642
|
`}`,
|
|
8463
8643
|
// 回滚函数(2026-09-04 事故后升级为「全树优先」):robocopy 支持长路径(Copy-Item 对 >260
|
|
@@ -8802,7 +8982,7 @@ async function handle(ctx, req, res) {
|
|
|
8802
8982
|
'升级完成后请重启 DSH 服务生效——重启后 Hub 自动:备份新版本配置快照、重打框架补丁(issue #5 容错)、给出适配提示。',
|
|
8803
8983
|
'官方配套组件(@deepseek-ai/*)随框架一起升级,请勿在市场单独更新。',
|
|
8804
8984
|
]
|
|
8805
|
-
sendJson(res, 200, { ok: true, current, latest, next, target, hasUpdate, upgraded, backupDir, registryError, configMigrations, preflight, steps, hints })
|
|
8985
|
+
sendJson(res, 200, { ok: true, current, latest, next, alpha, target, versions, tagDefault, hasUpdate, upgraded, backupDir, registryError, configMigrations, preflight, steps, hints })
|
|
8806
8986
|
return
|
|
8807
8987
|
}
|
|
8808
8988
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noob-stupid/dsh-plugin-console",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.67",
|
|
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",
|