@claw-camp/openclaw-plugin 2.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/index.ts ADDED
@@ -0,0 +1,722 @@
1
+ /**
2
+ * Claw Camp Agent Plugin - 渠道 + Agent
3
+ *
4
+ * 功能:
5
+ * 1. 作为 Agent 上报系统状态
6
+ * 2. 作为渠道接收和发送消息
7
+ * 3. 执行远程任务
8
+ */
9
+
10
+ import { Type } from "@sinclair/typebox";
11
+ import type { OpenClawPluginApi, ChannelOnboardingAdapter, ClawdbotConfig } from "openclaw/plugin-sdk";
12
+ import WebSocket from "ws";
13
+
14
+ // ============ Helper Functions ============
15
+
16
+ function json(data: unknown) {
17
+ return {
18
+ content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
19
+ details: data,
20
+ };
21
+ }
22
+
23
+ // ============ Channel Client ============
24
+
25
+ const MAX_RECONNECT_ATTEMPTS = 10;
26
+ const RECONNECT_BASE_MS = 5000;
27
+
28
+ class ClawCampChannel {
29
+ private ws: WebSocket | null = null;
30
+ private api: OpenClawPluginApi;
31
+ private config: any;
32
+ private heartbeatTimer: NodeJS.Timeout | null = null;
33
+ private reconnectTimeout: NodeJS.Timeout | null = null;
34
+ private isShuttingDown = false;
35
+ private reconnectAttempts = 0;
36
+
37
+ constructor(api: OpenClawPluginApi, config: any) {
38
+ this.api = api;
39
+ this.config = {
40
+ hubUrl: 'wss://camp.aigc.sx.cn/ws',
41
+ agentId: config.botId || config.agentId,
42
+ agentName: config.agentName || 'Bot',
43
+ ...config
44
+ };
45
+ }
46
+
47
+ connect() {
48
+ if (this.isShuttingDown) return;
49
+
50
+ if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
51
+ this.api.logger.warn(`[Claw Camp Channel] 已超过最大重连次数 (${MAX_RECONNECT_ATTEMPTS}),停止重连`);
52
+ return;
53
+ }
54
+
55
+ const { hubUrl, botToken, token, agentId, agentName } = this.config;
56
+ const authToken = botToken || token;
57
+
58
+ this.api.logger.info(`[Claw Camp Channel] 连接 Hub: ${hubUrl} (尝试 #${this.reconnectAttempts + 1})`);
59
+
60
+ let ws: WebSocket;
61
+ try {
62
+ ws = new WebSocket(`${hubUrl}?token=${authToken}&agentId=${agentId}`);
63
+ } catch (err) {
64
+ this.api.logger.error('[Claw Camp Channel] 创建 WebSocket 失败:', String(err));
65
+ this._scheduleReconnect();
66
+ return;
67
+ }
68
+
69
+ // 先赋值再绑定事件,防止 error 在绑定前触发
70
+ this.ws = ws;
71
+
72
+ ws.on('open', () => {
73
+ this.reconnectAttempts = 0; // 连接成功,重置计数
74
+ this.api.logger.info('[Claw Camp Channel] 已连接到 Hub');
75
+
76
+ this._sendRaw({
77
+ type: 'register',
78
+ payload: {
79
+ id: agentId,
80
+ name: agentName,
81
+ host: require('os').hostname(),
82
+ agentVersion: this._getAgentVersion(),
83
+ channel: 'claw-camp',
84
+ capabilities: ['message', 'task']
85
+ }
86
+ });
87
+
88
+ // 心跳 + 状态上报
89
+ this.heartbeatTimer = setInterval(() => {
90
+ this._sendRaw({ type: 'heartbeat', payload: { id: agentId } });
91
+ this.reportStatus().catch((e) =>
92
+ this.api.logger.error('[Claw Camp Channel] 状态上报失败:', String(e))
93
+ );
94
+ }, 30000);
95
+
96
+ // 启动后立即上报一次
97
+ setTimeout(() => {
98
+ this.reportStatus().catch((e) =>
99
+ this.api.logger.error('[Claw Camp Channel] 初始状态上报失败:', String(e))
100
+ );
101
+ }, 3000);
102
+ });
103
+
104
+ ws.on('message', (data) => {
105
+ try {
106
+ const msg = JSON.parse(data.toString());
107
+ this.handleMessage(msg);
108
+ } catch (e) {
109
+ this.api.logger.error('[Claw Camp Channel] 解析消息失败:', String(e));
110
+ }
111
+ });
112
+
113
+ ws.on('close', (code, reason) => {
114
+ if (this.heartbeatTimer) {
115
+ clearInterval(this.heartbeatTimer);
116
+ this.heartbeatTimer = null;
117
+ }
118
+ if (!this.isShuttingDown) {
119
+ this.api.logger.info(`[Claw Camp Channel] 连接断开 (code=${code}),准备重连...`);
120
+ this._scheduleReconnect();
121
+ }
122
+ });
123
+
124
+ ws.on('error', (err) => {
125
+ // 只记录日志,不抛出 —— 防止 uncaught exception 崩溃 gateway
126
+ try {
127
+ const msg = err instanceof Error ? err.message : String(err);
128
+ this.api.logger.error('[Claw Camp Channel] 连接错误:', msg);
129
+ } catch (_) {
130
+ // ignore logging errors
131
+ }
132
+ });
133
+ }
134
+
135
+ private _scheduleReconnect() {
136
+ if (this.isShuttingDown) return;
137
+ this.reconnectAttempts++;
138
+ // 指数退避:5s, 10s, 20s, 40s ... 最大 60s
139
+ const delay = Math.min(RECONNECT_BASE_MS * Math.pow(2, this.reconnectAttempts - 1), 60000);
140
+ this.api.logger.info(`[Claw Camp Channel] ${delay / 1000}s 后重连 (第 ${this.reconnectAttempts} 次)...`);
141
+ this.reconnectTimeout = setTimeout(() => this.connect(), delay);
142
+ }
143
+
144
+ disconnect() {
145
+ this.isShuttingDown = true;
146
+ if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
147
+ if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = null; }
148
+ if (this.ws) {
149
+ try { this.ws.close(); } catch (_) {}
150
+ this.ws = null;
151
+ }
152
+ this.api.logger.info('[Claw Camp Channel] 已断开连接');
153
+ }
154
+
155
+ private _sendRaw(msg: any) {
156
+ try {
157
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
158
+ this.ws.send(JSON.stringify(msg));
159
+ }
160
+ } catch (e) {
161
+ this.api.logger.error('[Claw Camp Channel] 发送消息失败:', String(e));
162
+ }
163
+ }
164
+
165
+ // 获取 Gateway sessions(扫 .jsonl 文件)
166
+ private async getGatewaySessions(): Promise<any[]> {
167
+ const fs = require('fs');
168
+ const path = require('path');
169
+ const os = require('os');
170
+
171
+ const sessionsDir = path.join(os.homedir(), '.openclaw', 'agents', 'main', 'sessions');
172
+ try {
173
+ if (!fs.existsSync(sessionsDir)) return [];
174
+
175
+ const now = Date.now();
176
+ const oneDayMs = 24 * 60 * 60 * 1000;
177
+ const sessions: any[] = [];
178
+
179
+ const files = fs.readdirSync(sessionsDir).filter((f: string) =>
180
+ f.endsWith('.jsonl') && !f.includes('.deleted') && !f.includes('.reset')
181
+ );
182
+
183
+ for (const file of files.slice(0, 50)) { // 最多读50个
184
+ try {
185
+ const filePath = path.join(sessionsDir, file);
186
+ const stat = fs.statSync(filePath);
187
+ const updatedAt = stat.mtimeMs;
188
+ // 只取最近7天有活动的
189
+ if (now - updatedAt > 7 * oneDayMs) continue;
190
+
191
+ // 读最后一行获取 token 信息
192
+ const content = fs.readFileSync(filePath, 'utf8');
193
+ const lines = content.trim().split('\n').filter((l: string) => l.trim());
194
+ let totalTokens = 0, model = '', inputTokens = 0, outputTokens = 0;
195
+
196
+ // 从最后往前找最新有效的 usage(在 message.usage 里)
197
+ for (let i = lines.length - 1; i >= 0; i--) {
198
+ try {
199
+ const entry = JSON.parse(lines[i]);
200
+ const usage = entry.message?.usage || entry.usage;
201
+ if (usage && usage.totalTokens > 0) {
202
+ inputTokens = usage.input || 0;
203
+ outputTokens = usage.output || 0;
204
+ totalTokens = usage.totalTokens;
205
+ model = entry.message?.model || entry.model || model;
206
+ break;
207
+ }
208
+ } catch { /* skip */ }
209
+ }
210
+
211
+ sessions.push({
212
+ key: file.replace('.jsonl', ''),
213
+ updatedAt,
214
+ model,
215
+ inputTokens,
216
+ outputTokens,
217
+ totalTokens,
218
+ lastActive: updatedAt,
219
+ });
220
+ } catch { /* skip */ }
221
+ }
222
+
223
+ return sessions;
224
+ } catch {
225
+ return [];
226
+ }
227
+ }
228
+
229
+ // 获取已加载的 plugins(从 extensions 目录扫描)
230
+ private async getGatewayPlugins(): Promise<any[]> {
231
+ const fs = require('fs');
232
+ const path = require('path');
233
+ const os = require('os');
234
+ const plugins: any[] = [];
235
+
236
+ // 只扫用户自装插件,stock 插件太多且大多未启用
237
+ const dirs = [
238
+ path.join(os.homedir(), '.openclaw', 'extensions'),
239
+ ];
240
+
241
+ for (const dir of dirs) {
242
+ try {
243
+ if (!fs.existsSync(dir)) continue;
244
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
245
+ for (const entry of entries) {
246
+ if (!entry.isDirectory()) continue;
247
+ const pkgPath = path.join(dir, entry.name, 'openclaw.plugin.json');
248
+ const pkg2Path = path.join(dir, entry.name, 'package.json');
249
+ try {
250
+ if (fs.existsSync(pkgPath)) {
251
+ const meta = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
252
+ plugins.push({ name: meta.name || entry.name, id: meta.id || entry.name, version: meta.version || '1.0.0' });
253
+ } else if (fs.existsSync(pkg2Path)) {
254
+ const meta = JSON.parse(fs.readFileSync(pkg2Path, 'utf8'));
255
+ if (meta.keywords?.includes('openclaw')) {
256
+ plugins.push({ name: meta.name || entry.name, id: entry.name, version: meta.version || '1.0.0' });
257
+ }
258
+ }
259
+ } catch { /* skip */ }
260
+ }
261
+ } catch { /* skip */ }
262
+ }
263
+
264
+ return plugins;
265
+ }
266
+
267
+ // 获取真实 openclaw 版本
268
+ private _getAgentVersion(): string {
269
+ const fs = require('fs');
270
+ const candidates = [
271
+ '/Users/phosa/.nvm/versions/node/v22.22.0/lib/node_modules/openclaw/package.json',
272
+ '/opt/homebrew/lib/node_modules/openclaw/package.json',
273
+ ];
274
+ for (const p of candidates) {
275
+ try {
276
+ if (fs.existsSync(p)) {
277
+ const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
278
+ if (pkg.version) return pkg.version;
279
+ }
280
+ } catch { /* skip */ }
281
+ }
282
+ return '1.5.0';
283
+ }
284
+
285
+ // 检测 Gateway 是否存活
286
+ private _checkGatewayAlive(): Promise<boolean> {
287
+ const http = require('http');
288
+ const port = parseInt(String(process.env.OPENCLAW_GATEWAY_PORT || '18789'));
289
+ return new Promise((resolve) => {
290
+ const req = http.request({
291
+ hostname: 'localhost', port, path: '/api/health',
292
+ method: 'GET', timeout: 2000
293
+ }, () => resolve(true));
294
+ req.on('error', () => resolve(false));
295
+ req.on('timeout', () => { req.destroy(); resolve(false); });
296
+ req.end();
297
+ });
298
+ }
299
+
300
+ // 上报详细状态
301
+ async reportStatus() {
302
+ if (this.isShuttingDown || !this.ws || this.ws.readyState !== WebSocket.OPEN) return;
303
+
304
+ let sessions: any[] = [];
305
+ let plugins: any[] = [];
306
+
307
+ try {
308
+ const [s, p] = await Promise.all([this.getGatewaySessions(), this.getGatewayPlugins()]);
309
+ sessions = s || [];
310
+ plugins = p || [];
311
+ } catch {
312
+ // 忽略,继续上报基础信息
313
+ }
314
+
315
+ const gatewayRunning = sessions.length > 0 || plugins.length > 0 ||
316
+ await this._checkGatewayAlive();
317
+ const todayStr = new Date().toDateString();
318
+ const status = {
319
+ id: this.config.agentId,
320
+ host: require('os').hostname(),
321
+ agentVersion: this._getAgentVersion(),
322
+ gateway: {
323
+ running: gatewayRunning,
324
+ status: gatewayRunning ? 'running' : 'stopped',
325
+ port: parseInt(String(process.env.OPENCLAW_GATEWAY_PORT || '18789'))
326
+ },
327
+ sessions: {
328
+ count: sessions.length,
329
+ todayActive: sessions.filter((s: any) => {
330
+ try {
331
+ const t = s.updatedAt || s.lastActive;
332
+ return t && new Date(t).toDateString() === todayStr;
333
+ } catch { return false; }
334
+ }).length,
335
+ list: sessions.slice(0, 10).map((s: any) => ({
336
+ key: s.key,
337
+ model: s.model,
338
+ updatedAt: s.updatedAt,
339
+ totalTokens: s.totalTokens || 0,
340
+ }))
341
+ },
342
+ plugins: plugins.map((p: any) => ({ name: p.name || p.id, version: p.version || '1.0.0' })),
343
+ tokenUsage: sessions.map((s: any) => ({
344
+ sessionKey: s.key,
345
+ inputTokens: s.inputTokens || 0,
346
+ outputTokens: s.outputTokens || 0,
347
+ totalTokens: s.totalTokens || 0,
348
+ model: s.model,
349
+ updatedAt: s.updatedAt,
350
+ })),
351
+ stats: {
352
+ cpu: (() => {
353
+ try {
354
+ const os = require('os');
355
+ const cpus = os.cpus();
356
+ const total = cpus.reduce((a: any, c: any) => {
357
+ const t = Object.values(c.times as Record<string, number>).reduce((x: number, y: number) => x + y, 0);
358
+ return { idle: a.idle + (c.times as any).idle, total: a.total + t };
359
+ }, { idle: 0, total: 0 });
360
+ return Math.round((1 - total.idle / total.total) * 100);
361
+ } catch { return 0; }
362
+ })(),
363
+ memory: {
364
+ heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
365
+ heapTotal: Math.round(process.memoryUsage().heapTotal / 1024 / 1024),
366
+ rss: Math.round(process.memoryUsage().rss / 1024 / 1024)
367
+ },
368
+ uptime: Math.round(process.uptime())
369
+ },
370
+ startTime: Date.now() - process.uptime() * 1000,
371
+ uptime: process.uptime()
372
+ };
373
+
374
+ this._sendRaw({ type: 'status', payload: status });
375
+ }
376
+
377
+ handleMessage(msg: any) {
378
+ const { type, payload } = msg;
379
+ switch (type) {
380
+ case 'message':
381
+ this.api.logger.info('[Claw Camp Channel] 收到消息:', payload);
382
+ try {
383
+ this.api.emit('message:received', {
384
+ channel: 'claw-camp',
385
+ accountId: this.config.agentId,
386
+ from: payload.from,
387
+ to: this.config.agentId,
388
+ content: payload.content,
389
+ timestamp: Date.now()
390
+ });
391
+ } catch (e) {
392
+ this.api.logger.error('[Claw Camp Channel] emit 失败:', String(e));
393
+ }
394
+ break;
395
+ case 'task':
396
+ this.api.logger.info('[Claw Camp Channel] 收到任务:', payload);
397
+ this.executeTask(payload).catch((e) =>
398
+ this.api.logger.error('[Claw Camp Channel] 任务执行失败:', String(e))
399
+ );
400
+ break;
401
+ case 'registered':
402
+ this.api.logger.info('[Claw Camp Channel] 注册成功:', payload?.id);
403
+ break;
404
+ default:
405
+ // 未知消息类型,忽略
406
+ break;
407
+ }
408
+ }
409
+
410
+ async executeTask(task: any) {
411
+ const { action } = task;
412
+ let result: any;
413
+
414
+ try {
415
+ switch (action) {
416
+ case 'check-social-monitor': {
417
+ const { execSync } = require('child_process');
418
+ const output = execSync('python3 ~/.openclaw/workspace/scripts/do-social-monitor.py', {
419
+ encoding: 'utf-8',
420
+ timeout: 60000
421
+ });
422
+ result = { success: true, output };
423
+ break;
424
+ }
425
+ case 'send-email':
426
+ result = { success: true };
427
+ break;
428
+ default:
429
+ result = { success: false, error: `未知任务: ${action}` };
430
+ }
431
+ } catch (e) {
432
+ result = { success: false, error: e instanceof Error ? e.message : String(e) };
433
+ }
434
+
435
+ this._sendRaw({ type: 'task-result', payload: { taskId: task.id, ...result } });
436
+ }
437
+
438
+ sendMessage(to: string, content: string) {
439
+ this._sendRaw({
440
+ type: 'message',
441
+ payload: { from: this.config.agentId, to, content, timestamp: Date.now() }
442
+ });
443
+ }
444
+ }
445
+
446
+ // ============ Onboarding Adapter ============
447
+
448
+ interface ClawCampAccountConfig {
449
+ botId?: string;
450
+ botToken?: string;
451
+ agentName?: string;
452
+ }
453
+
454
+ interface ClawCampChannelConfig {
455
+ enabled?: boolean;
456
+ dmPolicy?: string;
457
+ groupPolicy?: string;
458
+ accounts?: Record<string, ClawCampAccountConfig>;
459
+ allowFrom?: string[];
460
+ }
461
+
462
+ function getNextAccountName(accounts: Record<string, any>): string {
463
+ if (!accounts || Object.keys(accounts).length === 0) return 'default';
464
+ let maxN = 0;
465
+ for (const key of Object.keys(accounts)) {
466
+ if (key === 'default') { maxN = Math.max(maxN, 1); }
467
+ else if (key.startsWith('default')) {
468
+ const n = parseInt(key.replace('default', ''), 10);
469
+ if (!isNaN(n)) maxN = Math.max(maxN, n);
470
+ }
471
+ }
472
+ return maxN === 0 ? 'default' : `default${maxN + 1}`;
473
+ }
474
+
475
+ function setClawCampAccount(cfg: ClawdbotConfig, accountName: string, accountConfig: ClawCampAccountConfig): ClawdbotConfig {
476
+ const channelConfig = (cfg.channels?.['claw-camp'] as ClawCampChannelConfig | undefined) || {};
477
+ const accounts = channelConfig.accounts || {};
478
+ return {
479
+ ...cfg,
480
+ channels: {
481
+ ...cfg.channels,
482
+ 'claw-camp': {
483
+ ...channelConfig,
484
+ enabled: true,
485
+ dmPolicy: channelConfig.dmPolicy || 'open',
486
+ groupPolicy: channelConfig.groupPolicy || 'open',
487
+ accounts: { ...accounts, [accountName]: accountConfig },
488
+ allowFrom: channelConfig.allowFrom || ['*']
489
+ }
490
+ }
491
+ };
492
+ }
493
+
494
+ const clawCampOnboardingAdapter: ChannelOnboardingAdapter = {
495
+ channel: 'claw-camp',
496
+ getStatus: async ({ cfg }) => {
497
+ const channelConfig = cfg.channels?.['claw-camp'] as ClawCampChannelConfig | undefined;
498
+ const accounts = channelConfig?.accounts || {};
499
+ const accountCount = Object.keys(accounts).length;
500
+ const configured = accountCount > 0;
501
+ return {
502
+ channel: 'claw-camp',
503
+ configured,
504
+ statusLines: configured
505
+ ? [`Claw Camp: ${accountCount} 个账号 (${Object.keys(accounts).join(', ')})`]
506
+ : ['Claw Camp: 未配置'],
507
+ selectionHint: configured ? '已配置' : '需要 Bot 凭据',
508
+ quickstartScore: configured ? 2 : 0
509
+ };
510
+ },
511
+ configure: async ({ cfg, prompter }) => {
512
+ const channelConfig = cfg.channels?.['claw-camp'] as ClawCampChannelConfig | undefined;
513
+ const accounts = channelConfig?.accounts || {};
514
+ const defaultName = getNextAccountName(accounts);
515
+
516
+ await prompter.note(
517
+ ['配置 Claw Camp 渠道', '', 'Claw Camp 是一个 Agent 监控和管理平台。', '你需要先在 https://camp.aigc.sx.cn 创建 Bot,获取 Bot ID 和 Token。'].join('\n'),
518
+ 'Claw Camp 配置向导'
519
+ );
520
+
521
+ const accountName = await prompter.text({ message: '账号名称', placeholder: defaultName, initialValue: defaultName });
522
+ if (!accountName?.trim()) return { cfg, status: 'cancelled' };
523
+
524
+ const botId = await prompter.text({ message: 'Bot ID (格式: bot_xxxxx)', placeholder: 'bot_xxxxxxxxxxxxxxxx' });
525
+ if (!botId?.trim()) return { cfg, status: 'cancelled' };
526
+
527
+ const botToken = await prompter.text({ message: 'Bot Token', placeholder: '从 Dashboard 复制' });
528
+ if (!botToken?.trim()) return { cfg, status: 'cancelled' };
529
+
530
+ const newCfg = setClawCampAccount(cfg, accountName.trim(), { botId: botId.trim(), botToken: botToken.trim() });
531
+
532
+ await prompter.note(
533
+ ['✅ 配置完成!', '', `账号: ${accountName}`, `Bot ID: ${botId}`, '', '重启 Gateway 后生效。'].join('\n'),
534
+ '配置成功'
535
+ );
536
+
537
+ return { cfg: newCfg, status: 'configured', restartRequired: true };
538
+ }
539
+ };
540
+
541
+ // ============ Plugin Export ============
542
+
543
+ export default function (api: OpenClawPluginApi) {
544
+ const { logger, config } = api;
545
+
546
+ logger.info("[Claw Camp Agent] 插件已加载");
547
+
548
+ const channels = new Map<string, ClawCampChannel>();
549
+
550
+ const startChannels = () => {
551
+ const accounts = config.channels?.['claw-camp']?.accounts || {};
552
+ const accountKeys = Object.keys(accounts);
553
+
554
+ if (accountKeys.length === 0) {
555
+ logger.warn('[Claw Camp] 未配置 accounts,跳过连接');
556
+ return;
557
+ }
558
+
559
+ for (const accountKey of accountKeys) {
560
+ const accountConfig = accounts[accountKey];
561
+ const { botId, botToken } = accountConfig;
562
+ if (botId && botToken) {
563
+ const agentName = accountConfig.agentName || accountKey;
564
+ logger.info(`[Claw Camp] 启动连接: botId=${botId}, name=${agentName}`);
565
+ const channel = new ClawCampChannel(api, {
566
+ hubUrl: 'wss://camp.aigc.sx.cn/ws',
567
+ token: botToken,
568
+ agentId: botId,
569
+ agentName
570
+ });
571
+ channels.set(accountKey, channel);
572
+ channel.connect();
573
+ } else {
574
+ logger.warn(`[Claw Camp] account ${accountKey} 缺少 botId/botToken`);
575
+ }
576
+ }
577
+ };
578
+
579
+ const stopAllChannels = () => {
580
+ logger.info(`[Claw Camp] 停止 ${channels.size} 个连接...`);
581
+ for (const [key, channel] of channels) {
582
+ logger.info(`[Claw Camp] 断开 ${key}`);
583
+ channel.disconnect();
584
+ }
585
+ channels.clear();
586
+ };
587
+
588
+ // 注册 shutdown hook(只注册一次)
589
+ process.once('SIGTERM', stopAllChannels);
590
+ process.once('SIGINT', stopAllChannels);
591
+ api.on?.('shutdown', stopAllChannels);
592
+
593
+ // 延迟启动,等 Gateway 完全就绪
594
+ setTimeout(startChannels, 2000);
595
+
596
+ // 注册渠道
597
+ if (api.registerChannel) {
598
+ api.registerChannel({
599
+ plugin: {
600
+ id: 'claw-camp',
601
+ meta: {
602
+ id: 'claw-camp',
603
+ label: 'Claw Camp',
604
+ selectionLabel: 'Claw Camp (龙虾营地)',
605
+ docsPath: '/channels/claw-camp',
606
+ docsLabel: 'claw-camp',
607
+ blurb: 'Agent 监控和管理平台',
608
+ order: 100
609
+ },
610
+ capabilities: {
611
+ chatTypes: ['direct'],
612
+ polls: false,
613
+ threads: false,
614
+ media: false,
615
+ reactions: false,
616
+ edit: false,
617
+ reply: false
618
+ },
619
+ configSchema: {
620
+ schema: {
621
+ type: 'object',
622
+ properties: {
623
+ enabled: { type: 'boolean' },
624
+ dmPolicy: { type: 'string', enum: ['open', 'pairing', 'allowlist'] },
625
+ groupPolicy: { type: 'string', enum: ['open', 'allowlist', 'disabled'] },
626
+ allowFrom: { type: 'array', items: { type: 'string' } },
627
+ accounts: {
628
+ type: 'object',
629
+ additionalProperties: {
630
+ type: 'object',
631
+ properties: {
632
+ botId: { type: 'string' },
633
+ botToken: { type: 'string' },
634
+ agentName: { type: 'string' }
635
+ }
636
+ }
637
+ }
638
+ }
639
+ }
640
+ },
641
+ config: {
642
+ listAccountIds: (cfg: any) => Object.keys(cfg?.accounts || {}),
643
+ resolveAccount: (cfg: any, accountId: string) => cfg?.accounts?.[accountId] || null
644
+ },
645
+ onboarding: clawCampOnboardingAdapter,
646
+ reload: { configPrefixes: ['channels.claw-camp'] }
647
+ }
648
+ });
649
+ }
650
+
651
+ // 工具:启动 Agent
652
+ api.registerTool({
653
+ name: "start_claw_camp_agent",
654
+ description: "启动龙虾营地监控 Agent,连接到 Hub 并开始上报数据",
655
+ inputSchema: Type.Object({
656
+ hubUrl: Type.Optional(Type.String({ default: config.hubUrl || "wss://camp.aigc.sx.cn", description: "Hub WebSocket 地址" })),
657
+ token: Type.Optional(Type.String({ default: config.token || "", description: "Camp Token" })),
658
+ agentId: Type.Optional(Type.String({ default: config.agentId || "main", description: "Agent 唯一标识" })),
659
+ agentName: Type.Optional(Type.String({ default: config.agentName || "大龙虾", description: "Agent 显示名称" }))
660
+ }),
661
+ handler: async (params) => {
662
+ const { hubUrl, token, agentId, agentName } = params;
663
+ logger.info(`[Claw Camp Agent] 启动 Agent: ${agentId} -> ${hubUrl}`);
664
+ return json({ success: true, message: "请在终端运行以下命令启动 Agent:", command: `cd ~/.openclaw/extensions/claw-camp && node src/agent.js`, env: { CLAW_HUB_URL: hubUrl, CLAW_CAMP_TOKEN: token, CLAW_AGENT_ID: agentId, CLAW_AGENT_NAME: agentName } });
665
+ }
666
+ });
667
+
668
+ // 工具:查看 Agent 状态
669
+ api.registerTool({
670
+ name: "check_claw_camp_agent",
671
+ description: "查看龙虾营地监控 Agent 的运行状态",
672
+ inputSchema: Type.Object({}),
673
+ handler: async () => {
674
+ const { execSync } = require('child_process');
675
+ try {
676
+ const running = execSync('pgrep -f "node.*agent.js"', { encoding: 'utf-8' }).trim();
677
+ return json({ success: true, status: "running", pid: parseInt(running), message: "Agent 正在运行" });
678
+ } catch {
679
+ return json({ success: true, status: "stopped", message: "Agent 未运行" });
680
+ }
681
+ }
682
+ });
683
+
684
+ // 工具:停止 Agent
685
+ api.registerTool({
686
+ name: "stop_claw_camp_agent",
687
+ description: "停止龙虾营地监控 Agent",
688
+ inputSchema: Type.Object({}),
689
+ handler: async () => {
690
+ const { execSync } = require('child_process');
691
+ try {
692
+ execSync('pkill -f "node.*agent.js"');
693
+ logger.info("[Claw Camp Agent] Agent 已停止");
694
+ return json({ success: true, message: "Agent 已停止" });
695
+ } catch {
696
+ return json({ success: true, message: "Agent 未运行,无需停止" });
697
+ }
698
+ }
699
+ });
700
+
701
+ // 工具:查看 Hub 状态
702
+ api.registerTool({
703
+ name: "check_claw_camp_hub",
704
+ description: "查看龙虾营地 Hub 的状态和版本信息",
705
+ inputSchema: Type.Object({}),
706
+ handler: async () => {
707
+ const https = require('https');
708
+ return new Promise((resolve) => {
709
+ const req = https.get('https://camp.aigc.sx.cn/api/version', (res: any) => {
710
+ let data = '';
711
+ res.on('data', (chunk: any) => data += chunk);
712
+ res.on('end', () => {
713
+ try { resolve(json({ success: true, hub: JSON.parse(data), url: "https://camp.aigc.sx.cn" })); }
714
+ catch { resolve(json({ success: false, error: "无法解析 Hub 响应" })); }
715
+ });
716
+ });
717
+ req.on('error', (e: Error) => resolve(json({ success: false, error: e.message })));
718
+ req.setTimeout(5000, () => { req.destroy(); resolve(json({ success: false, error: "超时" })); });
719
+ });
720
+ }
721
+ });
722
+ }