@noob-stupid/dsh-plugin-console 0.5.60 → 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.
@@ -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
- const autoRun = changed === true ? scheduleAutoPreflight(rc, { current, previous, reasons, profileDir: profileDirOf(rc.ctx), patchPath }) : null
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)
@@ -183,11 +183,16 @@ async function routeStateGet(req, res, rc) {
183
183
  toggleable: false,
184
184
  userDisabled: false,
185
185
  userForced: false,
186
- extra: false,
186
+ // ★ 0.5.64(用户 2026-10-18「还是没显示出来那俩第三方插件」):客户端安装列表**默认**
187
+ // extraOnly=true,只显示 extra===true 的行;虚拟行原本写 false ⇒ 被默认筛选滤掉,
188
+ // 服务端发了、界面看不到。语义上也该是 true:它与"控制台插入的行"同属非核心行。
189
+ extra: true,
187
190
  installDate: null,
188
191
  version: b.version ?? null,
189
192
  repository: null,
190
- pendingCompat: false,
193
+ // ★ 0.5.61(用户 2026-10-18):虚拟行本质就是「待适配」——标成 true 才能拿到客户端那条
194
+ // 既有的「更新并适配」(updateAndAdapt)链路,也就是用户记得的"更新后启用"。
195
+ pendingCompat: true,
191
196
  compatRescinded: false,
192
197
  compatVerdict: null,
193
198
  adoptable: null,
@@ -225,16 +230,33 @@ async function routeDetails(req, res, rc) {
225
230
  const readExtraBundleRows = rc.deps.readExtraBundleRows
226
231
  const body = rc.body
227
232
  const { entryId } = body
228
- if (typeof entryId !== 'string' || !/^[A-Za-z0-9_:.-]{1,80}$/u.test(entryId)) {
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)) {
229
236
  sendError(res, 400, 'entryId 无效')
230
237
  return
231
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
+ }
232
254
  const entry = ctx.loader.entries().find((candidate) => candidate.id === entryId)
233
255
  if (!entry) {
234
256
  sendError(res, 404, `没有名为 ${entryId} 的插件条目`)
235
257
  return
236
258
  }
237
- const moduleName = entry.options.name
259
+ moduleName = entry.options.name
238
260
  const details = await readPluginDetails(moduleName, ctx.baseUrl ?? 'file:///', profileDirOf(ctx))
239
261
  sendJson(res, 200, { ok: true, entryId, rowId: rowIdOf(ctx, entryId), moduleName, ...details })
240
262
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noob-stupid/dsh-plugin-console",
3
- "version": "0.5.60",
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",