@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/INSTALL_GUIDE.md +315 -0
- package/Makefile +91 -0
- package/QUICKSTART.md +67 -0
- package/README.md +75 -0
- package/README_TEMPLATE.md +223 -0
- package/index.ts +722 -0
- package/install.sh +78 -0
- package/openclaw.plugin.json +48 -0
- package/package.json +24 -0
- package/quick-install.sh +109 -0
- package/src/agent.js +665 -0
- package/uninstall.sh +28 -0
package/src/agent.js
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claw Agent - 本地监控 Agent + 渠道
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* - 连接到 Hub
|
|
6
|
+
* - 采集 Gateway 状态
|
|
7
|
+
* - 采集会话列表
|
|
8
|
+
* - 采集系统资源
|
|
9
|
+
* - 解析 session .jsonl 获取精确 token 使用(按半小时槽聚合)
|
|
10
|
+
* - 定时上报
|
|
11
|
+
* - 接收和发送消息(渠道功能)
|
|
12
|
+
* - 执行远程任务
|
|
13
|
+
*
|
|
14
|
+
* @name 龙虾营地 Agent
|
|
15
|
+
* @version 2.0.0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const WebSocket = require('ws');
|
|
19
|
+
const { execSync, spawn } = require('child_process');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
// Agent 信息
|
|
25
|
+
const AGENT_NAME = '龙虾营地 Agent';
|
|
26
|
+
const AGENT_VERSION = '1.5.0';
|
|
27
|
+
const GITHUB_REPO = 'https://github.com/PhosAQy/claw-hub';
|
|
28
|
+
const AGENT_START_TIME = Date.now(); // Agent 启动时间
|
|
29
|
+
|
|
30
|
+
// 配置
|
|
31
|
+
const CONFIG = {
|
|
32
|
+
hubUrl: process.env.CLAW_HUB_URL || 'ws://server.aigc.sx.cn:8889',
|
|
33
|
+
agentId: process.env.CLAW_AGENT_ID || 'main',
|
|
34
|
+
agentName: process.env.CLAW_AGENT_NAME || '大龙虾',
|
|
35
|
+
reportInterval: 5000, // 上报间隔
|
|
36
|
+
gatewayPort: 18789, // Gateway 端口
|
|
37
|
+
gatewayToken: process.env.CLAW_GATEWAY_TOKEN || '', // Gateway Token (从环境变量读取)
|
|
38
|
+
sessionsDir: path.join(os.homedir(), '.openclaw/agents/main/sessions'),
|
|
39
|
+
updateToken: process.env.CLAW_UPDATE_TOKEN || '' // 更新令牌
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
let ws = null;
|
|
43
|
+
let reconnectTimer = null;
|
|
44
|
+
|
|
45
|
+
// 获取主机名
|
|
46
|
+
function getHostname() {
|
|
47
|
+
return os.hostname();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 检查 Gateway 状态
|
|
51
|
+
function getGatewayStatus() {
|
|
52
|
+
try {
|
|
53
|
+
const result = execSync(`ps aux | grep -v grep | grep -c "openclaw-gateway"`, {
|
|
54
|
+
encoding: 'utf-8',
|
|
55
|
+
timeout: 3000
|
|
56
|
+
}).trim();
|
|
57
|
+
const isRunning = parseInt(result) > 0;
|
|
58
|
+
return { status: isRunning ? 'running' : 'stopped', port: CONFIG.gatewayPort };
|
|
59
|
+
} catch (e) {
|
|
60
|
+
return { status: 'stopped', port: CONFIG.gatewayPort };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 启动 Gateway
|
|
65
|
+
async function startGateway() {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
try {
|
|
68
|
+
// 先安装服务(如果未安装)
|
|
69
|
+
execSync('openclaw gateway install', { timeout: 10000, encoding: 'utf8' });
|
|
70
|
+
console.log('[Agent] Gateway 服务已安装');
|
|
71
|
+
|
|
72
|
+
// 再启动服务
|
|
73
|
+
const stdout = execSync('openclaw gateway start', { timeout: 10000, encoding: 'utf8' });
|
|
74
|
+
console.log('[Agent] Gateway 已启动');
|
|
75
|
+
resolve({ success: true, message: 'Gateway 已启动', output: stdout });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error('[Agent] Gateway 启动失败:', err.stderr || err.message);
|
|
78
|
+
resolve({ success: false, error: err.stderr || err.message });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 停止 Gateway
|
|
84
|
+
async function stopGateway() {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
try {
|
|
87
|
+
const stdout = execSync('openclaw gateway stop', { timeout: 10000, encoding: 'utf8' });
|
|
88
|
+
console.log('[Agent] Gateway 已停止');
|
|
89
|
+
resolve({ success: true, message: 'Gateway 已停止', output: stdout });
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error('[Agent] Gateway 停止失败:', err.stderr || err.message);
|
|
92
|
+
resolve({ success: false, error: err.stderr || err.message });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 重启 Gateway
|
|
98
|
+
async function restartGateway() {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
try {
|
|
101
|
+
const stdout = execSync('openclaw gateway restart', { timeout: 30000, encoding: 'utf8' });
|
|
102
|
+
console.log('[Agent] Gateway 已重启');
|
|
103
|
+
resolve({ success: true, message: 'Gateway 已重启', output: stdout });
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.error('[Agent] Gateway 重启失败:', err.stderr || err.message);
|
|
106
|
+
resolve({ success: false, error: err.stderr || err.message });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 获取会话列表(用于显示)
|
|
112
|
+
function getSessions() {
|
|
113
|
+
try {
|
|
114
|
+
const result = execSync('openclaw sessions --json 2>/dev/null', {
|
|
115
|
+
encoding: 'utf-8',
|
|
116
|
+
timeout: 5000
|
|
117
|
+
});
|
|
118
|
+
const data = JSON.parse(result);
|
|
119
|
+
const sessions = data.sessions || [];
|
|
120
|
+
|
|
121
|
+
const recentSessions = sessions
|
|
122
|
+
.filter(s => s.ageMs < 24 * 60 * 60 * 1000)
|
|
123
|
+
.slice(0, 50)
|
|
124
|
+
.map(s => ({
|
|
125
|
+
key: s.key,
|
|
126
|
+
kind: s.kind || 'direct',
|
|
127
|
+
model: s.model,
|
|
128
|
+
tokens: s.totalTokens,
|
|
129
|
+
inputTokens: s.inputTokens || 0,
|
|
130
|
+
outputTokens: s.outputTokens || 0,
|
|
131
|
+
updatedAt: s.updatedAt,
|
|
132
|
+
age: Math.round((s.ageMs || 0) / 60000) + '分钟前'
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
count: sessions.length,
|
|
137
|
+
todayActive: sessions.filter(s => s.ageMs < 24 * 60 * 60 * 1000).length,
|
|
138
|
+
list: recentSessions
|
|
139
|
+
};
|
|
140
|
+
} catch (e) {
|
|
141
|
+
return { count: 0, todayActive: 0, list: [] };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 解析 session .jsonl 文件,获取精确的 token 使用数据
|
|
147
|
+
* 按半小时槽聚合
|
|
148
|
+
*/
|
|
149
|
+
function getTokenUsage(hours = 6) {
|
|
150
|
+
const cutoff = Date.now() - hours * 3600 * 1000;
|
|
151
|
+
const slots = {};
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const files = fs.readdirSync(CONFIG.sessionsDir)
|
|
155
|
+
.filter(f => f.endsWith('.jsonl') && !f.includes('.deleted.'));
|
|
156
|
+
|
|
157
|
+
for (const file of files) {
|
|
158
|
+
const sessionId = file.replace('.jsonl', '');
|
|
159
|
+
const filePath = path.join(CONFIG.sessionsDir, file);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
163
|
+
const lines = content.split('\n').filter(Boolean);
|
|
164
|
+
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
try {
|
|
167
|
+
const record = JSON.parse(line);
|
|
168
|
+
|
|
169
|
+
if (record.type === 'message' && record.message?.usage) {
|
|
170
|
+
const ts = new Date(record.timestamp).getTime();
|
|
171
|
+
if (ts >= cutoff) {
|
|
172
|
+
// 计算半小时槽
|
|
173
|
+
const d = new Date(ts);
|
|
174
|
+
const hour = d.getHours().toString().padStart(2, '0');
|
|
175
|
+
const minute = d.getMinutes() < 30 ? '00' : '30';
|
|
176
|
+
const slot = `${hour}:${minute}`;
|
|
177
|
+
|
|
178
|
+
if (!slots[slot]) {
|
|
179
|
+
slots[slot] = { slot, input: 0, output: 0, cacheRead: 0, count: 0 };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const usage = record.message.usage;
|
|
183
|
+
slots[slot].input += usage.input || 0;
|
|
184
|
+
slots[slot].output += usage.output || 0;
|
|
185
|
+
slots[slot].cacheRead += usage.cacheRead || 0;
|
|
186
|
+
slots[slot].count += 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
// 跳过解析失败的行
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch (e) {
|
|
194
|
+
// 跳过无法读取的文件
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch (e) {
|
|
198
|
+
// 目录不存在或无权限
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 转换为数组
|
|
202
|
+
return Object.values(slots)
|
|
203
|
+
.map(s => ({
|
|
204
|
+
...s,
|
|
205
|
+
// 总消耗 = 输入 + 输出(缓存命中是优化指标,不影响实际消耗)
|
|
206
|
+
netTokens: s.input + s.output
|
|
207
|
+
}))
|
|
208
|
+
.sort((a, b) => a.slot.localeCompare(b.slot));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 获取系统资源
|
|
212
|
+
function getSystemStats() {
|
|
213
|
+
try {
|
|
214
|
+
let cpu = 0;
|
|
215
|
+
const cpuInfo = execSync('top -l 1 -n 0 | grep "CPU usage" 2>/dev/null || echo ""', {
|
|
216
|
+
encoding: 'utf-8',
|
|
217
|
+
timeout: 3000
|
|
218
|
+
});
|
|
219
|
+
const cpuMatch = cpuInfo.match(/(\d+\.?\d*)\s*%/);
|
|
220
|
+
if (cpuMatch) cpu = parseFloat(cpuMatch[1]);
|
|
221
|
+
|
|
222
|
+
const totalMem = os.totalmem();
|
|
223
|
+
const freeMem = os.freemem();
|
|
224
|
+
const memory = Math.round((1 - freeMem / totalMem) * 100);
|
|
225
|
+
|
|
226
|
+
// 从精确数据计算今日 token
|
|
227
|
+
const tokenUsage = getTokenUsage(24);
|
|
228
|
+
const todayTokens = tokenUsage.reduce((sum, s) => sum + s.netTokens, 0);
|
|
229
|
+
|
|
230
|
+
return { cpu, memory, todayTokens };
|
|
231
|
+
} catch (e) {
|
|
232
|
+
return { cpu: 0, memory: 0, todayTokens: 0 };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 获取已加载的插件列表
|
|
237
|
+
function getPlugins() {
|
|
238
|
+
try {
|
|
239
|
+
const result = execSync('openclaw plugins list 2>/dev/null', {
|
|
240
|
+
encoding: 'utf-8',
|
|
241
|
+
timeout: 10000
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// 解析表格格式
|
|
245
|
+
const plugins = [];
|
|
246
|
+
const lines = result.split('\n');
|
|
247
|
+
let inTable = false;
|
|
248
|
+
|
|
249
|
+
for (const line of lines) {
|
|
250
|
+
// 检测表格开始(包含表头分隔符)
|
|
251
|
+
if (line.includes('─') && line.includes('┼')) {
|
|
252
|
+
inTable = true;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 检测表格结束
|
|
257
|
+
if (inTable && line.includes('└')) {
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 解析表格行(只处理包含 loaded 的行)
|
|
262
|
+
if (inTable && line.includes('│') && line.includes('loaded')) {
|
|
263
|
+
const cols = line.split('│').map(c => c.trim()).filter(c => c);
|
|
264
|
+
if (cols.length >= 4) {
|
|
265
|
+
const [name, id, status, source, version] = cols;
|
|
266
|
+
|
|
267
|
+
// 从 source 推断完整名称
|
|
268
|
+
let fullName = name || id;
|
|
269
|
+
if (source.includes('device-pair')) {
|
|
270
|
+
fullName = 'Device Pairing';
|
|
271
|
+
} else if (source.includes('feishu-card')) {
|
|
272
|
+
fullName = 'Feishu Interactive Card';
|
|
273
|
+
} else if (source.includes('feishu/index')) {
|
|
274
|
+
fullName = 'Feishu';
|
|
275
|
+
} else if (source.includes('memory-core')) {
|
|
276
|
+
fullName = 'Memory (Core)';
|
|
277
|
+
} else if (source.includes('phone-control')) {
|
|
278
|
+
fullName = 'Phone Control';
|
|
279
|
+
} else if (source.includes('talk-voice')) {
|
|
280
|
+
fullName = 'Talk Voice';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
plugins.push({
|
|
284
|
+
name: fullName,
|
|
285
|
+
id: id || 'unknown',
|
|
286
|
+
version: version || 'unknown',
|
|
287
|
+
source: source || ''
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return plugins;
|
|
294
|
+
} catch (e) {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ──────────────────────────────────────────────
|
|
300
|
+
// 版本管理和更新
|
|
301
|
+
// ──────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 获取最新版本(从 GitHub tags)
|
|
305
|
+
*/
|
|
306
|
+
async function getLatestVersion() {
|
|
307
|
+
return new Promise((resolve) => {
|
|
308
|
+
exec('git ls-remote --tags origin', { timeout: 10000 }, (err, stdout) => {
|
|
309
|
+
if (err) {
|
|
310
|
+
resolve(AGENT_VERSION);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const tags = stdout.split('\n')
|
|
315
|
+
.filter(line => line.includes('refs/tags/'))
|
|
316
|
+
.map(line => line.split('refs/tags/')[1])
|
|
317
|
+
.filter(tag => tag && tag.startsWith('v'))
|
|
318
|
+
.sort((a, b) => b.localeCompare(a));
|
|
319
|
+
|
|
320
|
+
resolve(tags[0] ? tags[0].replace('v', '') : AGENT_VERSION);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 执行更新
|
|
327
|
+
*/
|
|
328
|
+
async function doUpdate() {
|
|
329
|
+
return new Promise((resolve) => {
|
|
330
|
+
const projectDir = path.join(__dirname, '..');
|
|
331
|
+
|
|
332
|
+
exec('git pull', { cwd: projectDir, timeout: 30000 }, (err, stdout, stderr) => {
|
|
333
|
+
if (err) {
|
|
334
|
+
resolve({ success: false, error: stderr || err.message });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const updated = !stdout.includes('Already up to date');
|
|
339
|
+
|
|
340
|
+
if (updated) {
|
|
341
|
+
// 更新成功,准备重启
|
|
342
|
+
console.log('[Agent] 更新成功,即将重启...');
|
|
343
|
+
setTimeout(() => {
|
|
344
|
+
process.exit(0); // 退出进程,依赖外部进程管理器重启
|
|
345
|
+
}, 1000);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
resolve({
|
|
349
|
+
success: true,
|
|
350
|
+
updated,
|
|
351
|
+
message: updated ? '更新成功,即将重启' : '已是最新版本',
|
|
352
|
+
version: AGENT_VERSION
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 发送消息
|
|
359
|
+
function send(msg) {
|
|
360
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
361
|
+
ws.send(JSON.stringify(msg));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 上报状态
|
|
366
|
+
function reportStatus() {
|
|
367
|
+
const gateway = getGatewayStatus();
|
|
368
|
+
const sessions = getSessions();
|
|
369
|
+
const stats = getSystemStats();
|
|
370
|
+
const tokenUsage = getTokenUsage(6); // 最近 6 小时
|
|
371
|
+
const plugins = getPlugins(); // 获取插件列表
|
|
372
|
+
|
|
373
|
+
// 计算运行时长
|
|
374
|
+
const uptimeMs = Date.now() - AGENT_START_TIME;
|
|
375
|
+
const uptimeHours = Math.floor(uptimeMs / (1000 * 60 * 60));
|
|
376
|
+
const uptimeMinutes = Math.floor((uptimeMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
377
|
+
|
|
378
|
+
console.log(`[Agent] 上报状态: Gateway=${gateway.status}, Sessions=${sessions.count}, Uptime=${uptimeHours}h${uptimeMinutes}m`);
|
|
379
|
+
|
|
380
|
+
send({
|
|
381
|
+
type: 'status',
|
|
382
|
+
payload: {
|
|
383
|
+
id: CONFIG.agentId,
|
|
384
|
+
agentVersion: AGENT_VERSION, // Agent 版本
|
|
385
|
+
gateway,
|
|
386
|
+
sessions,
|
|
387
|
+
stats,
|
|
388
|
+
tokenUsage, // 精确的 token 使用数据
|
|
389
|
+
plugins, // 插件列表
|
|
390
|
+
startTime: AGENT_START_TIME, // 启动时间戳
|
|
391
|
+
uptime: { hours: uptimeHours, minutes: uptimeMinutes } // 运行时长
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 连接到 Hub
|
|
397
|
+
function connect() {
|
|
398
|
+
console.log(`[Agent] 连接 Hub: ${CONFIG.hubUrl}`);
|
|
399
|
+
|
|
400
|
+
ws = new WebSocket(CONFIG.hubUrl);
|
|
401
|
+
|
|
402
|
+
ws.on('open', () => {
|
|
403
|
+
console.log('[Agent] 已连接到 Hub');
|
|
404
|
+
|
|
405
|
+
send({
|
|
406
|
+
type: 'register',
|
|
407
|
+
payload: {
|
|
408
|
+
id: CONFIG.agentId,
|
|
409
|
+
name: CONFIG.agentName,
|
|
410
|
+
host: getHostname(),
|
|
411
|
+
agentVersion: AGENT_VERSION // Agent 版本
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
clearInterval(reconnectTimer);
|
|
416
|
+
reconnectTimer = setInterval(() => {
|
|
417
|
+
send({ type: 'heartbeat', payload: { id: CONFIG.agentId } });
|
|
418
|
+
reportStatus();
|
|
419
|
+
}, CONFIG.reportInterval);
|
|
420
|
+
|
|
421
|
+
setTimeout(reportStatus, 1000);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
ws.on('message', (data) => {
|
|
425
|
+
try {
|
|
426
|
+
const msg = JSON.parse(data.toString());
|
|
427
|
+
if (msg.type === 'registered') {
|
|
428
|
+
console.log(`[Agent] 注册成功: ${msg.payload.id}`);
|
|
429
|
+
} else if (msg.type === 'message') {
|
|
430
|
+
// 收到消息(渠道功能)
|
|
431
|
+
console.log('[Agent] 收到消息:', msg.payload);
|
|
432
|
+
handleMessage(msg.payload);
|
|
433
|
+
} else if (msg.type === 'task') {
|
|
434
|
+
// 收到任务
|
|
435
|
+
console.log('[Agent] 收到任务:', msg.payload);
|
|
436
|
+
handleTask(msg.payload);
|
|
437
|
+
} else if (msg.type === 'update') {
|
|
438
|
+
// 收到更新命令
|
|
439
|
+
console.log('[Agent] 收到更新命令');
|
|
440
|
+
handleUpdate(msg.payload?.token);
|
|
441
|
+
} else if (msg.type === 'gateway-start') {
|
|
442
|
+
// 启动 Gateway
|
|
443
|
+
console.log('[Agent] 收到启动 Gateway 命令');
|
|
444
|
+
handleGatewayStart(msg.payload?.token);
|
|
445
|
+
} else if (msg.type === 'gateway-stop') {
|
|
446
|
+
// 停止 Gateway
|
|
447
|
+
console.log('[Agent] 收到停止 Gateway 命令');
|
|
448
|
+
handleGatewayStop(msg.payload?.token);
|
|
449
|
+
} else if (msg.type === 'gateway-restart') {
|
|
450
|
+
// 重启 Gateway
|
|
451
|
+
console.log('[Agent] 收到重启 Gateway 命令');
|
|
452
|
+
handleGatewayRestart(msg.payload?.token);
|
|
453
|
+
} else if (msg.type === 'status-request') {
|
|
454
|
+
// 请求重新上报状态
|
|
455
|
+
console.log('[Agent] 收到状态刷新请求');
|
|
456
|
+
reportStatus();
|
|
457
|
+
}
|
|
458
|
+
} catch (e) {
|
|
459
|
+
console.error('[Agent] 解析消息失败:', e.message);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
ws.on('close', () => {
|
|
464
|
+
console.log('[Agent] 连接断开,5秒后重连...');
|
|
465
|
+
clearInterval(reconnectTimer);
|
|
466
|
+
setTimeout(connect, 5000);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
ws.on('error', (err) => {
|
|
470
|
+
console.error('[Agent] 连接错误:', err.message);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// 处理更新
|
|
475
|
+
async function handleUpdate(token) {
|
|
476
|
+
// 验证 token(可选)
|
|
477
|
+
if (CONFIG.updateToken && token !== CONFIG.updateToken) {
|
|
478
|
+
console.log('[Agent] 更新令牌无效');
|
|
479
|
+
send({ type: 'update-result', payload: { success: false, error: 'Invalid token' } });
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const result = await doUpdate();
|
|
484
|
+
send({ type: 'update-result', payload: result });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// 处理 Gateway 启动命令
|
|
488
|
+
async function handleGatewayStart(token) {
|
|
489
|
+
// 验证 token(可选)
|
|
490
|
+
if (CONFIG.updateToken && token !== CONFIG.updateToken) {
|
|
491
|
+
console.log('[Agent] Gateway 启动令牌无效');
|
|
492
|
+
send({ type: 'gateway-result', payload: { action: 'start', success: false, error: 'Invalid token' } });
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const result = await startGateway();
|
|
497
|
+
send({ type: 'gateway-result', payload: { action: 'start', ...result } });
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// 处理 Gateway 停止命令
|
|
501
|
+
async function handleGatewayStop(token) {
|
|
502
|
+
// 验证 token(可选)
|
|
503
|
+
if (CONFIG.updateToken && token !== CONFIG.updateToken) {
|
|
504
|
+
console.log('[Agent] Gateway 停止令牌无效');
|
|
505
|
+
send({ type: 'gateway-result', payload: { action: 'stop', success: false, error: 'Invalid token' } });
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const result = await stopGateway();
|
|
510
|
+
send({ type: 'gateway-result', payload: { action: 'stop', ...result } });
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// 处理 Gateway 重启命令
|
|
514
|
+
async function handleGatewayRestart(token) {
|
|
515
|
+
// 验证 token(可选)
|
|
516
|
+
if (CONFIG.updateToken && token !== CONFIG.updateToken) {
|
|
517
|
+
console.log('[Agent] Gateway 重启令牌无效');
|
|
518
|
+
send({ type: 'gateway-result', payload: { action: 'restart', success: false, error: 'Invalid token' } });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const result = await restartGateway();
|
|
523
|
+
send({ type: 'gateway-result', payload: { action: 'restart', ...result } });
|
|
524
|
+
|
|
525
|
+
// 重启后延迟 2 秒上报状态
|
|
526
|
+
setTimeout(reportStatus, 2000);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ──────────────────────────────────────────────
|
|
530
|
+
// 渠道功能:消息接收和发送
|
|
531
|
+
// ──────────────────────────────────────────────
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* 处理收到的消息(渠道功能)
|
|
535
|
+
*/
|
|
536
|
+
async function handleMessage(payload) {
|
|
537
|
+
const { from, to, content, timestamp } = payload;
|
|
538
|
+
|
|
539
|
+
console.log(`[Agent] 收到消息: ${from} -> ${to}: ${content}`);
|
|
540
|
+
|
|
541
|
+
// TODO: 触发 OpenClaw Gateway 处理消息
|
|
542
|
+
// 可以通过 HTTP API 或 WebSocket 转发给 Gateway
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
// 方案1: 调用 Gateway HTTP API
|
|
546
|
+
const http = require('http');
|
|
547
|
+
|
|
548
|
+
const postData = JSON.stringify({
|
|
549
|
+
channel: 'clawcamp',
|
|
550
|
+
accountId: CONFIG.agentId,
|
|
551
|
+
from: from,
|
|
552
|
+
to: to,
|
|
553
|
+
content: content,
|
|
554
|
+
timestamp: timestamp
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
const req = http.request({
|
|
558
|
+
hostname: 'localhost',
|
|
559
|
+
port: CONFIG.gatewayPort,
|
|
560
|
+
path: '/api/message',
|
|
561
|
+
method: 'POST',
|
|
562
|
+
headers: {
|
|
563
|
+
'Content-Type': 'application/json',
|
|
564
|
+
'Authorization': `Bearer ${CONFIG.gatewayToken}`
|
|
565
|
+
}
|
|
566
|
+
}, (res) => {
|
|
567
|
+
let data = '';
|
|
568
|
+
res.on('data', chunk => data += chunk);
|
|
569
|
+
res.on('end', () => {
|
|
570
|
+
console.log('[Agent] 消息已转发给 Gateway');
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
req.on('error', (e) => {
|
|
575
|
+
console.error('[Agent] 转发消息失败:', e.message);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
req.write(postData);
|
|
579
|
+
req.end();
|
|
580
|
+
} catch (e) {
|
|
581
|
+
console.error('[Agent] 处理消息失败:', e.message);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* 处理任务(渠道功能)
|
|
587
|
+
*/
|
|
588
|
+
async function handleTask(payload) {
|
|
589
|
+
const { id, action, params } = payload;
|
|
590
|
+
|
|
591
|
+
console.log(`[Agent] 执行任务: ${action}`);
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
let result;
|
|
595
|
+
|
|
596
|
+
switch (action) {
|
|
597
|
+
case 'check-social-monitor':
|
|
598
|
+
// 执行社交媒体监控
|
|
599
|
+
const output = execSync('python3 ~/.openclaw/workspace/scripts/do-social-monitor.py', {
|
|
600
|
+
encoding: 'utf-8',
|
|
601
|
+
timeout: 60000
|
|
602
|
+
});
|
|
603
|
+
result = { success: true, output: output.substring(0, 500) };
|
|
604
|
+
break;
|
|
605
|
+
|
|
606
|
+
case 'send-email':
|
|
607
|
+
// 发送邮件
|
|
608
|
+
// TODO: 实现邮件发送
|
|
609
|
+
result = { success: true };
|
|
610
|
+
break;
|
|
611
|
+
|
|
612
|
+
case 'check-health':
|
|
613
|
+
// 健康检查
|
|
614
|
+
const gateway = getGatewayStatus();
|
|
615
|
+
const stats = getSystemStats();
|
|
616
|
+
result = { success: true, gateway, stats };
|
|
617
|
+
break;
|
|
618
|
+
|
|
619
|
+
default:
|
|
620
|
+
result = { success: false, error: '未知任务' };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// 返回结果
|
|
624
|
+
send({
|
|
625
|
+
type: 'task-result',
|
|
626
|
+
payload: {
|
|
627
|
+
taskId: id,
|
|
628
|
+
...result
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
} catch (e) {
|
|
632
|
+
send({
|
|
633
|
+
type: 'task-result',
|
|
634
|
+
payload: {
|
|
635
|
+
taskId: id,
|
|
636
|
+
success: false,
|
|
637
|
+
error: e.message
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* 发送消息(渠道功能)
|
|
645
|
+
*/
|
|
646
|
+
function sendMessage(to, content) {
|
|
647
|
+
send({
|
|
648
|
+
type: 'message',
|
|
649
|
+
payload: {
|
|
650
|
+
from: CONFIG.agentId,
|
|
651
|
+
to: to,
|
|
652
|
+
content: content,
|
|
653
|
+
timestamp: Date.now()
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// 启动
|
|
659
|
+
console.log('');
|
|
660
|
+
console.log(`🦞 龙虾营地 Agent v${AGENT_VERSION}`);
|
|
661
|
+
console.log(` Agent: ${CONFIG.agentName} (${CONFIG.agentId})`);
|
|
662
|
+
console.log(` Hub: ${CONFIG.hubUrl}`);
|
|
663
|
+
console.log('');
|
|
664
|
+
|
|
665
|
+
connect();
|
package/uninstall.sh
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 龙虾营地 Agent 插件卸载脚本
|
|
3
|
+
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
GREEN='\033[0;32m'
|
|
7
|
+
RED='\033[0;31m'
|
|
8
|
+
NC='\033[0m'
|
|
9
|
+
|
|
10
|
+
log() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|
11
|
+
|
|
12
|
+
PLUGIN_DIR="$HOME/.openclaw/extensions/claw-camp-agent"
|
|
13
|
+
|
|
14
|
+
# 停止 Agent
|
|
15
|
+
log "停止 Agent..."
|
|
16
|
+
pkill -f "node.*agent.js" 2>/dev/null || true
|
|
17
|
+
|
|
18
|
+
# 从信任列表移除
|
|
19
|
+
log "从信任列表移除..."
|
|
20
|
+
CURRENT_ALLOW=$(openclaw config get plugins.allow 2>/dev/null || echo "[]")
|
|
21
|
+
NEW_ALLOW=$(echo "$CURRENT_ALLOW" | jq 'del(.[] | select(. == "claw-camp-agent"))' | jq -c .)
|
|
22
|
+
openclaw config set plugins.allow "$NEW_ALLOW" 2>/dev/null || true
|
|
23
|
+
|
|
24
|
+
# 删除插件目录
|
|
25
|
+
log "删除插件目录..."
|
|
26
|
+
rm -rf "$PLUGIN_DIR"
|
|
27
|
+
|
|
28
|
+
log "✅ 卸载完成"
|