@gehennawu/dsh-service 1.9.9 → 2.0.1

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/index.js CHANGED
@@ -3039,9 +3039,11 @@ async function repairPermissions(ctx, dshHome, plans, planId) {
3039
3039
 
3040
3040
  // 运行环境检查(v0.17):managed/declared → ok;疑似手动启动 → warning 但带 advisory 标记
3041
3041
  //(黄色行内提示,不参与 overall 聚合、不点亮标签 ⚠ 与顶部提醒——用户复核口径);unknown →
3042
- // info。detail 是客户端映射词典的令牌。
3042
+ // info。detail 是客户端映射词典的令牌。桌面端(Electron)由应用自身托管重启,是明确的 ok
3043
+ // 事实,不再落 unknown 的含糊提示,且优先于 manual 判定。
3043
3044
  function runtimeEnvCheck(runtimeEnv) {
3044
3045
  if (runtimeEnv === undefined || runtimeEnv === null) return null
3046
+ if (runtimeEnv.electronShell === true) return { id: 'runtime-env', status: 'ok', detail: 'desktop' }
3045
3047
  if (runtimeEnv.manualStartLikely === true) return { id: 'runtime-env', status: 'warning', detail: 'manual', advisory: true }
3046
3048
  if (runtimeEnv.supervisorKind === 'declared') return { id: 'runtime-env', status: 'ok', detail: 'declared' }
3047
3049
  if (typeof runtimeEnv.supervisorKind === 'string' && runtimeEnv.supervisorKind.length > 0) {
@@ -3256,7 +3258,9 @@ function collectActiveWork(ctx) {
3256
3258
  // manualStartLikely=true —— 无管理器且 stdin/stdout 双 TTY(终端直启特征),升级后宿主
3257
3259
  // 不自动退出,客户端改示手动重启指引;
3258
3260
  // 其余 —— unknown(输出重定向、NSSM/WinSW 等包装器),维持现状不折腾。
3259
- // 探测不到的包装器可用 DSH_SERVICE_RUNTIME_ENV=managed|manual 由用户显式声明。
3261
+ // 探测不到的包装器可用 DSH_SERVICE_RUNTIME_ENV=managed|manual|desktop 由用户显式声明。
3262
+ // 第四态 electronShell=true 是正交的平台事实(DSH 桌面端),三态取值不变:桌面端既没有
3263
+ // supervisor 标记、stdio 也非 TTY,本会落 unknown,靠这一位才能改走桌面端重启引导。
3260
3264
  const RUNTIME_SUPERVISOR_ENV = [
3261
3265
  ['pm2', ['pm_id', 'PM2_HOME', 'pm_uptime']],
3262
3266
  ['systemd', ['INVOCATION_ID', 'JOURNAL_STREAM', 'NOTIFY_SOCKET']],
@@ -3264,9 +3268,21 @@ const RUNTIME_SUPERVISOR_ENV = [
3264
3268
  ['kubernetes', ['KUBERNETES_SERVICE_HOST']],
3265
3269
  ]
3266
3270
 
3271
+ // 桌面端(DSH Desktop)判据:桌面端的 Host 是 Electron 以 RunAsNode 承载的子进程,该进程里
3272
+ // `process.versions.electron` 有值(2026-09-28 Windows 真机实测:electron 44.0.0 / node 24.18.1,
3273
+ // 同进程 ELECTRON_RUN_AS_NODE=1,stdin/stdout 均非 TTY)。只认版本号字段:它是进程自身的运行时
3274
+ // 事实,不像 env 那样会随父进程继承给普通子进程,也不会对 iframe/嵌入帧误判(`'dshDesktop' in
3275
+ // globalThis` 在嵌入帧里同样成立,不能当判据)。非 Electron 的桌面封装可显式声明
3276
+ // DSH_SERVICE_RUNTIME_ENV=desktop。
3277
+ function isElectronRuntime(versions) {
3278
+ return versions !== null && typeof versions === 'object'
3279
+ && typeof versions.electron === 'string' && versions.electron.length > 0
3280
+ }
3281
+
3267
3282
  function detectRuntimeEnv(options = {}) {
3268
3283
  const env = options.env ?? process.env
3269
3284
  const platform = options.platform ?? process.platform
3285
+ const versions = options.versions ?? process.versions
3270
3286
  const stdinIsTTY = options.stdinIsTTY ?? (process.stdin ? process.stdin.isTTY === true : false)
3271
3287
  const stdoutIsTTY = options.stdoutIsTTY ?? (process.stdout ? process.stdout.isTTY === true : false)
3272
3288
  // 默认探测器只在未被注入时执行;win32 直接短路(/.dockerenv 与 /proc 不存在),单测全部显式传原语。
@@ -3274,25 +3290,39 @@ function detectRuntimeEnv(options = {}) {
3274
3290
  const cgroupText = options.cgroupText ?? (platform === 'win32' ? '' : (() => { try { return readFileSync('/proc/1/cgroup', 'utf8') } catch (_) { return '' } })())
3275
3291
 
3276
3292
  const forced = typeof env.DSH_SERVICE_RUNTIME_ENV === 'string' ? env.DSH_SERVICE_RUNTIME_ENV.trim().toLowerCase() : ''
3277
- if (forced === 'manual') return { platform, supervisorKind: null, manualStartLikely: true }
3278
- if (forced === 'managed') return { platform, supervisorKind: 'declared', manualStartLikely: false }
3293
+ const electronShell = forced === 'desktop' || isElectronRuntime(versions)
3294
+ // 桌面端标记只在成立时出现在返回对象里:其余平台的原返回逐字不变,既有三态用例与下游
3295
+ // deepEqual 断言不受影响。判据只喂给「重启 / 升级 / 诊断」的用户可见行为,不作安全门槛。
3296
+ const shell = (result) => (electronShell ? { ...result, electronShell: true } : result)
3297
+
3298
+ if (forced === 'manual') return shell({ platform, supervisorKind: null, manualStartLikely: true })
3299
+ if (forced === 'managed') return shell({ platform, supervisorKind: 'declared', manualStartLikely: false })
3300
+ // 显式声明桌面端:不再往下看 supervisor 标记与 TTY(桌面端那些信号都不代表会被拉起)。
3301
+ if (forced === 'desktop') return shell({ platform, supervisorKind: null, manualStartLikely: false })
3279
3302
 
3280
3303
  for (const [kind, keys] of RUNTIME_SUPERVISOR_ENV) {
3281
3304
  if (keys.some((key) => env[key] !== undefined && env[key] !== '')) {
3282
- return { platform, supervisorKind: kind, manualStartLikely: false }
3305
+ return shell({ platform, supervisorKind: kind, manualStartLikely: false })
3283
3306
  }
3284
3307
  }
3285
3308
  // /.dockerenv 与 /proc 只在 POSIX 内核上存在;win32 原生进程不做这两个探测。
3286
3309
  if (platform !== 'win32') {
3287
- if (dockerEnvExists === true) return { platform, supervisorKind: 'docker', manualStartLikely: false }
3288
- if (/docker/i.test(cgroupText)) return { platform, supervisorKind: 'docker', manualStartLikely: false }
3289
- if (/containerd|kubepods|lxc|podman/i.test(cgroupText)) return { platform, supervisorKind: 'container', manualStartLikely: false }
3310
+ if (dockerEnvExists === true) return shell({ platform, supervisorKind: 'docker', manualStartLikely: false })
3311
+ if (/docker/i.test(cgroupText)) return shell({ platform, supervisorKind: 'docker', manualStartLikely: false })
3312
+ if (/containerd|kubepods|lxc|podman/i.test(cgroupText)) return shell({ platform, supervisorKind: 'container', manualStartLikely: false })
3290
3313
  }
3291
- if (stdinIsTTY === true && stdoutIsTTY === true) return { platform, supervisorKind: null, manualStartLikely: true }
3292
- return { platform, supervisorKind: null, manualStartLikely: false }
3314
+ if (stdinIsTTY === true && stdoutIsTTY === true) return shell({ platform, supervisorKind: null, manualStartLikely: true })
3315
+ return shell({ platform, supervisorKind: null, manualStartLikely: false })
3293
3316
  }
3294
3317
 
3295
- function scheduleRestart(ctx) {
3318
+ // 桌面端不退出:壳把 Host 的任何非 0 退出判为崩溃,写 crash report 并弹原生「启动失败」对话框
3319
+ // (退出 / 重启 / 禁用第三方插件),没有自动重拉,且官方没有任何可编程重启 RPC。因此桌面端一律
3320
+ // 不 exit(42),把「已是最新状态、需要重启」这一事实交给调用方转述(客户端改示桌面端重启指引)。
3321
+ // 返回值:scheduled=false + desktopManaged=true 时调用方不得再启动恢复轮询(不会出现新实例)。
3322
+ function scheduleRestart(ctx, runtimeEnv) {
3323
+ if (runtimeEnv !== undefined && runtimeEnv !== null && runtimeEnv.electronShell === true) {
3324
+ return { scheduled: false, desktopManaged: true }
3325
+ }
3296
3326
  const doExit = () => {
3297
3327
  try {
3298
3328
  // 退出码 42 交给 Docker/systemd/pm2 的重启策略处理。
@@ -3302,8 +3332,9 @@ function scheduleRestart(ctx) {
3302
3332
  }
3303
3333
  }
3304
3334
  const timer = ctx.get('timer')
3305
- if (timer !== undefined) return timer.timeout(doExit, 500)
3306
- return doExit()
3335
+ if (timer !== undefined) timer.timeout(doExit, 500)
3336
+ else doExit()
3337
+ return { scheduled: true, desktopManaged: false }
3307
3338
  }
3308
3339
 
3309
3340
  // ─── 技能管理(v0.22):扫描 / frontmatter 手术 / AI 补全 ─────────────────────
@@ -5209,7 +5240,7 @@ function apply(ctx, featureConfig) {
5209
5240
  isEnabled: () => featureEnabled('backupMaintenance'),
5210
5241
  runtimeEnv,
5211
5242
  previousInstanceId: instanceId,
5212
- scheduleRestart: () => scheduleRestart(ctx),
5243
+ scheduleRestart: () => scheduleRestart(ctx, runtimeEnv),
5213
5244
  })
5214
5245
  // 会话管理(v0.35):删除两段式计划(planId → {id, path, bytes}),TTL 过期自动驱逐。
5215
5246
  const sessionDeletePlans = new Map()
@@ -5701,7 +5732,11 @@ function apply(ctx, featureConfig) {
5701
5732
  if (invocation.rawInput.trim() !== '') return { kind: 'error', text: '/restart does not accept arguments.' }
5702
5733
  const activity = collectActiveWork(ctx)
5703
5734
  if (activity.hasActive) return { kind: 'error', text: `Restart refused: ${activity.items.length} active item(s) detected. Use the Service Control restart tab to review them.` }
5704
- scheduleRestart(ctx)
5735
+ // 桌面端不退出(见 scheduleRestart):命令反馈必须说清「要重启的是应用」,否则用户
5736
+ // 会以为宿主进程会自己回来。
5737
+ if (scheduleRestart(ctx, runtimeEnv).desktopManaged) {
5738
+ return { kind: 'success', text: 'DSH Desktop manages this process: quit and reopen the app to restart it.' }
5739
+ }
5705
5740
  return { kind: 'success', text: 'Restart scheduled. The DSH Web process will exit in 0.5 seconds.' }
5706
5741
  },
5707
5742
  }), 'dsh-service restart command')
@@ -5885,6 +5920,11 @@ function apply(ctx, featureConfig) {
5885
5920
 
5886
5921
  } },
5887
5922
  'upgrade': { audit: true, handle: async (payload, rpcEndpoint) => {
5923
+ // 桌面端短路(先于任何 subprocess):桌面端升级动作要 spawn `dsh plugin --profile desktop add`,
5924
+ // 而 bundled runtime 的 bin 不进 Host PATH,且官方明确 CLI 不能启动/改写 profiles/desktop
5925
+ // (该 profile 由 Electron 独占)⇒ 必然失败。官方通道是桌面端自带的插件管理页,直接告诉
5926
+ // 用户走那条路,不产生任何 subprocess 调用。
5927
+ if (runtimeEnv.electronShell === true) return rpcFailure(new Error('desktop-managed-upgrade'))
5888
5928
  try {
5889
5929
  const value = await upgradePlugin(ctx, dshHome, runtimeEnv)
5890
5930
  // 升级落地即作废更新缓存:下一次 check-update 要按新的磁盘版本重算「是否有新版本」,
@@ -5967,12 +6007,15 @@ function apply(ctx, featureConfig) {
5967
6007
  return { ok: false, error: 'active-work', value: activity }
5968
6008
  }
5969
6009
 
5970
- scheduleRestart(ctx)
6010
+ const restart = scheduleRestart(ctx, runtimeEnv)
5971
6011
  return {
5972
6012
  ok: true,
5973
6013
  value: {
5974
6014
  // 只下发客户端真正消费的实例 id;重启反馈文案由客户端词典渲染(双语约束)。
5975
6015
  instanceId,
6016
+ // 桌面端不退出(见 scheduleRestart):显式告知客户端「不会有新实例」,客户端据此
6017
+ // 改示桌面端重启指引、不启动恢复轮询(与升级落地那条 requiresManualRestart 同语义)。
6018
+ ...(restart.desktopManaged ? { requiresManualRestart: true } : {}),
5976
6019
  },
5977
6020
  }
5978
6021
 
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@gehennawu/dsh-service",
3
- "version": "1.9.9",
3
+ "version": "2.0.1",
4
4
  "description": "DSH Web 运维面板:安全重启、健康监控、备份和 Linux 权限维护。",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build:client": "node scripts/build-client.mjs",
8
8
  "check:client": "node scripts/check-client-source.mjs",
9
9
  "pretest": "npm run build:client",
10
- "test": "node --test",
10
+ "test": "node --test test/*.test.js",
11
11
  "prepack": "npm run build:client"
12
12
  },
13
13
  "main": "./index.js",
@@ -53,7 +53,7 @@
53
53
  "homepage": "https://github.com/gehennawu/dsh-service#readme",
54
54
  "engines": {
55
55
  "node": ">=22",
56
- "dsh": ">=0.1.1-rc.2 <0.1.7-rc.3"
56
+ "dsh": ">=0.1.1-rc.2 <0.2.0-rc.3"
57
57
  },
58
58
  "dependencies": {
59
59
  "@deepseek-ai/schemastery": "^3.18.3"