claw-subagent-service 0.0.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.md +44 -0
- package/cli.js +254 -0
- package/command/linux/restart.sh +98 -0
- package/command/linux/start.sh +101 -0
- package/command/linux/status.sh +140 -0
- package/command/linux/stop.sh +112 -0
- package/command/win/restart.bat +39 -0
- package/command/win/start.bat +65 -0
- package/command/win/status.bat +52 -0
- package/command/win/stop.bat +55 -0
- package/command/win/windows/345/220/257/345/212/250/350/204/232/346/234/254 +0 -0
- package/package.json +37 -0
- package/scripts/install-silent.js +167 -0
- package/scripts/uninstall.js +61 -0
- package/service/daemon.js +189 -0
- package/service/logger.js +31 -0
- package/service/modules/auth.js +17 -0
- package/service/modules/business-message-handler.js +118 -0
- package/service/modules/command-handler.js +152 -0
- package/service/modules/config.js +44 -0
- package/service/modules/dashboard-collector.js +588 -0
- package/service/modules/heartbeat-dashboard.js +153 -0
- package/service/modules/mac-address.js +15 -0
- package/service/modules/message-processor-example.js +72 -0
- package/service/modules/message-processor.js +62 -0
- package/service/modules/normal-message-handler.js +60 -0
- package/service/modules/openclaw-control.js +128 -0
- package/service/modules/openclaw-enum.js +48 -0
- package/service/modules/opencode-service.js +199 -0
- package/service/modules/opencode-starter.js +194 -0
- package/service/modules/port-checker.js +31 -0
- package/service/modules/rongyun-message-handler.js +250 -0
- package/service/modules/rongyun-message-sender.js +157 -0
- package/service/modules/rongyun-message-types.js +28 -0
- package/service/modules/script-executor.js +550 -0
- package/service/modules/service-manager.js +319 -0
- package/service/modules/structured-message-router.js +118 -0
- package/service/rongcloud/env-polyfill.js +95 -0
- package/service/rongcloud/index.js +19 -0
- package/service/rongcloud/message-handler.js +147 -0
- package/service/rongcloud/message-types.js +22 -0
- package/service/rongcloud/openclaw-client.js +98 -0
- package/service/rongcloud/openclaw-config.js +108 -0
- package/service/rongcloud/rongcloud-client.js +273 -0
- package/service/rongcloud/types.js +9 -0
- package/service/updater.js +348 -0
- package/service/worker.js +376 -0
- package/version.json +4 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
const { execSync, spawn } = require('child_process');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
const OPENCLAW_HOME = path.join(os.homedir(), '.openclaw');
|
|
7
|
+
|
|
8
|
+
// 查找 openclaw 可执行文件
|
|
9
|
+
function findOpenClawPath() {
|
|
10
|
+
const isWin = process.platform === 'win32';
|
|
11
|
+
|
|
12
|
+
// 1. 从 PATH 环境变量中搜索
|
|
13
|
+
const pathEnv = process.env.PATH || process.env.Path || process.env.path || '';
|
|
14
|
+
const pathDirs = pathEnv.split(isWin ? ';' : ':');
|
|
15
|
+
const pathCandidates = isWin
|
|
16
|
+
? ['openclaw.cmd', 'openclaw.exe', 'openclaw.ps1', 'openclaw']
|
|
17
|
+
: ['openclaw'];
|
|
18
|
+
|
|
19
|
+
for (const dir of pathDirs) {
|
|
20
|
+
for (const name of pathCandidates) {
|
|
21
|
+
const fullPath = path.join(dir.trim(), name);
|
|
22
|
+
if (fs.existsSync(fullPath)) return fullPath;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 2. 尝试 where/which 命令
|
|
27
|
+
const candidates = isWin
|
|
28
|
+
? ['openclaw.cmd', 'openclaw.exe', 'openclaw']
|
|
29
|
+
: ['openclaw'];
|
|
30
|
+
|
|
31
|
+
for (const cmd of candidates) {
|
|
32
|
+
try {
|
|
33
|
+
const result = execSync(`where ${cmd}`, { encoding: 'utf-8', windowsHide: true });
|
|
34
|
+
const p = result.trim().split('\n')[0].trim();
|
|
35
|
+
if (p) return p;
|
|
36
|
+
} catch {
|
|
37
|
+
try {
|
|
38
|
+
const result = execSync(`which ${cmd}`, { encoding: 'utf-8' });
|
|
39
|
+
const p = result.trim().split('\n')[0].trim();
|
|
40
|
+
if (p) return p;
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 3. 尝试 npx
|
|
46
|
+
try {
|
|
47
|
+
const npxCmd = isWin ? 'npx.cmd' : 'npx';
|
|
48
|
+
const result = execSync(`${npxCmd} which openclaw`, { encoding: 'utf-8', windowsHide: true });
|
|
49
|
+
const p = result.trim().split('\n')[0].trim();
|
|
50
|
+
if (p) return p;
|
|
51
|
+
} catch {}
|
|
52
|
+
|
|
53
|
+
// 4. 尝试常见路径
|
|
54
|
+
const commonPaths = isWin
|
|
55
|
+
? [
|
|
56
|
+
path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'openclaw.cmd'),
|
|
57
|
+
path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'openclaw.ps1'),
|
|
58
|
+
path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'openclaw'),
|
|
59
|
+
path.join('C:', 'Program Files', 'nodejs', 'openclaw.cmd'),
|
|
60
|
+
path.join('C:', 'Program Files (x86)', 'nodejs', 'openclaw.cmd'),
|
|
61
|
+
]
|
|
62
|
+
: [
|
|
63
|
+
path.join(os.homedir(), '.npm', 'global', 'bin', 'openclaw'),
|
|
64
|
+
'/usr/local/bin/openclaw',
|
|
65
|
+
'/usr/bin/openclaw',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
for (const p of commonPaths) {
|
|
69
|
+
if (fs.existsSync(p)) return p;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let openClawPath = null;
|
|
76
|
+
|
|
77
|
+
function getOpenClawPath() {
|
|
78
|
+
if (!openClawPath) {
|
|
79
|
+
openClawPath = findOpenClawPath();
|
|
80
|
+
}
|
|
81
|
+
return openClawPath;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function runCommandSpawn(cmd, args, timeoutMs = 15000) {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const isWin = process.platform === 'win32';
|
|
87
|
+
const isCmd = isWin && (cmd.endsWith('.cmd') || cmd.endsWith('.bat') || cmd.endsWith('.ps1'));
|
|
88
|
+
|
|
89
|
+
// Windows 上执行 .cmd 文件需要特殊处理
|
|
90
|
+
let actualCmd = cmd;
|
|
91
|
+
let actualArgs = args;
|
|
92
|
+
|
|
93
|
+
if (isCmd) {
|
|
94
|
+
// 使用 cmd /c 来执行 .cmd 文件
|
|
95
|
+
actualCmd = 'cmd';
|
|
96
|
+
actualArgs = ['/c', cmd, ...args];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const child = spawn(actualCmd, actualArgs, {
|
|
100
|
+
cwd: OPENCLAW_HOME,
|
|
101
|
+
shell: false,
|
|
102
|
+
windowsHide: true,
|
|
103
|
+
env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' }
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
let stdout = '';
|
|
107
|
+
let stderr = '';
|
|
108
|
+
let finished = false;
|
|
109
|
+
|
|
110
|
+
const finish = (result, err) => {
|
|
111
|
+
if (finished) return;
|
|
112
|
+
finished = true;
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
if (err && !result.trim()) {
|
|
115
|
+
reject(new Error(err));
|
|
116
|
+
} else {
|
|
117
|
+
resolve(result);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const timeout = setTimeout(() => {
|
|
122
|
+
if (stdout.trim()) {
|
|
123
|
+
finish(stdout);
|
|
124
|
+
killProcessTree(child, isWin);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
killProcessTree(child, isWin);
|
|
128
|
+
finish('', 'Command timeout');
|
|
129
|
+
}, timeoutMs);
|
|
130
|
+
|
|
131
|
+
child.stdout?.on('data', (data) => {
|
|
132
|
+
stdout += data.toString();
|
|
133
|
+
});
|
|
134
|
+
child.stderr?.on('data', (data) => {
|
|
135
|
+
stderr += data.toString();
|
|
136
|
+
});
|
|
137
|
+
child.on('close', (code) => {
|
|
138
|
+
finish(stdout, code === 0 ? undefined : (stderr || `Exit code ${code}`));
|
|
139
|
+
});
|
|
140
|
+
child.on('error', (err) => {
|
|
141
|
+
finish(stdout, err.message);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function killProcessTree(child, isWin) {
|
|
147
|
+
if (!child.pid) return;
|
|
148
|
+
try {
|
|
149
|
+
if (isWin) {
|
|
150
|
+
execSync(`taskkill /pid ${child.pid} /T /F`, { windowsHide: true });
|
|
151
|
+
} else {
|
|
152
|
+
child.kill('SIGKILL');
|
|
153
|
+
}
|
|
154
|
+
} catch {}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function runJsonCommand(args, timeoutMs = 30000) {
|
|
158
|
+
const cmdPath = getOpenClawPath();
|
|
159
|
+
if (!cmdPath) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const output = await runCommandSpawn(cmdPath, args, timeoutMs);
|
|
165
|
+
const trimmed = output.trim();
|
|
166
|
+
if (!trimmed) return null;
|
|
167
|
+
return JSON.parse(trimmed);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let cachedVersion = '';
|
|
174
|
+
|
|
175
|
+
async function getOpenClawVersion() {
|
|
176
|
+
if (cachedVersion) return cachedVersion;
|
|
177
|
+
|
|
178
|
+
// 尝试 package.json
|
|
179
|
+
const possiblePaths = [
|
|
180
|
+
path.join(OPENCLAW_HOME, 'package.json'),
|
|
181
|
+
path.join(os.homedir(), '.config', 'openclaw', 'package.json')
|
|
182
|
+
];
|
|
183
|
+
for (const p of possiblePaths) {
|
|
184
|
+
if (fs.existsSync(p)) {
|
|
185
|
+
try {
|
|
186
|
+
const pkg = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
187
|
+
if (pkg.version) {
|
|
188
|
+
cachedVersion = pkg.version;
|
|
189
|
+
return cachedVersion;
|
|
190
|
+
}
|
|
191
|
+
} catch {}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const cmdPath = getOpenClawPath();
|
|
196
|
+
if (cmdPath) {
|
|
197
|
+
try {
|
|
198
|
+
const output = await runCommandSpawn(cmdPath, ['--version'], 10000);
|
|
199
|
+
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
200
|
+
if (match) {
|
|
201
|
+
cachedVersion = match[1];
|
|
202
|
+
return cachedVersion;
|
|
203
|
+
}
|
|
204
|
+
if (output.trim()) {
|
|
205
|
+
cachedVersion = output.trim();
|
|
206
|
+
return cachedVersion;
|
|
207
|
+
}
|
|
208
|
+
} catch {}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const data = await runJsonCommand(['version'], 10000);
|
|
212
|
+
if (data && typeof data === 'string') {
|
|
213
|
+
cachedVersion = data.trim();
|
|
214
|
+
return cachedVersion;
|
|
215
|
+
}
|
|
216
|
+
if (data?.version) {
|
|
217
|
+
cachedVersion = String(data.version);
|
|
218
|
+
return cachedVersion;
|
|
219
|
+
}
|
|
220
|
+
} catch {}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
cachedVersion = 'unknown';
|
|
224
|
+
return cachedVersion;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let lastSessions = [];
|
|
228
|
+
let lastCronJobs = [];
|
|
229
|
+
let lastApprovals = null;
|
|
230
|
+
|
|
231
|
+
async function collectSessions() {
|
|
232
|
+
const data = await runJsonCommand(['sessions', '--json'], 30000);
|
|
233
|
+
if (data?.sessions) {
|
|
234
|
+
const sessions = data.sessions.map((s) => ({
|
|
235
|
+
...s,
|
|
236
|
+
sessionKey: s.key || s.sessionKey || '',
|
|
237
|
+
state: s.state || 'idle',
|
|
238
|
+
label: s.label || s.sessionId || s.key || '',
|
|
239
|
+
lastMessageAt: s.lastMessageAt || s.updatedAt
|
|
240
|
+
}));
|
|
241
|
+
lastSessions = sessions;
|
|
242
|
+
return sessions;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 回退:从文件系统读取
|
|
246
|
+
if (lastSessions.length === 0) {
|
|
247
|
+
const contexts = await collectSessionsContexts();
|
|
248
|
+
if (contexts.length > 0) {
|
|
249
|
+
lastSessions = contexts.map((ctx) => ({
|
|
250
|
+
sessionKey: ctx.sessionKey,
|
|
251
|
+
key: ctx.sessionKey,
|
|
252
|
+
sessionId: ctx.sessionId,
|
|
253
|
+
agentId: ctx.agentId,
|
|
254
|
+
model: ctx.model,
|
|
255
|
+
modelProvider: ctx.modelProvider,
|
|
256
|
+
contextTokens: ctx.contextTokens,
|
|
257
|
+
totalTokens: ctx.totalTokens,
|
|
258
|
+
state: 'idle',
|
|
259
|
+
label: ctx.sessionId || ctx.sessionKey,
|
|
260
|
+
lastMessageAt: null,
|
|
261
|
+
updatedAt: null
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return lastSessions;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function collectCronJobs() {
|
|
270
|
+
const data = await runJsonCommand(['cron', 'list', '--json'], 30000);
|
|
271
|
+
if (data?.jobs) {
|
|
272
|
+
const jobs = data.jobs.map((job) => {
|
|
273
|
+
if (!job.jobId && job.id) job.jobId = job.id;
|
|
274
|
+
if (!job.nextRunAt && job.schedule) {
|
|
275
|
+
const schedule = job.schedule;
|
|
276
|
+
if (schedule.kind === 'every' && schedule.everyMs > 0 && schedule.anchorMs > 0) {
|
|
277
|
+
const now = Date.now();
|
|
278
|
+
const elapsed = now - schedule.anchorMs;
|
|
279
|
+
job.nextRunAt = schedule.anchorMs + (Math.floor(elapsed / schedule.everyMs) + 1) * schedule.everyMs;
|
|
280
|
+
} else if (schedule.kind === 'cron') {
|
|
281
|
+
job.nextRunAt = null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (typeof job.enabled === 'undefined') {
|
|
285
|
+
job.enabled = ['enabled', 'active', true].includes(job.status);
|
|
286
|
+
}
|
|
287
|
+
return job;
|
|
288
|
+
});
|
|
289
|
+
lastCronJobs = jobs;
|
|
290
|
+
return jobs;
|
|
291
|
+
}
|
|
292
|
+
return lastCronJobs;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function collectApprovals() {
|
|
296
|
+
const data = await runJsonCommand(['approvals', 'get', '--json'], 30000);
|
|
297
|
+
if (data) {
|
|
298
|
+
lastApprovals = data;
|
|
299
|
+
return data;
|
|
300
|
+
}
|
|
301
|
+
return lastApprovals;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function collectSessionsContexts() {
|
|
305
|
+
const contexts = [];
|
|
306
|
+
const agentsDir = path.join(OPENCLAW_HOME, 'agents');
|
|
307
|
+
if (!fs.existsSync(agentsDir)) return contexts;
|
|
308
|
+
|
|
309
|
+
for (const agentName of fs.readdirSync(agentsDir)) {
|
|
310
|
+
const agentDir = path.join(agentsDir, agentName);
|
|
311
|
+
const sessionsIndex = path.join(agentDir, 'sessions', 'sessions.json');
|
|
312
|
+
if (!fs.existsSync(sessionsIndex)) continue;
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const data = JSON.parse(fs.readFileSync(sessionsIndex, 'utf-8'));
|
|
316
|
+
for (const [sessionKey, value] of Object.entries(data)) {
|
|
317
|
+
if (typeof value !== 'object' || value === null) continue;
|
|
318
|
+
const v = value;
|
|
319
|
+
const meta = v.meta || {};
|
|
320
|
+
contexts.push({
|
|
321
|
+
sessionKey,
|
|
322
|
+
sessionId: v.sessionId,
|
|
323
|
+
agentId: agentName,
|
|
324
|
+
model: v.model,
|
|
325
|
+
modelProvider: v.modelProvider,
|
|
326
|
+
contextTokens: v.contextTokens,
|
|
327
|
+
totalTokens: v.totalTokens,
|
|
328
|
+
channel: v.channel || v.lastChannel || meta.channel || meta.provider,
|
|
329
|
+
surface: meta.surface
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
} catch {}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return contexts;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function collectUsageEvents() {
|
|
339
|
+
const events = [];
|
|
340
|
+
const agentsDir = path.join(OPENCLAW_HOME, 'agents');
|
|
341
|
+
if (!fs.existsSync(agentsDir)) return events;
|
|
342
|
+
|
|
343
|
+
const lookbackTimestamp = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
344
|
+
|
|
345
|
+
for (const agentName of fs.readdirSync(agentsDir)) {
|
|
346
|
+
const agentDir = path.join(agentsDir, agentName);
|
|
347
|
+
const sessionsDir = path.join(agentDir, 'sessions');
|
|
348
|
+
if (!fs.existsSync(sessionsDir)) continue;
|
|
349
|
+
|
|
350
|
+
// 读取 session 映射
|
|
351
|
+
const sessionKeyMap = {};
|
|
352
|
+
const sessionsIndex = path.join(sessionsDir, 'sessions.json');
|
|
353
|
+
if (fs.existsSync(sessionsIndex)) {
|
|
354
|
+
try {
|
|
355
|
+
const data = JSON.parse(fs.readFileSync(sessionsIndex, 'utf-8'));
|
|
356
|
+
for (const [sessionKey, value] of Object.entries(data)) {
|
|
357
|
+
const v = value;
|
|
358
|
+
if (v?.sessionId) sessionKeyMap[v.sessionId] = sessionKey;
|
|
359
|
+
}
|
|
360
|
+
} catch {}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// 读取 jsonl 文件
|
|
364
|
+
for (const file of fs.readdirSync(sessionsDir)) {
|
|
365
|
+
if (!file.endsWith('.jsonl')) continue;
|
|
366
|
+
const filePath = path.join(sessionsDir, file);
|
|
367
|
+
try {
|
|
368
|
+
const lines = fs.readFileSync(filePath, 'utf-8').split('\n');
|
|
369
|
+
for (const line of lines) {
|
|
370
|
+
const trimmed = line.trim();
|
|
371
|
+
if (!trimmed) continue;
|
|
372
|
+
try {
|
|
373
|
+
const record = JSON.parse(trimmed);
|
|
374
|
+
if (record.type !== 'message') continue;
|
|
375
|
+
const message = record.message || {};
|
|
376
|
+
if (message.role !== 'assistant') continue;
|
|
377
|
+
const usage = message.usage;
|
|
378
|
+
if (!usage) continue;
|
|
379
|
+
|
|
380
|
+
const timestampStr = record.timestamp || message.timestamp;
|
|
381
|
+
if (!timestampStr) continue;
|
|
382
|
+
|
|
383
|
+
const ts = new Date(timestampStr.replace('Z', '+00:00'));
|
|
384
|
+
if (isNaN(ts.getTime()) || ts.getTime() < lookbackTimestamp) continue;
|
|
385
|
+
|
|
386
|
+
const sessionId = file.replace('.jsonl', '');
|
|
387
|
+
const costInfo = usage.cost || {};
|
|
388
|
+
|
|
389
|
+
events.push({
|
|
390
|
+
timestamp: ts.toISOString(),
|
|
391
|
+
day: ts.toISOString().split('T')[0],
|
|
392
|
+
sessionId,
|
|
393
|
+
sessionKey: sessionKeyMap[sessionId],
|
|
394
|
+
agentId: agentName,
|
|
395
|
+
model: message.model,
|
|
396
|
+
provider: message.provider,
|
|
397
|
+
tokens: usage.totalTokens || (usage.input || 0) + (usage.output || 0),
|
|
398
|
+
cost: costInfo.total || 0
|
|
399
|
+
});
|
|
400
|
+
} catch {}
|
|
401
|
+
}
|
|
402
|
+
} catch {}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return events;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function collectProjects() {
|
|
410
|
+
const p = path.join(OPENCLAW_HOME, 'projects', 'projects.json');
|
|
411
|
+
if (!fs.existsSync(p)) return { projects: [], updatedAt: '' };
|
|
412
|
+
try {
|
|
413
|
+
const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
414
|
+
return {
|
|
415
|
+
projects: data.projects || [],
|
|
416
|
+
updatedAt: data.updatedAt || ''
|
|
417
|
+
};
|
|
418
|
+
} catch {
|
|
419
|
+
return { projects: [], updatedAt: '' };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function collectTasks() {
|
|
424
|
+
const p = path.join(OPENCLAW_HOME, 'tasks', 'tasks.json');
|
|
425
|
+
if (!fs.existsSync(p)) return { tasks: [], agentBudgets: [], updatedAt: '' };
|
|
426
|
+
try {
|
|
427
|
+
const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
428
|
+
return {
|
|
429
|
+
tasks: data.tasks || [],
|
|
430
|
+
agentBudgets: data.agentBudgets || [],
|
|
431
|
+
updatedAt: data.updatedAt || ''
|
|
432
|
+
};
|
|
433
|
+
} catch {
|
|
434
|
+
return { tasks: [], agentBudgets: [], updatedAt: '' };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function collectRuntimeData() {
|
|
439
|
+
const [sessionsContexts, usageEvents, projects, tasks] = await Promise.all([
|
|
440
|
+
collectSessionsContexts(),
|
|
441
|
+
collectUsageEvents(),
|
|
442
|
+
collectProjects(),
|
|
443
|
+
collectTasks()
|
|
444
|
+
]);
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
sessionsContexts,
|
|
448
|
+
usageEvents,
|
|
449
|
+
projects,
|
|
450
|
+
tasks,
|
|
451
|
+
projectSummaries: []
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function buildSessionStatuses(sessions) {
|
|
456
|
+
return sessions.map((s) => ({
|
|
457
|
+
sessionKey: s.sessionKey || s.key || '',
|
|
458
|
+
model: s.model,
|
|
459
|
+
tokensIn: s.inputTokens || 0,
|
|
460
|
+
tokensOut: s.outputTokens || 0,
|
|
461
|
+
cost: null,
|
|
462
|
+
updatedAt: s.updatedAt || ''
|
|
463
|
+
}));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function buildApprovals(approvalsData) {
|
|
467
|
+
if (!approvalsData) return [];
|
|
468
|
+
if (Array.isArray(approvalsData.approvals)) return approvalsData.approvals;
|
|
469
|
+
for (const key of ['items', 'records', 'pending']) {
|
|
470
|
+
if (Array.isArray(approvalsData[key])) return approvalsData[key];
|
|
471
|
+
}
|
|
472
|
+
return [];
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function buildBudgetSummary(sessions, sessionStatuses, tasks, projects) {
|
|
476
|
+
const evaluations = [];
|
|
477
|
+
const totalTokens = sessionStatuses.reduce((sum, s) => sum + (s.tokensIn || 0) + (s.tokensOut || 0), 0);
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
total: totalTokens,
|
|
481
|
+
ok: totalTokens > 0 ? 1 : 0,
|
|
482
|
+
warn: 0,
|
|
483
|
+
over: 0,
|
|
484
|
+
evaluations
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function buildDiagnostics(version, gatewayStatus, sessions = []) {
|
|
489
|
+
const recentIssues = sessions
|
|
490
|
+
.slice(0, 5)
|
|
491
|
+
.map((s) => {
|
|
492
|
+
const updatedAt = s.updatedAt || s.lastMessageAt;
|
|
493
|
+
if (!updatedAt) return null;
|
|
494
|
+
return {
|
|
495
|
+
timestamp: typeof updatedAt === 'number' ? new Date(updatedAt).toISOString() : updatedAt,
|
|
496
|
+
action: '会话活动',
|
|
497
|
+
detail: `${s.label || s.sessionKey || 'Unknown'} - ${s.state || 'idle'}`
|
|
498
|
+
};
|
|
499
|
+
})
|
|
500
|
+
.filter(Boolean)
|
|
501
|
+
.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
generatedAt: new Date().toISOString(),
|
|
505
|
+
app: { name: 'OpenClaw', version },
|
|
506
|
+
runtime: {
|
|
507
|
+
platform: os.platform(),
|
|
508
|
+
arch: process.arch,
|
|
509
|
+
cpuCount: os.cpus().length,
|
|
510
|
+
totalMemoryBytes: os.totalmem(),
|
|
511
|
+
freeMemoryBytes: os.freemem(),
|
|
512
|
+
uptimeSeconds: Math.floor(os.uptime())
|
|
513
|
+
},
|
|
514
|
+
gateway: {
|
|
515
|
+
configuredUrl: 'ws://127.0.0.1:18789',
|
|
516
|
+
overallStatus: gatewayStatus
|
|
517
|
+
},
|
|
518
|
+
openclaw: {
|
|
519
|
+
status: 'ok',
|
|
520
|
+
currentVersion: version,
|
|
521
|
+
updateAvailable: false
|
|
522
|
+
},
|
|
523
|
+
tokens: { redacted: true, localTokenAuthRequired: false, entries: [] },
|
|
524
|
+
recentIssues
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function collectDashboardData() {
|
|
529
|
+
const [sessions, cronJobs, approvals, runtimeData, version] = await Promise.all([
|
|
530
|
+
collectSessions(),
|
|
531
|
+
collectCronJobs(),
|
|
532
|
+
collectApprovals(),
|
|
533
|
+
collectRuntimeData(),
|
|
534
|
+
getOpenClawVersion()
|
|
535
|
+
]);
|
|
536
|
+
|
|
537
|
+
const sessionStatuses = buildSessionStatuses(sessions);
|
|
538
|
+
const projects = runtimeData.projects;
|
|
539
|
+
const tasks = runtimeData.tasks;
|
|
540
|
+
|
|
541
|
+
// 简单网关状态检测
|
|
542
|
+
let gatewayStatus = 'unknown';
|
|
543
|
+
try {
|
|
544
|
+
const net = require('net');
|
|
545
|
+
await new Promise((resolve) => {
|
|
546
|
+
const sock = new net.Socket();
|
|
547
|
+
sock.setTimeout(2000);
|
|
548
|
+
sock.once('connect', () => {
|
|
549
|
+
gatewayStatus = 'ok';
|
|
550
|
+
sock.destroy();
|
|
551
|
+
resolve();
|
|
552
|
+
});
|
|
553
|
+
sock.once('error', () => {
|
|
554
|
+
sock.destroy();
|
|
555
|
+
resolve();
|
|
556
|
+
});
|
|
557
|
+
sock.connect(18789, '127.0.0.1');
|
|
558
|
+
});
|
|
559
|
+
} catch {}
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
sessions,
|
|
563
|
+
sessionStatuses,
|
|
564
|
+
cronJobs,
|
|
565
|
+
approvals: buildApprovals(approvals),
|
|
566
|
+
projects,
|
|
567
|
+
tasks,
|
|
568
|
+
projectSummaries: runtimeData.projectSummaries,
|
|
569
|
+
tasksSummary: {
|
|
570
|
+
projects: projects.projects?.length || 0,
|
|
571
|
+
tasks: tasks.tasks?.length || 0,
|
|
572
|
+
todo: 0,
|
|
573
|
+
inProgress: 0,
|
|
574
|
+
blocked: 0,
|
|
575
|
+
done: 0,
|
|
576
|
+
owners: 0,
|
|
577
|
+
artifacts: 0
|
|
578
|
+
},
|
|
579
|
+
budgetSummary: buildBudgetSummary(sessions, sessionStatuses, tasks, projects),
|
|
580
|
+
diagnostics: buildDiagnostics(version, gatewayStatus, sessions),
|
|
581
|
+
sessionsContexts: runtimeData.sessionsContexts,
|
|
582
|
+
usageEvents: runtimeData.usageEvents
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
module.exports = {
|
|
587
|
+
collectDashboardData
|
|
588
|
+
};
|