openclaw-tools 0.2.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/LICENSE +21 -0
- package/README.md +239 -0
- package/README.md.bak +247 -0
- package/README.openclaw-cli.md +239 -0
- package/README.openclaw-daemon.md +239 -0
- package/README.openclaw-gateway.md +239 -0
- package/README.openclaw-health.md +239 -0
- package/README.openclaw-helper.md +239 -0
- package/README.openclaw-install.md +239 -0
- package/README.openclaw-manage.md +239 -0
- package/README.openclaw-monitor.md +239 -0
- package/README.openclaw-run.md +239 -0
- package/README.openclaw-service.md +239 -0
- package/README.openclaw-setup.md +239 -0
- package/README.openclaw-start.md +239 -0
- package/README.openclaw-tools.md +239 -0
- package/README.openclaw-utils.md +239 -0
- package/README.openclaw-watch.md +239 -0
- package/README.zh-CN.md +213 -0
- package/dist/chunk-FUWKRMIF.js +470 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +567 -0
- package/dist/server-PP5Y3LAF.js +7 -0
- package/package.json +51 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/dashboard/server.ts
|
|
10
|
+
import { createServer } from "http";
|
|
11
|
+
import chalk2 from "chalk";
|
|
12
|
+
|
|
13
|
+
// src/config.ts
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
15
|
+
import { join as join2, resolve } from "path";
|
|
16
|
+
|
|
17
|
+
// src/brand.ts
|
|
18
|
+
import { basename } from "path";
|
|
19
|
+
import { homedir } from "os";
|
|
20
|
+
import { join } from "path";
|
|
21
|
+
var bin = basename(process.argv[1] ?? "openclaw-doctor").replace(/\.[cm]?js$/, "");
|
|
22
|
+
var KNOWN_BINS = [
|
|
23
|
+
"openclaw-cli",
|
|
24
|
+
"openclaw-manage",
|
|
25
|
+
"openclaw-doctor",
|
|
26
|
+
"openclaw-service",
|
|
27
|
+
"openclaw-daemon",
|
|
28
|
+
"openclaw-monitor",
|
|
29
|
+
"openclaw-helper",
|
|
30
|
+
"openclaw-tools",
|
|
31
|
+
"openclaw-utils",
|
|
32
|
+
"openclaw-gateway",
|
|
33
|
+
"openclaw-setup",
|
|
34
|
+
"openclaw-install",
|
|
35
|
+
"openclaw-run",
|
|
36
|
+
"openclaw-start",
|
|
37
|
+
"openclaw-watch",
|
|
38
|
+
"openclaw-health"
|
|
39
|
+
];
|
|
40
|
+
var BINARY_NAME = KNOWN_BINS.includes(bin) ? bin : "openclaw-doctor";
|
|
41
|
+
var APP_HOME = join(homedir(), ".openclaw-doctor");
|
|
42
|
+
var DISPLAY_NAMES = {
|
|
43
|
+
"openclaw-cli": "OpenClaw CLI",
|
|
44
|
+
"openclaw-manage": "OpenClaw Manage",
|
|
45
|
+
"openclaw-doctor": "OpenClaw Doctor",
|
|
46
|
+
"openclaw-service": "OpenClaw Service",
|
|
47
|
+
"openclaw-daemon": "OpenClaw Daemon",
|
|
48
|
+
"openclaw-monitor": "OpenClaw Monitor",
|
|
49
|
+
"openclaw-helper": "OpenClaw Helper",
|
|
50
|
+
"openclaw-tools": "OpenClaw Tools",
|
|
51
|
+
"openclaw-utils": "OpenClaw Utils",
|
|
52
|
+
"openclaw-gateway": "OpenClaw Gateway",
|
|
53
|
+
"openclaw-setup": "OpenClaw Setup",
|
|
54
|
+
"openclaw-install": "OpenClaw Install",
|
|
55
|
+
"openclaw-run": "OpenClaw Run",
|
|
56
|
+
"openclaw-start": "OpenClaw Start",
|
|
57
|
+
"openclaw-watch": "OpenClaw Watch",
|
|
58
|
+
"openclaw-health": "OpenClaw Health"
|
|
59
|
+
};
|
|
60
|
+
var DISPLAY_NAME = DISPLAY_NAMES[BINARY_NAME] ?? "OpenClaw Doctor";
|
|
61
|
+
|
|
62
|
+
// src/config.ts
|
|
63
|
+
var DOCTOR_HOME = APP_HOME;
|
|
64
|
+
var CONFIG_PATH = join2(APP_HOME, "config.json");
|
|
65
|
+
var DOCTOR_LOG_DIR = join2(APP_HOME, "logs");
|
|
66
|
+
var PID_FILE = join2(APP_HOME, "daemon.pid");
|
|
67
|
+
var defaults = {
|
|
68
|
+
checkInterval: 30,
|
|
69
|
+
failThreshold: 5,
|
|
70
|
+
dashboardPort: 9090,
|
|
71
|
+
maxRestartsPerHour: 5,
|
|
72
|
+
openclawProfile: "default",
|
|
73
|
+
notify: {
|
|
74
|
+
webhook: {
|
|
75
|
+
enabled: false,
|
|
76
|
+
url: "",
|
|
77
|
+
bodyTemplate: '{"msgtype":"text","text":{"content":"{{message}}"}}'
|
|
78
|
+
},
|
|
79
|
+
system: {
|
|
80
|
+
enabled: true
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
function ensureDoctorHome() {
|
|
85
|
+
if (!existsSync(DOCTOR_HOME)) {
|
|
86
|
+
mkdirSync(DOCTOR_HOME, { recursive: true });
|
|
87
|
+
}
|
|
88
|
+
if (!existsSync(DOCTOR_LOG_DIR)) {
|
|
89
|
+
mkdirSync(DOCTOR_LOG_DIR, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
var LOCAL_CONFIG = resolve(process.cwd(), "doctor.config.json");
|
|
93
|
+
function resolveConfigPath(configPath) {
|
|
94
|
+
if (configPath) return configPath;
|
|
95
|
+
if (existsSync(LOCAL_CONFIG)) return LOCAL_CONFIG;
|
|
96
|
+
return CONFIG_PATH;
|
|
97
|
+
}
|
|
98
|
+
function loadConfig(configPath) {
|
|
99
|
+
const file = resolveConfigPath(configPath);
|
|
100
|
+
if (existsSync(file)) {
|
|
101
|
+
const raw = JSON.parse(readFileSync(file, "utf-8"));
|
|
102
|
+
return {
|
|
103
|
+
...defaults,
|
|
104
|
+
...raw,
|
|
105
|
+
notify: {
|
|
106
|
+
webhook: { ...defaults.notify.webhook, ...raw.notify?.webhook ?? {} },
|
|
107
|
+
system: { ...defaults.notify.system, ...raw.notify?.system ?? {} }
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
ensureDoctorHome();
|
|
112
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(defaults, null, 2) + "\n");
|
|
113
|
+
return { ...defaults };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/core/openclaw.ts
|
|
117
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync } from "fs";
|
|
118
|
+
import { join as join3 } from "path";
|
|
119
|
+
import { homedir as homedir2 } from "os";
|
|
120
|
+
import { execSync } from "child_process";
|
|
121
|
+
import { exec } from "child_process";
|
|
122
|
+
import { promisify } from "util";
|
|
123
|
+
import * as http from "http";
|
|
124
|
+
var execAsync = promisify(exec);
|
|
125
|
+
function getOpenClawHome(profile) {
|
|
126
|
+
if (profile === "dev") return join3(homedir2(), ".openclaw-dev");
|
|
127
|
+
if (profile !== "default") return join3(homedir2(), `.openclaw-${profile}`);
|
|
128
|
+
return join3(homedir2(), ".openclaw");
|
|
129
|
+
}
|
|
130
|
+
function findOpenClawBin() {
|
|
131
|
+
const plistDir = join3(homedir2(), "Library", "LaunchAgents");
|
|
132
|
+
if (existsSync2(plistDir)) {
|
|
133
|
+
const plists = readdirSync(plistDir).filter(
|
|
134
|
+
(f) => f.includes("openclaw") && f.endsWith(".plist")
|
|
135
|
+
);
|
|
136
|
+
for (const plist of plists) {
|
|
137
|
+
const content = readFileSync2(join3(plistDir, plist), "utf-8");
|
|
138
|
+
const nodeMatch = content.match(
|
|
139
|
+
/<string>(\/[^<]*\/bin\/node)<\/string>/
|
|
140
|
+
);
|
|
141
|
+
const cliMatch = content.match(
|
|
142
|
+
/<string>(\/[^<]*openclaw[^<]*\.(?:js|mjs))<\/string>/
|
|
143
|
+
);
|
|
144
|
+
if (nodeMatch && cliMatch) {
|
|
145
|
+
return { nodePath: nodeMatch[1], cliBinPath: cliMatch[1] };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const bin2 = execSync("which openclaw", { encoding: "utf-8" }).trim();
|
|
151
|
+
if (bin2) return { nodePath: process.execPath, cliBinPath: bin2 };
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function findLaunchdLabel() {
|
|
157
|
+
const plistDir = join3(homedir2(), "Library", "LaunchAgents");
|
|
158
|
+
if (!existsSync2(plistDir)) return "ai.openclaw.gateway";
|
|
159
|
+
const plists = readdirSync(plistDir).filter(
|
|
160
|
+
(f) => f.includes("openclaw") && f.endsWith(".plist")
|
|
161
|
+
);
|
|
162
|
+
if (plists.length > 0) {
|
|
163
|
+
return plists[0].replace(".plist", "");
|
|
164
|
+
}
|
|
165
|
+
return "ai.openclaw.gateway";
|
|
166
|
+
}
|
|
167
|
+
function detectOpenClaw(profile = "default") {
|
|
168
|
+
const home = getOpenClawHome(profile);
|
|
169
|
+
const configPath = join3(home, "openclaw.json");
|
|
170
|
+
const logDir2 = join3(home, "logs");
|
|
171
|
+
const defaults2 = {
|
|
172
|
+
configPath,
|
|
173
|
+
gatewayPort: 18789,
|
|
174
|
+
gatewayToken: "",
|
|
175
|
+
launchdLabel: findLaunchdLabel(),
|
|
176
|
+
nodePath: process.execPath,
|
|
177
|
+
cliBinPath: "",
|
|
178
|
+
logDir: logDir2,
|
|
179
|
+
profile,
|
|
180
|
+
channels: [],
|
|
181
|
+
agents: [],
|
|
182
|
+
version: null
|
|
183
|
+
};
|
|
184
|
+
const binInfo = findOpenClawBin();
|
|
185
|
+
if (binInfo) {
|
|
186
|
+
defaults2.nodePath = binInfo.nodePath;
|
|
187
|
+
defaults2.cliBinPath = binInfo.cliBinPath;
|
|
188
|
+
}
|
|
189
|
+
if (!existsSync2(configPath)) {
|
|
190
|
+
return defaults2;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const raw = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
194
|
+
defaults2.gatewayPort = raw.gateway?.port ?? defaults2.gatewayPort;
|
|
195
|
+
defaults2.gatewayToken = raw.gateway?.auth?.token ?? "";
|
|
196
|
+
defaults2.version = raw.meta?.lastTouchedVersion ?? null;
|
|
197
|
+
if (raw.channels) {
|
|
198
|
+
defaults2.channels = Object.entries(raw.channels).filter(([, v]) => v.enabled !== false).map(([k]) => k);
|
|
199
|
+
}
|
|
200
|
+
if (raw.agents?.list) {
|
|
201
|
+
defaults2.agents = raw.agents.list.map(
|
|
202
|
+
(a) => ({
|
|
203
|
+
id: a.id,
|
|
204
|
+
name: a.name ?? a.id,
|
|
205
|
+
isDefault: a.default ?? false
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
return defaults2;
|
|
212
|
+
}
|
|
213
|
+
async function runOpenClawCmd(info, args) {
|
|
214
|
+
if (!info.cliBinPath) return null;
|
|
215
|
+
try {
|
|
216
|
+
const { stdout } = await execAsync(`"${info.nodePath}" "${info.cliBinPath}" ${args}`, {
|
|
217
|
+
timeout: 3e4,
|
|
218
|
+
env: { ...process.env, NODE_NO_WARNINGS: "1" }
|
|
219
|
+
});
|
|
220
|
+
return stdout.trim();
|
|
221
|
+
} catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function getGatewayHealthHttp(info) {
|
|
226
|
+
return new Promise((resolve2) => {
|
|
227
|
+
const url = `http://127.0.0.1:${info.gatewayPort}/health`;
|
|
228
|
+
const req = http.get(url, { timeout: 5e3 }, (res) => {
|
|
229
|
+
let data = "";
|
|
230
|
+
res.on("data", (chunk) => {
|
|
231
|
+
data += chunk;
|
|
232
|
+
});
|
|
233
|
+
res.on("end", () => {
|
|
234
|
+
try {
|
|
235
|
+
resolve2(JSON.parse(data));
|
|
236
|
+
} catch {
|
|
237
|
+
resolve2(null);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
req.on("error", () => resolve2(null));
|
|
242
|
+
req.on("timeout", () => {
|
|
243
|
+
req.destroy();
|
|
244
|
+
resolve2(null);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
async function getGatewayHealth(info) {
|
|
249
|
+
const raw = await runOpenClawCmd(info, "health --json");
|
|
250
|
+
if (!raw) return null;
|
|
251
|
+
try {
|
|
252
|
+
const jsonStart = raw.indexOf("{");
|
|
253
|
+
if (jsonStart === -1) return null;
|
|
254
|
+
return JSON.parse(raw.slice(jsonStart));
|
|
255
|
+
} catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function getRestartCommand(info) {
|
|
260
|
+
const uid = process.getuid?.() ?? 501;
|
|
261
|
+
return `launchctl kickstart -k gui/${uid}/${info.launchdLabel}`;
|
|
262
|
+
}
|
|
263
|
+
function getStopCommand(info) {
|
|
264
|
+
const uid = process.getuid?.() ?? 501;
|
|
265
|
+
return `launchctl kill SIGTERM gui/${uid}/${info.launchdLabel}`;
|
|
266
|
+
}
|
|
267
|
+
function getStartCommand(info) {
|
|
268
|
+
const uid = process.getuid?.() ?? 501;
|
|
269
|
+
return `launchctl kickstart gui/${uid}/${info.launchdLabel}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/core/logger.ts
|
|
273
|
+
import { appendFileSync } from "fs";
|
|
274
|
+
import { join as join4 } from "path";
|
|
275
|
+
import chalk from "chalk";
|
|
276
|
+
var logDir = DOCTOR_LOG_DIR;
|
|
277
|
+
function initLogger(dir) {
|
|
278
|
+
logDir = dir ?? DOCTOR_LOG_DIR;
|
|
279
|
+
ensureDoctorHome();
|
|
280
|
+
}
|
|
281
|
+
function getLogFile() {
|
|
282
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
283
|
+
return join4(logDir, `${date}.log`);
|
|
284
|
+
}
|
|
285
|
+
function log(level, message) {
|
|
286
|
+
const time = (/* @__PURE__ */ new Date()).toISOString();
|
|
287
|
+
const line = `[${time}] [${level.toUpperCase()}] ${message}`;
|
|
288
|
+
const colorFn = level === "error" ? chalk.red : level === "warn" ? chalk.yellow : level === "success" ? chalk.green : chalk.blue;
|
|
289
|
+
console.log(colorFn(line));
|
|
290
|
+
try {
|
|
291
|
+
appendFileSync(getLogFile(), line + "\n");
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
var checkHistory = [];
|
|
296
|
+
var restartHistory = [];
|
|
297
|
+
var MAX_HISTORY = 100;
|
|
298
|
+
function addCheckRecord(record) {
|
|
299
|
+
checkHistory.push(record);
|
|
300
|
+
if (checkHistory.length > MAX_HISTORY) checkHistory.shift();
|
|
301
|
+
}
|
|
302
|
+
function addRestartRecord(record) {
|
|
303
|
+
restartHistory.push(record);
|
|
304
|
+
if (restartHistory.length > MAX_HISTORY) restartHistory.shift();
|
|
305
|
+
}
|
|
306
|
+
function getCheckHistory() {
|
|
307
|
+
return [...checkHistory];
|
|
308
|
+
}
|
|
309
|
+
function getRestartHistory() {
|
|
310
|
+
return [...restartHistory];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/core/health-checker.ts
|
|
314
|
+
async function checkHealth(info) {
|
|
315
|
+
const start = Date.now();
|
|
316
|
+
let health = await getGatewayHealthHttp(info);
|
|
317
|
+
if (!health) {
|
|
318
|
+
log("warn", "HTTP probe failed, falling back to CLI health check");
|
|
319
|
+
health = await getGatewayHealth(info);
|
|
320
|
+
}
|
|
321
|
+
const durationMs = Date.now() - start;
|
|
322
|
+
if (!health) {
|
|
323
|
+
const error = "Gateway unreachable (openclaw health failed)";
|
|
324
|
+
addCheckRecord({
|
|
325
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
326
|
+
healthy: false,
|
|
327
|
+
error,
|
|
328
|
+
responseTime: durationMs
|
|
329
|
+
});
|
|
330
|
+
log("error", `Health check failed: ${error} (${durationMs}ms)`);
|
|
331
|
+
return { healthy: false, gateway: false, channels: [], durationMs, error };
|
|
332
|
+
}
|
|
333
|
+
const channels = health.channels ? Object.entries(health.channels).map(([name, ch]) => ({
|
|
334
|
+
name,
|
|
335
|
+
ok: ch.probe?.ok ?? false
|
|
336
|
+
})) : [];
|
|
337
|
+
const healthy = health.ok;
|
|
338
|
+
addCheckRecord({
|
|
339
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
340
|
+
healthy,
|
|
341
|
+
responseTime: durationMs
|
|
342
|
+
});
|
|
343
|
+
if (healthy) {
|
|
344
|
+
log("success", `Health OK \u2014 gateway up, ${channels.length} channels (${durationMs}ms)`);
|
|
345
|
+
} else {
|
|
346
|
+
log("warn", `Health degraded \u2014 gateway responded but ok=false (${durationMs}ms)`);
|
|
347
|
+
}
|
|
348
|
+
return { healthy, gateway: true, channels, durationMs, raw: health };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/dashboard/server.ts
|
|
352
|
+
async function renderHTML(info) {
|
|
353
|
+
const checks = getCheckHistory();
|
|
354
|
+
const restarts = getRestartHistory();
|
|
355
|
+
const live = await checkHealth(info);
|
|
356
|
+
const statusText = live.healthy ? "HEALTHY" : live.gateway ? "DEGRADED" : "UNREACHABLE";
|
|
357
|
+
const statusColor = live.healthy ? "#22c55e" : live.gateway ? "#eab308" : "#ef4444";
|
|
358
|
+
const channelRows = live.channels.map(
|
|
359
|
+
(c) => `
|
|
360
|
+
<tr>
|
|
361
|
+
<td>${c.name}</td>
|
|
362
|
+
<td style="color:${c.ok ? "#22c55e" : "#ef4444"}">${c.ok ? "OK" : "FAIL"}</td>
|
|
363
|
+
</tr>`
|
|
364
|
+
).join("");
|
|
365
|
+
const agentList = info.agents.map((a) => `${a.name}${a.isDefault ? " (default)" : ""}`).join(", ");
|
|
366
|
+
const checksRows = checks.slice().reverse().slice(0, 30).map(
|
|
367
|
+
(c) => `
|
|
368
|
+
<tr>
|
|
369
|
+
<td>${c.timestamp}</td>
|
|
370
|
+
<td style="color:${c.healthy ? "#22c55e" : "#ef4444"}">${c.healthy ? "OK" : "FAIL"}</td>
|
|
371
|
+
<td>${c.responseTime ?? "-"}ms</td>
|
|
372
|
+
<td>${c.error ?? ""}</td>
|
|
373
|
+
</tr>`
|
|
374
|
+
).join("");
|
|
375
|
+
const restartsRows = restarts.slice().reverse().slice(0, 20).map(
|
|
376
|
+
(r) => `
|
|
377
|
+
<tr>
|
|
378
|
+
<td>${r.timestamp}</td>
|
|
379
|
+
<td>${r.reason}</td>
|
|
380
|
+
<td style="color:${r.success ? "#22c55e" : "#ef4444"}">${r.success ? "OK" : "FAIL"}</td>
|
|
381
|
+
</tr>`
|
|
382
|
+
).join("");
|
|
383
|
+
return `<!DOCTYPE html>
|
|
384
|
+
<html>
|
|
385
|
+
<head>
|
|
386
|
+
<meta charset="utf-8">
|
|
387
|
+
<title>OpenClaw Doctor</title>
|
|
388
|
+
<meta http-equiv="refresh" content="10">
|
|
389
|
+
<style>
|
|
390
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
391
|
+
body { font-family:-apple-system,system-ui,sans-serif; background:#0f172a; color:#e2e8f0; padding:2rem; }
|
|
392
|
+
h1 { font-size:1.5rem; margin-bottom:0.5rem; }
|
|
393
|
+
.subtitle { color:#64748b; margin-bottom:1.5rem; font-size:0.85rem; }
|
|
394
|
+
.status { font-size:2rem; font-weight:bold; color:${statusColor}; margin-bottom:0.5rem; }
|
|
395
|
+
.meta { color:#64748b; font-size:0.8rem; margin-bottom:2rem; }
|
|
396
|
+
h2 { font-size:1.1rem; margin:1.5rem 0 0.5rem; color:#94a3b8; }
|
|
397
|
+
table { width:100%; border-collapse:collapse; margin-bottom:1rem; }
|
|
398
|
+
th,td { text-align:left; padding:0.4rem 0.8rem; border-bottom:1px solid #1e293b; font-size:0.85rem; }
|
|
399
|
+
th { color:#64748b; }
|
|
400
|
+
.grid { display:grid; grid-template-columns:1fr 1fr; gap:2rem; }
|
|
401
|
+
@media (max-width:768px) { .grid { grid-template-columns:1fr; } }
|
|
402
|
+
</style>
|
|
403
|
+
</head>
|
|
404
|
+
<body>
|
|
405
|
+
<h1>OpenClaw Doctor</h1>
|
|
406
|
+
<div class="subtitle">Gateway :${info.gatewayPort} | OpenClaw ${info.version ?? "?"} | ${info.profile} profile</div>
|
|
407
|
+
<div class="status">${statusText}</div>
|
|
408
|
+
<div class="meta">${live.durationMs}ms | Agents: ${agentList || "none"}</div>
|
|
409
|
+
|
|
410
|
+
<div class="grid">
|
|
411
|
+
<div>
|
|
412
|
+
<h2>Channels</h2>
|
|
413
|
+
<table>
|
|
414
|
+
<tr><th>Channel</th><th>Status</th></tr>
|
|
415
|
+
${channelRows || "<tr><td colspan=2>No channels</td></tr>"}
|
|
416
|
+
</table>
|
|
417
|
+
|
|
418
|
+
<h2>Restart History</h2>
|
|
419
|
+
<table>
|
|
420
|
+
<tr><th>Time</th><th>Reason</th><th>Result</th></tr>
|
|
421
|
+
${restartsRows || "<tr><td colspan=3>No restarts</td></tr>"}
|
|
422
|
+
</table>
|
|
423
|
+
</div>
|
|
424
|
+
<div>
|
|
425
|
+
<h2>Health Check History</h2>
|
|
426
|
+
<table>
|
|
427
|
+
<tr><th>Time</th><th>Status</th><th>Latency</th><th>Error</th></tr>
|
|
428
|
+
${checksRows || "<tr><td colspan=4>No checks yet</td></tr>"}
|
|
429
|
+
</table>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
</body>
|
|
433
|
+
</html>`;
|
|
434
|
+
}
|
|
435
|
+
function startDashboard(options) {
|
|
436
|
+
const config = loadConfig(options.config);
|
|
437
|
+
const info = detectOpenClaw(options.profile ?? config.openclawProfile);
|
|
438
|
+
const port = config.dashboardPort;
|
|
439
|
+
const server = createServer(async (_req, res) => {
|
|
440
|
+
const html = await renderHTML(info);
|
|
441
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
442
|
+
res.end(html);
|
|
443
|
+
});
|
|
444
|
+
server.listen(port, () => {
|
|
445
|
+
log("info", `Dashboard running at http://localhost:${port}`);
|
|
446
|
+
console.log(chalk2.green.bold(`
|
|
447
|
+
Dashboard: http://localhost:${port}
|
|
448
|
+
`));
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export {
|
|
453
|
+
__require,
|
|
454
|
+
BINARY_NAME,
|
|
455
|
+
DISPLAY_NAME,
|
|
456
|
+
DOCTOR_LOG_DIR,
|
|
457
|
+
PID_FILE,
|
|
458
|
+
ensureDoctorHome,
|
|
459
|
+
loadConfig,
|
|
460
|
+
initLogger,
|
|
461
|
+
log,
|
|
462
|
+
addRestartRecord,
|
|
463
|
+
detectOpenClaw,
|
|
464
|
+
runOpenClawCmd,
|
|
465
|
+
getRestartCommand,
|
|
466
|
+
getStopCommand,
|
|
467
|
+
getStartCommand,
|
|
468
|
+
checkHealth,
|
|
469
|
+
startDashboard
|
|
470
|
+
};
|
package/dist/index.d.ts
ADDED