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 +21 -0
- package/README.md +260 -0
- package/bin/ocdiag +14 -0
- package/bin/openclaw-diag.js +275 -0
- package/diag/01_sys_health.py +443 -0
- package/diag/02_environment.py +292 -0
- package/diag/03_configuration.py +131 -0
- package/diag/04_gateway.py +651 -0
- package/diag/05_recent_errors.py +246 -0
- package/diag/06_cron_jobs.py +694 -0
- package/diag/07_performance.py +687 -0
- package/diag/08_sessions.py +518 -0
- package/diag/09_plugin_diag.py +535 -0
- package/diag/10_shell_history.py +121 -0
- package/diag/__init__.py +0 -0
- package/lib/bundle.py +204 -0
- package/ocdiag/__init__.py +3 -0
- package/ocdiag/cli.py +39 -0
- package/ocdiag/dispatcher.py +137 -0
- package/ocdiag/jsonlog.py +65 -0
- package/ocdiag/output.py +131 -0
- package/ocdiag/paths.py +48 -0
- package/ocdiag/recent_logs.py +53 -0
- package/ocdiag/sensitive.py +41 -0
- package/ocdiag/timeutil.py +77 -0
- package/ocdiag/tokens.py +46 -0
- package/package.json +42 -0
- package/tools/__init__.py +0 -0
- package/tools/oc_session_extract.py +254 -0
- package/tools/oc_session_trace.py +715 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""模块 2:基础环境(版本一致性、Gateway 进程、端口、环境变量)。"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import shlex
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
17
|
+
|
|
18
|
+
from ocdiag import cli, output, paths
|
|
19
|
+
from ocdiag.sensitive import safe_val
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run(cmd, timeout=5):
|
|
23
|
+
try:
|
|
24
|
+
r = subprocess.run(
|
|
25
|
+
cmd, shell=isinstance(cmd, str), capture_output=True,
|
|
26
|
+
text=True, timeout=timeout, check=False,
|
|
27
|
+
)
|
|
28
|
+
return r.returncode, r.stdout, r.stderr
|
|
29
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
30
|
+
return 1, "", ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def detect_oc_version() -> Optional[str]:
|
|
34
|
+
rc, stdout, _ = run(["openclaw", "--version"])
|
|
35
|
+
if rc == 0 and stdout:
|
|
36
|
+
return stdout.splitlines()[0].strip()
|
|
37
|
+
|
|
38
|
+
home = os.path.expanduser("~")
|
|
39
|
+
pnpm_root = os.path.join(home, ".local", "share", "pnpm")
|
|
40
|
+
if os.path.isdir(pnpm_root):
|
|
41
|
+
for root, _, files in os.walk(pnpm_root):
|
|
42
|
+
if "package.json" in files and root.endswith(os.sep + "openclaw"):
|
|
43
|
+
try:
|
|
44
|
+
with open(os.path.join(root, "package.json")) as f:
|
|
45
|
+
return json.load(f).get("version")
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
pkg = "/usr/lib/node_modules/openclaw/package.json"
|
|
50
|
+
if os.path.isfile(pkg):
|
|
51
|
+
try:
|
|
52
|
+
with open(pkg) as f:
|
|
53
|
+
return json.load(f).get("version")
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def detect_node_version() -> Optional[str]:
|
|
60
|
+
rc, stdout, _ = run(["node", "--version"])
|
|
61
|
+
if rc == 0 and stdout:
|
|
62
|
+
return stdout.strip()
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def gateway_systemctl_status() -> str:
|
|
67
|
+
_, stdout, _ = run(["systemctl", "--user", "status", "openclaw-gateway"])
|
|
68
|
+
return stdout
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def gateway_pid() -> Optional[str]:
|
|
72
|
+
_, stdout, _ = run(["pgrep", "-f", "openclaw.*gateway"])
|
|
73
|
+
pid = stdout.splitlines()[0].strip() if stdout else ""
|
|
74
|
+
if pid:
|
|
75
|
+
return pid
|
|
76
|
+
rc, stdout, _ = run(["systemctl", "--user", "show", "openclaw-gateway.service",
|
|
77
|
+
"--property=MainPID"])
|
|
78
|
+
if rc == 0 and "=" in stdout:
|
|
79
|
+
v = stdout.strip().split("=", 1)[1]
|
|
80
|
+
if v and v != "0":
|
|
81
|
+
return v
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def parse_proc_environ(pid: str) -> Optional[list]:
|
|
86
|
+
p = f"/proc/{pid}/environ"
|
|
87
|
+
if not os.access(p, os.R_OK):
|
|
88
|
+
return None
|
|
89
|
+
try:
|
|
90
|
+
with open(p, "rb") as f:
|
|
91
|
+
raw = f.read()
|
|
92
|
+
except (PermissionError, FileNotFoundError, OSError):
|
|
93
|
+
return None
|
|
94
|
+
pairs = []
|
|
95
|
+
for entry in raw.split(b"\x00"):
|
|
96
|
+
if not entry:
|
|
97
|
+
continue
|
|
98
|
+
try:
|
|
99
|
+
s = entry.decode("utf-8", errors="replace")
|
|
100
|
+
except Exception:
|
|
101
|
+
continue
|
|
102
|
+
eq = s.find("=")
|
|
103
|
+
if eq <= 0:
|
|
104
|
+
continue
|
|
105
|
+
k, v = s[:eq], s[eq + 1:]
|
|
106
|
+
pairs.append((k, safe_val(k, v)))
|
|
107
|
+
return sorted(pairs)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def main() -> int:
|
|
111
|
+
parser = cli.build_common_parser(
|
|
112
|
+
description="模块 2:采集 OpenClaw 基础环境",
|
|
113
|
+
prog="02_environment",
|
|
114
|
+
)
|
|
115
|
+
args = parser.parse_args()
|
|
116
|
+
out = output.init("environment", json_mode=args.json, no_color=args.no_color)
|
|
117
|
+
out.section("模块 2:基础环境")
|
|
118
|
+
|
|
119
|
+
oc_version = detect_oc_version()
|
|
120
|
+
if oc_version:
|
|
121
|
+
out.item(f"ArkClaw 版本: {oc_version}")
|
|
122
|
+
else:
|
|
123
|
+
out.item("ArkClaw 版本: 无法确定")
|
|
124
|
+
out.evidence("openclaw --version", "命令未找到或无输出")
|
|
125
|
+
out.set_data("oc_version", oc_version)
|
|
126
|
+
|
|
127
|
+
service_file = paths.SERVICE_FILE
|
|
128
|
+
svc_version = None
|
|
129
|
+
if oc_version and os.path.isfile(service_file):
|
|
130
|
+
try:
|
|
131
|
+
with open(service_file) as f:
|
|
132
|
+
m = re.search(r"openclaw@([0-9]+\.[0-9]+\.[0-9]+)", f.read())
|
|
133
|
+
if m:
|
|
134
|
+
svc_version = m.group(1)
|
|
135
|
+
except OSError:
|
|
136
|
+
pass
|
|
137
|
+
if svc_version:
|
|
138
|
+
cli_clean = re.search(r"[0-9]+\.[0-9]+\.[0-9]+", oc_version)
|
|
139
|
+
cli_clean = cli_clean.group(0) if cli_clean else ""
|
|
140
|
+
if cli_clean and cli_clean != svc_version:
|
|
141
|
+
out.item(f"版本不一致: CLI={cli_clean} vs Gateway service={svc_version}")
|
|
142
|
+
out.item(" 原因: pnpm 升级后 service 文件未重生,Gateway 实际跑的是旧版本")
|
|
143
|
+
out.item(" 修复: 在目标机器上执行 `openclaw gateway install --force` 然后 `openclaw gateway restart`")
|
|
144
|
+
else:
|
|
145
|
+
out.item(f"版本一致: CLI={cli_clean} = service={svc_version}")
|
|
146
|
+
out.set_data("service_version", svc_version)
|
|
147
|
+
|
|
148
|
+
node_ver = detect_node_version()
|
|
149
|
+
if node_ver:
|
|
150
|
+
major = node_ver.lstrip("v").split(".", 1)[0]
|
|
151
|
+
out.item(f"Node.js 版本: {node_ver} (major: {major})")
|
|
152
|
+
else:
|
|
153
|
+
out.item("Node.js: 未找到")
|
|
154
|
+
out.evidence("node --version", "命令未找到")
|
|
155
|
+
out.set_data("node_version", node_ver)
|
|
156
|
+
|
|
157
|
+
rc, stdout, _ = run(["free", "-m"])
|
|
158
|
+
mem_avail = ""
|
|
159
|
+
if rc == 0:
|
|
160
|
+
for line in stdout.splitlines():
|
|
161
|
+
if line.startswith("Mem:"):
|
|
162
|
+
parts = line.split()
|
|
163
|
+
if len(parts) >= 7:
|
|
164
|
+
mem_avail = parts[6]
|
|
165
|
+
break
|
|
166
|
+
if mem_avail:
|
|
167
|
+
out.item(f"可用内存: {mem_avail} MB")
|
|
168
|
+
out.set_data("memory_available_mb", mem_avail)
|
|
169
|
+
|
|
170
|
+
rc, stdout, _ = run(["df", "-m", paths.OPENCLAW_HOME])
|
|
171
|
+
disk_avail = ""
|
|
172
|
+
if rc == 0:
|
|
173
|
+
lines = stdout.splitlines()
|
|
174
|
+
if len(lines) >= 2:
|
|
175
|
+
parts = lines[1].split()
|
|
176
|
+
if len(parts) >= 4:
|
|
177
|
+
disk_avail = parts[3]
|
|
178
|
+
if disk_avail:
|
|
179
|
+
out.item(f"磁盘可用 ({paths.OPENCLAW_HOME}): {disk_avail} MB")
|
|
180
|
+
out.set_data("disk_available_mb", disk_avail)
|
|
181
|
+
|
|
182
|
+
gw_status = gateway_systemctl_status()
|
|
183
|
+
if gw_status:
|
|
184
|
+
active_state = ""
|
|
185
|
+
main_pid = ""
|
|
186
|
+
since = ""
|
|
187
|
+
m = re.search(r"Active:\s+(\S+)", gw_status)
|
|
188
|
+
if m:
|
|
189
|
+
active_state = m.group(1)
|
|
190
|
+
m = re.search(r"Main PID:\s+(\d+)", gw_status)
|
|
191
|
+
if m:
|
|
192
|
+
main_pid = m.group(1)
|
|
193
|
+
m = re.search(r"since\s+(.*)", gw_status)
|
|
194
|
+
if m:
|
|
195
|
+
since = m.group(1).splitlines()[0].strip()
|
|
196
|
+
if active_state == "active":
|
|
197
|
+
out.item(f"Gateway 服务: 运行中 (PID {main_pid or '?'}, since {since or '?'})")
|
|
198
|
+
else:
|
|
199
|
+
out.item(f"Gateway 服务: {active_state or 'unknown'}")
|
|
200
|
+
out.evidence("systemctl --user status openclaw-gateway", "\n".join(gw_status.splitlines()[:5]))
|
|
201
|
+
out.set_data("gateway_state", active_state)
|
|
202
|
+
out.set_data("gateway_main_pid", main_pid)
|
|
203
|
+
else:
|
|
204
|
+
_, pids, _ = run(["pgrep", "-f", "openclaw-gatewa"])
|
|
205
|
+
pids_clean = " ".join(pids.splitlines()[:5]) if pids else ""
|
|
206
|
+
if pids_clean:
|
|
207
|
+
out.item(f"Gateway 进程: 已发现 (PIDs: {pids_clean})")
|
|
208
|
+
else:
|
|
209
|
+
out.item("Gateway 进程: 未通过 systemctl 或 pgrep 检测到")
|
|
210
|
+
out.evidence("pgrep -f openclaw-gatewa", "无输出")
|
|
211
|
+
out.set_data("gateway_pids", pids_clean.split() if pids_clean else [])
|
|
212
|
+
|
|
213
|
+
port = 18789
|
|
214
|
+
if os.path.isfile(args.config):
|
|
215
|
+
try:
|
|
216
|
+
with open(args.config) as f:
|
|
217
|
+
cfg = json.load(f)
|
|
218
|
+
cfg_port = cfg.get("gateway", {}).get("port")
|
|
219
|
+
if cfg_port:
|
|
220
|
+
port = int(cfg_port)
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
223
|
+
rc, stdout, _ = run(["ss", "-tlnp"])
|
|
224
|
+
listening = any(f":{port} " in ln for ln in stdout.splitlines()) if rc == 0 else False
|
|
225
|
+
if listening:
|
|
226
|
+
out.item(f"端口 {port}: 监听中")
|
|
227
|
+
else:
|
|
228
|
+
out.item(f"端口 {port}: 未监听")
|
|
229
|
+
out.evidence(f"ss -tlnp | grep :{port}", "<无输出>")
|
|
230
|
+
out.set_data("port", port)
|
|
231
|
+
out.set_data("port_listening", listening)
|
|
232
|
+
|
|
233
|
+
out.line("")
|
|
234
|
+
out.line(" ── Gateway 进程环境变量(OpenClaw 实际运行环境) ──")
|
|
235
|
+
out.line("")
|
|
236
|
+
pid = gateway_pid()
|
|
237
|
+
env_pairs = parse_proc_environ(pid) if pid else None
|
|
238
|
+
if pid and env_pairs is not None:
|
|
239
|
+
out.item(f"Gateway PID: {pid}")
|
|
240
|
+
out.line("")
|
|
241
|
+
out.item(f"共 {len(env_pairs)} 个环境变量")
|
|
242
|
+
out.line("")
|
|
243
|
+
for k, v in env_pairs:
|
|
244
|
+
out.item(f"{k} = {v}")
|
|
245
|
+
out.set_data("gateway_env", [{"key": k, "value": v} for k, v in env_pairs])
|
|
246
|
+
elif pid:
|
|
247
|
+
out.item(f"无法读取 /proc/{pid}/environ(权限不足?)")
|
|
248
|
+
else:
|
|
249
|
+
out.item("Gateway 进程未运行,跳过")
|
|
250
|
+
|
|
251
|
+
if os.path.isfile(paths.SERVICE_ENV_FILE):
|
|
252
|
+
out.line("")
|
|
253
|
+
out.line(f" ── Systemd 服务环境变量配置 ({paths.SERVICE_ENV_FILE}) ──")
|
|
254
|
+
out.line("")
|
|
255
|
+
try:
|
|
256
|
+
with open(paths.SERVICE_ENV_FILE) as f:
|
|
257
|
+
for line in f:
|
|
258
|
+
line = line.strip()
|
|
259
|
+
if not line or line.startswith("#") or line.startswith("["):
|
|
260
|
+
continue
|
|
261
|
+
if line.startswith("Environment="):
|
|
262
|
+
raw = line[len("Environment="):].strip()
|
|
263
|
+
# systemd 支持单行多键:Environment="A=1" "B=2" 或 Environment=A=1 B=2
|
|
264
|
+
try:
|
|
265
|
+
tokens = shlex.split(raw, posix=True)
|
|
266
|
+
except ValueError:
|
|
267
|
+
tokens = [raw]
|
|
268
|
+
for tok in tokens:
|
|
269
|
+
if "=" not in tok:
|
|
270
|
+
out.item(tok)
|
|
271
|
+
continue
|
|
272
|
+
key, val = tok.split("=", 1)
|
|
273
|
+
out.item(f"{key} = {safe_val(key, val)}")
|
|
274
|
+
except OSError as e:
|
|
275
|
+
out.item(f"读取失败: {e}")
|
|
276
|
+
|
|
277
|
+
if os.path.isfile(service_file):
|
|
278
|
+
out.line("")
|
|
279
|
+
out.line(f" ── Systemd 服务文件 ({service_file}) ──")
|
|
280
|
+
out.line("")
|
|
281
|
+
try:
|
|
282
|
+
with open(service_file) as f:
|
|
283
|
+
for line in f:
|
|
284
|
+
out.item(line.rstrip("\n"))
|
|
285
|
+
except OSError:
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
return out.done()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
if __name__ == "__main__":
|
|
292
|
+
sys.exit(main())
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""模块 3:采集 openclaw.json 配置(含敏感字段脱敏,模型 providers 折叠)。"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
13
|
+
|
|
14
|
+
from ocdiag import cli, output
|
|
15
|
+
from ocdiag.sensitive import is_sensitive_key, mask
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_INSTALLS_RE = re.compile(r"^plugins\.installs\.[^.]+\.([^.]+)$")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def format_value(val):
|
|
22
|
+
if isinstance(val, str):
|
|
23
|
+
return val if val else '""'
|
|
24
|
+
if isinstance(val, bool):
|
|
25
|
+
return "true" if val else "false"
|
|
26
|
+
if val is None:
|
|
27
|
+
return "null"
|
|
28
|
+
return str(val)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def emit_config(out: output.Output, data: list, obj, prefix: str = "") -> None:
|
|
32
|
+
if isinstance(obj, dict):
|
|
33
|
+
if not obj:
|
|
34
|
+
data.append(f"{prefix} = {{}}")
|
|
35
|
+
return
|
|
36
|
+
for k, v in obj.items():
|
|
37
|
+
new_key = f"{prefix}.{k}" if prefix else k
|
|
38
|
+
m = _INSTALLS_RE.match(new_key)
|
|
39
|
+
if m and m.group(1) not in ("source", "version", "installPath"):
|
|
40
|
+
continue
|
|
41
|
+
if new_key.startswith("models.providers.") and k == "models" and isinstance(v, list):
|
|
42
|
+
for md in v:
|
|
43
|
+
if isinstance(md, dict):
|
|
44
|
+
mid = md.get("id", "?")
|
|
45
|
+
cw = md.get("contextWindow", "?")
|
|
46
|
+
mt = md.get("maxTokens", "?")
|
|
47
|
+
r_str = "true" if md.get("reasoning", False) else "false"
|
|
48
|
+
data.append(
|
|
49
|
+
f"{new_key}.{mid} = (contextWindow={cw}, "
|
|
50
|
+
f"maxTokens={mt}, reasoning={r_str})"
|
|
51
|
+
)
|
|
52
|
+
continue
|
|
53
|
+
emit_config(out, data, v, new_key)
|
|
54
|
+
elif isinstance(obj, list):
|
|
55
|
+
if not obj:
|
|
56
|
+
data.append(f"{prefix} = []")
|
|
57
|
+
return
|
|
58
|
+
all_scalar = all(isinstance(x, (str, int, float, bool)) for x in obj)
|
|
59
|
+
if all_scalar:
|
|
60
|
+
n = len(obj)
|
|
61
|
+
if n <= 5:
|
|
62
|
+
data.append(f"{prefix} = [{n} 项]: {', '.join(str(x) for x in obj)}")
|
|
63
|
+
else:
|
|
64
|
+
data.append(f"{prefix} = [{n} 项]: {', '.join(str(x) for x in obj[:3])}, ... (完整列表省略)")
|
|
65
|
+
else:
|
|
66
|
+
for i, v in enumerate(obj):
|
|
67
|
+
emit_config(out, data, v, f"{prefix}[{i}]")
|
|
68
|
+
else:
|
|
69
|
+
if is_sensitive_key(prefix):
|
|
70
|
+
if isinstance(obj, str) and obj:
|
|
71
|
+
display = mask(obj)
|
|
72
|
+
elif isinstance(obj, (int, float, bool)):
|
|
73
|
+
display = str(obj)
|
|
74
|
+
elif obj is None:
|
|
75
|
+
display = "null"
|
|
76
|
+
else:
|
|
77
|
+
display = "****"
|
|
78
|
+
else:
|
|
79
|
+
display = format_value(obj)
|
|
80
|
+
if len(str(display)) > 500:
|
|
81
|
+
display = str(display)[:500] + "..."
|
|
82
|
+
data.append(f"{prefix} = {display}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main() -> int:
|
|
86
|
+
parser = cli.build_common_parser(
|
|
87
|
+
description="模块 3:采集 OpenClaw 配置(含敏感字段脱敏)",
|
|
88
|
+
prog="03_configuration",
|
|
89
|
+
)
|
|
90
|
+
args = parser.parse_args()
|
|
91
|
+
|
|
92
|
+
out = output.init("configuration", json_mode=args.json, no_color=args.no_color)
|
|
93
|
+
out.section("模块 3:配置")
|
|
94
|
+
|
|
95
|
+
config_path = args.config
|
|
96
|
+
if not os.path.isfile(config_path):
|
|
97
|
+
out.item(f"配置文件未找到: {config_path}")
|
|
98
|
+
out.evidence(config_path, "<文件缺失>")
|
|
99
|
+
out.set_data("config_path", config_path)
|
|
100
|
+
out.set_data("found", False)
|
|
101
|
+
out.fail("配置文件未找到")
|
|
102
|
+
return out.done()
|
|
103
|
+
|
|
104
|
+
out.set_data("config_path", config_path)
|
|
105
|
+
out.set_data("found", True)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
with open(config_path) as f:
|
|
109
|
+
config = json.load(f)
|
|
110
|
+
out.item("JSON 语法: 有效")
|
|
111
|
+
out.set_data("json_valid", True)
|
|
112
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
113
|
+
out.item("JSON 语法: 无效")
|
|
114
|
+
out.evidence(config_path, str(e))
|
|
115
|
+
out.set_data("json_valid", False)
|
|
116
|
+
out.set_data("parse_error", str(e))
|
|
117
|
+
out.fail("配置 JSON 无效")
|
|
118
|
+
return out.done()
|
|
119
|
+
|
|
120
|
+
out.line("")
|
|
121
|
+
flat: list = []
|
|
122
|
+
emit_config(out, flat, config)
|
|
123
|
+
for line in flat:
|
|
124
|
+
out.item(line)
|
|
125
|
+
out.set_data("flattened", flat)
|
|
126
|
+
|
|
127
|
+
return out.done()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if __name__ == "__main__":
|
|
131
|
+
sys.exit(main())
|