@gehennawu/dsh-service 0.15.0 → 0.17.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.en.md +9 -2
- package/README.md +9 -2
- package/client.js +200 -12
- package/index.js +96 -9
- 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
|
+
- Overview shows the runtime environment; when a manual terminal launch is suspected, the confirmation flow warns that nothing will bring the process back
|
|
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 flagged as a warning (no restart assurance), while an unrecognized environment is informational only (not a warning) — both 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, labels the runtime environment in the overview, 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,22 @@ 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.line': '运行环境:{value}',
|
|
155
|
+
'env.kind.docker': 'Docker 容器',
|
|
156
|
+
'env.kind.container': '容器(containerd/Kubernetes)',
|
|
157
|
+
'env.kind.systemd': 'systemd 服务',
|
|
158
|
+
'env.kind.pm2': 'pm2',
|
|
159
|
+
'env.kind.supervisord': 'supervisord',
|
|
160
|
+
'env.kind.kubernetes': 'Kubernetes',
|
|
161
|
+
'env.kind.declared': '外部进程管理器(手动指定)',
|
|
162
|
+
'env.kind.unrecognized': '检测到未识别的进程管理器',
|
|
163
|
+
'env.likelyManual': '疑似终端手动启动',
|
|
164
|
+
'env.unknown': '未检测到进程管理器',
|
|
165
|
+
'update.manualConfirmTitle': '升级前请确认:当前疑似手动启动环境',
|
|
166
|
+
'update.manualConfirm': '未检测到 Docker/systemd/pm2 等进程管理器。升级完成后 DSH 不会自动重启——新版本写入磁盘,但当前进程继续运行旧版本;你需要手动关闭并重新运行 dsh 才能启用新版本。',
|
|
167
|
+
'update.manualProceed': '仍要升级',
|
|
168
|
+
'update.manualRestartTitle': '升级完成,需要手动重启',
|
|
169
|
+
'update.manualRestartBody': '新版本已安装,但当前进程仍在运行旧版本。请在运行 dsh 的终端窗口按 Ctrl+C(或直接关闭窗口),然后重新启动 dsh。',
|
|
144
170
|
'restart.title': '服务重启',
|
|
145
171
|
'restart.description': '重启 dsh web 进程。运行中的工作会中断,持久化会话可恢复。也可在对话中输入 /restart。',
|
|
146
172
|
'restart.button': '重启 dsh web',
|
|
@@ -151,6 +177,8 @@ window.__ModuleLoader__.load({
|
|
|
151
177
|
'restart.sent': '重启指令已发出。',
|
|
152
178
|
'restart.sentHint': '页面连接即将断开,服务恢复后将自动刷新。',
|
|
153
179
|
'restart.idleHint': '当前没有检测到运行中的工作。确认后将断开连接,等待服务自动重启。',
|
|
180
|
+
'restart.manualWarn': '当前疑似终端手动启动环境:确认后进程将退出且不会自动拉起,需要你手动重新运行 dsh。',
|
|
181
|
+
'restart.sentManualHint': '当前为手动启动环境,服务不会自动拉起。请在原终端重新运行 dsh;服务起来后刷新此页面即可恢复。',
|
|
154
182
|
'restart.navToggle': '设置页左列显示「重启」入口',
|
|
155
183
|
'restart.navToggleHint': '默认关闭;开启后在设置页左侧标签列底部显示快捷重启入口',
|
|
156
184
|
'activity.agent': 'Agent',
|
|
@@ -228,6 +256,8 @@ window.__ModuleLoader__.load({
|
|
|
228
256
|
'health.title': 'Health',
|
|
229
257
|
'health.uptime': 'Uptime',
|
|
230
258
|
'health.rss': 'Memory RSS',
|
|
259
|
+
'health.platform': 'Platform',
|
|
260
|
+
'health.nodeVersion': 'Node version',
|
|
231
261
|
'health.liveSessions': 'Live sessions',
|
|
232
262
|
'health.persistedSessions': 'Persisted sessions',
|
|
233
263
|
'health.activeAgents': 'Active agents',
|
|
@@ -251,6 +281,14 @@ window.__ModuleLoader__.load({
|
|
|
251
281
|
'health.check.backup-storage': 'Backup storage',
|
|
252
282
|
'health.check.tar': 'tar',
|
|
253
283
|
'health.check.permissions': 'File permissions',
|
|
284
|
+
'health.check.runtime-env': 'Runtime environment',
|
|
285
|
+
'health.check.node-version': 'Node runtime',
|
|
286
|
+
'health.detail.runtime-env.managed': 'Managed by {kind}; the process restarts automatically after exit',
|
|
287
|
+
'health.detail.runtime-env.declared': 'Declared externally managed via DSH_SERVICE_RUNTIME_ENV',
|
|
288
|
+
'health.detail.runtime-env.manual': 'Likely a manual terminal launch — nothing restarts the process after exit; one-click upgrade already keeps it running',
|
|
289
|
+
'health.detail.runtime-env.unknown': 'No process manager detected, so automatic restart cannot be confirmed; set DSH_SERVICE_RUNTIME_ENV=managed if one exists',
|
|
290
|
+
'health.detail.node-version.ok': '{version}, meets the >={required} requirement',
|
|
291
|
+
'health.detail.node-version.warning': '{version} is below the required {required}.x; upgrading Node is recommended',
|
|
254
292
|
'health.detail.session-storage.ok': 'Available, with {count} session snapshots',
|
|
255
293
|
'health.detail.workspace-registry.ok': 'Available, with {count} workspaces',
|
|
256
294
|
'health.detail.dsh-home.ok': 'Directory is accessible with mode {mode}',
|
|
@@ -264,7 +302,7 @@ window.__ModuleLoader__.load({
|
|
|
264
302
|
'tabs.notify': 'Notifications',
|
|
265
303
|
'tabs.health': 'Health',
|
|
266
304
|
'tabs.usage': 'Models',
|
|
267
|
-
'overview.container': '
|
|
305
|
+
'overview.container': 'Process and runtime',
|
|
268
306
|
'overview.errors': 'Errors',
|
|
269
307
|
'tabs.backup': 'Backup',
|
|
270
308
|
'tabs.restart': 'Restart',
|
|
@@ -349,6 +387,22 @@ window.__ModuleLoader__.load({
|
|
|
349
387
|
'update.failAddingToRoot': 'pnpm refused to add at the workspace root (missing -w)',
|
|
350
388
|
'update.failNotWorkspace': '-w was passed but the profile is not a pnpm workspace',
|
|
351
389
|
'update.failIgnoredBuilds': 'Dependency build scripts are blocked by pnpm by default, so the upgrade could not finish',
|
|
390
|
+
'env.line': 'Runtime environment: {value}',
|
|
391
|
+
'env.kind.docker': 'Docker container',
|
|
392
|
+
'env.kind.container': 'Container (containerd/Kubernetes)',
|
|
393
|
+
'env.kind.systemd': 'systemd service',
|
|
394
|
+
'env.kind.pm2': 'pm2',
|
|
395
|
+
'env.kind.supervisord': 'supervisord',
|
|
396
|
+
'env.kind.kubernetes': 'Kubernetes',
|
|
397
|
+
'env.kind.declared': 'External process manager (declared manually)',
|
|
398
|
+
'env.kind.unrecognized': 'Unrecognized process manager detected',
|
|
399
|
+
'env.likelyManual': 'Likely launched manually from a terminal',
|
|
400
|
+
'env.unknown': 'No process manager detected',
|
|
401
|
+
'update.manualConfirmTitle': 'Confirm before upgrading: manual launch suspected',
|
|
402
|
+
'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.',
|
|
403
|
+
'update.manualProceed': 'Upgrade anyway',
|
|
404
|
+
'update.manualRestartTitle': 'Upgrade finished — manual restart required',
|
|
405
|
+
'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
406
|
'restart.title': 'Service restart',
|
|
353
407
|
'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
408
|
'restart.button': 'Restart dsh web',
|
|
@@ -359,6 +413,8 @@ window.__ModuleLoader__.load({
|
|
|
359
413
|
'restart.sent': 'Restart request sent.',
|
|
360
414
|
'restart.sentHint': 'The connection will close shortly. The page will reload automatically after recovery.',
|
|
361
415
|
'restart.idleHint': 'No active work was detected. Confirm to disconnect and wait for the service to restart.',
|
|
416
|
+
'restart.manualWarn': 'This looks like a manual terminal launch: once confirmed the process exits and nothing restarts it — you must run dsh again yourself.',
|
|
417
|
+
'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
418
|
'restart.navToggle': 'Show "Restart" entry in settings left nav',
|
|
363
419
|
'restart.navToggleHint': 'Off by default; when enabled, a quick-restart entry appears at the bottom of the settings left navigation',
|
|
364
420
|
'activity.agent': 'Agent',
|
|
@@ -607,6 +663,53 @@ window.__ModuleLoader__.load({
|
|
|
607
663
|
}, [])
|
|
608
664
|
return snapshot
|
|
609
665
|
}
|
|
666
|
+
|
|
667
|
+
// 运行环境(version RPC 随 instanceId 返回):概览展示、升级前置确认与重启警告共用。
|
|
668
|
+
// null = 宿主未返回该字段(旧版本)或尚未拉取,一切行为静默退回现状。
|
|
669
|
+
const runtimeEnvListeners = new Set()
|
|
670
|
+
let runtimeEnvState = null
|
|
671
|
+
const setRuntimeEnvState = (next) => {
|
|
672
|
+
runtimeEnvState = next
|
|
673
|
+
for (const listener of runtimeEnvListeners) listener(next)
|
|
674
|
+
}
|
|
675
|
+
const useRuntimeEnv = () => {
|
|
676
|
+
const [snapshot, setSnapshot] = useState(runtimeEnvState)
|
|
677
|
+
useEffect(() => {
|
|
678
|
+
runtimeEnvListeners.add(setSnapshot)
|
|
679
|
+
setSnapshot(runtimeEnvState)
|
|
680
|
+
return () => runtimeEnvListeners.delete(setSnapshot)
|
|
681
|
+
}, [])
|
|
682
|
+
return snapshot
|
|
683
|
+
}
|
|
684
|
+
// 宿主字段按形状校验后才入库:manualStartLikely 必须 boolean、supervisorKind 缺省或 string,
|
|
685
|
+
// 形状不对按「旧宿主无该字段」降级,绝不让坏值悄悄关掉确认门。
|
|
686
|
+
const applyVersionRuntimeEnv = (value) => {
|
|
687
|
+
const env = value ? value.runtimeEnv : undefined
|
|
688
|
+
if (env === null || typeof env !== 'object') return
|
|
689
|
+
if (typeof env.manualStartLikely !== 'boolean') return
|
|
690
|
+
if (env.supervisorKind !== undefined && env.supervisorKind !== null && typeof env.supervisorKind !== 'string') return
|
|
691
|
+
setRuntimeEnvState({ platform: typeof env.platform === 'string' ? env.platform : '', supervisorKind: env.supervisorKind === undefined || env.supervisorKind === null ? null : env.supervisorKind, manualStartLikely: env.manualStartLikely })
|
|
692
|
+
}
|
|
693
|
+
// version 快照全插件只取一次:ServicePanel 与左列入口无论谁先挂载都共享同一请求,
|
|
694
|
+
// 升级前取 instanceId/运行环境也复用它。失败清缓存,下一个消费者重试。
|
|
695
|
+
let versionSnapshotPromise = null
|
|
696
|
+
const fetchVersionSnapshot = () => {
|
|
697
|
+
if (versionSnapshotPromise === null) {
|
|
698
|
+
versionSnapshotPromise = ctx.connection.rpc.call('/dsh-service', 'version', {})
|
|
699
|
+
.then((res) => {
|
|
700
|
+
if (res && res.ok) applyVersionRuntimeEnv(res.value)
|
|
701
|
+
return res
|
|
702
|
+
})
|
|
703
|
+
.catch(() => {
|
|
704
|
+
versionSnapshotPromise = null
|
|
705
|
+
return null
|
|
706
|
+
})
|
|
707
|
+
}
|
|
708
|
+
return versionSnapshotPromise
|
|
709
|
+
}
|
|
710
|
+
// 升级执行中标志放在 factory 作用域:闭包状态挡不住同一 tick 的重入,跨渲染的新闭包
|
|
711
|
+
// 也各自持有独立的 false,只有插件级可变标志能同时覆盖两种情况。
|
|
712
|
+
let upgradeInFlight = false
|
|
610
713
|
const checkRestart = async () => {
|
|
611
714
|
setRestartFlow({ ...restartFlow, busy: true, error: null })
|
|
612
715
|
try {
|
|
@@ -692,6 +795,7 @@ window.__ModuleLoader__.load({
|
|
|
692
795
|
recoveryGeneration += 1
|
|
693
796
|
recoveryListeners.clear()
|
|
694
797
|
restartFlowListeners.clear()
|
|
798
|
+
runtimeEnvListeners.clear()
|
|
695
799
|
updateListeners.clear()
|
|
696
800
|
}, 'dsh-service recovery')
|
|
697
801
|
|
|
@@ -797,6 +901,11 @@ window.__ModuleLoader__.load({
|
|
|
797
901
|
function RestartSection({ showNavToggle }) {
|
|
798
902
|
const translate = useTranslation()
|
|
799
903
|
const flow = useRestartFlow()
|
|
904
|
+
const runtimeEnv = useRuntimeEnv()
|
|
905
|
+
// 左列专属入口可能先于「服务控制」面板挂载:走共享 version 快照(与面板同一次请求)。
|
|
906
|
+
useEffect(() => {
|
|
907
|
+
fetchVersionSnapshot()
|
|
908
|
+
}, [])
|
|
800
909
|
const [navEnabled, setNavEnabled] = useRestartNavEnabled()
|
|
801
910
|
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
911
|
const danger = { ...btn, background: 'var(--dsw-alias-state-error-primary)', color: '#fff', borderColor: 'var(--dsw-alias-state-error-primary)' }
|
|
@@ -807,12 +916,12 @@ window.__ModuleLoader__.load({
|
|
|
807
916
|
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
917
|
const sectionTitle = { fontSize: '14px', fontWeight: 700, margin: '0 0 8px', color: 'var(--dsw-alias-label-primary)' }
|
|
809
918
|
|
|
810
|
-
//
|
|
919
|
+
// 重启后提示:手动启动环境不会自动拉起,等待文案换成手动指引。
|
|
811
920
|
if (flow.stage === 2) {
|
|
812
921
|
return React.createElement('div', { 'data-testid': 'restart-card', style: card },
|
|
813
922
|
React.createElement('div', { style: sectionTitle }, translate('restart.title')),
|
|
814
923
|
React.createElement('p', { style: { margin: 0, fontSize: '13px' } }, translate('restart.sent')),
|
|
815
|
-
React.createElement('p', { style: hint }, translate('restart.sentHint')))
|
|
924
|
+
React.createElement('p', { style: hint }, translate(runtimeEnv !== null && runtimeEnv.manualStartLikely === true ? 'restart.sentManualHint' : 'restart.sentHint')))
|
|
816
925
|
}
|
|
817
926
|
|
|
818
927
|
const activityLabels = {
|
|
@@ -821,6 +930,10 @@ window.__ModuleLoader__.load({
|
|
|
821
930
|
terminal: translate('activity.terminal'),
|
|
822
931
|
}
|
|
823
932
|
const activityItems = flow.activity && Array.isArray(flow.activity.items) ? flow.activity.items : []
|
|
933
|
+
// 疑似终端手动启动:确认前就把「退出后无人拉起」讲清楚,两段式确认的后果清单。
|
|
934
|
+
const manualWarn = runtimeEnv !== null && runtimeEnv.manualStartLikely === true
|
|
935
|
+
? 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'))
|
|
936
|
+
: null
|
|
824
937
|
const activityWarning = flow.stage === 3
|
|
825
938
|
? 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
939
|
React.createElement('p', { style: { margin: '0 0 8px', color: 'var(--dsw-alias-state-error-primary)', fontSize: '13px', fontWeight: 600 } },
|
|
@@ -840,6 +953,7 @@ window.__ModuleLoader__.load({
|
|
|
840
953
|
React.createElement('div', { style: sectionTitle }, translate('restart.title')),
|
|
841
954
|
React.createElement('div', { 'data-testid': 'restart-region', style: displaySurface },
|
|
842
955
|
React.createElement('p', { style: { margin: 0, fontSize: '13px' } }, translate('restart.description')),
|
|
956
|
+
manualWarn,
|
|
843
957
|
activityWarning,
|
|
844
958
|
React.createElement('div', { style: row },
|
|
845
959
|
flow.stage === 0
|
|
@@ -906,6 +1020,9 @@ window.__ModuleLoader__.load({
|
|
|
906
1020
|
const [usageError, setUsageError] = useState(null)
|
|
907
1021
|
const [upgradeBusy, setUpgradeBusy] = useState(false)
|
|
908
1022
|
const [upgradeError, setUpgradeError] = useState(null)
|
|
1023
|
+
// 疑似手动启动环境的升级两段式:确认后果 → 仍要升级;成功后不自动退出,改示指引。
|
|
1024
|
+
const [upgradeManualConfirm, setUpgradeManualConfirm] = useState(false)
|
|
1025
|
+
const [upgradeManualPending, setUpgradeManualPending] = useState(false)
|
|
909
1026
|
const [hoveredUsageSegment, setHoveredUsageSegment] = useState(null)
|
|
910
1027
|
const [usageProject, setUsageProject] = useState('all')
|
|
911
1028
|
const [modelErrorsOpen, setModelErrorsOpen] = useState(false)
|
|
@@ -916,16 +1033,18 @@ window.__ModuleLoader__.load({
|
|
|
916
1033
|
const [channelOpen, setChannelOpen] = useState(false)
|
|
917
1034
|
// 重启流程状态来自共享流(与设置页左列底部的专属入口同源)
|
|
918
1035
|
const restartFlowState = useRestartFlow()
|
|
1036
|
+
const runtimeEnv = useRuntimeEnv()
|
|
919
1037
|
const usageRequestPayload = { timezoneOffsetMinutes: new Date().getTimezoneOffset() }
|
|
920
1038
|
|
|
921
1039
|
// 进入面板时拉取当前版本和健康快照;健康数据每 5 秒刷新,卸载即停止。
|
|
1040
|
+
// version 走全插件共享的缓存快照:无论哪个挂载点先到,都只有一次请求。
|
|
922
1041
|
useEffect(() => {
|
|
923
|
-
|
|
1042
|
+
fetchVersionSnapshot().then((res) => {
|
|
924
1043
|
if (res && res.ok) {
|
|
925
1044
|
setVersion(res.value.current)
|
|
926
1045
|
setPluginVersion(res.value.pluginVersion || null)
|
|
927
1046
|
}
|
|
928
|
-
})
|
|
1047
|
+
})
|
|
929
1048
|
}, [])
|
|
930
1049
|
useEffect(() => {
|
|
931
1050
|
let active = true
|
|
@@ -1173,10 +1292,25 @@ window.__ModuleLoader__.load({
|
|
|
1173
1292
|
'ignored-builds': 'update.failIgnoredBuilds',
|
|
1174
1293
|
}
|
|
1175
1294
|
const upgradePlugin = async () => {
|
|
1176
|
-
|
|
1177
|
-
|
|
1295
|
+
if (upgradeInFlight) return
|
|
1296
|
+
upgradeInFlight = true
|
|
1178
1297
|
try {
|
|
1179
|
-
|
|
1298
|
+
// check-update 先于 version 返回时按钮可能先出现:env 未知就等共享快照落地再判。
|
|
1299
|
+
let env = runtimeEnv
|
|
1300
|
+
if (env === null) {
|
|
1301
|
+
await fetchVersionSnapshot()
|
|
1302
|
+
env = runtimeEnvState
|
|
1303
|
+
}
|
|
1304
|
+
// 疑似终端手动启动:升级成功不会自动重启,先两段式确认后果再动手(安全教义)。
|
|
1305
|
+
if (!upgradeManualConfirm && env !== null && env.manualStartLikely === true) {
|
|
1306
|
+
setUpgradeError(null)
|
|
1307
|
+
setUpgradeManualConfirm(true)
|
|
1308
|
+
return
|
|
1309
|
+
}
|
|
1310
|
+
setUpgradeManualConfirm(false)
|
|
1311
|
+
setUpgradeBusy(true)
|
|
1312
|
+
setUpgradeError(null)
|
|
1313
|
+
const versionRes = await fetchVersionSnapshot()
|
|
1180
1314
|
const previousInstanceId = versionRes && versionRes.ok ? versionRes.value.instanceId : undefined
|
|
1181
1315
|
const res = await ctx.connection.rpc.call('/dsh-service', 'upgrade', {})
|
|
1182
1316
|
if (!res || res.ok === false) {
|
|
@@ -1190,6 +1324,11 @@ window.__ModuleLoader__.load({
|
|
|
1190
1324
|
}
|
|
1191
1325
|
return
|
|
1192
1326
|
}
|
|
1327
|
+
if (res.value && res.value.requiresManualRestart === true) {
|
|
1328
|
+
// 宿主保持运行(没有 exit,就不会有新实例):不启动恢复轮询,改示手动重启指引。
|
|
1329
|
+
setUpgradeManualPending(true)
|
|
1330
|
+
return
|
|
1331
|
+
}
|
|
1193
1332
|
if (typeof previousInstanceId === 'string' && previousInstanceId.length > 0) {
|
|
1194
1333
|
startRecovery(previousInstanceId).catch(() => {})
|
|
1195
1334
|
}
|
|
@@ -1198,6 +1337,7 @@ window.__ModuleLoader__.load({
|
|
|
1198
1337
|
console.error('dsh-service: upgrade failed', detail || err)
|
|
1199
1338
|
setUpgradeError(detail ? translate('update.upgradeErrorDetail', { detail }) : translate('update.upgradeError'))
|
|
1200
1339
|
} finally {
|
|
1340
|
+
upgradeInFlight = false
|
|
1201
1341
|
setUpgradeBusy(false)
|
|
1202
1342
|
}
|
|
1203
1343
|
}
|
|
@@ -1338,6 +1478,17 @@ window.__ModuleLoader__.load({
|
|
|
1338
1478
|
}
|
|
1339
1479
|
if (check.id === 'tar' && check.status === 'ok') return translate('health.detail.tar.ok')
|
|
1340
1480
|
if (check.id === 'permissions') return translate(check.status === 'ok' ? 'health.detail.permissions.ok' : 'health.detail.permissions.warning', { count: detail || '0' })
|
|
1481
|
+
if (check.id === 'runtime-env') {
|
|
1482
|
+
if (detail === 'manual') return translate('health.detail.runtime-env.manual')
|
|
1483
|
+
if (detail === 'declared') return translate('health.detail.runtime-env.declared')
|
|
1484
|
+
if (detail === 'unknown') return translate('health.detail.runtime-env.unknown')
|
|
1485
|
+
// 已识别的管理器 kind:复用概览运行环境的标签词典。
|
|
1486
|
+
return translate('health.detail.runtime-env.managed', { kind: translate(`env.kind.${detail}`) })
|
|
1487
|
+
}
|
|
1488
|
+
if (check.id === 'node-version') {
|
|
1489
|
+
const [version = '', required = ''] = detail.split(':')
|
|
1490
|
+
return translate(check.status === 'ok' ? 'health.detail.node-version.ok' : 'health.detail.node-version.warning', { version, required })
|
|
1491
|
+
}
|
|
1341
1492
|
return translate('health.detail.generic', { status: translate(`health.status.${check.status}`) })
|
|
1342
1493
|
}
|
|
1343
1494
|
const summaryItems = (totals) => [
|
|
@@ -1440,10 +1591,17 @@ window.__ModuleLoader__.load({
|
|
|
1440
1591
|
: React.createElement('p', { style: hint }, usageError || translate('usage.empty')),
|
|
1441
1592
|
React.createElement('div', { style: row }, React.createElement('button', { style: neutral, 'data-variant': 'neutral', onClick: refreshUsage, disabled: usageBusy }, translate(usageBusy ? 'usage.refreshing' : 'usage.refresh'))))
|
|
1442
1593
|
|
|
1594
|
+
// 平台标签:process.platform 映射为常见系统名,arch 跟在后面(均为专有名词,不走词典)。
|
|
1595
|
+
const platformNames = { win32: 'Windows', darwin: 'macOS', linux: 'Linux', freebsd: 'FreeBSD', openbsd: 'OpenBSD' }
|
|
1596
|
+
const platformLabel = (health && typeof health.platform === 'string' && health.platform)
|
|
1597
|
+
? `${platformNames[health.platform] || health.platform}${typeof health.arch === 'string' && health.arch ? ` · ${health.arch}` : ''}`
|
|
1598
|
+
: '—'
|
|
1443
1599
|
const containerInfoBlock = React.createElement('div', { key: 'container-info', style: { marginTop: '18px' } },
|
|
1444
1600
|
React.createElement('div', { style: sectionTitle }, translate('overview.container')),
|
|
1445
1601
|
health
|
|
1446
1602
|
? React.createElement('div', { 'data-testid': 'health-display', style: Object.assign({}, displaySurface, { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '8px' }) },
|
|
1603
|
+
metric('health.platform', platformLabel),
|
|
1604
|
+
metric('health.nodeVersion', typeof health.nodeVersion === 'string' && health.nodeVersion ? health.nodeVersion : '—'),
|
|
1447
1605
|
metric('health.uptime', formatUptime(health.uptimeSeconds)),
|
|
1448
1606
|
metric('health.rss', formatBytes(health.rssBytes)),
|
|
1449
1607
|
metric('health.liveSessions', String(health.liveSessions)),
|
|
@@ -1473,7 +1631,7 @@ window.__ModuleLoader__.load({
|
|
|
1473
1631
|
? React.createElement('div', { 'data-testid': 'health-check-list', style: Object.assign({}, displaySurface, { marginTop: '10px', padding: '8px 10px' }) },
|
|
1474
1632
|
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
1633
|
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)))))
|
|
1634
|
+
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
1635
|
: null)
|
|
1478
1636
|
|
|
1479
1637
|
const permissionAbnormal = permissions && permissions.supported === true
|
|
@@ -1602,9 +1760,22 @@ window.__ModuleLoader__.load({
|
|
|
1602
1760
|
const dshUpdate = updateInfo?.dsh
|
|
1603
1761
|
const dshExpandable = dshUpdate && dshUpdate.status !== 'unpublished' && dshUpdate.status !== 'unavailable' && !dshUpdate.upToDate
|
|
1604
1762
|
const pluginUpdate = updateInfo?.plugin && !updateInfo.plugin.upToDate && updateInfo.plugin.status === 'available'
|
|
1605
|
-
|
|
1763
|
+
// 确认后果或已装好待手动重启期间收起升级按钮,避免重复触发或撞 no-newer-version 守卫。
|
|
1764
|
+
const pluginAction = pluginUpdate && !upgradeManualConfirm && !upgradeManualPending
|
|
1606
1765
|
? 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
1766
|
: null
|
|
1767
|
+
// 运行环境一行:managed 显示管理器标签;疑似手动启动用警示色;未知非空 kind 不得
|
|
1768
|
+
// 显示成「未检测到」(那是 supervisorKind 为 null 时的语义)。
|
|
1769
|
+
const runtimeEnvKinds = ['docker', 'container', 'systemd', 'pm2', 'supervisord', 'kubernetes', 'declared']
|
|
1770
|
+
const runtimeEnvLabelKey = runtimeEnv === null
|
|
1771
|
+
? null
|
|
1772
|
+
: runtimeEnv.manualStartLikely === true
|
|
1773
|
+
? 'env.likelyManual'
|
|
1774
|
+
: typeof runtimeEnv.supervisorKind === 'string' && runtimeEnvKinds.includes(runtimeEnv.supervisorKind)
|
|
1775
|
+
? `env.kind.${runtimeEnv.supervisorKind}`
|
|
1776
|
+
: typeof runtimeEnv.supervisorKind === 'string' && runtimeEnv.supervisorKind.length > 0
|
|
1777
|
+
? 'env.kind.unrecognized'
|
|
1778
|
+
: 'env.unknown'
|
|
1608
1779
|
const versionBlock = React.createElement('div', { key: 'version-card', 'data-testid': 'version-card', style: card },
|
|
1609
1780
|
React.createElement('div', { key: 'title', style: sectionTitle }, translate('version.title')),
|
|
1610
1781
|
React.createElement('div', { style: displaySurface },
|
|
@@ -1616,6 +1787,23 @@ window.__ModuleLoader__.load({
|
|
|
1616
1787
|
React.createElement('div', null, translate('update.details.latest', { version: dshUpdate?.latest || '—' })),
|
|
1617
1788
|
channelLines(translate, dshUpdate?.tags))
|
|
1618
1789
|
: null,
|
|
1790
|
+
runtimeEnvLabelKey !== null
|
|
1791
|
+
? React.createElement('div', { 'data-testid': 'runtime-env-line', style: { marginTop: '8px', paddingTop: '8px', borderTop: '1px solid var(--dsw-alias-border-l1)', fontSize: '12px', color: runtimeEnv.manualStartLikely === true ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-label-secondary)' } },
|
|
1792
|
+
translate('env.line', { value: translate(runtimeEnvLabelKey) }))
|
|
1793
|
+
: null,
|
|
1794
|
+
upgradeManualConfirm
|
|
1795
|
+
? 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)' } },
|
|
1796
|
+
React.createElement('p', { style: { margin: '0 0 6px', color: 'var(--dsw-alias-state-error-primary)', fontSize: '13px', fontWeight: 600 } }, translate('update.manualConfirmTitle')),
|
|
1797
|
+
React.createElement('p', { style: Object.assign({}, hint, { margin: '0 0 8px' }) }, translate('update.manualConfirm')),
|
|
1798
|
+
React.createElement('div', { style: row },
|
|
1799
|
+
React.createElement('button', { style: dangerGhost, 'data-variant': 'danger', disabled: upgradeBusy, onClick: upgradePlugin }, translate(upgradeBusy ? 'update.upgrading' : 'update.manualProceed')),
|
|
1800
|
+
React.createElement('button', { style: ghost, disabled: upgradeBusy, onClick: () => setUpgradeManualConfirm(false) }, translate('restart.cancel'))))
|
|
1801
|
+
: null,
|
|
1802
|
+
upgradeManualPending
|
|
1803
|
+
? 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)' } },
|
|
1804
|
+
React.createElement('p', { style: { margin: '0 0 4px', color: 'var(--dsw-alias-state-warn-primary)', fontSize: '13px', fontWeight: 650 } }, translate('update.manualRestartTitle')),
|
|
1805
|
+
React.createElement('p', { style: Object.assign({}, hint, { margin: 0 }) }, translate('update.manualRestartBody')))
|
|
1806
|
+
: null,
|
|
1619
1807
|
upgradeError ? React.createElement('p', { style: Object.assign({}, hint, { color: 'var(--dsw-alias-state-error-primary)', margin: '4px 0 0' }) }, upgradeError) : null))
|
|
1620
1808
|
|
|
1621
1809
|
// 重启区块复用共享组件(「重启」标签还承载设置页左列入口的显示开关;左侧入口默认关闭)
|
|
@@ -1659,7 +1847,7 @@ window.__ModuleLoader__.load({
|
|
|
1659
1847
|
// 任务通知独立成顶部标签(v0.14 起不再混在概览里)
|
|
1660
1848
|
const notifyBlock = React.createElement('div', null, notificationBlock)
|
|
1661
1849
|
const maintenanceBlock = React.createElement('div', { key: 'maintenance-card', 'data-testid': 'maintenance-card', style: card }, backupBlock)
|
|
1662
|
-
const diagnosticFailure = diagnostics?.checks?.some((check) => check.status === 'error' ||
|
|
1850
|
+
const diagnosticFailure = diagnostics?.checks?.some((check) => check.status === 'error' || check.status === 'warning') === true
|
|
1663
1851
|
const tabWarnings = {
|
|
1664
1852
|
overview: false,
|
|
1665
1853
|
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(重启无保障);
|
|
1017
|
+
// unknown → info(未检测到管理器也无终端特征:输出重定向、NSSM/WinSW 等包装器——维持现状
|
|
1018
|
+
// 行为本就是默认路径,不算异常,只提示可显式声明)。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' }
|
|
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,6 +1072,14 @@ 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'
|
|
1048
1084
|
if (checks.some((check) => check.status === 'error')) status = 'error'
|
|
1049
1085
|
else if (checks.some((check) => check.status === 'warning')) status = 'warning'
|
|
@@ -1060,6 +1096,9 @@ async function collectHealth(ctx) {
|
|
|
1060
1096
|
return {
|
|
1061
1097
|
uptimeSeconds: process.uptime(),
|
|
1062
1098
|
rssBytes: memory.rss,
|
|
1099
|
+
platform: process.platform,
|
|
1100
|
+
arch: process.arch,
|
|
1101
|
+
nodeVersion: process.version,
|
|
1063
1102
|
liveSessions: sessionsService === undefined ? 0 : sessionsService.list().length,
|
|
1064
1103
|
persistedSessions: sessionRecords.filter((record) => record.persisted === true).length,
|
|
1065
1104
|
activeAgents: activity.items.filter((item) => item.type === 'agent').length,
|
|
@@ -1127,6 +1166,50 @@ function collectActiveWork(ctx) {
|
|
|
1127
1166
|
return { hasActive: items.length > 0, items }
|
|
1128
1167
|
}
|
|
1129
1168
|
|
|
1169
|
+
// ---- 运行环境探测(v0.16:Windows 终端手动启动场景,用户点名)----
|
|
1170
|
+
// 重启 = process.exit(42),依赖外层进程管理器拉起(见 AGENTS.md);PowerShell/CMD 手动
|
|
1171
|
+
// 启动时没有任何东西会把它拉起来。这里只用跨平台被动信号(env + 固定路径 fs + Node 自带
|
|
1172
|
+
// isTTY,零 spawn、浏览器零输入)把环境分成三态:
|
|
1173
|
+
// supervisorKind 非 null —— 检测到管理器,exit 后会被自动拉起,维持自动重启;
|
|
1174
|
+
// manualStartLikely=true —— 无管理器且 stdin/stdout 双 TTY(终端直启特征),升级后宿主
|
|
1175
|
+
// 不自动退出,客户端改示手动重启指引;
|
|
1176
|
+
// 其余 —— unknown(输出重定向、NSSM/WinSW 等包装器),维持现状不折腾。
|
|
1177
|
+
// 探测不到的包装器可用 DSH_SERVICE_RUNTIME_ENV=managed|manual 由用户显式声明。
|
|
1178
|
+
const RUNTIME_SUPERVISOR_ENV = [
|
|
1179
|
+
['pm2', ['pm_id', 'PM2_HOME', 'pm_uptime']],
|
|
1180
|
+
['systemd', ['INVOCATION_ID', 'JOURNAL_STREAM', 'NOTIFY_SOCKET']],
|
|
1181
|
+
['supervisord', ['SUPERVISOR_ENABLED', 'SUPERVISOR_PROCESS_NAME']],
|
|
1182
|
+
['kubernetes', ['KUBERNETES_SERVICE_HOST']],
|
|
1183
|
+
]
|
|
1184
|
+
|
|
1185
|
+
function detectRuntimeEnv(options = {}) {
|
|
1186
|
+
const env = options.env ?? process.env
|
|
1187
|
+
const platform = options.platform ?? process.platform
|
|
1188
|
+
const stdinIsTTY = options.stdinIsTTY ?? (process.stdin ? process.stdin.isTTY === true : false)
|
|
1189
|
+
const stdoutIsTTY = options.stdoutIsTTY ?? (process.stdout ? process.stdout.isTTY === true : false)
|
|
1190
|
+
// 默认探测器只在未被注入时执行;win32 直接短路(/.dockerenv 与 /proc 不存在),单测全部显式传原语。
|
|
1191
|
+
const dockerEnvExists = options.dockerEnvExists ?? (platform === 'win32' ? false : (() => { try { return existsSync('/.dockerenv') } catch (_) { return false } })())
|
|
1192
|
+
const cgroupText = options.cgroupText ?? (platform === 'win32' ? '' : (() => { try { return readFileSync('/proc/1/cgroup', 'utf8') } catch (_) { return '' } })())
|
|
1193
|
+
|
|
1194
|
+
const forced = typeof env.DSH_SERVICE_RUNTIME_ENV === 'string' ? env.DSH_SERVICE_RUNTIME_ENV.trim().toLowerCase() : ''
|
|
1195
|
+
if (forced === 'manual') return { platform, supervisorKind: null, manualStartLikely: true }
|
|
1196
|
+
if (forced === 'managed') return { platform, supervisorKind: 'declared', manualStartLikely: false }
|
|
1197
|
+
|
|
1198
|
+
for (const [kind, keys] of RUNTIME_SUPERVISOR_ENV) {
|
|
1199
|
+
if (keys.some((key) => env[key] !== undefined && env[key] !== '')) {
|
|
1200
|
+
return { platform, supervisorKind: kind, manualStartLikely: false }
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
// /.dockerenv 与 /proc 只在 POSIX 内核上存在;win32 原生进程不做这两个探测。
|
|
1204
|
+
if (platform !== 'win32') {
|
|
1205
|
+
if (dockerEnvExists === true) return { platform, supervisorKind: 'docker', manualStartLikely: false }
|
|
1206
|
+
if (/docker/i.test(cgroupText)) return { platform, supervisorKind: 'docker', manualStartLikely: false }
|
|
1207
|
+
if (/containerd|kubepods|lxc|podman/i.test(cgroupText)) return { platform, supervisorKind: 'container', manualStartLikely: false }
|
|
1208
|
+
}
|
|
1209
|
+
if (stdinIsTTY === true && stdoutIsTTY === true) return { platform, supervisorKind: null, manualStartLikely: true }
|
|
1210
|
+
return { platform, supervisorKind: null, manualStartLikely: false }
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1130
1213
|
function scheduleRestart(ctx) {
|
|
1131
1214
|
const doExit = () => {
|
|
1132
1215
|
try {
|
|
@@ -1143,6 +1226,8 @@ function scheduleRestart(ctx) {
|
|
|
1143
1226
|
|
|
1144
1227
|
function apply(ctx) {
|
|
1145
1228
|
const dshHome = resolveDshHome()
|
|
1229
|
+
// 进程运行环境在生命周期内不变:挂载时探测一次,version RPC 与升级分支共用。
|
|
1230
|
+
const runtimeEnv = detectRuntimeEnv()
|
|
1146
1231
|
const permissionPlans = new Map()
|
|
1147
1232
|
const downloadTokens = new Map()
|
|
1148
1233
|
let usageIndexPromise = loadUsageIndex(dshHome)
|
|
@@ -1205,7 +1290,9 @@ function apply(ctx) {
|
|
|
1205
1290
|
// 合法示例:channel=/dsh-service,endpoint=version/check-update/activity/web。
|
|
1206
1291
|
ctx.connection.rpc.handle('/dsh-service', async (endpoint, payload) => {
|
|
1207
1292
|
if (endpoint === 'version') {
|
|
1208
|
-
|
|
1293
|
+
// runtimeEnv 随进程身份(instanceId)一起返回:概览展示、升级前置确认与重启警告共用,
|
|
1294
|
+
// 客户端对缺字段的老宿主静默降级。
|
|
1295
|
+
return { ok: true, value: { current: dshVersion, pluginVersion, instanceId, runtimeEnv } }
|
|
1209
1296
|
}
|
|
1210
1297
|
|
|
1211
1298
|
if (endpoint === 'check-update') {
|
|
@@ -1243,7 +1330,7 @@ function apply(ctx) {
|
|
|
1243
1330
|
|
|
1244
1331
|
if (endpoint === 'upgrade') {
|
|
1245
1332
|
try {
|
|
1246
|
-
return { ok: true, value: await upgradePlugin(ctx, dshHome) }
|
|
1333
|
+
return { ok: true, value: await upgradePlugin(ctx, dshHome, runtimeEnv) }
|
|
1247
1334
|
} catch (error) {
|
|
1248
1335
|
return { ok: false, error: error?.message || String(error) }
|
|
1249
1336
|
}
|
|
@@ -1263,7 +1350,7 @@ function apply(ctx) {
|
|
|
1263
1350
|
|
|
1264
1351
|
if (endpoint === 'diagnostics') {
|
|
1265
1352
|
try {
|
|
1266
|
-
return { ok: true, value: await collectDiagnostics(ctx, dshHome) }
|
|
1353
|
+
return { ok: true, value: await collectDiagnostics(ctx, dshHome, runtimeEnv) }
|
|
1267
1354
|
} catch (error) {
|
|
1268
1355
|
return { ok: false, error: error?.message || String(error) }
|
|
1269
1356
|
}
|
|
@@ -1393,5 +1480,5 @@ function apply(ctx) {
|
|
|
1393
1480
|
}, { authority: 'loopback' })
|
|
1394
1481
|
}
|
|
1395
1482
|
|
|
1396
|
-
export { apply, inject, name }
|
|
1397
|
-
export default { apply, inject, name }
|
|
1483
|
+
export { apply, detectRuntimeEnv, inject, name, runtimeEnvCheck }
|
|
1484
|
+
export default { apply, detectRuntimeEnv, inject, name, runtimeEnvCheck }
|