@noob-stupid/dsh-plugin-console 0.5.71 → 0.5.77

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
@@ -280,6 +280,11 @@ window.__ModuleLoader__.load({
280
280
  migrateBtn: "迁移并适配",
281
281
  aiEmpowerFrameworkCheck: "框架适配检测",
282
282
  userForcedTag: "补丁强制启用",
283
+ // ★ 0.5.76(用户 2026-10-18):禁用**出处**要写明,别只给一句"已停用"
284
+ disabledByBundleTag: "插件默认禁用",
285
+ forcedNotLoadedTag: "强制启用未生效",
286
+ revokeForceLabel: "撤销该强制启用",
287
+ revokeForceHint: "这条 disabled: false 覆盖没有让插件加载起来 —— 撤掉它,界面不再显示「强制启用」的假象(不会改动清单)",
283
288
  protectedTag: "受保护",
284
289
  unobserved: "未挂载",
285
290
  pending: "等待依赖",
@@ -891,6 +896,10 @@ window.__ModuleLoader__.load({
891
896
  migrateBtn: "Migrate & adapt",
892
897
  aiEmpowerFrameworkCheck: "Framework compat check",
893
898
  userForcedTag: "Forced on by patch",
899
+ disabledByBundleTag: "Disabled by plugin default",
900
+ forcedNotLoadedTag: "Force-enable had no effect",
901
+ revokeForceLabel: "Revoke this force-enable",
902
+ revokeForceHint: "This disabled: false override did not make the plugin load; revoking it stops the false forced-on badge (the manifest is untouched)",
894
903
  protectedTag: "Protected",
895
904
  unobserved: "Not mounted",
896
905
  pending: "Waiting for dependencies",
@@ -2306,7 +2315,14 @@ function healthSignature(health) {
2306
2315
  const [toolbarPos, setToolbarPos] = react.useState(() => {
2307
2316
  try {
2308
2317
  const v = JSON.parse(localStorage.getItem("pc-toolbar-pos") || "null");
2309
- if (v && Number.isFinite(v.left) && Number.isFinite(v.top)) return { left: v.left, top: v.top };
2318
+ if (v && Number.isFinite(v.left) && Number.isFinite(v.top)) {
2319
+ // ★ 用户 2026-10-18:存下来的位置**必须钳进可视区**,否则拖到标题栏/屏幕外就再也拿不回来。
2320
+ // 上边界 64px 是标题栏与窗口按钮的安全区(原来只钳到 8px,正好落在标题栏里)。
2321
+ const SAFE_TOP = 64;
2322
+ const maxLeft = Math.max(8, (window.innerWidth || 1280) - 120);
2323
+ const maxTop = Math.max(SAFE_TOP, (window.innerHeight || 800) - 80);
2324
+ return { left: Math.min(Math.max(8, v.left), maxLeft), top: Math.min(Math.max(SAFE_TOP, v.top), maxTop) };
2325
+ }
2310
2326
  } catch {}
2311
2327
  return null;
2312
2328
  });
@@ -2929,6 +2945,15 @@ function healthSignature(health) {
2929
2945
  (error) => setDetails({ entryId: entry.entryId, status: "error", error }),
2930
2946
  );
2931
2947
  };
2948
+ // ★ 0.5.76:撤销"强制启用"覆盖(补丁里的 disabled: false)。只对确实 forced 的行生效,服务端会校验。
2949
+ const revokeForce = (entry) => {
2950
+ if (busy !== null) return;
2951
+ setBusy(entry.entryId);
2952
+ call("/plugin-console/revoke-force", { entryId: entry.entryId }).then(
2953
+ (data) => { setMessage(data?.note ?? t("revokeForceLabel")); refresh(); },
2954
+ (error) => { setMessage(friendlyGithubError(error).message); },
2955
+ ).finally(() => setBusy(null));
2956
+ };
2932
2957
  const checkUpdate = (entry) => {
2933
2958
  const pkgName = entry.moduleName;
2934
2959
  setUpdateCheck({ status: "checking" });
@@ -4135,6 +4160,10 @@ function healthSignature(health) {
4135
4160
  entry.protected ? el("span", { className: styles.tag, "data-user": "true" }, t("protectedTag")) : null,
4136
4161
  entry.userDisabled ? el("span", { className: styles.tag, "data-user": "true" }, t("userDisabledTag")) : null,
4137
4162
  entry.userForced ? el("span", { className: styles.tag, "data-user": "true" }, t("userForcedTag")) : null,
4163
+ // ★ 0.5.76:被**别处**(插件自身补丁/框架默认)禁用的行 —— 这就是"补丁强制启用"的来由
4164
+ entry.disableSource === "bundle" ? el("span", { className: styles.tag, "data-user": "true", "data-source": "bundle" }, t("disabledByBundleTag")) : null,
4165
+ // ★ 0.5.76:补丁写了强制开启、运行时却仍没起来 ⇒ 这条覆盖是**无效的**,必须说出来
4166
+ entry.forcedNotLoaded === true ? el("span", { className: styles.tag, "data-user": "true", "data-source": "ineffective" }, t("forcedNotLoadedTag")) : null,
4138
4167
  ),
4139
4168
  el("div", { className: styles.meta },
4140
4169
  el("span", { className: styles.phase, "data-phase": entry.fiberPhase ?? "unobserved" },
@@ -4164,11 +4193,18 @@ function healthSignature(health) {
4164
4193
  el("button", {
4165
4194
  type: "button",
4166
4195
  className: styles.toggle,
4167
- disabled: !entry.toggleable || busy === entry.entryId || entry.pendingCompat === true,
4196
+ disabled: !entry.toggleable || busy === entry.entryId, // ★ 待适配不再禁用启用(用户 2026-10-18):启用=确认启用,服务端会把它标为已采纳
4168
4197
  // 0.5.34:结论已被当前判据撤回的行不再显示「待适配」(旧判据的误报不许当事实展示)
4169
4198
  title: entry.pendingCompat === true ? (entry.compatRescinded === true ? t("compatRescindedHint") : t("pendingCompatHint")) : undefined,
4170
4199
  onClick: () => toggle(entry, !entry.enabled),
4171
4200
  }, busy === entry.entryId ? t("loadingPhase") : (entry.pendingCompat === true ? (entry.compatRescinded === true ? t("compatRescindedTag") : t("pendingCompatTag")) : t(entry.enabled ? "off" : "on"))),
4201
+ // ★ 0.5.76:补丁写了强制开启但运行时没起来 ⇒ 给一个撤销覆盖的入口
4202
+ entry.forcedNotLoaded === true ? el("button", {
4203
+ type: "button",
4204
+ className: styles.toggle,
4205
+ title: t("revokeForceHint"),
4206
+ onClick: () => revokeForce(entry),
4207
+ }, t("revokeForceLabel")) : null,
4172
4208
  el("button", {
4173
4209
  type: "button",
4174
4210
  className: styles.toggle,
@@ -4922,9 +4958,7 @@ function healthSignature(health) {
4922
4958
  el("p", { className: styles.message }, t("compatGateHint")),
4923
4959
  el("p", { className: styles.status, "data-pending": gatePendingCount > 0 ? "true" : undefined },
4924
4960
  gatePendingCount > 0 ? t("gatePendingInfo").replace("{n}", String(gatePendingCount)) : t("gatePendingNone")),
4925
- el("p", { className: styles.status, "data-pending": gatePendingCount > 0 ? "true" : undefined },
4926
- gatePendingCount > 0 ? t("gatePendingInfo").replace("{n}", String(gatePendingCount)) : t("gatePendingNone")),
4927
- // 明细清单(2026-09-24 用户要求:面板要能看到是哪些插件,有列表数据)
4961
+ // 明细清单(2026-09-24 用户要求:面板要能看到是哪些插件,有列表数据)
4928
4962
  gateRows !== null && gateRows.pendingCount + gateRows.adoptedCount > 0
4929
4963
  ? el("div", { style: { flexDirection: "column", gap: 6, display: "flex" } },
4930
4964
  el("strong", { className: styles.name }, t("gateListPending").replace("{n}", String(gateRows.pendingCount))),
@@ -5339,7 +5373,7 @@ function healthSignature(health) {
5339
5373
  const bw = e.currentTarget.offsetWidth || 96;
5340
5374
  const bh = e.currentTarget.offsetHeight || 30;
5341
5375
  const left = Math.max(8, Math.min(w - bw - 8, e.clientX - dragRef.current.grabX));
5342
- const top = Math.max(8, Math.min(h - bh - 8, e.clientY - dragRef.current.grabY));
5376
+ const top = Math.max(64, Math.min(h - bh - 8, e.clientY - dragRef.current.grabY)); // ★ 64 = 标题栏安全区
5343
5377
  setDragPos({ left, top });
5344
5378
  },
5345
5379
  onPointerUp: () => {
@@ -226,7 +226,21 @@ async function declareProfileDependency(profileDir, packageName, version = null,
226
226
  sourceInstall = await installSourceDependency({ profileDir, spec: installSpec, registries, install: installSource })
227
227
  recordSourceSpec({ packageName, spec: installSpec, kind: sourceKind, install: sourceInstall, ...homeOpt })
228
228
  if (sourceInstall.ok !== true) {
229
- plan = await planDependencySpec({ profileDir, packageName, version: resolved, registries, probe, home, profile, sourceSpec: null })
229
+ // ★ 0.5.72(用户 2026-10-18 批准):**不再回落成 link:**。
230
+ // 有真实来源规格却装不上 ⇒ 如实报错、**不写清单、不建 link**;
231
+ // 偷偷挂一份本地副本会让人以为"装上了",而依赖与 peer 其实没有被包管理器保证过。
232
+ // 仍保留「一键真装」动作:给的是"重试真实通道"的出路,不是假装成功。
233
+ const why = typeof sourceInstall.error === 'string' && sourceInstall.error !== '' ? sourceInstall.error : '(原因未记录)'
234
+ return {
235
+ ...empty,
236
+ version: resolved,
237
+ form: plan.form,
238
+ reason: `真装失败:${why}`,
239
+ sourceInstall,
240
+ depNote: `${packageName}:已获取源码但**真装失败**(${why})—— 未改动清单,也未按 link: 记录。` +
241
+ `可点「一键真装」重试;或改用「从本地目录安装」显式走开发式链接。`,
242
+ suggested: suggestedRealInstallAction({ packageName, spec: installSpec, profile, profileDir }),
243
+ }
230
244
  }
231
245
  }
232
246
  const installNote = composeSourceInstallNote({ packageName, install: sourceInstall, plan })
@@ -60,13 +60,33 @@ function dropStaleUpdateCandidates(candidates, packageName, baseUrl, profileDir)
60
60
  * @param {{ latest?: string|null, next?: string|null, beta?: string|null }} candidates
61
61
  * @returns {string|null} 目标版本;全空 ⇒ null(真的没有更高版本)
62
62
  */
63
- function pickUpdateTarget(candidates) {
63
+ /**
64
+ * 从 { latest, next, beta } 里选出**该更新的目标版本**。
65
+ * ★ 0.5.74(用户 2026-10-18):按**版本高低**选,不再按通道顺序取第一个非空。
66
+ * 真机证据:官方包只往 next 发版而 latest 停在远古 alpha ——
67
+ * dsh-experimental-speech-to-text: latest 0.1.7-alpha.1 / next 0.2.1-rc.2
68
+ * 旧实现返回 latest ⇒ 界面永远"已是最新版",next 里的新版永远看不见。
69
+ * 比较**不做第二份实现**:共用 infra/semver.js#pickNewerSemver(本模块既有的唯一判据)。
70
+ *
71
+ * @param {{latest?:string,next?:string,beta?:string}} candidates
72
+ * @param {{installedVersion?:string|null}} [options] 给了就优先选**严格高于它**的;没有更高的则退回全体最高
73
+ */
74
+ function pickUpdateTarget(candidates, options = {}) {
64
75
  const src = candidates !== null && typeof candidates === 'object' ? candidates : {}
65
- for (const key of ['latest', 'next', 'beta']) {
66
- const value = src[key]
67
- if (typeof value === 'string' && value !== '') return value
76
+ const list = ['latest', 'next', 'beta']
77
+ .map((key) => src[key])
78
+ .filter((value) => typeof value === 'string' && value !== '')
79
+ if (list.length === 0) return null
80
+ const installed = typeof options?.installedVersion === 'string' && options.installedVersion !== ''
81
+ ? options.installedVersion
82
+ : null
83
+ // ① 有已装版本:取严格高于它的最高者
84
+ if (installed !== null) {
85
+ const newer = pickNewerSemver(installed, list)
86
+ if (newer !== null) return newer
68
87
  }
69
- return null
88
+ // ② 没有更高的(或根本没给已装版本):取全体最高的 —— 以 0.0.0 为基准即可(唯一判据仍是同一个函数)
89
+ return pickNewerSemver('0.0.0', list)
70
90
  }
71
91
 
72
92
  export { dropStaleUpdateCandidates, installedVersionOf, pickUpdateTarget }
@@ -27,6 +27,7 @@ import { routeGithubLogin, routeGithubOpenLogin } from './github-login.js'
27
27
  import { routeInstall, routeInstallStatus } from './install.js'
28
28
  import { routeLockfileCheck, routeLockfileRepair } from './lockfile.js'
29
29
  import { routeEnrich, routeMarketIndex, routeRepo, routeSearch, routeSubpackages } from './market.js'
30
+ import { routeRevokeForce } from './revoke-force.js'
30
31
  import { routeAdaptUnlock, routeAdaptUnlockAll, routeCleanResiduals, routeSelfUpdate, routeToggle, routeUninstall } from './plugins.js'
31
32
  import { routeSkillRemove, routeSkillToggle, routeSkillsInstalledGet } from './skills.js'
32
33
  import { routeGiteeOauthCallbackGet, routeGiteeOauthUrlGet, routeRegistryScan, routeSources, routeSourcesGet } from './sources.js'
@@ -130,6 +131,7 @@ const ROUTES = [
130
131
  { methods: ['POST'], path: `${ROUTE_PREFIX}/toggle`, handler: routeToggle },
131
132
  { methods: ['POST'], path: `${ROUTE_PREFIX}/uninstall`, handler: routeUninstall },
132
133
  { methods: ['POST'], path: `${ROUTE_PREFIX}/adapt-unlock`, handler: routeAdaptUnlock },
134
+ { methods: ['POST'], path: `${ROUTE_PREFIX}/revoke-force`, handler: routeRevokeForce },
133
135
  { methods: ['POST'], path: `${ROUTE_PREFIX}/adapt-unlock-all`, handler: routeAdaptUnlockAll },
134
136
  { methods: ['POST'], path: `${ROUTE_PREFIX}/clean-residuals`, handler: routeCleanResiduals },
135
137
  { methods: ['POST'], path: `${ROUTE_PREFIX}/self-update`, handler: routeSelfUpdate },
@@ -0,0 +1,35 @@
1
+ // L2 · routes —— 撤销「无效的强制启用覆盖」(0.5.76,用户 2026-10-18)
2
+ //
3
+ // 场景:补丁里写了 `disabled: false`(强制开启),但该行运行时根本没加载、也不是 peer 否决造成的
4
+ // —— 这条覆盖是**死的**,留着只会让界面继续显示「强制启用未生效」而毫无作用。
5
+ //
6
+ // 语义(写死):
7
+ // · 只对**确实在 forced 列表**里的行生效 —— 不凭空改别人的补丁;
8
+ // · 复用 patch.js#removeDisableBlock(与 adapt-unlock 同一实现,不写第二份);
9
+ // · 移除后**不承诺"已启用"**:撤掉覆盖只代表不再强压,实际是否加载由框架决定。
10
+ import { existsSync } from 'node:fs'
11
+ import { findPatchPath } from '../infra/paths.js'
12
+ import { removeDisableBlock, readPatchState } from '../domain/patch.js'
13
+ import { sendError, sendJson } from '../infra/httpd.js'
14
+
15
+ async function routeRevokeForce(req, res, rc) {
16
+ const body = rc.body !== null && typeof rc.body === 'object' ? rc.body : {}
17
+ const entryId = typeof body.entryId === 'string' ? body.entryId : ''
18
+ if (entryId === '') { sendError(res, 400, '缺少 entryId'); return }
19
+ const rowId = entryId.includes(':') ? entryId.split(':').slice(1).join(':') : entryId
20
+ let patchPath = null
21
+ try { patchPath = findPatchPath(rc.ctx) } catch { patchPath = null }
22
+ if (typeof patchPath !== 'string' || patchPath === '' || !existsSync(patchPath)) {
23
+ sendError(res, 400, '当前 profile 没有可写的补丁文件'); return
24
+ }
25
+ let forced = []
26
+ try { const state = await readPatchState(patchPath); forced = Array.isArray(state?.forced) ? state.forced : [] } catch { forced = [] }
27
+ if (!forced.includes(rowId)) {
28
+ sendError(res, 400, `${rowId} 当前不是「补丁强制启用」状态(没有 disabled: false 覆盖块),无需撤销`); return
29
+ }
30
+ try { await removeDisableBlock(patchPath, rowId) }
31
+ catch (error) { sendError(res, 500, `撤销失败:${error instanceof Error ? error.message : String(error)}`); return }
32
+ sendJson(res, 200, { ok: true, rowId, revoked: true, note: `已移除 ${rowId} 的强制启用覆盖(disabled: false)。该行是否加载由框架决定,本次未改清单。` })
33
+ }
34
+
35
+ export { routeRevokeForce }
@@ -98,7 +98,7 @@ async function routeStateGet(req, res, rc) {
98
98
  repository: meta?.repository ?? null,
99
99
  // v0.3.45:只有"补丁里此刻确实还禁着它"的行才显示【待适配】——
100
100
  // 记录与开关脱节时(用户手动启用过 / 补丁块被清掉)不能继续挂着「待适配」误导人
101
- pendingCompat: compatPendingRows.has(entry.rowId) && disabledByPatch,
101
+ pendingCompat: (compatPendingRows.has(entry.rowId) && disabledByPatch) || veto !== null,
102
102
  // 0.5.34 加法:记录的 `fail` 若已被**当前判据**撤回(旧误报),面板据此不再说「待适配」,
103
103
  // 改说「判据已撤回·可解锁」并给一键解锁入口(判据本身一个字都没放宽:真 fail 仍 fail)
104
104
  compatRescinded: compatPendingRows.has(entry.rowId) && verdict?.rescinded === true,
@@ -109,6 +109,14 @@ async function routeStateGet(req, res, rc) {
109
109
  adoptable: adoptable.get(entry.rowId) ?? null,
110
110
  // 框架 peer 预检的本地复算结果(null = 无话可说;只在这类行上下发,不新增常驻面板行)
111
111
  veto,
112
+ // ★ 0.5.76(用户 2026-10-18):**禁用出处** + 无效强制覆盖的可见性(只加字段,不改既有判定)
113
+ // 'patch' = 本控制台补丁禁的(删块即可正常启用)
114
+ // 'bundle' = 插件自身/框架默认禁的(我们只能写 disabled:false 覆盖 ⇒ 补丁强制启用)
115
+ disableSource: !entry.enabled && entry.fiberPhase === null
116
+ ? (disabledByPatch ? 'patch' : 'bundle')
117
+ : null,
118
+ // 强制开启写了、但运行时仍没起来,且不是 peer 否决造成的 ⇒ 这条覆盖是**无效的**,要说出来
119
+ forcedNotLoaded: patch.forced.includes(entry.rowId) && entry.enabled !== true && veto === null,
112
120
  }
113
121
  })
114
122
  const compat = await detectCompat(ctx.baseUrl ?? 'file:///')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noob-stupid/dsh-plugin-console",
3
- "version": "0.5.71",
3
+ "version": "0.5.77",
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",