@mzzsfy/dsh-maintain 0.7.0 → 0.8.0

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
@@ -10,7 +10,7 @@ DeepSeek Harness 设置页插件:版本与进程运维一体化——监测 npm
10
10
  - 失败分类与自动重试:升级命令失败按输出特征分类(文件锁 EBUSY/EPERM 等 Windows 全局目录占用、网络瞬断与 registry 5xx、命令未找到、超时强杀、其他未知),文件锁与网络类自动重试,最多 3 次尝试,退避序列文件锁 5s/15s、网络 3s/9s,重试时升级锁有效期顺延;命令未找到、超时与未知形态不重试直接落定。重试链的整体观察窗口与宿主侧重试上限同源锁定(parity),浮条不会在宿主仍在重试时抢跑转"状态未知"。
11
11
  - 升级结果校验:升级命令成功且宿主继续运行(手动直跑或本次未勾选自动重启)时复读磁盘版本对比——版本未前进(镜像滞后或静默失败)或未达通道目标版本时,面板与浮条标注 stale 提示;自动重启路径不做复读与 stale 判定(升级落定钩子禁止网络请求与磁盘读取),版本是否前进由重启后的启动检查呈现。运行版本已是通道最新时升级入口直接拒绝(409 防降级,面板按钮同步禁用),重装场景走命令行。超时强杀意味着安装可能只完成一半,重启宿主前先确认命令需否重跑。
12
12
  - 运行环境检测与升级后自动重启:host 启动时检测运行环境(pm2 / systemd / supervisord / Kubernetes / 容器,依据进程环境变量与 POSIX 容器特征,win32 跳过容器探测;stdin+stdout 双 TTY 判定为手动直跑;环境变量 DSH_MAINTAIN_RUNTIME_ENV=managed|manual 可强制指定,最高优先)。升级确认弹窗勾选自动重启且命令成功(退出码 0)时,托管环境下延迟 3 秒自动重启宿主——与面板"确认重启"共用同一退出调度入口(同一互斥、同一审计、同一延迟退出,仅审计行不同),落定钩子零网络请求零磁盘读取(npm 刚替换过宿主文件,避免半写状态故障);浮条提示宿主将自动重启并自动进入重启等待探测,页面随宿主恢复自动刷新;手动直跑环境不自动退出,浮条提示手动重启(弹窗内同步提示该环境约束);检测为未知形态按托管处理(维持自动重启,由重启等待总超时兜底提示)。升级失败或未勾选自动重启时不调度,后者升级完成仅提示,需在面板手动重启。
13
- - 活跃工作门控:升级与重启前检测运行中的 agent 会话、后台 job 与终端(agents/jobs/terminals 服务全部软依赖方法面守卫;terminals 主路径逐 agent 的 realm 作用域解析,共享根探测仅兜底;降级按服务留痕限频一次)。部分降级时已实证的活跃计数仍生效(计数>0 照常拒绝);仅当完全不可用(计数为 0 且检测不可用)时放行,不因安全网缺失死锁操作。存在活跃工作时升级被拒绝(409,不可越过);重启被拒但确认态显示活跃计数,再次点击以 force 越过;概要随 status 提供给面板。
13
+ - 活跃工作门控:升级与重启前检测运行中的 agent 会话、后台 job 与终端(agents/jobs/terminals 服务全部软依赖方法面守卫;terminals 主路径逐 agent 的 realm 作用域解析,共享根探测仅兜底,无 agent 时服务面不可见属常态不计降级;降级按服务留痕限频一次)。部分降级时已实证的活跃计数仍生效(计数>0 照常拒绝);仅当完全不可用(计数为 0 且检测不可用)时放行,不因安全网缺失死锁操作。存在活跃工作时升级被拒绝(409,不可越过);重启被拒但确认态显示活跃计数,再次点击以 force 越过;概要随 status 提供给面板。
14
14
  - 审计日志:升级/重启的触发、落定与拒绝各输出一行结构化日志(`audit endpoint=… outcome=…`),动态串单行化,便于事后排查。
15
15
  - 面板设置编辑:升级命令、轮询间隔、镜像地址均可在面板直接编辑保存,与 settings.yaml 等效(轮询间隔保存即重排,镜像地址保存即重查);host 侧拒绝空命令、非 http(s) 地址与带查询串/锚点的镜像地址(拼接 dist-tags 路径时 query 会吞掉路径),面板清空输入即恢复对应默认值。
16
16
  - 操作反馈:检查更新、通道切换、面板保存与升级触发的失败经全站浮出通知(@mzzsfy/dsh-toast)提示;页面加载失败与重启超时等状态提示仍在面板内展示。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mzzsfy/dsh-maintain",
3
3
  "description": "DSH 自身的版本管理:追踪 npm dist-tag 新版本、一键升级、安全重启",
4
- "version": "0.7.0",
4
+ "version": "0.8.0",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "exports": {
package/src/client.js CHANGED
@@ -23,9 +23,9 @@ window.__ModuleLoader__.load({
23
23
  else console.warn('[dsh-maintain] ' + text)
24
24
  }
25
25
 
26
- // 导航图标声明:交给 dsh-settings-nav-icons 统一渲染(本插件分区 → wrench);
27
- // 该插件未就绪时入队,由其启动时排空
28
- const NAV_ICON = { '版本与运维': 'wrench' }
26
+ // 导航图标声明:交给 dsh-settings-nav-icons 统一渲染;双键 = 分区 label + 市场
27
+ // 短名(发现页收录显示形态);该插件未就绪时入队,由其启动时排空
28
+ const NAV_ICON = { '版本与运维': 'wrench', 'dsh-maintain': 'wrench' }
29
29
  if (window.__navicIcons !== undefined) window.__navicIcons.register(NAV_ICON)
30
30
  else if (Array.isArray(window.__navicIconQueue)) window.__navicIconQueue.push(NAV_ICON)
31
31
  else window.__navicIconQueue = [NAV_ICON]
@@ -179,6 +179,11 @@ async function api(url, options) {
179
179
  return payload
180
180
  }
181
181
 
182
+ // 状态预取:模块加载即页面加载时发起,首次进入面板不再与宿主的初始请求风暴
183
+ // (会话树全量扫描等单请求秒级负载)排队;预取只服务首次挂载,消费即置空,
184
+ // 后续挂载保持现拉,不因复用旧快照而退化
185
+ let statusPrefetch = api(STATUS_URL).catch(() => null)
186
+
182
187
  function post(url, body) {
183
188
  return api(url, { method: 'POST', body: body === undefined ? '{}' : JSON.stringify(body) })
184
189
  }
@@ -623,7 +628,11 @@ function MaintainApp() {
623
628
  }
624
629
 
625
630
  function load() {
626
- return api(STATUS_URL)
631
+ // 首次挂载消费预取快照(缺失/失败回退现拉),此后每次挂载现拉
632
+ const primed = statusPrefetch
633
+ statusPrefetch = null
634
+ const base = primed !== null ? primed.then((snap) => (snap !== null ? snap : api(STATUS_URL))) : api(STATUS_URL)
635
+ return base
627
636
  .then((next) => { setStatus(next); setError(null); return next })
628
637
  .catch((loadError) => { setError('读取状态失败:' + (loadError && loadError.message ? loadError.message : String(loadError))); return null })
629
638
  }
package/src/index.js CHANGED
@@ -74,8 +74,8 @@ const audit = (endpoint, outcome, extra) => {
74
74
 
75
75
  // 活跃工作检测:agents/jobs/terminals 全软依赖方法面守卫,缺失或异常即降级放行(fail-open,
76
76
  // 不因安全网缺失死锁重启)。terminals 主路径逐 agent 的 realm 作用域解析(PTY 注册表随
77
- // agent 挂载且 isolate,共享根作用域因 realm 隔离恒缺,仅兜底);sessions 不作活跃指标;
78
- // 禁止顶层 inject(阻塞装载)。导出仅供测试
77
+ // agent 挂载且 isolate,共享根作用域因 realm 隔离恒缺,仅兜底;无 agent 时服务面
78
+ // 不可见属常态,不计降级);sessions 不作活跃指标;禁止顶层 inject(阻塞装载)。导出仅供测试
79
79
  // 降级留痕按服务键进程首见一次:status 每拍重检,不限频会刷屏
80
80
  const degradedWarned = new Set()
81
81
 
@@ -145,7 +145,9 @@ export function collectActiveWork(ctx) {
145
145
  } catch (error) {
146
146
  degrade('terminals', '根探测异常: ' + (error && error.message ? error.message : String(error)))
147
147
  }
148
- if (!sawTerminalsService) degrade('terminals', 'terminals 服务面不可用')
148
+ // 服务面随 agent realm 挂载:无 agent 时不可见属常态(无对象即无终端),不计降级;
149
+ // 有 agent 而探不到服务面(可探性缺失或面缺失)属异常,降级留痕
150
+ if (!sawTerminalsService && agentList.length > 0) degrade('terminals', 'terminals 服务面不可用')
149
151
  } catch (error) {
150
152
  degrade('terminals', error && error.message ? error.message : String(error))
151
153
  }
@@ -740,6 +740,33 @@ test('collectActiveWork:服务缺失或异常 fail-open 降级', () => {
740
740
  assert.equal(realmOnly.detectionAvailable, true)
741
741
  })
742
742
 
743
+ test('collectActiveWork:无 agent 时 terminals 服务面不可见属常态,不降级', () => {
744
+ const warns = []
745
+ const originalWarn = console.warn
746
+ console.warn = (text) => warns.push(String(text))
747
+ try {
748
+ const agents = { list: () => [] }
749
+ const jobs = { list: () => [] }
750
+ const result = collectActiveWork({ get: (n) => (n === 'agents' ? agents : n === 'jobs' ? jobs : undefined) })
751
+ assert.deepEqual([result.agents, result.jobs, result.terminals], [0, 0, 0])
752
+ assert.equal(result.detectionAvailable, true)
753
+ // 告警断言受模块级去重影响恒真,防线是上一行 detectionAvailable 断言
754
+ assert.equal(warns.some((text) => text.includes('活跃工作检测降级')), false, '常态空窗不得打降级告警')
755
+ } finally {
756
+ console.warn = originalWarn
757
+ }
758
+ })
759
+
760
+ test('collectActiveWork:有 agent 而服务面缺失(可探或不可探),仍降级', () => {
761
+ const probeless = { id: 'a1', status: 'idle' }
762
+ const blindProbe = { id: 'a2', status: 'idle', ctx: { get: () => undefined } }
763
+ const probelessResult = collectActiveWork({ get: (n) => (n === 'agents' ? { list: () => [probeless] } : n === 'jobs' ? { list: () => [] } : undefined) })
764
+ assert.equal(probelessResult.detectionAvailable, false)
765
+ const blindResult = collectActiveWork({ get: (n) => (n === 'agents' ? { list: () => [blindProbe] } : n === 'jobs' ? { list: () => [] } : undefined) })
766
+ assert.equal(blindResult.detectionAvailable, false)
767
+ assert.equal(blindResult.terminals, 0)
768
+ })
769
+
743
770
  test('upgrade:存在活跃工作 409 拒绝,不可越', async () => {
744
771
  const store = { upgradeCommandTemplate: 'node -e "process.exit(0)"' }
745
772
  const agents = { list: () => [{ id: 'a1', status: 'running' }] }