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.
Files changed (48) hide show
  1. package/README.md +44 -0
  2. package/cli.js +254 -0
  3. package/command/linux/restart.sh +98 -0
  4. package/command/linux/start.sh +101 -0
  5. package/command/linux/status.sh +140 -0
  6. package/command/linux/stop.sh +112 -0
  7. package/command/win/restart.bat +39 -0
  8. package/command/win/start.bat +65 -0
  9. package/command/win/status.bat +52 -0
  10. package/command/win/stop.bat +55 -0
  11. package/command/win/windows/345/220/257/345/212/250/350/204/232/346/234/254 +0 -0
  12. package/package.json +37 -0
  13. package/scripts/install-silent.js +167 -0
  14. package/scripts/uninstall.js +61 -0
  15. package/service/daemon.js +189 -0
  16. package/service/logger.js +31 -0
  17. package/service/modules/auth.js +17 -0
  18. package/service/modules/business-message-handler.js +118 -0
  19. package/service/modules/command-handler.js +152 -0
  20. package/service/modules/config.js +44 -0
  21. package/service/modules/dashboard-collector.js +588 -0
  22. package/service/modules/heartbeat-dashboard.js +153 -0
  23. package/service/modules/mac-address.js +15 -0
  24. package/service/modules/message-processor-example.js +72 -0
  25. package/service/modules/message-processor.js +62 -0
  26. package/service/modules/normal-message-handler.js +60 -0
  27. package/service/modules/openclaw-control.js +128 -0
  28. package/service/modules/openclaw-enum.js +48 -0
  29. package/service/modules/opencode-service.js +199 -0
  30. package/service/modules/opencode-starter.js +194 -0
  31. package/service/modules/port-checker.js +31 -0
  32. package/service/modules/rongyun-message-handler.js +250 -0
  33. package/service/modules/rongyun-message-sender.js +157 -0
  34. package/service/modules/rongyun-message-types.js +28 -0
  35. package/service/modules/script-executor.js +550 -0
  36. package/service/modules/service-manager.js +319 -0
  37. package/service/modules/structured-message-router.js +118 -0
  38. package/service/rongcloud/env-polyfill.js +95 -0
  39. package/service/rongcloud/index.js +19 -0
  40. package/service/rongcloud/message-handler.js +147 -0
  41. package/service/rongcloud/message-types.js +22 -0
  42. package/service/rongcloud/openclaw-client.js +98 -0
  43. package/service/rongcloud/openclaw-config.js +108 -0
  44. package/service/rongcloud/rongcloud-client.js +273 -0
  45. package/service/rongcloud/types.js +9 -0
  46. package/service/updater.js +348 -0
  47. package/service/worker.js +376 -0
  48. 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
+ };