@gehennawu/dsh-service 0.15.0 → 0.17.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/README.en.md +9 -2
- package/README.md +9 -2
- package/client.js +178 -12
- package/index.js +98 -10
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -14,19 +14,22 @@ The Settings panel "Service Control" page has six top-level tabs: **Overview, No
|
|
|
14
14
|
|
|
15
15
|
- Displays current DSH and plugin versions with links to GitHub Releases
|
|
16
16
|
- Automatically checks npm registry for stable and preview releases; when a new version exists, the right-side status text (chevron + `New version`) is clickable to expand/collapse, showing the current/latest versions and both dist-tag versions inline, each with npmjs and npmmirror links (npmjs.com is blocked on some networks; npmmirror serves as the mirror entry)
|
|
17
|
-
- One-click plugin upgrade with automatic restart after completion
|
|
17
|
+
- One-click plugin upgrade with automatic restart after completion; when no process manager is detected (for example, a manual launch from a Windows terminal), the upgrade first confirms the consequences, then keeps the process running and shows manual-restart instructions
|
|
18
18
|
|
|
19
19
|
### Safe restart
|
|
20
20
|
|
|
21
21
|
- Detects active agents, background jobs, and terminals before restart; lists them and requires explicit confirmation
|
|
22
22
|
- `/restart` command also available in conversations; automatically refuses when active work is detected
|
|
23
23
|
- Automatically probes for the new process after restart and reloads the page; manual reload available after 60 seconds
|
|
24
|
+
- When a manual terminal launch is suspected, the restart confirmation flow warns that nothing will bring the process back, and Health diagnostics marks it with a yellow inline caution
|
|
24
25
|
- Optional `Restart` entry at the bottom of the settings left navigation, enabled by a switch in the Restart tab (off by default), sharing the exact same confirmation flow as the Restart tab
|
|
25
26
|
|
|
26
27
|
### Health diagnostics
|
|
27
28
|
|
|
28
29
|
- Shows uptime, memory, session count, active agents, and background jobs
|
|
29
|
-
-
|
|
30
|
+
- The "Process and runtime" card shows platform, architecture, and Node version
|
|
31
|
+
- Full diagnostics check session storage, workspace registry, backup storage, tar availability, file permissions, runtime environment, and Node runtime version; a manual launch is marked with a yellow inline caution (no restart assurance) that does not trigger the health alert banner, the service-control reminder, or the tab ⚠; an unrecognized environment and an empty backup list are informational only — all can be declared explicitly via `DSH_SERVICE_RUNTIME_ENV`
|
|
32
|
+
- Having no backups is an informational note, not a warning, and does not light the Health tab ⚠
|
|
30
33
|
- File-permission deep scan and repair: checks whether the Agent can read/write DSH_HOME and workspaces; repair requires two-step confirmation
|
|
31
34
|
|
|
32
35
|
### Model statistics
|
|
@@ -90,6 +93,8 @@ dsh plugin --profile web add link:/path/to/dsh-service
|
|
|
90
93
|
|
|
91
94
|
The plugin only sends an exit signal; it does not start the process again. Without a process manager, selecting restart stops DSH Web.
|
|
92
95
|
|
|
96
|
+
The plugin passively detects whether a process manager is present (environment variables, `/.dockerenv`, `/proc/1/cgroup`, terminal TTY): with Docker/systemd/pm2/supervisord/Kubernetes detected it restarts as usual; when nothing is detected and stdin/stdout are an interactive terminal, it treats the environment as a likely manual launch, flags it in Health diagnostics with a yellow caution, and switches one-click upgrade to keep the process running with manual-restart instructions. Heuristics cannot cover redirected output or wrappers like NSSM/WinSW; set `DSH_SERVICE_RUNTIME_ENV=managed|manual` to declare it explicitly.
|
|
97
|
+
|
|
93
98
|
### Docker Compose
|
|
94
99
|
|
|
95
100
|
```yaml
|
|
@@ -122,6 +127,8 @@ pm2 start "dsh web --host 127.0.0.1" --name dsh-web
|
|
|
122
127
|
| macOS / Windows + pm2 or similar | Not blocked by the code | Managed externally | Not tested |
|
|
123
128
|
| Direct `dsh web` execution | Supported | Not supported | Expected behavior |
|
|
124
129
|
|
|
130
|
+
When launched directly from a terminal (PowerShell/CMD/bash), the panel labels the environment as a likely manual launch and the one-click upgrade no longer exits the process automatically.
|
|
131
|
+
|
|
125
132
|
Requirements: Node.js `>=22`, and a DSH Web installation capable of loading both Host and Client plugin halves. Update checks require access to `registry.npmjs.org`; network failures do not affect other features.
|
|
126
133
|
|
|
127
134
|
## Security design
|
package/README.md
CHANGED
|
@@ -14,19 +14,22 @@
|
|
|
14
14
|
|
|
15
15
|
- 显示当前 DSH 和插件版本,版本号链接到 GitHub Releases
|
|
16
16
|
- 自动检查 npm registry 的正式版和预览版;有新版本时右侧「小三角 + 有新版本」状态文本可点击展开/收起,行内展示当前/最新版本与正式版/预览版双 tag,版本号分别带 npmjs 与 npmmirror 链接(部分网络会被 npmjs.com 拦截,npmmirror 作为镜像入口)
|
|
17
|
-
-
|
|
17
|
+
- 一键升级插件,升级后自动重启;未检测到进程管理器(如在 Windows 终端手动启动)时,升级前先确认后果,安装完成后保持运行并提示手动重启
|
|
18
18
|
|
|
19
19
|
### 安全重启
|
|
20
20
|
|
|
21
21
|
- 重启前检测活跃 Agent、后台任务和终端,展示清单并要求显式确认
|
|
22
22
|
- 对话中输入 `/restart` 也可触发,检测到运行中工作时自动拒绝
|
|
23
23
|
- 重启后自动探测新进程并刷新页面,60 秒未恢复时提供手动刷新
|
|
24
|
+
- 疑似终端手动启动时,重启确认流程会提示「退出后不会自动拉起」,健康诊断中以黄色行内警示标注
|
|
24
25
|
- 可在「重启」标签开启「设置页左列显示入口」开关(默认关闭),开启后在设置页左侧标签列底部显示「重启」快捷入口,与「重启」标签共用同一套确认流程
|
|
25
26
|
|
|
26
27
|
### 健康诊断
|
|
27
28
|
|
|
28
29
|
- 显示运行时间、内存、会话数、活跃 Agent 和后台任务
|
|
29
|
-
-
|
|
30
|
+
- 「进程与运行环境」卡显示平台、架构和 Node 版本
|
|
31
|
+
- 完整诊断检查会话存储、工作区注册表、备份目录、tar 可用性、文件权限、运行环境和 Node 运行时版本;手动启动环境以黄色行内警示标注重启无保障(不触发健康提醒横幅、服务控制提醒和标签 ⚠),未识别环境与空备份为信息级提示(不算警告),均可通过 `DSH_SERVICE_RUNTIME_ENV` 显式声明
|
|
32
|
+
- 没有备份属于信息级提示,不算警告,也不点亮健康诊断标签的 ⚠
|
|
30
33
|
- 文件权限深检与修复:检查 Agent 是否能读写 DSH_HOME 和工作区,修复需两段式确认
|
|
31
34
|
|
|
32
35
|
### 模型统计
|
|
@@ -90,6 +93,8 @@ dsh plugin --profile web add link:/path/to/dsh-service
|
|
|
90
93
|
|
|
91
94
|
插件只发送退出信号,不负责重新启动进程。没有进程管理器时,点击重启会直接停止 DSH Web。
|
|
92
95
|
|
|
96
|
+
插件会用被动信号(环境变量、`/.dockerenv`、`/proc/1/cgroup`、终端 TTY)判断当前是否由进程管理器拉起:检测到 Docker/systemd/pm2/supervisord/Kubernetes 时照常自动重启;都没有且 stdin/stdout 是交互终端时视为「疑似手动启动」,健康诊断会以黄色警示标注,一键升级改为「不自动退出 + 提示手动重启」。启发式无法覆盖输出重定向、NSSM/WinSW 等场景,可用环境变量 `DSH_SERVICE_RUNTIME_ENV=managed|manual` 显式声明。
|
|
97
|
+
|
|
93
98
|
### Docker Compose
|
|
94
99
|
|
|
95
100
|
```yaml
|
|
@@ -122,6 +127,8 @@ pm2 start "dsh web --host 127.0.0.1" --name dsh-web
|
|
|
122
127
|
| macOS / Windows + pm2 等 | 代码未限制 | 由进程管理器负责 | 未验证 |
|
|
123
128
|
| 直接运行 `dsh web` | 支持 | 不支持 | 预期行为 |
|
|
124
129
|
|
|
130
|
+
直接在终端运行(PowerShell/CMD/bash)时,面板会标注「疑似终端手动启动」,一键升级不再自动退出进程。
|
|
131
|
+
|
|
125
132
|
运行要求:Node.js `>=22`,DSH Web 能加载 Host 和 Client 两半插件。检查更新需要访问 `registry.npmjs.org`;网络失败不影响其他功能。
|
|
126
133
|
|
|
127
134
|
## 安全设计
|
package/client.js
CHANGED
|
@@ -20,6 +20,8 @@ window.__ModuleLoader__.load({
|
|
|
20
20
|
'health.title': '运行状况',
|
|
21
21
|
'health.uptime': '运行时间',
|
|
22
22
|
'health.rss': '内存 RSS',
|
|
23
|
+
'health.platform': '平台',
|
|
24
|
+
'health.nodeVersion': 'Node 版本',
|
|
23
25
|
'health.liveSessions': '存活会话',
|
|
24
26
|
'health.persistedSessions': '持久化会话',
|
|
25
27
|
'health.activeAgents': '活跃 Agent',
|
|
@@ -43,6 +45,14 @@ window.__ModuleLoader__.load({
|
|
|
43
45
|
'health.check.backup-storage': '备份存储',
|
|
44
46
|
'health.check.tar': 'tar',
|
|
45
47
|
'health.check.permissions': '文件权限',
|
|
48
|
+
'health.check.runtime-env': '运行环境',
|
|
49
|
+
'health.check.node-version': 'Node 运行时',
|
|
50
|
+
'health.detail.runtime-env.managed': '由{kind}管理,重启后会自动拉起',
|
|
51
|
+
'health.detail.runtime-env.declared': '已通过 DSH_SERVICE_RUNTIME_ENV 声明由外部进程管理器管理',
|
|
52
|
+
'health.detail.runtime-env.manual': '疑似终端手动启动,重启后不会自动拉起;一键升级已改为不自动退出',
|
|
53
|
+
'health.detail.runtime-env.unknown': '未检测到进程管理器,无法确认重启后是否自动拉起;如实际有,可设置 DSH_SERVICE_RUNTIME_ENV=managed 声明',
|
|
54
|
+
'health.detail.node-version.ok': '{version},满足 ≥{required} 要求',
|
|
55
|
+
'health.detail.node-version.warning': '{version} 低于插件要求的 {required}.x,建议升级 Node',
|
|
46
56
|
'health.detail.session-storage.ok': '可用,共 {count} 个会话快照',
|
|
47
57
|
'health.detail.workspace-registry.ok': '可用,共 {count} 个工作区',
|
|
48
58
|
'health.detail.dsh-home.ok': '目录可访问,权限模式 {mode}',
|
|
@@ -56,7 +66,7 @@ window.__ModuleLoader__.load({
|
|
|
56
66
|
'tabs.notify': '通知',
|
|
57
67
|
'tabs.health': '健康诊断',
|
|
58
68
|
'tabs.usage': '模型统计',
|
|
59
|
-
'overview.container': '
|
|
69
|
+
'overview.container': '进程与运行环境',
|
|
60
70
|
'overview.errors': '报错信息',
|
|
61
71
|
'tabs.backup': '备份维护',
|
|
62
72
|
'tabs.restart': '重启',
|
|
@@ -141,6 +151,18 @@ window.__ModuleLoader__.load({
|
|
|
141
151
|
'update.failAddingToRoot': 'pnpm 拒绝在 workspace 根目录安装(缺少 -w)',
|
|
142
152
|
'update.failNotWorkspace': 'profile 不是 pnpm workspace 却传入了 -w',
|
|
143
153
|
'update.failIgnoredBuilds': '依赖构建脚本被 pnpm 默认拦截,无法完成升级',
|
|
154
|
+
'env.kind.docker': 'Docker 容器',
|
|
155
|
+
'env.kind.container': '容器(containerd/Kubernetes)',
|
|
156
|
+
'env.kind.systemd': 'systemd 服务',
|
|
157
|
+
'env.kind.pm2': 'pm2',
|
|
158
|
+
'env.kind.supervisord': 'supervisord',
|
|
159
|
+
'env.kind.kubernetes': 'Kubernetes',
|
|
160
|
+
'env.kind.declared': '外部进程管理器(手动指定)',
|
|
161
|
+
'update.manualConfirmTitle': '升级前请确认:当前疑似手动启动环境',
|
|
162
|
+
'update.manualConfirm': '未检测到 Docker/systemd/pm2 等进程管理器。升级完成后 DSH 不会自动重启——新版本写入磁盘,但当前进程继续运行旧版本;你需要手动关闭并重新运行 dsh 才能启用新版本。',
|
|
163
|
+
'update.manualProceed': '仍要升级',
|
|
164
|
+
'update.manualRestartTitle': '升级完成,需要手动重启',
|
|
165
|
+
'update.manualRestartBody': '新版本已安装,但当前进程仍在运行旧版本。请在运行 dsh 的终端窗口按 Ctrl+C(或直接关闭窗口),然后重新启动 dsh。',
|
|
144
166
|
'restart.title': '服务重启',
|
|
145
167
|
'restart.description': '重启 dsh web 进程。运行中的工作会中断,持久化会话可恢复。也可在对话中输入 /restart。',
|
|
146
168
|
'restart.button': '重启 dsh web',
|
|
@@ -151,6 +173,8 @@ window.__ModuleLoader__.load({
|
|
|
151
173
|
'restart.sent': '重启指令已发出。',
|
|
152
174
|
'restart.sentHint': '页面连接即将断开,服务恢复后将自动刷新。',
|
|
153
175
|
'restart.idleHint': '当前没有检测到运行中的工作。确认后将断开连接,等待服务自动重启。',
|
|
176
|
+
'restart.manualWarn': '当前疑似终端手动启动环境:确认后进程将退出且不会自动拉起,需要你手动重新运行 dsh。',
|
|
177
|
+
'restart.sentManualHint': '当前为手动启动环境,服务不会自动拉起。请在原终端重新运行 dsh;服务起来后刷新此页面即可恢复。',
|
|
154
178
|
'restart.navToggle': '设置页左列显示「重启」入口',
|
|
155
179
|
'restart.navToggleHint': '默认关闭;开启后在设置页左侧标签列底部显示快捷重启入口',
|
|
156
180
|
'activity.agent': 'Agent',
|
|
@@ -228,6 +252,8 @@ window.__ModuleLoader__.load({
|
|
|
228
252
|
'health.title': 'Health',
|
|
229
253
|
'health.uptime': 'Uptime',
|
|
230
254
|
'health.rss': 'Memory RSS',
|
|
255
|
+
'health.platform': 'Platform',
|
|
256
|
+
'health.nodeVersion': 'Node version',
|
|
231
257
|
'health.liveSessions': 'Live sessions',
|
|
232
258
|
'health.persistedSessions': 'Persisted sessions',
|
|
233
259
|
'health.activeAgents': 'Active agents',
|
|
@@ -251,6 +277,14 @@ window.__ModuleLoader__.load({
|
|
|
251
277
|
'health.check.backup-storage': 'Backup storage',
|
|
252
278
|
'health.check.tar': 'tar',
|
|
253
279
|
'health.check.permissions': 'File permissions',
|
|
280
|
+
'health.check.runtime-env': 'Runtime environment',
|
|
281
|
+
'health.check.node-version': 'Node runtime',
|
|
282
|
+
'health.detail.runtime-env.managed': 'Managed by {kind}; the process restarts automatically after exit',
|
|
283
|
+
'health.detail.runtime-env.declared': 'Declared externally managed via DSH_SERVICE_RUNTIME_ENV',
|
|
284
|
+
'health.detail.runtime-env.manual': 'Likely a manual terminal launch — nothing restarts the process after exit; one-click upgrade already keeps it running',
|
|
285
|
+
'health.detail.runtime-env.unknown': 'No process manager detected, so automatic restart cannot be confirmed; set DSH_SERVICE_RUNTIME_ENV=managed if one exists',
|
|
286
|
+
'health.detail.node-version.ok': '{version}, meets the >={required} requirement',
|
|
287
|
+
'health.detail.node-version.warning': '{version} is below the required {required}.x; upgrading Node is recommended',
|
|
254
288
|
'health.detail.session-storage.ok': 'Available, with {count} session snapshots',
|
|
255
289
|
'health.detail.workspace-registry.ok': 'Available, with {count} workspaces',
|
|
256
290
|
'health.detail.dsh-home.ok': 'Directory is accessible with mode {mode}',
|
|
@@ -264,7 +298,7 @@ window.__ModuleLoader__.load({
|
|
|
264
298
|
'tabs.notify': 'Notifications',
|
|
265
299
|
'tabs.health': 'Health',
|
|
266
300
|
'tabs.usage': 'Models',
|
|
267
|
-
'overview.container': '
|
|
301
|
+
'overview.container': 'Process and runtime',
|
|
268
302
|
'overview.errors': 'Errors',
|
|
269
303
|
'tabs.backup': 'Backup',
|
|
270
304
|
'tabs.restart': 'Restart',
|
|
@@ -349,6 +383,18 @@ window.__ModuleLoader__.load({
|
|
|
349
383
|
'update.failAddingToRoot': 'pnpm refused to add at the workspace root (missing -w)',
|
|
350
384
|
'update.failNotWorkspace': '-w was passed but the profile is not a pnpm workspace',
|
|
351
385
|
'update.failIgnoredBuilds': 'Dependency build scripts are blocked by pnpm by default, so the upgrade could not finish',
|
|
386
|
+
'env.kind.docker': 'Docker container',
|
|
387
|
+
'env.kind.container': 'Container (containerd/Kubernetes)',
|
|
388
|
+
'env.kind.systemd': 'systemd service',
|
|
389
|
+
'env.kind.pm2': 'pm2',
|
|
390
|
+
'env.kind.supervisord': 'supervisord',
|
|
391
|
+
'env.kind.kubernetes': 'Kubernetes',
|
|
392
|
+
'env.kind.declared': 'External process manager (declared manually)',
|
|
393
|
+
'update.manualConfirmTitle': 'Confirm before upgrading: manual launch suspected',
|
|
394
|
+
'update.manualConfirm': 'No process manager such as Docker/systemd/pm2 was detected. After the upgrade DSH will NOT restart automatically — the new version is written to disk while the current process keeps running the old one; close and rerun dsh manually to activate it.',
|
|
395
|
+
'update.manualProceed': 'Upgrade anyway',
|
|
396
|
+
'update.manualRestartTitle': 'Upgrade finished — manual restart required',
|
|
397
|
+
'update.manualRestartBody': 'The new version is installed, but the current process still runs the old one. Press Ctrl+C in the terminal running dsh (or simply close the window), then start dsh again.',
|
|
352
398
|
'restart.title': 'Service restart',
|
|
353
399
|
'restart.description': 'Restart the dsh web process. Active work will be interrupted; persisted sessions can be resumed. You can also type /restart in a conversation.',
|
|
354
400
|
'restart.button': 'Restart dsh web',
|
|
@@ -359,6 +405,8 @@ window.__ModuleLoader__.load({
|
|
|
359
405
|
'restart.sent': 'Restart request sent.',
|
|
360
406
|
'restart.sentHint': 'The connection will close shortly. The page will reload automatically after recovery.',
|
|
361
407
|
'restart.idleHint': 'No active work was detected. Confirm to disconnect and wait for the service to restart.',
|
|
408
|
+
'restart.manualWarn': 'This looks like a manual terminal launch: once confirmed the process exits and nothing restarts it — you must run dsh again yourself.',
|
|
409
|
+
'restart.sentManualHint': 'This is a manual-launch environment, so the service will not come back on its own. Run dsh again in the original terminal; refresh this page once the service is up.',
|
|
362
410
|
'restart.navToggle': 'Show "Restart" entry in settings left nav',
|
|
363
411
|
'restart.navToggleHint': 'Off by default; when enabled, a quick-restart entry appears at the bottom of the settings left navigation',
|
|
364
412
|
'activity.agent': 'Agent',
|
|
@@ -607,6 +655,53 @@ window.__ModuleLoader__.load({
|
|
|
607
655
|
}, [])
|
|
608
656
|
return snapshot
|
|
609
657
|
}
|
|
658
|
+
|
|
659
|
+
// 运行环境(version RPC 随 instanceId 返回):概览展示、升级前置确认与重启警告共用。
|
|
660
|
+
// null = 宿主未返回该字段(旧版本)或尚未拉取,一切行为静默退回现状。
|
|
661
|
+
const runtimeEnvListeners = new Set()
|
|
662
|
+
let runtimeEnvState = null
|
|
663
|
+
const setRuntimeEnvState = (next) => {
|
|
664
|
+
runtimeEnvState = next
|
|
665
|
+
for (const listener of runtimeEnvListeners) listener(next)
|
|
666
|
+
}
|
|
667
|
+
const useRuntimeEnv = () => {
|
|
668
|
+
const [snapshot, setSnapshot] = useState(runtimeEnvState)
|
|
669
|
+
useEffect(() => {
|
|
670
|
+
runtimeEnvListeners.add(setSnapshot)
|
|
671
|
+
setSnapshot(runtimeEnvState)
|
|
672
|
+
return () => runtimeEnvListeners.delete(setSnapshot)
|
|
673
|
+
}, [])
|
|
674
|
+
return snapshot
|
|
675
|
+
}
|
|
676
|
+
// 宿主字段按形状校验后才入库:manualStartLikely 必须 boolean、supervisorKind 缺省或 string,
|
|
677
|
+
// 形状不对按「旧宿主无该字段」降级,绝不让坏值悄悄关掉确认门。
|
|
678
|
+
const applyVersionRuntimeEnv = (value) => {
|
|
679
|
+
const env = value ? value.runtimeEnv : undefined
|
|
680
|
+
if (env === null || typeof env !== 'object') return
|
|
681
|
+
if (typeof env.manualStartLikely !== 'boolean') return
|
|
682
|
+
if (env.supervisorKind !== undefined && env.supervisorKind !== null && typeof env.supervisorKind !== 'string') return
|
|
683
|
+
setRuntimeEnvState({ platform: typeof env.platform === 'string' ? env.platform : '', supervisorKind: env.supervisorKind === undefined || env.supervisorKind === null ? null : env.supervisorKind, manualStartLikely: env.manualStartLikely })
|
|
684
|
+
}
|
|
685
|
+
// version 快照全插件只取一次:ServicePanel 与左列入口无论谁先挂载都共享同一请求,
|
|
686
|
+
// 升级前取 instanceId/运行环境也复用它。失败清缓存,下一个消费者重试。
|
|
687
|
+
let versionSnapshotPromise = null
|
|
688
|
+
const fetchVersionSnapshot = () => {
|
|
689
|
+
if (versionSnapshotPromise === null) {
|
|
690
|
+
versionSnapshotPromise = ctx.connection.rpc.call('/dsh-service', 'version', {})
|
|
691
|
+
.then((res) => {
|
|
692
|
+
if (res && res.ok) applyVersionRuntimeEnv(res.value)
|
|
693
|
+
return res
|
|
694
|
+
})
|
|
695
|
+
.catch(() => {
|
|
696
|
+
versionSnapshotPromise = null
|
|
697
|
+
return null
|
|
698
|
+
})
|
|
699
|
+
}
|
|
700
|
+
return versionSnapshotPromise
|
|
701
|
+
}
|
|
702
|
+
// 升级执行中标志放在 factory 作用域:闭包状态挡不住同一 tick 的重入,跨渲染的新闭包
|
|
703
|
+
// 也各自持有独立的 false,只有插件级可变标志能同时覆盖两种情况。
|
|
704
|
+
let upgradeInFlight = false
|
|
610
705
|
const checkRestart = async () => {
|
|
611
706
|
setRestartFlow({ ...restartFlow, busy: true, error: null })
|
|
612
707
|
try {
|
|
@@ -692,6 +787,7 @@ window.__ModuleLoader__.load({
|
|
|
692
787
|
recoveryGeneration += 1
|
|
693
788
|
recoveryListeners.clear()
|
|
694
789
|
restartFlowListeners.clear()
|
|
790
|
+
runtimeEnvListeners.clear()
|
|
695
791
|
updateListeners.clear()
|
|
696
792
|
}, 'dsh-service recovery')
|
|
697
793
|
|
|
@@ -797,6 +893,11 @@ window.__ModuleLoader__.load({
|
|
|
797
893
|
function RestartSection({ showNavToggle }) {
|
|
798
894
|
const translate = useTranslation()
|
|
799
895
|
const flow = useRestartFlow()
|
|
896
|
+
const runtimeEnv = useRuntimeEnv()
|
|
897
|
+
// 左列专属入口可能先于「服务控制」面板挂载:走共享 version 快照(与面板同一次请求)。
|
|
898
|
+
useEffect(() => {
|
|
899
|
+
fetchVersionSnapshot()
|
|
900
|
+
}, [])
|
|
800
901
|
const [navEnabled, setNavEnabled] = useRestartNavEnabled()
|
|
801
902
|
const btn = { minHeight: '32px', padding: '6px 14px', borderRadius: '7px', border: '1px solid transparent', cursor: 'pointer', fontSize: '13px', fontWeight: 550, transition: 'border-color 120ms ease, color 120ms ease, background 120ms ease', lineHeight: '20px' }
|
|
802
903
|
const danger = { ...btn, background: 'var(--dsw-alias-state-error-primary)', color: '#fff', borderColor: 'var(--dsw-alias-state-error-primary)' }
|
|
@@ -807,12 +908,12 @@ window.__ModuleLoader__.load({
|
|
|
807
908
|
const displaySurface = { background: 'var(--dsh-svc-surface-bg)', color: 'var(--dsw-alias-label-primary)', border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '8px', padding: '10px' }
|
|
808
909
|
const sectionTitle = { fontSize: '14px', fontWeight: 700, margin: '0 0 8px', color: 'var(--dsw-alias-label-primary)' }
|
|
809
910
|
|
|
810
|
-
//
|
|
911
|
+
// 重启后提示:手动启动环境不会自动拉起,等待文案换成手动指引。
|
|
811
912
|
if (flow.stage === 2) {
|
|
812
913
|
return React.createElement('div', { 'data-testid': 'restart-card', style: card },
|
|
813
914
|
React.createElement('div', { style: sectionTitle }, translate('restart.title')),
|
|
814
915
|
React.createElement('p', { style: { margin: 0, fontSize: '13px' } }, translate('restart.sent')),
|
|
815
|
-
React.createElement('p', { style: hint }, translate('restart.sentHint')))
|
|
916
|
+
React.createElement('p', { style: hint }, translate(runtimeEnv !== null && runtimeEnv.manualStartLikely === true ? 'restart.sentManualHint' : 'restart.sentHint')))
|
|
816
917
|
}
|
|
817
918
|
|
|
818
919
|
const activityLabels = {
|
|
@@ -821,6 +922,10 @@ window.__ModuleLoader__.load({
|
|
|
821
922
|
terminal: translate('activity.terminal'),
|
|
822
923
|
}
|
|
823
924
|
const activityItems = flow.activity && Array.isArray(flow.activity.items) ? flow.activity.items : []
|
|
925
|
+
// 疑似终端手动启动:确认前就把「退出后无人拉起」讲清楚,两段式确认的后果清单。
|
|
926
|
+
const manualWarn = runtimeEnv !== null && runtimeEnv.manualStartLikely === true
|
|
927
|
+
? React.createElement('p', { 'data-testid': 'restart-manual-warn', style: Object.assign({}, hint, { color: 'var(--dsw-alias-state-warn-primary)', margin: '6px 0 0' }) }, translate('restart.manualWarn'))
|
|
928
|
+
: null
|
|
824
929
|
const activityWarning = flow.stage === 3
|
|
825
930
|
? React.createElement('div', { style: { marginTop: '12px', padding: '10px 12px', borderRadius: '6px', background: 'rgba(211,51,51,0.1)', border: '1px solid rgba(211,51,51,0.35)' } },
|
|
826
931
|
React.createElement('p', { style: { margin: '0 0 8px', color: 'var(--dsw-alias-state-error-primary)', fontSize: '13px', fontWeight: 600 } },
|
|
@@ -840,6 +945,7 @@ window.__ModuleLoader__.load({
|
|
|
840
945
|
React.createElement('div', { style: sectionTitle }, translate('restart.title')),
|
|
841
946
|
React.createElement('div', { 'data-testid': 'restart-region', style: displaySurface },
|
|
842
947
|
React.createElement('p', { style: { margin: 0, fontSize: '13px' } }, translate('restart.description')),
|
|
948
|
+
manualWarn,
|
|
843
949
|
activityWarning,
|
|
844
950
|
React.createElement('div', { style: row },
|
|
845
951
|
flow.stage === 0
|
|
@@ -906,6 +1012,9 @@ window.__ModuleLoader__.load({
|
|
|
906
1012
|
const [usageError, setUsageError] = useState(null)
|
|
907
1013
|
const [upgradeBusy, setUpgradeBusy] = useState(false)
|
|
908
1014
|
const [upgradeError, setUpgradeError] = useState(null)
|
|
1015
|
+
// 疑似手动启动环境的升级两段式:确认后果 → 仍要升级;成功后不自动退出,改示指引。
|
|
1016
|
+
const [upgradeManualConfirm, setUpgradeManualConfirm] = useState(false)
|
|
1017
|
+
const [upgradeManualPending, setUpgradeManualPending] = useState(false)
|
|
909
1018
|
const [hoveredUsageSegment, setHoveredUsageSegment] = useState(null)
|
|
910
1019
|
const [usageProject, setUsageProject] = useState('all')
|
|
911
1020
|
const [modelErrorsOpen, setModelErrorsOpen] = useState(false)
|
|
@@ -916,16 +1025,18 @@ window.__ModuleLoader__.load({
|
|
|
916
1025
|
const [channelOpen, setChannelOpen] = useState(false)
|
|
917
1026
|
// 重启流程状态来自共享流(与设置页左列底部的专属入口同源)
|
|
918
1027
|
const restartFlowState = useRestartFlow()
|
|
1028
|
+
const runtimeEnv = useRuntimeEnv()
|
|
919
1029
|
const usageRequestPayload = { timezoneOffsetMinutes: new Date().getTimezoneOffset() }
|
|
920
1030
|
|
|
921
1031
|
// 进入面板时拉取当前版本和健康快照;健康数据每 5 秒刷新,卸载即停止。
|
|
1032
|
+
// version 走全插件共享的缓存快照:无论哪个挂载点先到,都只有一次请求。
|
|
922
1033
|
useEffect(() => {
|
|
923
|
-
|
|
1034
|
+
fetchVersionSnapshot().then((res) => {
|
|
924
1035
|
if (res && res.ok) {
|
|
925
1036
|
setVersion(res.value.current)
|
|
926
1037
|
setPluginVersion(res.value.pluginVersion || null)
|
|
927
1038
|
}
|
|
928
|
-
})
|
|
1039
|
+
})
|
|
929
1040
|
}, [])
|
|
930
1041
|
useEffect(() => {
|
|
931
1042
|
let active = true
|
|
@@ -1173,10 +1284,25 @@ window.__ModuleLoader__.load({
|
|
|
1173
1284
|
'ignored-builds': 'update.failIgnoredBuilds',
|
|
1174
1285
|
}
|
|
1175
1286
|
const upgradePlugin = async () => {
|
|
1176
|
-
|
|
1177
|
-
|
|
1287
|
+
if (upgradeInFlight) return
|
|
1288
|
+
upgradeInFlight = true
|
|
1178
1289
|
try {
|
|
1179
|
-
|
|
1290
|
+
// check-update 先于 version 返回时按钮可能先出现:env 未知就等共享快照落地再判。
|
|
1291
|
+
let env = runtimeEnv
|
|
1292
|
+
if (env === null) {
|
|
1293
|
+
await fetchVersionSnapshot()
|
|
1294
|
+
env = runtimeEnvState
|
|
1295
|
+
}
|
|
1296
|
+
// 疑似终端手动启动:升级成功不会自动重启,先两段式确认后果再动手(安全教义)。
|
|
1297
|
+
if (!upgradeManualConfirm && env !== null && env.manualStartLikely === true) {
|
|
1298
|
+
setUpgradeError(null)
|
|
1299
|
+
setUpgradeManualConfirm(true)
|
|
1300
|
+
return
|
|
1301
|
+
}
|
|
1302
|
+
setUpgradeManualConfirm(false)
|
|
1303
|
+
setUpgradeBusy(true)
|
|
1304
|
+
setUpgradeError(null)
|
|
1305
|
+
const versionRes = await fetchVersionSnapshot()
|
|
1180
1306
|
const previousInstanceId = versionRes && versionRes.ok ? versionRes.value.instanceId : undefined
|
|
1181
1307
|
const res = await ctx.connection.rpc.call('/dsh-service', 'upgrade', {})
|
|
1182
1308
|
if (!res || res.ok === false) {
|
|
@@ -1190,6 +1316,11 @@ window.__ModuleLoader__.load({
|
|
|
1190
1316
|
}
|
|
1191
1317
|
return
|
|
1192
1318
|
}
|
|
1319
|
+
if (res.value && res.value.requiresManualRestart === true) {
|
|
1320
|
+
// 宿主保持运行(没有 exit,就不会有新实例):不启动恢复轮询,改示手动重启指引。
|
|
1321
|
+
setUpgradeManualPending(true)
|
|
1322
|
+
return
|
|
1323
|
+
}
|
|
1193
1324
|
if (typeof previousInstanceId === 'string' && previousInstanceId.length > 0) {
|
|
1194
1325
|
startRecovery(previousInstanceId).catch(() => {})
|
|
1195
1326
|
}
|
|
@@ -1198,6 +1329,7 @@ window.__ModuleLoader__.load({
|
|
|
1198
1329
|
console.error('dsh-service: upgrade failed', detail || err)
|
|
1199
1330
|
setUpgradeError(detail ? translate('update.upgradeErrorDetail', { detail }) : translate('update.upgradeError'))
|
|
1200
1331
|
} finally {
|
|
1332
|
+
upgradeInFlight = false
|
|
1201
1333
|
setUpgradeBusy(false)
|
|
1202
1334
|
}
|
|
1203
1335
|
}
|
|
@@ -1338,6 +1470,17 @@ window.__ModuleLoader__.load({
|
|
|
1338
1470
|
}
|
|
1339
1471
|
if (check.id === 'tar' && check.status === 'ok') return translate('health.detail.tar.ok')
|
|
1340
1472
|
if (check.id === 'permissions') return translate(check.status === 'ok' ? 'health.detail.permissions.ok' : 'health.detail.permissions.warning', { count: detail || '0' })
|
|
1473
|
+
if (check.id === 'runtime-env') {
|
|
1474
|
+
if (detail === 'manual') return translate('health.detail.runtime-env.manual')
|
|
1475
|
+
if (detail === 'declared') return translate('health.detail.runtime-env.declared')
|
|
1476
|
+
if (detail === 'unknown') return translate('health.detail.runtime-env.unknown')
|
|
1477
|
+
// 已识别的管理器 kind:复用概览运行环境的标签词典。
|
|
1478
|
+
return translate('health.detail.runtime-env.managed', { kind: translate(`env.kind.${detail}`) })
|
|
1479
|
+
}
|
|
1480
|
+
if (check.id === 'node-version') {
|
|
1481
|
+
const [version = '', required = ''] = detail.split(':')
|
|
1482
|
+
return translate(check.status === 'ok' ? 'health.detail.node-version.ok' : 'health.detail.node-version.warning', { version, required })
|
|
1483
|
+
}
|
|
1341
1484
|
return translate('health.detail.generic', { status: translate(`health.status.${check.status}`) })
|
|
1342
1485
|
}
|
|
1343
1486
|
const summaryItems = (totals) => [
|
|
@@ -1440,10 +1583,17 @@ window.__ModuleLoader__.load({
|
|
|
1440
1583
|
: React.createElement('p', { style: hint }, usageError || translate('usage.empty')),
|
|
1441
1584
|
React.createElement('div', { style: row }, React.createElement('button', { style: neutral, 'data-variant': 'neutral', onClick: refreshUsage, disabled: usageBusy }, translate(usageBusy ? 'usage.refreshing' : 'usage.refresh'))))
|
|
1442
1585
|
|
|
1586
|
+
// 平台标签:process.platform 映射为常见系统名,arch 跟在后面(均为专有名词,不走词典)。
|
|
1587
|
+
const platformNames = { win32: 'Windows', darwin: 'macOS', linux: 'Linux', freebsd: 'FreeBSD', openbsd: 'OpenBSD' }
|
|
1588
|
+
const platformLabel = (health && typeof health.platform === 'string' && health.platform)
|
|
1589
|
+
? `${platformNames[health.platform] || health.platform}${typeof health.arch === 'string' && health.arch ? ` · ${health.arch}` : ''}`
|
|
1590
|
+
: '—'
|
|
1443
1591
|
const containerInfoBlock = React.createElement('div', { key: 'container-info', style: { marginTop: '18px' } },
|
|
1444
1592
|
React.createElement('div', { style: sectionTitle }, translate('overview.container')),
|
|
1445
1593
|
health
|
|
1446
1594
|
? React.createElement('div', { 'data-testid': 'health-display', style: Object.assign({}, displaySurface, { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '8px' }) },
|
|
1595
|
+
metric('health.platform', platformLabel),
|
|
1596
|
+
metric('health.nodeVersion', typeof health.nodeVersion === 'string' && health.nodeVersion ? health.nodeVersion : '—'),
|
|
1447
1597
|
metric('health.uptime', formatUptime(health.uptimeSeconds)),
|
|
1448
1598
|
metric('health.rss', formatBytes(health.rssBytes)),
|
|
1449
1599
|
metric('health.liveSessions', String(health.liveSessions)),
|
|
@@ -1473,7 +1623,7 @@ window.__ModuleLoader__.load({
|
|
|
1473
1623
|
? React.createElement('div', { 'data-testid': 'health-check-list', style: Object.assign({}, displaySurface, { marginTop: '10px', padding: '8px 10px' }) },
|
|
1474
1624
|
diagnostics.checks.map((check, index) => React.createElement('div', { key: check.id, style: { display: 'flex', justifyContent: 'space-between', gap: '14px', fontSize: '12px', padding: '9px 2px', borderTop: index === 0 ? 0 : '1px solid var(--dsw-alias-border-l1)' } },
|
|
1475
1625
|
React.createElement('span', null, translate(`health.check.${check.id}`)),
|
|
1476
|
-
React.createElement('span', { style: { color: check.status === 'ok' ? 'var(--dsw-alias-state-success-primary)' : check.status === 'warning' ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-state-error-primary)', textAlign: 'right' } }, diagnosticDetail(check)))))
|
|
1626
|
+
React.createElement('span', { style: { color: check.status === 'ok' ? 'var(--dsw-alias-state-success-primary)' : check.status === 'warning' ? 'var(--dsw-alias-state-warn-primary)' : check.status === 'info' ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-state-error-primary)', textAlign: 'right' } }, diagnosticDetail(check)))))
|
|
1477
1627
|
: null)
|
|
1478
1628
|
|
|
1479
1629
|
const permissionAbnormal = permissions && permissions.supported === true
|
|
@@ -1602,9 +1752,11 @@ window.__ModuleLoader__.load({
|
|
|
1602
1752
|
const dshUpdate = updateInfo?.dsh
|
|
1603
1753
|
const dshExpandable = dshUpdate && dshUpdate.status !== 'unpublished' && dshUpdate.status !== 'unavailable' && !dshUpdate.upToDate
|
|
1604
1754
|
const pluginUpdate = updateInfo?.plugin && !updateInfo.plugin.upToDate && updateInfo.plugin.status === 'available'
|
|
1605
|
-
|
|
1755
|
+
// 确认后果或已装好待手动重启期间收起升级按钮,避免重复触发或撞 no-newer-version 守卫。
|
|
1756
|
+
const pluginAction = pluginUpdate && !upgradeManualConfirm && !upgradeManualPending
|
|
1606
1757
|
? React.createElement('button', { style: Object.assign({}, neutral, { minHeight: '24px', padding: '2px 8px', fontSize: '11px' }), disabled: upgradeBusy, onClick: upgradePlugin }, translate(upgradeBusy ? 'update.upgrading' : 'update.upgrade'))
|
|
1607
1758
|
: null
|
|
1759
|
+
// 版本卡只放版本与升级:运行环境信息在健康诊断检查项与重启确认提示中呈现(用户复核口径)。
|
|
1608
1760
|
const versionBlock = React.createElement('div', { key: 'version-card', 'data-testid': 'version-card', style: card },
|
|
1609
1761
|
React.createElement('div', { key: 'title', style: sectionTitle }, translate('version.title')),
|
|
1610
1762
|
React.createElement('div', { style: displaySurface },
|
|
@@ -1616,6 +1768,19 @@ window.__ModuleLoader__.load({
|
|
|
1616
1768
|
React.createElement('div', null, translate('update.details.latest', { version: dshUpdate?.latest || '—' })),
|
|
1617
1769
|
channelLines(translate, dshUpdate?.tags))
|
|
1618
1770
|
: null,
|
|
1771
|
+
upgradeManualConfirm
|
|
1772
|
+
? React.createElement('div', { 'data-testid': 'upgrade-manual-confirm', style: { marginTop: '10px', padding: '10px 12px', borderRadius: '6px', background: 'rgba(211,51,51,0.08)', border: '1px solid rgba(211,51,51,0.3)' } },
|
|
1773
|
+
React.createElement('p', { style: { margin: '0 0 6px', color: 'var(--dsw-alias-state-error-primary)', fontSize: '13px', fontWeight: 600 } }, translate('update.manualConfirmTitle')),
|
|
1774
|
+
React.createElement('p', { style: Object.assign({}, hint, { margin: '0 0 8px' }) }, translate('update.manualConfirm')),
|
|
1775
|
+
React.createElement('div', { style: row },
|
|
1776
|
+
React.createElement('button', { style: dangerGhost, 'data-variant': 'danger', disabled: upgradeBusy, onClick: upgradePlugin }, translate(upgradeBusy ? 'update.upgrading' : 'update.manualProceed')),
|
|
1777
|
+
React.createElement('button', { style: ghost, disabled: upgradeBusy, onClick: () => setUpgradeManualConfirm(false) }, translate('restart.cancel'))))
|
|
1778
|
+
: null,
|
|
1779
|
+
upgradeManualPending
|
|
1780
|
+
? React.createElement('div', { 'data-testid': 'upgrade-manual-pending', style: { marginTop: '10px', padding: '10px 12px', borderRadius: '6px', border: '1px solid var(--dsw-alias-state-warn-primary)', background: 'var(--dsw-alias-bg-layer-2)' } },
|
|
1781
|
+
React.createElement('p', { style: { margin: '0 0 4px', color: 'var(--dsw-alias-state-warn-primary)', fontSize: '13px', fontWeight: 650 } }, translate('update.manualRestartTitle')),
|
|
1782
|
+
React.createElement('p', { style: Object.assign({}, hint, { margin: 0 }) }, translate('update.manualRestartBody')))
|
|
1783
|
+
: null,
|
|
1619
1784
|
upgradeError ? React.createElement('p', { style: Object.assign({}, hint, { color: 'var(--dsw-alias-state-error-primary)', margin: '4px 0 0' }) }, upgradeError) : null))
|
|
1620
1785
|
|
|
1621
1786
|
// 重启区块复用共享组件(「重启」标签还承载设置页左列入口的显示开关;左侧入口默认关闭)
|
|
@@ -1659,7 +1824,8 @@ window.__ModuleLoader__.load({
|
|
|
1659
1824
|
// 任务通知独立成顶部标签(v0.14 起不再混在概览里)
|
|
1660
1825
|
const notifyBlock = React.createElement('div', null, notificationBlock)
|
|
1661
1826
|
const maintenanceBlock = React.createElement('div', { key: 'maintenance-card', 'data-testid': 'maintenance-card', style: card }, backupBlock)
|
|
1662
|
-
|
|
1827
|
+
// advisory 警告(如手动启动环境的黄色提示)只做行内呈现,不点亮标签 ⚠ 与顶部服务控制提醒。
|
|
1828
|
+
const diagnosticFailure = diagnostics?.checks?.some((check) => check.status === 'error' || (check.status === 'warning' && check.advisory !== true)) === true
|
|
1663
1829
|
const tabWarnings = {
|
|
1664
1830
|
overview: false,
|
|
1665
1831
|
notify: false,
|
package/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// 「服务控制」:版本信息 + 检查更新 + 一键重启 dsh web
|
|
3
3
|
import { createHmac, randomBytes, randomUUID } from 'node:crypto'
|
|
4
4
|
import { Buffer } from 'node:buffer'
|
|
5
|
-
import { constants as fsConstants } from 'node:fs'
|
|
5
|
+
import { constants as fsConstants, existsSync, readFileSync } from 'node:fs'
|
|
6
6
|
import { access, cp, lstat, mkdir, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from 'node:fs/promises'
|
|
7
7
|
import { homedir } from 'node:os'
|
|
8
8
|
import { basename, dirname, join, relative, resolve, sep } from 'node:path'
|
|
@@ -44,6 +44,13 @@ try {
|
|
|
44
44
|
} catch (__) {}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// Node 最低要求只读 package.json engines 一处(与版本号同源),启动时解析一次。
|
|
48
|
+
let requiredNodeMajor = 22
|
|
49
|
+
try {
|
|
50
|
+
const enginesNode = require('./package.json').engines?.node || ''
|
|
51
|
+
requiredNodeMajor = parseInt(enginesNode.replace(/[^0-9].*$/, ''), 10) || 22
|
|
52
|
+
} catch (_) {}
|
|
53
|
+
|
|
47
54
|
function isSemverIdentifier(value) {
|
|
48
55
|
if (value.length === 0) return false
|
|
49
56
|
return [...value].every((char) => (char >= '0' && char <= '9') || (char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || char === '-')
|
|
@@ -794,7 +801,8 @@ function classifyUpgradeFailure(output) {
|
|
|
794
801
|
// 一键升级:走「dsh plugin --profile <p> add <pkg>@<版本>」,落在 DSH 真正读取的 profile
|
|
795
802
|
// node_modules,替代旧的无升级语义的 npm install -g。命令、包名、版本全部宿主白名单常量。
|
|
796
803
|
// 安全教义守卫:活动工作拒用、link/file 拒绝、latest 不高于当前版本拒绝(防降级)一律先于网络。
|
|
797
|
-
|
|
804
|
+
// runtimeEnv 来自 apply() 的 detectRuntimeEnv():疑似终端手动启动时安装成功不调度 exit(42)。
|
|
805
|
+
async function upgradePlugin(ctx, dshHome, runtimeEnv) {
|
|
798
806
|
const activity = collectActiveWork(ctx)
|
|
799
807
|
if (activity.hasActive) throw new Error('active-work')
|
|
800
808
|
|
|
@@ -849,6 +857,12 @@ async function upgradePlugin(ctx, dshHome) {
|
|
|
849
857
|
: installed !== pluginVersion
|
|
850
858
|
if (!advanced) throw new Error('upgrade-stale')
|
|
851
859
|
|
|
860
|
+
// 手动终端启动环境没有进程管理器拉起 exit(42):保持旧版本继续服务,把重启时机交给用户。
|
|
861
|
+
// requiresManualRestart 让客户端改走「手动重启指引」而不是恢复轮询(不会有新实例)。
|
|
862
|
+
if (runtimeEnv !== undefined && runtimeEnv !== null && runtimeEnv.manualStartLikely === true) {
|
|
863
|
+
return { result: 'upgraded', profile: profile.name, previous: pluginVersion, installed, requiresManualRestart: true }
|
|
864
|
+
}
|
|
865
|
+
|
|
852
866
|
scheduleRestart(ctx)
|
|
853
867
|
return { result: 'upgraded', profile: profile.name, previous: pluginVersion, installed }
|
|
854
868
|
}
|
|
@@ -999,7 +1013,20 @@ async function repairPermissions(ctx, dshHome, plans, planId) {
|
|
|
999
1013
|
return permissionSnapshot(ctx, dshHome, plans)
|
|
1000
1014
|
}
|
|
1001
1015
|
|
|
1002
|
-
|
|
1016
|
+
// 运行环境检查(v0.17):managed/declared → ok;疑似手动启动 → warning 但带 advisory 标记
|
|
1017
|
+
//(黄色行内提示,不参与 overall 聚合、不点亮标签 ⚠ 与顶部提醒——用户复核口径);unknown →
|
|
1018
|
+
// info。detail 是客户端映射词典的令牌。
|
|
1019
|
+
function runtimeEnvCheck(runtimeEnv) {
|
|
1020
|
+
if (runtimeEnv === undefined || runtimeEnv === null) return null
|
|
1021
|
+
if (runtimeEnv.manualStartLikely === true) return { id: 'runtime-env', status: 'warning', detail: 'manual', advisory: true }
|
|
1022
|
+
if (runtimeEnv.supervisorKind === 'declared') return { id: 'runtime-env', status: 'ok', detail: 'declared' }
|
|
1023
|
+
if (typeof runtimeEnv.supervisorKind === 'string' && runtimeEnv.supervisorKind.length > 0) {
|
|
1024
|
+
return { id: 'runtime-env', status: 'ok', detail: runtimeEnv.supervisorKind }
|
|
1025
|
+
}
|
|
1026
|
+
return { id: 'runtime-env', status: 'info', detail: 'unknown' }
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
async function collectDiagnostics(ctx, dshHome, runtimeEnv) {
|
|
1003
1030
|
const checks = []
|
|
1004
1031
|
const add = (id, status, detail) => checks.push({ id, status, ...(detail === undefined ? {} : { detail: String(detail) }) })
|
|
1005
1032
|
|
|
@@ -1027,7 +1054,8 @@ async function collectDiagnostics(ctx, dshHome) {
|
|
|
1027
1054
|
|
|
1028
1055
|
try {
|
|
1029
1056
|
const backups = await listBackups(dshHome)
|
|
1030
|
-
|
|
1057
|
+
// 没有备份不算警告(v0.14 口径的落地):空列表是信息级提示,不点亮标签 ⚠、不影响 overall。
|
|
1058
|
+
add('backup-storage', backups.items.length === 0 ? 'info' : 'ok', `${backups.items.length}:${backups.totalBytes}`)
|
|
1031
1059
|
} catch (error) { add('backup-storage', 'error', error?.code || error?.message || error) }
|
|
1032
1060
|
|
|
1033
1061
|
const subprocess = ctx.get('subprocess')
|
|
@@ -1044,9 +1072,18 @@ async function collectDiagnostics(ctx, dshHome) {
|
|
|
1044
1072
|
} catch (error) { add('permissions', 'warning', error?.message || error) }
|
|
1045
1073
|
}
|
|
1046
1074
|
|
|
1075
|
+
const runtimeCheck = runtimeEnvCheck(runtimeEnv)
|
|
1076
|
+
if (runtimeCheck !== null) checks.push(runtimeCheck)
|
|
1077
|
+
|
|
1078
|
+
// Node 运行时版本检查:低于 package.json engines 的最低 major 时告警。
|
|
1079
|
+
// detail 形如 "v22.14.0:22"(当前版本:要求 major),客户端按 backup-storage 同款分隔符解析。
|
|
1080
|
+
const nodeMajor = parseInt(process.versions.node, 10)
|
|
1081
|
+
add('node-version', Number.isFinite(nodeMajor) && nodeMajor >= requiredNodeMajor ? 'ok' : 'warning', `${process.version}:${requiredNodeMajor}`)
|
|
1082
|
+
|
|
1047
1083
|
let status = 'ok'
|
|
1084
|
+
// advisory 警告(手动启动环境的黄色提示)只做行内呈现:不把 overall 拉成 warning。
|
|
1048
1085
|
if (checks.some((check) => check.status === 'error')) status = 'error'
|
|
1049
|
-
else if (checks.some((check) => check.status === 'warning')) status = 'warning'
|
|
1086
|
+
else if (checks.some((check) => check.status === 'warning' && check.advisory !== true)) status = 'warning'
|
|
1050
1087
|
return { status, checkedAt: Date.now(), checks }
|
|
1051
1088
|
}
|
|
1052
1089
|
|
|
@@ -1060,6 +1097,9 @@ async function collectHealth(ctx) {
|
|
|
1060
1097
|
return {
|
|
1061
1098
|
uptimeSeconds: process.uptime(),
|
|
1062
1099
|
rssBytes: memory.rss,
|
|
1100
|
+
platform: process.platform,
|
|
1101
|
+
arch: process.arch,
|
|
1102
|
+
nodeVersion: process.version,
|
|
1063
1103
|
liveSessions: sessionsService === undefined ? 0 : sessionsService.list().length,
|
|
1064
1104
|
persistedSessions: sessionRecords.filter((record) => record.persisted === true).length,
|
|
1065
1105
|
activeAgents: activity.items.filter((item) => item.type === 'agent').length,
|
|
@@ -1127,6 +1167,50 @@ function collectActiveWork(ctx) {
|
|
|
1127
1167
|
return { hasActive: items.length > 0, items }
|
|
1128
1168
|
}
|
|
1129
1169
|
|
|
1170
|
+
// ---- 运行环境探测(v0.16:Windows 终端手动启动场景,用户点名)----
|
|
1171
|
+
// 重启 = process.exit(42),依赖外层进程管理器拉起(见 AGENTS.md);PowerShell/CMD 手动
|
|
1172
|
+
// 启动时没有任何东西会把它拉起来。这里只用跨平台被动信号(env + 固定路径 fs + Node 自带
|
|
1173
|
+
// isTTY,零 spawn、浏览器零输入)把环境分成三态:
|
|
1174
|
+
// supervisorKind 非 null —— 检测到管理器,exit 后会被自动拉起,维持自动重启;
|
|
1175
|
+
// manualStartLikely=true —— 无管理器且 stdin/stdout 双 TTY(终端直启特征),升级后宿主
|
|
1176
|
+
// 不自动退出,客户端改示手动重启指引;
|
|
1177
|
+
// 其余 —— unknown(输出重定向、NSSM/WinSW 等包装器),维持现状不折腾。
|
|
1178
|
+
// 探测不到的包装器可用 DSH_SERVICE_RUNTIME_ENV=managed|manual 由用户显式声明。
|
|
1179
|
+
const RUNTIME_SUPERVISOR_ENV = [
|
|
1180
|
+
['pm2', ['pm_id', 'PM2_HOME', 'pm_uptime']],
|
|
1181
|
+
['systemd', ['INVOCATION_ID', 'JOURNAL_STREAM', 'NOTIFY_SOCKET']],
|
|
1182
|
+
['supervisord', ['SUPERVISOR_ENABLED', 'SUPERVISOR_PROCESS_NAME']],
|
|
1183
|
+
['kubernetes', ['KUBERNETES_SERVICE_HOST']],
|
|
1184
|
+
]
|
|
1185
|
+
|
|
1186
|
+
function detectRuntimeEnv(options = {}) {
|
|
1187
|
+
const env = options.env ?? process.env
|
|
1188
|
+
const platform = options.platform ?? process.platform
|
|
1189
|
+
const stdinIsTTY = options.stdinIsTTY ?? (process.stdin ? process.stdin.isTTY === true : false)
|
|
1190
|
+
const stdoutIsTTY = options.stdoutIsTTY ?? (process.stdout ? process.stdout.isTTY === true : false)
|
|
1191
|
+
// 默认探测器只在未被注入时执行;win32 直接短路(/.dockerenv 与 /proc 不存在),单测全部显式传原语。
|
|
1192
|
+
const dockerEnvExists = options.dockerEnvExists ?? (platform === 'win32' ? false : (() => { try { return existsSync('/.dockerenv') } catch (_) { return false } })())
|
|
1193
|
+
const cgroupText = options.cgroupText ?? (platform === 'win32' ? '' : (() => { try { return readFileSync('/proc/1/cgroup', 'utf8') } catch (_) { return '' } })())
|
|
1194
|
+
|
|
1195
|
+
const forced = typeof env.DSH_SERVICE_RUNTIME_ENV === 'string' ? env.DSH_SERVICE_RUNTIME_ENV.trim().toLowerCase() : ''
|
|
1196
|
+
if (forced === 'manual') return { platform, supervisorKind: null, manualStartLikely: true }
|
|
1197
|
+
if (forced === 'managed') return { platform, supervisorKind: 'declared', manualStartLikely: false }
|
|
1198
|
+
|
|
1199
|
+
for (const [kind, keys] of RUNTIME_SUPERVISOR_ENV) {
|
|
1200
|
+
if (keys.some((key) => env[key] !== undefined && env[key] !== '')) {
|
|
1201
|
+
return { platform, supervisorKind: kind, manualStartLikely: false }
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
// /.dockerenv 与 /proc 只在 POSIX 内核上存在;win32 原生进程不做这两个探测。
|
|
1205
|
+
if (platform !== 'win32') {
|
|
1206
|
+
if (dockerEnvExists === true) return { platform, supervisorKind: 'docker', manualStartLikely: false }
|
|
1207
|
+
if (/docker/i.test(cgroupText)) return { platform, supervisorKind: 'docker', manualStartLikely: false }
|
|
1208
|
+
if (/containerd|kubepods|lxc|podman/i.test(cgroupText)) return { platform, supervisorKind: 'container', manualStartLikely: false }
|
|
1209
|
+
}
|
|
1210
|
+
if (stdinIsTTY === true && stdoutIsTTY === true) return { platform, supervisorKind: null, manualStartLikely: true }
|
|
1211
|
+
return { platform, supervisorKind: null, manualStartLikely: false }
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1130
1214
|
function scheduleRestart(ctx) {
|
|
1131
1215
|
const doExit = () => {
|
|
1132
1216
|
try {
|
|
@@ -1143,6 +1227,8 @@ function scheduleRestart(ctx) {
|
|
|
1143
1227
|
|
|
1144
1228
|
function apply(ctx) {
|
|
1145
1229
|
const dshHome = resolveDshHome()
|
|
1230
|
+
// 进程运行环境在生命周期内不变:挂载时探测一次,version RPC 与升级分支共用。
|
|
1231
|
+
const runtimeEnv = detectRuntimeEnv()
|
|
1146
1232
|
const permissionPlans = new Map()
|
|
1147
1233
|
const downloadTokens = new Map()
|
|
1148
1234
|
let usageIndexPromise = loadUsageIndex(dshHome)
|
|
@@ -1205,7 +1291,9 @@ function apply(ctx) {
|
|
|
1205
1291
|
// 合法示例:channel=/dsh-service,endpoint=version/check-update/activity/web。
|
|
1206
1292
|
ctx.connection.rpc.handle('/dsh-service', async (endpoint, payload) => {
|
|
1207
1293
|
if (endpoint === 'version') {
|
|
1208
|
-
|
|
1294
|
+
// runtimeEnv 随进程身份(instanceId)一起返回:概览展示、升级前置确认与重启警告共用,
|
|
1295
|
+
// 客户端对缺字段的老宿主静默降级。
|
|
1296
|
+
return { ok: true, value: { current: dshVersion, pluginVersion, instanceId, runtimeEnv } }
|
|
1209
1297
|
}
|
|
1210
1298
|
|
|
1211
1299
|
if (endpoint === 'check-update') {
|
|
@@ -1243,7 +1331,7 @@ function apply(ctx) {
|
|
|
1243
1331
|
|
|
1244
1332
|
if (endpoint === 'upgrade') {
|
|
1245
1333
|
try {
|
|
1246
|
-
return { ok: true, value: await upgradePlugin(ctx, dshHome) }
|
|
1334
|
+
return { ok: true, value: await upgradePlugin(ctx, dshHome, runtimeEnv) }
|
|
1247
1335
|
} catch (error) {
|
|
1248
1336
|
return { ok: false, error: error?.message || String(error) }
|
|
1249
1337
|
}
|
|
@@ -1263,7 +1351,7 @@ function apply(ctx) {
|
|
|
1263
1351
|
|
|
1264
1352
|
if (endpoint === 'diagnostics') {
|
|
1265
1353
|
try {
|
|
1266
|
-
return { ok: true, value: await collectDiagnostics(ctx, dshHome) }
|
|
1354
|
+
return { ok: true, value: await collectDiagnostics(ctx, dshHome, runtimeEnv) }
|
|
1267
1355
|
} catch (error) {
|
|
1268
1356
|
return { ok: false, error: error?.message || String(error) }
|
|
1269
1357
|
}
|
|
@@ -1393,5 +1481,5 @@ function apply(ctx) {
|
|
|
1393
1481
|
}, { authority: 'loopback' })
|
|
1394
1482
|
}
|
|
1395
1483
|
|
|
1396
|
-
export { apply, inject, name }
|
|
1397
|
-
export default { apply, inject, name }
|
|
1484
|
+
export { apply, detectRuntimeEnv, inject, name, runtimeEnvCheck }
|
|
1485
|
+
export default { apply, detectRuntimeEnv, inject, name, runtimeEnvCheck }
|