@noob-stupid/dsh-plugin-console 0.3.54 → 0.3.56

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 CHANGED
@@ -308,7 +308,7 @@ MIT
308
308
 
309
309
  The layered-refactor preview lives in its **own repository**: [**`Noob-stupid/dsh-plugin-hub-refactor`**](https://github.com/Noob-stupid/dsh-plugin-hub-refactor)
310
310
 
311
- - **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.54`**, maintainability only;
311
+ - **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.56`**, maintainability only;
312
312
  - **Verified**: 19 test suites green · 8 architecture-guard assertions · **route inventory identical** (`status` + response fields) against the stable build;
313
313
  - **Not yet exercised**: real framework upgrade / rollback, restart guardian, component start/stop, real AI run, Gitee OAuth (~10–25% long-tail risk, subjective);
314
- - **For everyday use** stick to npm `@noob-stupid/dsh-plugin-console@0.3.54`, or `main` of this repo.
314
+ - **For everyday use** stick to npm `@noob-stupid/dsh-plugin-console@0.3.56`, or `main` of this repo.
package/README.zh.md CHANGED
@@ -275,9 +275,9 @@ MIT
275
275
 
276
276
  分层重构预览线在**独立仓库**:[**`Noob-stupid/dsh-plugin-hub-refactor`**](https://github.com/Noob-stupid/dsh-plugin-hub-refactor)
277
277
 
278
- - **它是什么**:把单体 `lib/index.js`(8547 行)拆成分层结构 —— `lib/index.js` **141 行** + `lib/server/**` **38 个模块**,**功能与 `0.3.54` 等价**,只为可维护性;
278
+ - **它是什么**:把单体 `lib/index.js`(8547 行)拆成分层结构 —— `lib/index.js` **141 行** + `lib/server/**` **38 个模块**,**功能与 `0.3.56` 等价**,只为可维护性;
279
279
  - **已验证**:19 套测试全绿 · 8 条架构守卫断言 · 与稳定版逐条对打**路由清单一致**(`status` + 响应字段);
280
280
  - **已实测**(2026-09-20 真装真卸演练):普通插件 / bundle 插件 / 无 npm 仓库 / 套装 / 技能 / 聚合仓库子包 / 仓库落地 / 服务器组件启停;
281
281
  - **尚未实测**:真框架升级 / 真回滚、重启守护链路、AI 真跑、Gitee OAuth 回调(长尾风险主观估计 **10%~25%**);
282
- - **日常使用请继续用**:npm `@noob-stupid/dsh-plugin-console@0.3.54`,或本仓库 `main`。
282
+ - **日常使用请继续用**:npm `@noob-stupid/dsh-plugin-console@0.3.56`,或本仓库 `main`。
283
283
  - 请多反馈问题
package/lib/client.js CHANGED
@@ -3851,7 +3851,12 @@ setMessage(t("installing"));
3851
3851
  call("/plugin-console/self-update", {}).then(
3852
3852
  (data) => {
3853
3853
  if (data && data.ok === true) {
3854
- setMessage((data.note ?? "已更新") + ",正在重启生效…");
3854
+ // 未写进 lockfile 时必须**显眼**说清(用户实测报告:只铺文件不写 lock → 之后任何 pnpm 操作
3855
+ // 都会把版本还原,而用户以为升级成功了)
3856
+ const lockWarn = data.lockUpdated === false && typeof data.lockNote === "string" && data.lockNote !== ""
3857
+ ? "⚠️ " + data.lockNote
3858
+ : "";
3859
+ setMessage((data.note ?? "已更新") + (lockWarn === "" ? "" : "。" + lockWarn) + ",正在重启生效…");
3855
3860
  call("/plugin-console/restart", {}).then(() => {}, () => {});
3856
3861
  } else {
3857
3862
  setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
package/lib/index.js CHANGED
@@ -5563,6 +5563,137 @@ function listEntries(ctx) { const entries = []
5563
5563
  }
5564
5564
 
5565
5565
  /** 应用插件:注册 /plugin-console 路由。 */
5566
+ /**
5567
+ * 控制台自更新:**包管理器优先**,保证升级真的写进 pnpm-lock.yaml。
5568
+ *
5569
+ * 缺陷背景(2026-09-20 用户实测报告,附完整时间线):旧的 `/self-update` 直接把 npm tarball 的文件铺进
5570
+ * profile 的 node_modules,**不碰 pnpm-lock.yaml**;而 profile 依赖由 pnpm 按 lock 管理(`dsh plugin`
5571
+ * 就是 pnpm 的薄转发器)。于是只要发生**任何一次 pnpm 操作**——开关任意插件(改 `dsh.profile.bundles`)、
5572
+ * `dsh plugin add/remove/install`——pnpm 就按 lock 重装,把刚"升级"上去的版本**还原**回旧版本。
5573
+ * 用户侧现象:UI 一直提示有新版、点更新显示成功、重启后还是旧版。
5574
+ *
5575
+ * 顺序:① spec 是版本范围 → `pnpm update <pkg>`(spec 不变、lock 提到范围内最新)
5576
+ * ② 仍不是 latest(超出范围 / git·file 来源)→ `pnpm add <pkg>@<latest>`(spec 与 lock 同步改写)
5577
+ * ③ 回读核实 installedVersion + lockVersion;都不匹配才回落手铺文件,且**必须**带 lockNote 警告。
5578
+ */
5579
+
5580
+ /** spec 是否是 registry 版本范围(`^1.2.3` / `~1.2.3` / `1.2.3` / `>=1`):只有这种才能用 `pnpm update`
5581
+ * 在**不改写 spec**的前提下把 lock 提到范围内最新;git/file/link/workspace 来源没有"范围"可言。 */
5582
+ export function isRegistryRange(spec) {
5583
+ if (typeof spec !== 'string') return false
5584
+ const s = spec.trim()
5585
+ if (/^[\^~]?\d/u.test(s)) return true
5586
+ return /^(>=|>|=)\s*\d/u.test(s)
5587
+ }
5588
+
5589
+ /** profile 里该包的依赖声明(spec)。读不到返回 null。 */
5590
+ function profileSpecOf(profileDir, name) {
5591
+ try {
5592
+ const pkg = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'))
5593
+ const spec = pkg?.dependencies?.[name]
5594
+ return typeof spec === 'string' ? spec : null
5595
+ } catch {
5596
+ return null
5597
+ }
5598
+ }
5599
+
5600
+ /** 已安装版本(直接读 node_modules 里那份 package.json)。 */
5601
+ export function readInstalledVersion(profileDir, name) {
5602
+ try {
5603
+ const pkg = JSON.parse(readFileSync(join(profileDir, 'node_modules', ...name.split('/'), 'package.json'), 'utf8'))
5604
+ return typeof pkg?.version === 'string' ? pkg.version : null
5605
+ } catch {
5606
+ return null
5607
+ }
5608
+ }
5609
+
5610
+ /** 从 pnpm-lock.yaml 读出该包被**钉住**的版本。解析只认两种确定性位置,避免子串误命中:
5611
+ * ① importers 段里恰为 `'<name>':` 的行,随后几行内的 `version: X`;② packages 段 `<name>@<版本>:` 的键名。
5612
+ * 读不到返回 null(调用方据此判定"没能核实",绝不谎报成功)。 */
5613
+ export function lockVersionOf(profileDir, name) {
5614
+ const file = join(profileDir, 'pnpm-lock.yaml')
5615
+ if (!existsSync(file)) return null
5616
+ let lines = []
5617
+ try {
5618
+ lines = readFileSync(file, 'utf8').split(/\r?\n/u)
5619
+ } catch {
5620
+ return null
5621
+ }
5622
+ for (let i = 0; i < lines.length; i += 1) {
5623
+ if (lines[i].trim() !== `'${name}':`) continue
5624
+ for (let k = i + 1; k < Math.min(i + 5, lines.length); k += 1) {
5625
+ const m = /^\s*version:\s*(\S+)\s*$/u.exec(lines[k])
5626
+ if (m) return m[1]
5627
+ if (/^\s*\S+:/u.test(lines[k]) && !/^\s*version:/u.test(lines[k])) break
5628
+ }
5629
+ }
5630
+ const escaped = name.replace(/[/\\^$*+?.()|[\]{}]/gu, '\\$&')
5631
+ const keyRe = new RegExp(`^\\s*'?${escaped}@([^':\\s]+)'?:`, 'u')
5632
+ for (const line of lines) {
5633
+ const m = keyRe.exec(line)
5634
+ if (m) return m[1]
5635
+ }
5636
+ return null
5637
+ }
5638
+
5639
+ /** 给用户的兜底建议命令(让 lock 也变成新版本,而不是只改文件)。 */
5640
+ function selfUpdateCommandFor(latest, profileName = '<你的profile>') {
5641
+ return `dsh plugin --profile ${profileName} add @noob-stupid/dsh-plugin-console@${latest}`
5642
+ }
5643
+
5644
+ /** 依赖以参数注入(便于单测替换 runPnpm / pnpmAdd)。 */
5645
+ export async function selfUpdateToLatest({ profileDir, latest, registries, curlManualInstall, runPnpm = runPnpmWithFallback, pnpmAdd = pnpmInstall, profileName = null, execOpts = {} }) {
5646
+ const NAME = '@noob-stupid/dsh-plugin-console'
5647
+ const command = selfUpdateCommandFor(latest, profileName ?? '<你的profile>')
5648
+ const spec = profileSpecOf(profileDir, NAME)
5649
+ const errors = []
5650
+ let method = null
5651
+ const label = (e) => (e instanceof Error ? e.message : String(e))
5652
+
5653
+ if (isRegistryRange(spec)) {
5654
+ try {
5655
+ await runPnpm(['update', NAME], { execOpts: { cwd: profileDir, timeout: 300000, ...execOpts } })
5656
+ method = 'pnpm-update'
5657
+ } catch (error) {
5658
+ errors.push(`pnpm update 失败:${label(error)}`)
5659
+ }
5660
+ }
5661
+ if (readInstalledVersion(profileDir, NAME) !== latest) {
5662
+ try {
5663
+ await pnpmAdd(profileDir, `${NAME}@${latest}`, registries?.[0])
5664
+ method = method === null ? 'pnpm-add' : `${method}+pnpm-add`
5665
+ } catch (error) {
5666
+ errors.push(`pnpm add 失败:${label(error)}`)
5667
+ }
5668
+ }
5669
+
5670
+ let installed = readInstalledVersion(profileDir, NAME)
5671
+ let lock = lockVersionOf(profileDir, NAME)
5672
+ let lockUpdated = installed === latest && lock === latest
5673
+ let lockNote = null
5674
+
5675
+ if (!lockUpdated) {
5676
+ try {
5677
+ const info = await curlManualInstall(profileDir, NAME, registries, null, latest)
5678
+ method = method === null ? 'manual-copy' : `${method}+manual-copy`
5679
+ installed = info?.version ?? readInstalledVersion(profileDir, NAME)
5680
+ lock = lockVersionOf(profileDir, NAME)
5681
+ lockUpdated = installed === latest && lock === latest
5682
+ } catch (error) {
5683
+ errors.push(`手动铺文件失败:${label(error)}`)
5684
+ }
5685
+ if (!lockUpdated) {
5686
+ lockNote = `此更新未写入 pnpm-lock.yaml(lock 里仍是 ${lock ?? '未知'}):之后任何 pnpm 操作(开关插件、dsh plugin add/remove)都会把它还原成 lock 里的版本。要真正落地请执行:${command}`
5687
+ }
5688
+ }
5689
+
5690
+ const sourceSwitch = spec !== null && !isRegistryRange(spec) ? `安装来源已从 ${spec} 变为 registry 版本 ${latest}` : null
5691
+ const noteParts = [lockUpdated ? '已写入 pnpm-lock.yaml,重启服务后生效' : '已铺入文件但未写入 pnpm-lock.yaml:重启后可用,但会被 pnpm 还原']
5692
+ if (sourceSwitch !== null) noteParts.push(sourceSwitch)
5693
+
5694
+ return { method, spec, installedVersion: installed, lockVersion: lock, lockUpdated, lockNote, command, note: noteParts.join(';'), errors }
5695
+ }
5696
+
5566
5697
  export function apply(ctx) {
5567
5698
  // 恢复 AI 赋能任务(restart 后 running 置失败,plan-ready/结果保留)
5568
5699
  loadAiJobs()
@@ -7982,8 +8113,25 @@ async function handle(ctx, req, res) {
7982
8113
  const profileDir = dirname(findPatchPath(ctx))
7983
8114
  const registries = orderedRegistries(readSources())
7984
8115
  try {
7985
- const info = await curlManualInstall(profileDir, '@noob-stupid/dsh-plugin-console', registries)
7986
- sendJson(res, 200, { ok: true, current: selfVersion, latest, updated: true, version: info.version, note: '已下载最新代码,重启服务后生效' })
8116
+ // 包管理器优先(见 selfUpdateToLatest 注释):旧实现只把文件铺进 node_modules、不写 pnpm-lock.yaml,
8117
+ // 导致"看起来升级成功、下一次 pnpm 操作就被还原"(用户 2026-09-20 实测报告)。
8118
+ const result = await selfUpdateToLatest({ profileDir, latest, registries, curlManualInstall })
8119
+ sendJson(res, 200, {
8120
+ ok: true,
8121
+ current: selfVersion,
8122
+ latest,
8123
+ updated: true,
8124
+ version: result.installedVersion ?? latest,
8125
+ method: result.method,
8126
+ spec: result.spec,
8127
+ installedVersion: result.installedVersion,
8128
+ lockVersion: result.lockVersion,
8129
+ lockUpdated: result.lockUpdated,
8130
+ lockNote: result.lockNote,
8131
+ command: result.command,
8132
+ note: result.note,
8133
+ errors: result.errors,
8134
+ })
7987
8135
  } catch (error) {
7988
8136
  sendError(res, 500, `Hub 自动更新失败:${error instanceof Error ? error.message : String(error)}`)
7989
8137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noob-stupid/dsh-plugin-console",
3
- "version": "0.3.54",
3
+ "version": "0.3.56",
4
4
  "description": "DSH plugin management panel & marketplace: one-click enable/disable, multi-source market (GitHub/Gitee/custom), static-index market (500+ plugins / 300 skills), skills, suites, and one-click framework upgrade.",
5
5
  "repository": {
6
6
  "type": "git",