@noob-stupid/dsh-plugin-console 0.5.59 → 0.5.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/lib/client.js
CHANGED
|
@@ -1578,6 +1578,14 @@ window.__ModuleLoader__.load({
|
|
|
1578
1578
|
}
|
|
1579
1579
|
// <<<update-pick:end>>>
|
|
1580
1580
|
const el = react.createElement;
|
|
1581
|
+
// ★ 0.5.60:被跳过 bundle 的签名(包名+版本)。关掉提示时记下它;清单变了就重新出现。
|
|
1582
|
+
// 模块级 —— 上一版把辅助函数放子组件里、主面板误用,导致整块面板空白。
|
|
1583
|
+
function skippedSignature(skipped) {
|
|
1584
|
+
try {
|
|
1585
|
+
if (skipped === null || skipped === undefined || !Array.isArray(skipped.items)) return "0";
|
|
1586
|
+
return skipped.items.map((x) => String(x.packageName) + "@" + String(x.version ?? "?")).join("|");
|
|
1587
|
+
} catch { return "0"; }
|
|
1588
|
+
}
|
|
1581
1589
|
// ★ 0.5.59:健康问题的**签名**(类别+条数)。关掉提示时记下它;签名一变说明问题变了,提示重新出现。
|
|
1582
1590
|
// 刻意放在模块级(不放组件内)—— 上一版就是因为把辅助函数放在子组件里、主面板误用而整块空白。
|
|
1583
1591
|
function healthSignature(health) {
|
|
@@ -1938,6 +1946,11 @@ function healthSignature(health) {
|
|
|
1938
1946
|
const [healthClosed, setHealthClosed] = react.useState(() => {
|
|
1939
1947
|
try { return localStorage.getItem("pc-health-closed") ?? ""; } catch { return ""; }
|
|
1940
1948
|
});
|
|
1949
|
+
// ★ 0.5.60:被跳过 bundle 的提示也可以关掉(用户 2026-10-18「不是说这个可以关闭吗」)。
|
|
1950
|
+
// 同样记"签名"(包名+版本),清单变了会重新出现 —— 关掉不等于永远看不见。
|
|
1951
|
+
const [skippedClosed, setSkippedClosed] = react.useState(() => {
|
|
1952
|
+
try { return localStorage.getItem("pc-skipped-closed") ?? ""; } catch { return ""; }
|
|
1953
|
+
});
|
|
1941
1954
|
const [busy, setBusy] = react.useState(null);
|
|
1942
1955
|
const [adaptUnlocking, setAdaptUnlocking] = react.useState(null);
|
|
1943
1956
|
// 待适配行「强行启用」的风险确认态:{ entryId, moduleName, frameworkVersion, checkNote } | null
|
|
@@ -4670,9 +4683,21 @@ function healthSignature(health) {
|
|
|
4670
4683
|
&& state.data.skippedBundles.reliable === true
|
|
4671
4684
|
&& Array.isArray(state.data.skippedBundles.items)
|
|
4672
4685
|
&& state.data.skippedBundles.items.length > 0
|
|
4686
|
+
&& skippedSignature(state.data.skippedBundles) !== skippedClosed
|
|
4673
4687
|
? el("div", { className: styles.message, "data-error": "true", style: { marginTop: "8px" } },
|
|
4674
4688
|
el("p", null,
|
|
4675
|
-
t("skippedBundlesTitle").replace("{n}", String(state.data.skippedBundles.items.length))
|
|
4689
|
+
t("skippedBundlesTitle").replace("{n}", String(state.data.skippedBundles.items.length)),
|
|
4690
|
+
" ",
|
|
4691
|
+
el("button", {
|
|
4692
|
+
type: "button",
|
|
4693
|
+
style: { marginLeft: "8px", cursor: "pointer" },
|
|
4694
|
+
title: t("healthDismissTitle"),
|
|
4695
|
+
onClick: () => {
|
|
4696
|
+
const sig = skippedSignature(state.data.skippedBundles);
|
|
4697
|
+
try { localStorage.setItem("pc-skipped-closed", sig); } catch {}
|
|
4698
|
+
setSkippedClosed(sig);
|
|
4699
|
+
},
|
|
4700
|
+
}, t("healthDismissBtn"))),
|
|
4676
4701
|
...state.data.skippedBundles.items.map((b) => el("p", { key: b.packageName, title: b.hint ?? undefined, style: { margin: "4px 0 0 12px" } },
|
|
4677
4702
|
"• " + b.packageName + (b.version ? " v" + b.version : "") + " — " + b.reason,
|
|
4678
4703
|
// ★ 0.5.56:把提示里的"三条出路"之一真正做成可点的动作。
|
|
@@ -389,7 +389,13 @@ function summarizeReadme(text) {
|
|
|
389
389
|
async function readPluginDetails(moduleName, baseUrl, profileDir) {
|
|
390
390
|
if (typeof moduleName !== 'string' || moduleName.startsWith('cordis:')) return { meta: null, readme: null }
|
|
391
391
|
try {
|
|
392
|
-
|
|
392
|
+
// ★ 0.5.60(真机 2026-10-18):与 entryPkgMeta 同一口径 —— **profile 优先、安装锚点兜底**。
|
|
393
|
+
// 详情卡片过去读 app.asar 那份(0.2.1-rc.2),于是显示"已是最新"、更新按钮永远点不动,
|
|
394
|
+
// 而真正被加载的是 profile 里那份 0.2.0-rc.2(正是被 peer 否决的那份)。
|
|
395
|
+
const hasProfile = typeof profileDir === 'string' && profileDir !== ''
|
|
396
|
+
const pkgPath = hasProfile
|
|
397
|
+
? resolvePackageJson(moduleName, profileDir, baseDirOf(baseUrl))
|
|
398
|
+
: resolvePackageJson(moduleName, baseDirOf(baseUrl), null)
|
|
393
399
|
if (pkgPath === null) throw new Error('not found')
|
|
394
400
|
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
395
401
|
const meta = {
|
|
@@ -180,7 +180,15 @@ function entryPkgMeta(moduleName, baseUrl, profileDir) {
|
|
|
180
180
|
if (hit !== undefined && Date.now() - hit.at < PKG_META_TTL) return hit
|
|
181
181
|
const meta = { at: Date.now(), installDate: null, version: null, repository: null }
|
|
182
182
|
try {
|
|
183
|
-
|
|
183
|
+
// ★ 0.5.60(真机 2026-10-18):**profile 优先、安装锚点兜底**。
|
|
184
|
+
// 过去是反的(asar 优先),导致"徽标显示 0.2.1-rc.2(最新)→ 检测更新无更新可点 →
|
|
185
|
+
// 但实际加载的是 profile 里 0.2.0-rc.2 → peer 永远不匹配"的死局。
|
|
186
|
+
// loader 是从 profile 上下文解析模块名的,所以要显示/要比对的必须是 profile 那份;
|
|
187
|
+
// 只有 profile 里没有该包时(纯内置插件)才回落到安装锚点。
|
|
188
|
+
const hasProfile = typeof profileDir === 'string' && profileDir !== ''
|
|
189
|
+
const pkgPath = hasProfile
|
|
190
|
+
? resolvePackageJson(moduleName, profileDir, baseDirOf(baseUrl))
|
|
191
|
+
: resolvePackageJson(moduleName, baseDirOf(baseUrl), null)
|
|
184
192
|
if (pkgPath === null) throw new Error('not found')
|
|
185
193
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
186
194
|
// 安装日期:优先 curl 通道写入的 .dsh-installed-at 标记(真实安装时刻);
|
|
@@ -24,7 +24,7 @@ import { dirname, join } from 'node:path'
|
|
|
24
24
|
import { applyEvidenceDisable } from '../domain/auto-disable.js'
|
|
25
25
|
import { DEFAULT_TIMEOUT_MS, autoPreflightView, runAutoPreflight } from '../domain/auto-preflight.js'
|
|
26
26
|
import { runAutoPreflightScan } from '../domain/auto-preflight-run.js'
|
|
27
|
-
import { NOTICE_MODES, detectCompat, readCompatAck, readCompatGate, writeCompatAck, writeCompatGate } from '../domain/compat.js'
|
|
27
|
+
import { NOTICE_MODES, detectCompat, readCompatAck, readCompatGate, readCompatPending, rowIdModuleMap, writeCompatAck, writeCompatGate, writeCompatPending } from '../domain/compat.js'
|
|
28
28
|
import { compareFingerprint, computeFingerprint, planDesktopHostSwitch, readCompatMode, readFingerprint, readRollbackRecord, stampFingerprint, writeCompatMode } from '../domain/compat-state.js'
|
|
29
29
|
import { currentFrameworkVersion, detectHostShape } from '../domain/framework.js'
|
|
30
30
|
import { applyDependencyBackfill, planDependencyBackfill } from '../domain/manifest.js'
|
|
@@ -170,6 +170,38 @@ function maybeAutoDisable({ rc, current, previous, reasons, patchPath, record })
|
|
|
170
170
|
reasons,
|
|
171
171
|
isProtected: isProtectedModule,
|
|
172
172
|
})
|
|
173
|
+
// ★ 0.5.62(用户 2026-10-18):**证据禁用的行必须进入待适配清单** —— 否则它们虽然被禁用了,
|
|
174
|
+
// 却拿不到「待适配」标签与「更新并适配」链路(两条禁用通道体验不一致,用户看到的就是"没有待适配")。
|
|
175
|
+
// 只处理**真的被禁用**的行(result.disabled),候选/开关关闭的情况一个字都不改。
|
|
176
|
+
try {
|
|
177
|
+
const disabledRows = Array.isArray(result?.disabled) ? result.disabled : []
|
|
178
|
+
if (disabledRows.length > 0) {
|
|
179
|
+
const pending = readCompatPending()
|
|
180
|
+
if (pending !== null && Array.isArray(pending.pending)) {
|
|
181
|
+
const existing = new Set(pending.pending.map((p) => p?.rowId))
|
|
182
|
+
const modules = (() => { try { return rowIdModuleMap() } catch { return null } })()
|
|
183
|
+
let added = 0
|
|
184
|
+
for (const rowId of disabledRows) {
|
|
185
|
+
if (typeof rowId !== 'string' || existing.has(rowId)) continue
|
|
186
|
+
pending.pending.push({
|
|
187
|
+
rowId,
|
|
188
|
+
// moduleName 尽力而为:拿不到就留 null(面板按 rowId 显示,不编造)
|
|
189
|
+
moduleName: typeof modules?.get === 'function' ? (modules.get(rowId) ?? null) : null,
|
|
190
|
+
status: 'pending',
|
|
191
|
+
reason: '框架变化后由证据预检自动禁用(观察者模式同样生效)',
|
|
192
|
+
source: 'auto-preflight',
|
|
193
|
+
at: Date.now(),
|
|
194
|
+
})
|
|
195
|
+
added += 1
|
|
196
|
+
}
|
|
197
|
+
if (added > 0) {
|
|
198
|
+
pending.frameworkVersion = current?.frameworkVersion ?? pending.frameworkVersion ?? null
|
|
199
|
+
pending.updatedAt = new Date().toISOString()
|
|
200
|
+
writeCompatPending(pending)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} catch {}
|
|
173
205
|
lastAutoDisable = { at: Date.now(), snapshotId: result?.snapshot?.id ?? null, ...result }
|
|
174
206
|
return result
|
|
175
207
|
} catch (error) {
|
|
@@ -245,7 +277,15 @@ async function routeCompatStatusGet(req, res, rc) {
|
|
|
245
277
|
const patchPath = (() => { try { return findPatchPath(rc.ctx) } catch { return null } })()
|
|
246
278
|
|
|
247
279
|
// D1:变了就自动跑一次只读预检(后台,不阻塞;同一指纹只跑一次)
|
|
248
|
-
|
|
280
|
+
// ★ 0.5.61(真机 2026-10-18):**框架版本变了也必须跑自动预检** —— 不能只看环境指纹。
|
|
281
|
+
// 实测:桌面端自带更新器把 0.2.0-rc.2 升到 0.2.1-rc.2 后指纹基线被推进,changed 变成 false
|
|
282
|
+
// ⇒ 自动预检(禁用不适配行 + 标记待适配)从未执行 ⇒ 用户看到"升级后没有待适配的了"。
|
|
283
|
+
const gatedVersion = (() => { try { const g = readCompatGate(); return typeof g?.version === "string" ? g.version : null } catch { return null } })()
|
|
284
|
+
const runtimeVersion = typeof current?.frameworkVersion === "string" ? current.frameworkVersion : null
|
|
285
|
+
// 空记录(gatedVersion === null)= 首次见到这个运行时版本,同样必须跑 ——
|
|
286
|
+
// 否则"从来没跑过"的机器会永远不跑(实测就是这个洞)。
|
|
287
|
+
const versionChanged = runtimeVersion !== null && runtimeVersion !== gatedVersion
|
|
288
|
+
const autoRun = (changed === true || versionChanged === true) ? scheduleAutoPreflight(rc, { current, previous, reasons, profileDir: profileDirOf(rc.ctx), patchPath }) : null
|
|
249
289
|
|
|
250
290
|
// D2:确认服务正常 → 落 last-known-good 快照(节流:指纹与补丁都没变时零写盘)
|
|
251
291
|
const snapshot = maybeWriteSnapshot({ current, changed, patchPath })
|
|
@@ -314,6 +314,12 @@ async function routeRepo(req, res, rc) {
|
|
|
314
314
|
const method = rc.method
|
|
315
315
|
const body = rc.body
|
|
316
316
|
const repo = githubRepoInfo(typeof body.repo === 'string' ? body.repo : '')
|
|
317
|
+
// ★ 0.5.63(全量实测 2026-10-18):**空载荷必须 400,不能 500** ——
|
|
318
|
+
// 实测 /repo 空载荷返回 500(body 却是正确的校验文案),客户端会把"你填错了"当成"服务崩了"。
|
|
319
|
+
if (repo === null) {
|
|
320
|
+
sendError(res, 400, '仓库名格式应为 owner/name(支持完整仓库 URL 与中文路径)')
|
|
321
|
+
return
|
|
322
|
+
}
|
|
317
323
|
const auth = readGithubAuth()
|
|
318
324
|
// meta 降级策略:githubJson(https+镜像+gh)与 curl 竞速,8 秒超时即降级——
|
|
319
325
|
// Promise.any 全失败时要等最慢分支(黑洞期 https 41.5s),加 race 超时避免拖累整体。
|
|
@@ -386,6 +392,12 @@ async function routeSubpackages(req, res, rc) {
|
|
|
386
392
|
const body = rc.body
|
|
387
393
|
const repo = githubRepoInfo(typeof body.repo === 'string' ? body.repo : '')
|
|
388
394
|
const branch = typeof body.branch === 'string' && body.branch ? body.branch : 'main'
|
|
395
|
+
// ★ 0.5.63(全量实测 2026-10-18):**空载荷必须 400,不能 500** ——
|
|
396
|
+
// 实测 /repo 空载荷返回 500(body 却是正确的校验文案),客户端会把"你填错了"当成"服务崩了"。
|
|
397
|
+
if (repo === null) {
|
|
398
|
+
sendError(res, 400, '仓库名格式应为 owner/name(支持完整仓库 URL 与中文路径)')
|
|
399
|
+
return
|
|
400
|
+
}
|
|
389
401
|
const auth = readGithubAuth()
|
|
390
402
|
// 复用安装链的防护实现:任一 raw 拉取失败只跳过该子包,不整体 500
|
|
391
403
|
const subpackages = await fetchSubpackageNames(repo, branch, auth.token)
|
|
@@ -163,6 +163,46 @@ async function routeStateGet(req, res, rc) {
|
|
|
163
163
|
const shape = detectHostShape()
|
|
164
164
|
host = { hosted: shape.hosted === true, kind: shape.kind, notice: shape.hosted === true ? shape.notice : null }
|
|
165
165
|
} catch {}
|
|
166
|
+
// ★ 0.5.60 加法(真机 2026-10-18):**为被 bundle 级跳过的插件合成虚拟行**。
|
|
167
|
+
// 为什么必须做:bundle 层被跳过 ⇒ 它要插的 loader 行**根本不存在** ⇒ 列表里没有位置可挂,
|
|
168
|
+
// 用户只能看到一条横幅,无法像别的插件那样"看到它、更新它、再启用它"。
|
|
169
|
+
// 合成后它出现在列表里,显示为「已停用 / 未加载」+ 否决原因;toggleable=false 表示
|
|
170
|
+
// **没有真实行可开关**(开关自动置灰),但「详情 → 检测更新」照常可用 —— 更新到兼容版本后
|
|
171
|
+
// 重启,bundle 才会真正装载并出现真实行,那时才谈得上"启用"。
|
|
172
|
+
const realModuleNames = new Set(entries.map((e) => e.moduleName))
|
|
173
|
+
const virtualEntries = skippedBundles.reliable === true && Array.isArray(skippedBundles.items)
|
|
174
|
+
? skippedBundles.items
|
|
175
|
+
.filter((b) => b !== null && b !== undefined && typeof b.packageName === 'string' && !realModuleNames.has(b.packageName))
|
|
176
|
+
.map((b) => ({
|
|
177
|
+
entryId: `bundle-skipped:${b.packageName}`,
|
|
178
|
+
rowId: null,
|
|
179
|
+
moduleName: b.packageName,
|
|
180
|
+
enabled: false,
|
|
181
|
+
fiberPhase: null,
|
|
182
|
+
protected: false,
|
|
183
|
+
toggleable: false,
|
|
184
|
+
userDisabled: false,
|
|
185
|
+
userForced: false,
|
|
186
|
+
// ★ 0.5.64(用户 2026-10-18「还是没显示出来那俩第三方插件」):客户端安装列表**默认**
|
|
187
|
+
// extraOnly=true,只显示 extra===true 的行;虚拟行原本写 false ⇒ 被默认筛选滤掉,
|
|
188
|
+
// 服务端发了、界面看不到。语义上也该是 true:它与"控制台插入的行"同属非核心行。
|
|
189
|
+
extra: true,
|
|
190
|
+
installDate: null,
|
|
191
|
+
version: b.version ?? null,
|
|
192
|
+
repository: null,
|
|
193
|
+
// ★ 0.5.61(用户 2026-10-18):虚拟行本质就是「待适配」——标成 true 才能拿到客户端那条
|
|
194
|
+
// 既有的「更新并适配」(updateAndAdapt)链路,也就是用户记得的"更新后启用"。
|
|
195
|
+
pendingCompat: true,
|
|
196
|
+
compatRescinded: false,
|
|
197
|
+
compatVerdict: null,
|
|
198
|
+
adoptable: null,
|
|
199
|
+
// 复用既有的 veto 通道:卡片会照常渲染原因 + 出路(tooltip),无需新 UI。
|
|
200
|
+
veto: { reason: b.reason ?? null, hint: b.hint ?? null, kind: b.kind ?? null, judged: b.judged ?? null },
|
|
201
|
+
virtual: true,
|
|
202
|
+
}))
|
|
203
|
+
: []
|
|
204
|
+
const allEntries = virtualEntries.length === 0 ? entries : [...entries, ...virtualEntries]
|
|
205
|
+
|
|
166
206
|
// ★ 0.5.55 加法:把已经算好的各块**折成一行健康摘要**(不新增 IO、不新增判据)。
|
|
167
207
|
// 面板 176 个按钮、信息散在六处,用户没有一个地方能回答"整体健康吗、我该先处理哪件"。
|
|
168
208
|
let health = { issues: 0, items: [] }
|
|
@@ -174,7 +214,7 @@ async function routeStateGet(req, res, rc) {
|
|
|
174
214
|
patchUncertain: patchHeal === null ? [] : (patchHeal.uncertain ?? []),
|
|
175
215
|
})
|
|
176
216
|
} catch {}
|
|
177
|
-
sendJson(res, 200, { ok: true, entries, skippedBundles, host, health, 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, written: patchHeal.written === true, writeError: patchHeal.writeError ?? null, skippedFrameworkOwned: (patchHeal.skipped ?? []).map((s) => s.id ?? null), uncertain: (patchHeal.uncertain ?? []).map((u) => u.id ?? null) }, gating: gatingSummary(ctx, compatPending, entries, compatVerdicts), 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, verdict: compatVerdicts.get(p.rowId) ?? null })), verdictRefresh: pendingState.refresh === null ? null : { version: pendingState.refresh.version ?? null, updated: pendingState.refresh.updated ?? [], unchanged: pendingState.refresh.unchanged ?? [], unresolved: pendingState.refresh.unresolved ?? [], wrote: pendingState.refresh.wrote === true, writeError: pendingState.refresh.writeError ?? 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 })) })
|
|
217
|
+
sendJson(res, 200, { ok: true, entries: allEntries, skippedBundles, host, health, 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, written: patchHeal.written === true, writeError: patchHeal.writeError ?? null, skippedFrameworkOwned: (patchHeal.skipped ?? []).map((s) => s.id ?? null), uncertain: (patchHeal.uncertain ?? []).map((u) => u.id ?? null) }, gating: gatingSummary(ctx, compatPending, entries, compatVerdicts), 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, verdict: compatVerdicts.get(p.rowId) ?? null })), verdictRefresh: pendingState.refresh === null ? null : { version: pendingState.refresh.version ?? null, updated: pendingState.refresh.updated ?? [], unchanged: pendingState.refresh.unchanged ?? [], unresolved: pendingState.refresh.unresolved ?? [], wrote: pendingState.refresh.wrote === true, writeError: pendingState.refresh.writeError ?? 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 })) })
|
|
178
218
|
return
|
|
179
219
|
}
|
|
180
220
|
|
|
@@ -190,16 +230,33 @@ async function routeDetails(req, res, rc) {
|
|
|
190
230
|
const readExtraBundleRows = rc.deps.readExtraBundleRows
|
|
191
231
|
const body = rc.body
|
|
192
232
|
const { entryId } = body
|
|
193
|
-
|
|
233
|
+
// ★ 0.5.66:白名单补 `@` `/` —— 带 scope 的虚拟行 id 形如 bundle-skipped:@scope/pkg。
|
|
234
|
+
// 放宽外层的安全性由后续校验保证:虚拟行会再用 npm 包名正则验一次(非法即 400)。
|
|
235
|
+
if (typeof entryId !== 'string' || !/^[A-Za-z0-9_:.@/-]{1,120}$/u.test(entryId)) {
|
|
194
236
|
sendError(res, 400, 'entryId 无效')
|
|
195
237
|
return
|
|
196
238
|
}
|
|
239
|
+
// ★ 0.5.65:虚拟行(bundle 被跳过、没有 loader 行)也要能看详情 —— 否则"详情 → 检测更新 → 更新并适配"
|
|
240
|
+
// 这条唯一可行的出路就断了(实测报"没有名为 bundle-skipped:xxx 的插件条目")。
|
|
241
|
+
const VIRTUAL_PREFIX = 'bundle-skipped:'
|
|
242
|
+
let moduleName = null
|
|
243
|
+
if (entryId.startsWith(VIRTUAL_PREFIX)) {
|
|
244
|
+
const virtualName = entryId.slice(VIRTUAL_PREFIX.length)
|
|
245
|
+
// 包名必须像 npm 包名,避免把任意字符串当包名去解析
|
|
246
|
+
if (!/^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u.test(virtualName)) {
|
|
247
|
+
sendError(res, 400, `虚拟条目 id 不是合法包名:${entryId}`)
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
const details = await readPluginDetails(virtualName, ctx.baseUrl ?? 'file:///', profileDirOf(ctx))
|
|
251
|
+
sendJson(res, 200, { ok: true, entryId, rowId: null, moduleName: virtualName, virtual: true, ...details })
|
|
252
|
+
return
|
|
253
|
+
}
|
|
197
254
|
const entry = ctx.loader.entries().find((candidate) => candidate.id === entryId)
|
|
198
255
|
if (!entry) {
|
|
199
256
|
sendError(res, 404, `没有名为 ${entryId} 的插件条目`)
|
|
200
257
|
return
|
|
201
258
|
}
|
|
202
|
-
|
|
259
|
+
moduleName = entry.options.name
|
|
203
260
|
const details = await readPluginDetails(moduleName, ctx.baseUrl ?? 'file:///', profileDirOf(ctx))
|
|
204
261
|
sendJson(res, 200, { ok: true, entryId, rowId: rowIdOf(ctx, entryId), moduleName, ...details })
|
|
205
262
|
return
|
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.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",
|