openclaw-diag-cli 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenClaw Diagnostic CLI Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,260 @@
1
+ # openclaw-diag-cli
2
+
3
+ > OpenClaw / ArkClaw 故障诊断工具集。零依赖、只读、可组合的纯 Python 脚本。
4
+
5
+ ## 快速开始
6
+
7
+ 无需 git clone,通过 npm 拉一份缓存即可(之后离线可用):
8
+
9
+ ```bash
10
+ # 一次性运行(npm 缓存后离线可用)
11
+ npx openclaw-diag-cli list
12
+ npx openclaw-diag-cli run gateway
13
+ npx openclaw-diag-cli run all --json | jq -s '.'
14
+
15
+ # 装到 PATH(更短的命令)
16
+ npm install -g openclaw-diag-cli
17
+ openclaw-diag list
18
+ openclaw-diag doctor # 检查环境是否就绪
19
+ openclaw-diag bundle gateway > gw.py # 生成单文件诊断脚本
20
+ ```
21
+
22
+ 依赖:Node 18+(npx)和 Python 3.8+。Node 层是零 npm 依赖的薄壳,只负责定位
23
+ `python3` 并把参数透传给现有的 dispatcher,所以 `python3 diag/04_gateway.py`
24
+ 和 `python3 bin/ocdiag run gateway` 仍然完全可用。
25
+
26
+ ## 为什么存在
27
+
28
+ 排查 OpenClaw 故障时面对的真实痛点:
29
+
30
+ - **数据散在多个角落**:session.jsonl 在 agents/ 下,配置在 openclaw.json,进程行为在 journalctl,cron 状态在 cron/jobs.json,模型耗时藏在 trajectory 里…… 手敲 jq + grep 组合费时且易漏。
31
+ - **`openclaw-diag.sh` 已成为 4391 行单体 bash**,里面塞着 10 段 heredoc 嵌入的 Python,难修改、难单测、难复用。
32
+ - **诊断脚本应该是"原子操作"**:每条数据有明确来源,每个模块解决一类问题,可以单独跑、可以组合管道、可以被自动化驱动。
33
+
34
+ 这个仓库就是把那个 4391 行 bash 拆开重写——每个采集动作独立成一个 Python 脚本,按一组公理设计,让"采集 → 分析 → 上报"变成可推理的工程而不是手工活。
35
+
36
+ ---
37
+
38
+ ## 设计公理(First Principles)
39
+
40
+ 下面 6 条是**不可让步**的硬约束。所有目录结构、API、输出格式都从这 6 条推导出来。
41
+
42
+ ### 1. 只读(Read-Only)
43
+ 诊断脚本**永远不能**修改文件、写配置、重启服务。代价:再难拿的数据也要靠"读"获得;不允许走 `openclaw <subcmd>` 修改类入口。
44
+ **收益**:在生产环境、在排查事故现场、在客户机器上跑都安全。
45
+
46
+ ### 2. 零运行时依赖(Zero Runtime Dependencies)
47
+ **只用 Python 3.8+ 标准库**。不写 `requirements.txt`,不要 `pip install`。唯一例外:`croniter` 在 `06_cron_jobs.py` 中可选导入(缺失时退化到从历史 runs 推算间隔)。
48
+ **收益**:任何能跑 OpenClaw 的节点都能跑诊断(OpenClaw 自己依赖 Node.js,但诊断脚本不依赖 OpenClaw 装在 Python 端的任何包)。`git clone` 完直接 `python3 diag/04_gateway.py`。
49
+
50
+ ### 3. 独立可执行(Independent)
51
+ **每个诊断脚本必须能单独跑通**,不依赖 dispatcher、不需要 source 任何 env 文件、不需要先执行别的脚本。
52
+ **推论**:脚本顶部用 `sys.path.insert(0, ...)` 把仓库根加进去再 import 共享库;不强制装包。
53
+
54
+ ### 4. 可组合(Composable)
55
+ 默认输出是人类可读文本(中文,带 emoji 装饰),加 `--json` 输出**结构化 JSON**。
56
+ - 单脚本:`{"module": "<id>", "status": "ok|error", "data": {...}}`
57
+ - `bin/ocdiag run all --json` 输出 **NDJSON**(每行一个模块的 JSON),可以 `... | jq -s '.'` 聚合,或者 `... | jq 'select(.module=="cron_jobs") | .data'` 抽取。
58
+ **推论**:进度信息走 stderr,永远不污染 stdout 的 JSON 流。
59
+
60
+ ### 5. 数据可靠(Data Fidelity)
61
+ 脚本输出的每个数字、每个状态都必须能溯源:
62
+ - 系统数据 → `subprocess.run(["free","-m"])`、`/proc/<pid>/environ`、`journalctl ...`
63
+ - OpenClaw 数据 → `~/.openclaw/openclaw.json`、`~/.openclaw/cron/jobs.json`、`~/.openclaw/agents/*/sessions/*.jsonl`
64
+ - 日志数据 → `/tmp/openclaw/openclaw-*.log`(按 mtime 取今日)
65
+
66
+ 数据来源在文档里逐模块列清,不允许"看上去合理就行"。同一字段,文本输出和 JSON 输出必须**值一致**。
67
+
68
+ ### 6. 故障隔离(Failure Isolation)
69
+ - 单个模块崩溃**不能**带崩 `run all`:dispatcher 在 `runpy.run_path` 外包 try/except。
70
+ - 单个数据源缺失(配置不存在、日志没生成、session 文件被删)**不能**抛异常,要明确报告"未找到"。
71
+ - 不要 swallow 异常变 silent:失败要在 stderr 留 traceback,rc 非 0。
72
+
73
+ ---
74
+
75
+ ## 推导出的架构
76
+
77
+ ```
78
+ openclaw-diag-cli/
79
+ ├── ocdiag/ 共享原语(公理 #2 推论:库小而稳)
80
+ │ ├── paths.py 路径常量 + 环境变量覆盖
81
+ │ ├── jsonlog.py OpenClaw JSON 日志解析(公理 #5)
82
+ │ ├── timeutil.py ISO/epoch 时间转换 + 人类友好格式化
83
+ │ ├── tokens.py fmt_tokens / percentile / human_size
84
+ │ ├── sensitive.py 密钥/Token 脱敏(公理 #1 的延伸:输出也要安全)
85
+ │ ├── output.py 双模式输出(人类可读 + JSON)— 公理 #4 实现
86
+ │ ├── recent_logs.py 发现今日更新日志
87
+ │ ├── cli.py 公共 argparse(--config / --log-dir / --json)
88
+ │ └── dispatcher.py bin/ocdiag 复用的入口
89
+
90
+ ├── diag/ 诊断模块(公理 #3:每个能独立跑)
91
+ │ ├── 01_sys_health.py 系统健康(DNS/网络/CPU/内存/磁盘/IO/进程/时间同步)
92
+ │ ├── 02_environment.py OpenClaw 基础环境(版本一致性、Gateway 进程 env)
93
+ │ ├── 03_configuration.py openclaw.json 展平(脱敏后)
94
+ │ ├── 04_gateway.py Gateway 状态(WS 生命周期 + 错误码统一视图)
95
+ │ ├── 05_recent_errors.py 近期错误(多日志聚合 + journalctl + tool 错误)
96
+ │ ├── 06_cron_jobs.py 定时任务(jobs.json + state + runs/ 三源合并)
97
+ │ ├── 07_performance.py 模型/工具性能(慢调用 Top 20 / E2E 延迟 / Cache)
98
+ │ ├── 08_sessions.py Session 数据(六维分析 + Stuck 探测)
99
+ │ ├── 09_plugin_diag.py 插件诊断(一致性 + ERROR/WARN + Hook + Channel + DNS)
100
+ │ └── 10_shell_history.py Shell 历史(高危命令 + openclaw 命令)
101
+
102
+ ├── tools/ 单点深挖工具(不是采集,是分析特定对象)
103
+ │ ├── oc_session_trace.py 跟踪一条 user 消息从进入到响应的完整时间轴
104
+ │ └── oc_session_extract.py 把 session jsonl 导出为可读格式(含 reset/bak/deleted 全状态)
105
+
106
+ └── bin/
107
+ └── ocdiag 可选的总入口(list / run <id> / run all)
108
+ ```
109
+
110
+ ---
111
+
112
+ ## 数据来源(每条数据从哪里读)
113
+
114
+ 公理 #5 的具体落地——下游用任何字段都能查到它从哪来:
115
+
116
+ | 模块 | 数据来源 |
117
+ |---|---|
118
+ | 01_sys_health | `dig`/`getent`、`free -m`、`df -m`、`/proc/<pid>/limits`、`timedatectl` |
119
+ | 02_environment | `openclaw --version`、`/proc/<gw-pid>/environ`、`~/.config/systemd/user/openclaw-gateway.service.d/env.conf` |
120
+ | 03_configuration | `~/.openclaw/openclaw.json`(脱敏:key/secret/token/password 等关键词命中后 mask) |
121
+ | 04_gateway | `systemctl status` + `journalctl --since 24h` + `~/.openclaw/openclaw.json:gateway.port` + `/tmp/openclaw/openclaw-*.log`(subsystem 白名单过滤) |
122
+ | 05_recent_errors | 今日 `openclaw-*.log` 的 ERROR/FATAL + `journalctl --priority err` + 最近 session.jsonl 的 toolResult.isError |
123
+ | 06_cron_jobs | `~/.openclaw/cron/jobs.json` + `jobs-state.json` + `runs/<jobId>.jsonl`(合并三源) |
124
+ | 07_performance | 最近 20 个 `agents/*/sessions/*.jsonl`(含 reset 文件,按 mtime) |
125
+ | 08_sessions | 同上 + `/tmp/openclaw/openclaw-*.log` 中 subsystem=diagnostic 的 stuck-session 行 |
126
+ | 09_plugin_diag | 今日日志 `_meta.name` 解析 + `~/.openclaw/openclaw.json:plugins.entries` + `~/.openclaw/extensions/` + DNS 探测 |
127
+ | 10_shell_history | `~/.bash_history` + `~/.zsh_history` |
128
+ | oc_session_trace | session.jsonl + 同目录 `*.trajectory.jsonl`(可选) + Gateway 日志(可选) |
129
+ | oc_session_extract | session.jsonl + 兄弟文件 `.deleted` / `.reset.N` / `.bak-*` |
130
+
131
+ ---
132
+
133
+ ## 用法
134
+
135
+ ### 最小用法(独立脚本)
136
+
137
+ ```bash
138
+ git clone https://github.com/wujiaming88/openclaw-diag-cli.git
139
+ cd openclaw-diag-cli
140
+ python3 diag/04_gateway.py # 直接跑,零配置
141
+ python3 diag/04_gateway.py --json # 同样的数据,JSON 格式
142
+ ```
143
+
144
+ ### 总入口(可选)
145
+
146
+ ```bash
147
+ python3 bin/ocdiag list # 列出 10 个模块
148
+ python3 bin/ocdiag run gateway # 跑 04_gateway
149
+ python3 bin/ocdiag run all # 全部跑一遍(任一模块崩了不影响其他)
150
+ python3 bin/ocdiag run all --skip performance,sessions # 跳过重模块
151
+ ```
152
+
153
+ ### npm / npx 入口(同样支持上述全部参数)
154
+
155
+ ```bash
156
+ npx openclaw-diag-cli list
157
+ npx openclaw-diag-cli run gateway --json
158
+ npx openclaw-diag-cli run all --skip performance,sessions
159
+ npx openclaw-diag-cli doctor # 检查 Node/Python/ocdiag/OpenClaw
160
+ npx openclaw-diag-cli bundle 04_gateway > standalone-gateway.py
161
+ ```
162
+
163
+ ### JSON 管道(公理 #4 的真正用法)
164
+
165
+ ```bash
166
+ # 1) 单模块 JSON → jq 抽取关键字段
167
+ python3 diag/06_cron_jobs.py --json | jq '.data.jobs | length'
168
+
169
+ # 2) run all NDJSON → 聚合为单文档
170
+ python3 bin/ocdiag run all --json 2>/dev/null | jq -s '.' > report.json
171
+
172
+ # 3) 找出有错误的模块
173
+ python3 bin/ocdiag run all --json 2>/dev/null | jq 'select(.status=="error")'
174
+
175
+ # 4) 提取所有 cron 任务的成功率
176
+ python3 bin/ocdiag run all --json 2>/dev/null \
177
+ | jq 'select(.module=="cron_jobs") | .data.jobs[] | {name, success_rate}'
178
+ ```
179
+
180
+ ### 工具:单点深挖
181
+
182
+ ```bash
183
+ # 跟踪一条 user 消息的处理时间轴
184
+ python3 tools/oc_session_trace.py <session-uuid> --msg-index 0
185
+
186
+ # 导出 session 为可读格式
187
+ python3 tools/oc_session_extract.py <session-uuid> --summary
188
+ python3 tools/oc_session_extract.py <session-uuid> --types message --no-pretty
189
+ ```
190
+
191
+ ### 环境变量覆盖
192
+
193
+ 跑别人机器/容器时不用改代码,覆盖路径即可:
194
+
195
+ | 变量 | 默认值 | 说明 |
196
+ |---|---|---|
197
+ | `OPENCLAW_HOME` | `~/.openclaw` | OpenClaw 主目录 |
198
+ | `OPENCLAW_CONFIG` | `$OPENCLAW_HOME/openclaw.json` | 配置文件 |
199
+ | `OPENCLAW_LOG_DIR` | `/tmp/openclaw` | 日志目录 |
200
+ | `OPENCLAW_SESSIONS` | `$OPENCLAW_HOME/agents` | Session 根 |
201
+ | `OPENCLAW_SERVICE_FILE` | `~/.config/systemd/user/openclaw-gateway.service` | systemd 服务单元 |
202
+
203
+ 也可以用 `--config /path/to/openclaw.json --log-dir /path/to/logs` 覆盖单个参数。
204
+
205
+ ---
206
+
207
+ ## 退出码与错误隔离
208
+
209
+ | rc | 含义 |
210
+ |---|---|
211
+ | 0 | 模块成功,data 字段已填 |
212
+ | 1 | 模块运行成功但报告 `status: "error"`(数据源缺失等业务错误) |
213
+ | 2 | 单模块崩溃(dispatcher 已隔离,不影响其他模块) |
214
+
215
+ `bin/ocdiag run all` 的总 rc 取最大值;任一模块崩溃 stderr 留 traceback,但 stdout 流仍完整。
216
+
217
+ ---
218
+
219
+ ## 扩展:加一个新诊断模块
220
+
221
+ 遵循公理即可:
222
+
223
+ 1. 新建 `diag/11_my_check.py`,shebang `#!/usr/bin/env python3`
224
+ 2. 顶部 docstring 说明:**采集什么 + 数据来源 + 输出含义**
225
+ 3. `sys.path.insert(0, str(Path(__file__).resolve().parent.parent))` 接入共享库
226
+ 4. `from ocdiag import cli, output, paths` 拿到统一基础设施
227
+ 5. `parser = cli.build_common_parser(...); args = parser.parse_args()`
228
+ 6. `out = output.init("my_check", json_mode=args.json, ...)`
229
+ 7. 业务逻辑——文本输出用 `out.item / out.evidence / out.section`,JSON 数据用 `out.set_data("key", value)`
230
+ 8. 流式读 JSONL(`for line in open(...)`),不能 `.read().split('\n')`
231
+ 9. 子进程调用必须带 `timeout`
232
+ 10. 数据源缺失要明确报告"未找到",不抛异常
233
+
234
+ 注册到 `bin/ocdiag` 只需在 `ocdiag/dispatcher.py:MODULES` 列表加一行。
235
+
236
+ ---
237
+
238
+ ## 不做的事(反模式)
239
+
240
+ | 不做 | 原因 |
241
+ |---|---|
242
+ | 不写测试框架 | 优先靠 ground truth 对齐验证;测试以后补 |
243
+ | 不加 web UI / TUI / Rich | 公理 #2(零依赖)+ 公理 #4(管道友好)冲突 |
244
+ | 不需要 `pip install` | 公理 #2 + #3 |
245
+ | 不重启 / 不修改 / 不发请求 | 公理 #1 |
246
+ | 不强制配置 / 不强制 token | 任何节点 clone 即跑 |
247
+ | 不引入 jq 子进程 | Python 自带 json,更可控 |
248
+ | 不内嵌 Python 在 bash heredoc 里 | 这就是我们要替代的旧形态 |
249
+
250
+ ---
251
+
252
+ ## 来历
253
+
254
+ 由 4391 行的 `openclaw-diag.sh`(10 个 bash 模块 + 10 段 heredoc Python)拆分重写。原脚本仍在维护,作为"打包采集 + 远程发送报告"的 all-in-one 用例存在;本仓库面向"模块化、自动化、可推理"的诊断场景。
255
+
256
+ ---
257
+
258
+ ## License
259
+
260
+ MIT
package/bin/ocdiag ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env python3
2
+ """ocdiag entry-point shim that runs the dispatcher from the repo root."""
3
+
4
+ import os
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ _REPO_ROOT = Path(__file__).resolve().parent.parent
9
+ sys.path.insert(0, str(_REPO_ROOT))
10
+
11
+ from ocdiag.dispatcher import main
12
+
13
+ if __name__ == "__main__":
14
+ sys.exit(main())
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env node
2
+ // openclaw-diag — Node entry shell.
3
+ // Locates python3, forwards args to ocdiag.dispatcher, transparently passes stdio
4
+ // and exit code. Implements two Node-side commands (doctor, bundle dispatch,
5
+ // --version, --help) so the user gets useful UX even before Python runs.
6
+
7
+ 'use strict';
8
+
9
+ const { spawnSync, spawn } = require('child_process');
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+
13
+ const REPO_ROOT = path.resolve(__dirname, '..');
14
+ const PKG = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8'));
15
+
16
+ const PYTHON_CANDIDATES = process.platform === 'win32'
17
+ ? ['python3', 'python', 'py']
18
+ : ['python3', 'python'];
19
+
20
+ function findPython() {
21
+ for (const cmd of PYTHON_CANDIDATES) {
22
+ try {
23
+ const r = spawnSync(cmd, ['--version'], { stdio: ['ignore', 'pipe', 'pipe'] });
24
+ if (r.status === 0) {
25
+ const out = ((r.stdout || '') + (r.stderr || '')).trim();
26
+ const m = out.match(/Python\s+(\d+)\.(\d+)\.(\d+)/);
27
+ if (m) {
28
+ const major = parseInt(m[1], 10);
29
+ const minor = parseInt(m[2], 10);
30
+ if (major > 3 || (major === 3 && minor >= 8)) {
31
+ return { cmd, version: `${m[1]}.${m[2]}.${m[3]}` };
32
+ }
33
+ }
34
+ }
35
+ } catch (_) {
36
+ // try next
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+
42
+ function pythonNotFound() {
43
+ console.error('Error: Python 3.8+ required but not found.');
44
+ console.error(' Install: https://www.python.org/downloads/ or apt install python3');
45
+ process.exit(127);
46
+ }
47
+
48
+ function printVersion() {
49
+ console.log(PKG.version);
50
+ }
51
+
52
+ function printHelp() {
53
+ const lines = [
54
+ 'openclaw-diag — OpenClaw / ArkClaw read-only diagnostic CLI',
55
+ '',
56
+ 'Usage:',
57
+ ' openclaw-diag Show banner + module list',
58
+ ' openclaw-diag list List all diagnostic modules',
59
+ ' openclaw-diag run <id> Run a single module (or "all")',
60
+ ' openclaw-diag run all [--skip a,b] Run all modules (skip optional)',
61
+ ' openclaw-diag run <id> --json Emit JSON (NDJSON for "all")',
62
+ ' openclaw-diag bundle <id> Print self-contained single-file .py to stdout',
63
+ ' openclaw-diag doctor [--json] Check Node / Python / ocdiag / OpenClaw env',
64
+ ' openclaw-diag --version Print package version',
65
+ ' openclaw-diag --help Print this help',
66
+ '',
67
+ 'Module ids: sys_health environment configuration gateway recent_errors',
68
+ ' cron_jobs performance sessions plugin_diag shell_history',
69
+ '',
70
+ 'Pass-through flags (forwarded to Python): --config --log-dir --json --no-color',
71
+ ];
72
+ console.log(lines.join('\n'));
73
+ }
74
+
75
+ function runDispatcher(args) {
76
+ const py = findPython();
77
+ if (!py) pythonNotFound();
78
+ const dispatcher = path.join(REPO_ROOT, 'bin', 'ocdiag');
79
+ const child = spawn(py.cmd, [dispatcher, ...args], { stdio: 'inherit' });
80
+ child.on('error', (err) => {
81
+ console.error(`Error: failed to spawn ${py.cmd}: ${err.message}`);
82
+ process.exit(1);
83
+ });
84
+ child.on('exit', (code, signal) => {
85
+ if (signal) {
86
+ process.kill(process.pid, signal);
87
+ return;
88
+ }
89
+ process.exit(code == null ? 1 : code);
90
+ });
91
+ }
92
+
93
+ function runBundle(args) {
94
+ if (args.length === 0) {
95
+ console.error('Error: bundle requires a module id (e.g. `openclaw-diag bundle gateway`)');
96
+ process.exit(2);
97
+ }
98
+ const py = findPython();
99
+ if (!py) pythonNotFound();
100
+ const bundleScript = path.join(REPO_ROOT, 'lib', 'bundle.py');
101
+ const child = spawn(py.cmd, [bundleScript, ...args], { stdio: 'inherit' });
102
+ child.on('error', (err) => {
103
+ console.error(`Error: failed to spawn ${py.cmd}: ${err.message}`);
104
+ process.exit(1);
105
+ });
106
+ child.on('exit', (code, signal) => {
107
+ if (signal) {
108
+ process.kill(process.pid, signal);
109
+ return;
110
+ }
111
+ process.exit(code == null ? 1 : code);
112
+ });
113
+ }
114
+
115
+ // ── doctor ──
116
+
117
+ const DIAG_SCRIPTS = [
118
+ '01_sys_health.py', '02_environment.py', '03_configuration.py',
119
+ '04_gateway.py', '05_recent_errors.py', '06_cron_jobs.py',
120
+ '07_performance.py', '08_sessions.py', '09_plugin_diag.py',
121
+ '10_shell_history.py',
122
+ ];
123
+
124
+ function nodeVersionOk() {
125
+ const m = process.versions.node.match(/^(\d+)\./);
126
+ return m && parseInt(m[1], 10) >= 18;
127
+ }
128
+
129
+ function checkOcdiagImport(pyCmd) {
130
+ const r = spawnSync(
131
+ pyCmd,
132
+ ['-c', 'import sys, os; sys.path.insert(0, os.environ["OCDIAG_REPO_ROOT"]); import ocdiag; print(ocdiag.__version__)'],
133
+ {
134
+ stdio: ['ignore', 'pipe', 'pipe'],
135
+ env: { ...process.env, OCDIAG_REPO_ROOT: REPO_ROOT },
136
+ },
137
+ );
138
+ if (r.status === 0) {
139
+ return { ok: true, version: (r.stdout || '').toString().trim() };
140
+ }
141
+ return { ok: false, error: ((r.stderr || '') + (r.stdout || '')).toString().trim() };
142
+ }
143
+
144
+ function checkDiagScripts(pyCmd) {
145
+ const failed = [];
146
+ for (const name of DIAG_SCRIPTS) {
147
+ const p = path.join(REPO_ROOT, 'diag', name);
148
+ const r = spawnSync(pyCmd, [p, '--help'], {
149
+ stdio: ['ignore', 'pipe', 'pipe'],
150
+ timeout: 10000,
151
+ });
152
+ if (r.status !== 0) {
153
+ failed.push({ script: name, status: r.status, stderr: ((r.stderr || '').toString().trim()).slice(0, 200) });
154
+ }
155
+ }
156
+ return failed;
157
+ }
158
+
159
+ function checkOpenclawConfig() {
160
+ const home = process.env.HOME || require('os').homedir();
161
+ const cfg = process.env.OPENCLAW_CONFIG
162
+ || path.join(process.env.OPENCLAW_HOME || path.join(home, '.openclaw'), 'openclaw.json');
163
+ return { path: cfg, exists: fs.existsSync(cfg) };
164
+ }
165
+
166
+ function runDoctor(args) {
167
+ const jsonMode = args.includes('--json');
168
+ const result = {
169
+ node: { version: process.versions.node, ok: nodeVersionOk() },
170
+ python: null,
171
+ ocdiag: null,
172
+ diag_scripts: null,
173
+ openclaw_config: null,
174
+ };
175
+
176
+ const py = findPython();
177
+ if (!py) {
178
+ result.python = { ok: false, error: 'Python 3.8+ not found in PATH' };
179
+ if (jsonMode) {
180
+ console.log(JSON.stringify(result, null, 2));
181
+ } else {
182
+ console.log(`✓ Node v${result.node.version}${result.node.ok ? '' : ' (need >= 18)'}`);
183
+ console.log('✗ Python 3.8+ not found in PATH');
184
+ console.log(' Install: https://www.python.org/downloads/ or apt install python3');
185
+ }
186
+ process.exit(1);
187
+ }
188
+ result.python = { ok: true, version: py.version, cmd: py.cmd };
189
+
190
+ const ocdiag = checkOcdiagImport(py.cmd);
191
+ result.ocdiag = ocdiag;
192
+
193
+ const failed = checkDiagScripts(py.cmd);
194
+ result.diag_scripts = {
195
+ ok: failed.length === 0,
196
+ total: DIAG_SCRIPTS.length,
197
+ failed,
198
+ };
199
+
200
+ const cfg = checkOpenclawConfig();
201
+ result.openclaw_config = cfg;
202
+
203
+ if (jsonMode) {
204
+ console.log(JSON.stringify(result, null, 2));
205
+ } else {
206
+ console.log(`${result.node.ok ? '✓' : '✗'} Node v${result.node.version}${result.node.ok ? '' : ' (need >= 18)'}`);
207
+ console.log(`✓ Python ${py.version} (${py.cmd})`);
208
+ if (ocdiag.ok) {
209
+ console.log(`✓ ocdiag package importable (version ${ocdiag.version})`);
210
+ } else {
211
+ console.log('✗ ocdiag package not importable');
212
+ if (ocdiag.error) {
213
+ console.log(' ' + ocdiag.error.split('\n').slice(-3).join(' | '));
214
+ }
215
+ }
216
+ if (failed.length === 0) {
217
+ console.log(`✓ All ${DIAG_SCRIPTS.length} diag modules respond to --help`);
218
+ } else {
219
+ console.log(`✗ ${failed.length}/${DIAG_SCRIPTS.length} diag modules failed --help:`);
220
+ for (const f of failed) {
221
+ console.log(` ${f.script} (rc=${f.status})`);
222
+ }
223
+ }
224
+ if (cfg.exists) {
225
+ console.log(`✓ OpenClaw config present (${cfg.path})`);
226
+ } else {
227
+ console.log(`ℹ OpenClaw config not found (${cfg.path}) — diagnostics will run but report missing`);
228
+ }
229
+ }
230
+
231
+ const ok = result.node.ok && result.python.ok && ocdiag.ok && failed.length === 0;
232
+ process.exit(ok ? 0 : 1);
233
+ }
234
+
235
+ // ── main ──
236
+
237
+ function main() {
238
+ const argv = process.argv.slice(2);
239
+
240
+ if (argv.length === 0) {
241
+ console.log(`openclaw-diag v${PKG.version} — OpenClaw / ArkClaw 诊断 CLI`);
242
+ console.log('');
243
+ console.log(' npx openclaw-diag-cli list 列出所有诊断模块');
244
+ console.log(' npx openclaw-diag-cli run <id> 运行单个模块(或 all)');
245
+ console.log(' npx openclaw-diag-cli doctor 检查环境是否就绪');
246
+ console.log(' npx openclaw-diag-cli --help 查看完整帮助');
247
+ console.log('');
248
+ runDispatcher(['list']);
249
+ return;
250
+ }
251
+
252
+ const head = argv[0];
253
+
254
+ if (head === '--version' || head === '-v') {
255
+ printVersion();
256
+ process.exit(0);
257
+ }
258
+ if (head === '--help' || head === '-h') {
259
+ printHelp();
260
+ process.exit(0);
261
+ }
262
+ if (head === 'doctor') {
263
+ runDoctor(argv.slice(1));
264
+ return;
265
+ }
266
+ if (head === 'bundle') {
267
+ runBundle(argv.slice(1));
268
+ return;
269
+ }
270
+
271
+ // Pass through everything else to the Python dispatcher.
272
+ runDispatcher(argv);
273
+ }
274
+
275
+ main();