@voko/lite 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +32 -0
- package/scripts/build-native.js +72 -0
- package/src/bankHeadOffices.js +20543 -0
- package/src/channels/email.js +35 -0
- package/src/channels/feishu.js +31 -0
- package/src/channels/qq-email.js +30 -0
- package/src/channels/registry.js +279 -0
- package/src/channels/telegram.js +28 -0
- package/src/channels/voko-email.js +7 -0
- package/src/channels/wechat.js +35 -0
- package/src/cli.js +120 -0
- package/src/context.js +164 -0
- package/src/core/access-control-api.js +150 -0
- package/src/core/access-control.js +56 -0
- package/src/core/agent-registration.js +319 -0
- package/src/core/api-signature.js +33 -0
- package/src/core/audit.js +133 -0
- package/src/core/database.js +1409 -0
- package/src/core/did-auth.js +54 -0
- package/src/core/hermes-paths.js +57 -0
- package/src/core/invitation.js +49 -0
- package/src/core/lite-bus.js +16 -0
- package/src/core/llm-client.js +1032 -0
- package/src/core/messenger.js +456 -0
- package/src/core/notifier.js +99 -0
- package/src/core/offline-sync.js +150 -0
- package/src/core/payment.js +285 -0
- package/src/core/publish-agent.js +166 -0
- package/src/core/register-capabilities.js +119 -0
- package/src/core/search-capabilities.js +136 -0
- package/src/core/send-message.js +85 -0
- package/src/core/set-agent-status.js +65 -0
- package/src/core/update-agent-profile.js +102 -0
- package/src/core/worker-manager.js +332 -0
- package/src/endpoints.json +21 -0
- package/src/index.js +712 -0
- package/src/mcp/CLAUDE_TEST.md +82 -0
- package/src/mcp/FULL_TEST.md +139 -0
- package/src/mcp/TEST.md +124 -0
- package/src/mcp/TEST_STEPS.md +75 -0
- package/src/mcp/server.js +612 -0
- package/src/mcp/tools.js +1367 -0
- package/src/mcp/transport/http.js +95 -0
- package/src/mcp/transport/stdio.js +20 -0
- package/src/preload.js +27 -0
- package/src/server/agent-email-api.js +120 -0
- package/src/server/agent-manager.js +580 -0
- package/src/server/email-handler.js +329 -0
- package/src/server/feishu-handler.js +249 -0
- package/src/server/hermes-api-client.js +166 -0
- package/src/server/hermes-discovery.js +80 -0
- package/src/server/hermes-handler.js +287 -0
- package/src/server/openclaw-handler-cli.js +131 -0
- package/src/server/openclaw-websocket-handler.js +1290 -0
- package/src/server/oss.js +186 -0
- package/src/server/owner-intervention-notifier.js +320 -0
- package/src/server/release-page.html +204 -0
- package/src/server/telegram-handler.js +208 -0
- package/src/server/voko-email-handler.js +68 -0
- package/src/server/wechat-handler.js +439 -0
- package/src/workers/agent-worker.js +378 -0
- package/src/workers/message-content.js +51 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const { EventEmitter } = require('events');
|
|
3
|
+
|
|
4
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
5
|
+
const DEFAULT_PORT = 8642;
|
|
6
|
+
const CHAT_TIMEOUT = 120000;
|
|
7
|
+
const PING_TIMEOUT = 5000;
|
|
8
|
+
|
|
9
|
+
// 从配置的 profiles 映射中查询端口
|
|
10
|
+
function profilePort(profileName, profiles) {
|
|
11
|
+
const entry = profiles?.[profileName];
|
|
12
|
+
return entry?.port || DEFAULT_PORT;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class HermesApiClient extends EventEmitter {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
super();
|
|
18
|
+
this.host = options.host || DEFAULT_HOST;
|
|
19
|
+
this.port = options.port || DEFAULT_PORT;
|
|
20
|
+
this.apiKey = options.apiKey || '';
|
|
21
|
+
this.profiles = options.profiles || {}; // { agentId: { port } }
|
|
22
|
+
this.connected = false;
|
|
23
|
+
this._destroyed = false;
|
|
24
|
+
this._healthTimer = null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_agentPort(agentId) {
|
|
28
|
+
return this.profiles?.[agentId]?.port || this.port;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_request(method, path, body, timeoutMs, extraHeaders = {}, connOverrides = {}) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const data = body ? JSON.stringify(body) : null;
|
|
34
|
+
const options = {
|
|
35
|
+
hostname: connOverrides.host || this.host,
|
|
36
|
+
port: connOverrides.port || this.port,
|
|
37
|
+
path,
|
|
38
|
+
method,
|
|
39
|
+
timeout: timeoutMs,
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
...(this.apiKey ? { 'Authorization': `Bearer ${this.apiKey}` } : {}),
|
|
43
|
+
...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}),
|
|
44
|
+
...extraHeaders
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const req = http.request(options, (res) => {
|
|
49
|
+
let buf = '';
|
|
50
|
+
res.on('data', (chunk) => { buf += chunk; });
|
|
51
|
+
res.on('end', () => {
|
|
52
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
53
|
+
if (res.statusCode === 401) {
|
|
54
|
+
const keyLog = this.apiKey ? `"${this.apiKey}" (len=${this.apiKey.length})` : '(空,未带 Authorization)';
|
|
55
|
+
console.warn(`[HermesApiClient] 401 ${method} ${path} port=${options.port} apiKey=${keyLog}`);
|
|
56
|
+
}
|
|
57
|
+
reject(new Error(`HTTP ${res.statusCode}: ${buf.substring(0, 200)}`));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
resolve(JSON.parse(buf));
|
|
62
|
+
} catch (e) {
|
|
63
|
+
resolve(buf);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
req.on('error', (err) => reject(err));
|
|
69
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('请求超时')); });
|
|
70
|
+
|
|
71
|
+
if (data) req.write(data);
|
|
72
|
+
req.end();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_sessionKey(agentId, visitorId) {
|
|
77
|
+
return `hermes:${agentId}:${visitorId}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 发送聊天消息并等待回复
|
|
82
|
+
*/
|
|
83
|
+
async chat(agentId, visitorId, message, timeoutMs) {
|
|
84
|
+
const sessionId = this._sessionKey(agentId, visitorId);
|
|
85
|
+
const enriched = `[访客 ${visitorId}]: ${message}`;
|
|
86
|
+
const conn = { port: this._agentPort(agentId) };
|
|
87
|
+
const keyLog = this.apiKey ? `"${this.apiKey}" (len=${this.apiKey.length})` : '(空)';
|
|
88
|
+
console.log(`[HermesApiClient] chat agentId=${agentId} port=${conn.port} apiKey=${keyLog}`);
|
|
89
|
+
|
|
90
|
+
const resp = await this._request('POST', '/v1/chat/completions', {
|
|
91
|
+
messages: [{ role: 'user', content: enriched }],
|
|
92
|
+
model: 'hermes-agent',
|
|
93
|
+
stream: false
|
|
94
|
+
}, timeoutMs || CHAT_TIMEOUT, { 'X-Hermes-Session-Id': sessionId }, conn);
|
|
95
|
+
|
|
96
|
+
const reply = resp?.choices?.[0]?.message?.content || '';
|
|
97
|
+
return { reply, runId: resp?.id || '', sessionKey: sessionId };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 注入系统消息到会话
|
|
102
|
+
*/
|
|
103
|
+
async steer(agentId, visitorId, content) {
|
|
104
|
+
const sessionId = this._sessionKey(agentId, visitorId);
|
|
105
|
+
const enriched = `[系统消息] ${content}`;
|
|
106
|
+
const conn = { port: this._agentPort(agentId) };
|
|
107
|
+
|
|
108
|
+
const resp = await this._request('POST', '/v1/chat/completions', {
|
|
109
|
+
messages: [
|
|
110
|
+
{ role: 'system', content: `[Owner Instruction] ${content}` },
|
|
111
|
+
{ role: 'user', content: enriched }
|
|
112
|
+
],
|
|
113
|
+
model: 'hermes-agent',
|
|
114
|
+
stream: false
|
|
115
|
+
}, CHAT_TIMEOUT, { 'X-Hermes-Session-Id': sessionId }, conn);
|
|
116
|
+
|
|
117
|
+
const output = resp?.choices?.[0]?.message?.content || '';
|
|
118
|
+
return { accepted: true, output, sessionKey: sessionId };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 健康检查
|
|
123
|
+
*/
|
|
124
|
+
async ping(agentId) {
|
|
125
|
+
try {
|
|
126
|
+
const conn = agentId ? { port: this._agentPort(agentId) } : {};
|
|
127
|
+
await this._request('GET', '/health', null, PING_TIMEOUT, {}, conn);
|
|
128
|
+
return true;
|
|
129
|
+
} catch (e) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 定期健康检查 + 状态发射
|
|
136
|
+
*/
|
|
137
|
+
startHealthCheck(intervalMs = 30000) {
|
|
138
|
+
this.stopHealthCheck();
|
|
139
|
+
const check = async () => {
|
|
140
|
+
const ok = await this.ping();
|
|
141
|
+
if (ok !== this.connected) {
|
|
142
|
+
this.connected = ok;
|
|
143
|
+
this.emit('status', { connected: ok });
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
check().then(() => {
|
|
147
|
+
if (this.connected) this.emit('ready');
|
|
148
|
+
});
|
|
149
|
+
this._healthTimer = setInterval(check, intervalMs);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
stopHealthCheck() {
|
|
153
|
+
if (this._healthTimer) {
|
|
154
|
+
clearInterval(this._healthTimer);
|
|
155
|
+
this._healthTimer = null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
destroy() {
|
|
160
|
+
this._destroyed = true;
|
|
161
|
+
this.stopHealthCheck();
|
|
162
|
+
this.removeAllListeners();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { HermesApiClient };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const { execSync, spawnSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { getHermesProfilesDir } = require('../core/hermes-paths');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 发现 Hermes 下的所有 profiles(agents)
|
|
8
|
+
* 从 demo/discover.js 适配而来
|
|
9
|
+
* 返回: [{ name, model, isDefault }]
|
|
10
|
+
*/
|
|
11
|
+
function discoverHermes() {
|
|
12
|
+
const profiles = [];
|
|
13
|
+
|
|
14
|
+
// 方案1:解析 hermes profile list 输出(用 spawnSync 避免 Windows cmd.exe 乱码)
|
|
15
|
+
try {
|
|
16
|
+
const result = spawnSync('hermes', ['profile', 'list'], {
|
|
17
|
+
encoding: 'utf-8',
|
|
18
|
+
timeout: 2000,
|
|
19
|
+
windowsHide: true,
|
|
20
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
21
|
+
});
|
|
22
|
+
if (result.error || result.status !== 0) throw result.error || new Error('non-zero exit');
|
|
23
|
+
const output = result.stdout;
|
|
24
|
+
const lines = output.split('\n').filter(l => l.trim());
|
|
25
|
+
|
|
26
|
+
let parsing = false;
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
if (line.includes('───')) {
|
|
29
|
+
parsing = true;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (!parsing) continue;
|
|
33
|
+
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
if (!trimmed) continue;
|
|
36
|
+
|
|
37
|
+
const isDefault = trimmed.startsWith('◆');
|
|
38
|
+
const clean = trimmed.replace(/^◆\s*/, '');
|
|
39
|
+
const parts = clean.split(/\s{2,}/).map(s => s.trim());
|
|
40
|
+
|
|
41
|
+
if (parts.length >= 1) {
|
|
42
|
+
profiles.push({
|
|
43
|
+
name: parts[0],
|
|
44
|
+
model: parts[1] || 'unknown',
|
|
45
|
+
isDefault
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
// hermes CLI 未安装或不在 PATH 中(Windows 系统错误信息为 GBK 编码,
|
|
51
|
+
// 输出 err.message 会显示乱码),使用固定提示替代
|
|
52
|
+
console.warn('[HermesDiscover] hermes CLI 未安装或不可用,跳过 discovery');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 方案2:回退读取 profiles 目录
|
|
56
|
+
if (profiles.length === 0) {
|
|
57
|
+
const profilesDir = getHermesProfilesDir();
|
|
58
|
+
try {
|
|
59
|
+
if (fs.existsSync(profilesDir)) {
|
|
60
|
+
const dirs = fs.readdirSync(profilesDir);
|
|
61
|
+
for (const dir of dirs) {
|
|
62
|
+
const configPath = path.join(profilesDir, dir, 'config.yaml');
|
|
63
|
+
let model = 'unknown';
|
|
64
|
+
if (fs.existsSync(configPath)) {
|
|
65
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
66
|
+
const modelMatch = configContent.match(/default:\s*(\S+)/);
|
|
67
|
+
if (modelMatch) model = modelMatch[1];
|
|
68
|
+
}
|
|
69
|
+
profiles.push({ name: dir, model, isDefault: false });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.warn('[HermesDiscover] 读取 profiles 目录失败:', err.message);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return profiles;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { discoverHermes };
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
const { EventEmitter } = require('events');
|
|
2
|
+
const { spawn } = require('child_process');
|
|
3
|
+
const { HermesApiClient } = require('./hermes-api-client');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
6
|
+
const DEFAULT_PORT = 8642;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hermes Handler — voko 与 Hermes Agent API Server 之间的适配层
|
|
10
|
+
*
|
|
11
|
+
* 职责:
|
|
12
|
+
* - 管理 HermesApiClient 生命周期
|
|
13
|
+
* - 提供 sendToSession() / steer() 接口给 main.js 调用
|
|
14
|
+
* - 转发 agent.reply 事件
|
|
15
|
+
*
|
|
16
|
+
* 通过 HTTP 与 hermes-agent 原生 API Server 通信,取代旧版 TCP Bridge。
|
|
17
|
+
*/
|
|
18
|
+
class HermesHandler extends EventEmitter {
|
|
19
|
+
constructor(database, mainWindow, options = {}) {
|
|
20
|
+
super();
|
|
21
|
+
this.db = database;
|
|
22
|
+
this.mainWindow = mainWindow;
|
|
23
|
+
this.options = options;
|
|
24
|
+
this.enabled = false;
|
|
25
|
+
this.connected = false;
|
|
26
|
+
this.client = null;
|
|
27
|
+
this._destroyed = false;
|
|
28
|
+
this.logs = [];
|
|
29
|
+
this.maxLogSize = 200;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
addLog(msg) {
|
|
33
|
+
const entry = `[${new Date().toLocaleTimeString('zh-CN', { hour12: false })}] ${msg}`;
|
|
34
|
+
this.logs.push(entry);
|
|
35
|
+
if (this.logs.length > this.maxLogSize) this.logs.shift();
|
|
36
|
+
console.log(`[HermesHandler] ${msg}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 启用/停用 Hermes API 客户端
|
|
41
|
+
*/
|
|
42
|
+
setEnabled(enabled) {
|
|
43
|
+
this.enabled = enabled;
|
|
44
|
+
if (enabled && !this.client) {
|
|
45
|
+
this.addLog('🚀 Hermes Handler 初始化中...');
|
|
46
|
+
this._initClient().catch(err => {
|
|
47
|
+
this.addLog(`❌ 客户端初始化失败: ${err.message}`);
|
|
48
|
+
this.connected = false;
|
|
49
|
+
this.emit('status', { connected: false, enabled: true });
|
|
50
|
+
});
|
|
51
|
+
} else if (!enabled && this.client) {
|
|
52
|
+
this.addLog('⏹ Hermes Handler 已停用');
|
|
53
|
+
this.destroy();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async _initClient() {
|
|
58
|
+
this.client = new HermesApiClient({
|
|
59
|
+
host: this.options.host || DEFAULT_HOST,
|
|
60
|
+
port: this.options.port || DEFAULT_PORT,
|
|
61
|
+
apiKey: this.options.apiKey || '',
|
|
62
|
+
profiles: this.options.profiles || {}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const profileCount = Object.keys(this.options.profiles || {}).length;
|
|
66
|
+
this.addLog(`🔌 Hermes API 客户端已创建 (${profileCount} 个 profile)`);
|
|
67
|
+
|
|
68
|
+
this.client.on('ready', () => {
|
|
69
|
+
this.connected = true;
|
|
70
|
+
this.addLog('✅ Hermes API 客户端已就绪');
|
|
71
|
+
this.emit('status', { connected: true, enabled: this.enabled });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
this.client.on('status', ({ connected }) => {
|
|
75
|
+
this.connected = connected;
|
|
76
|
+
this.addLog(connected ? '🟢 Gateway 已连接' : '🔴 Gateway 已断开');
|
|
77
|
+
this.emit('status', { connected, enabled: this.enabled });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 健康检查(由外部 60s 定时器驱动)
|
|
84
|
+
* 逐个检查所有已配置的 agent gateway HTTP 端口,记录可达的 agentId
|
|
85
|
+
*/
|
|
86
|
+
async healthCheck() {
|
|
87
|
+
if (!this.client) return;
|
|
88
|
+
this.connectedAgents = new Set();
|
|
89
|
+
const profilePorts = Object.keys(this.options.profiles || {});
|
|
90
|
+
let anyOk = false;
|
|
91
|
+
if (profilePorts.length > 0) {
|
|
92
|
+
for (const agentId of profilePorts) {
|
|
93
|
+
let ok = await this.client.ping(agentId);
|
|
94
|
+
if (ok) {
|
|
95
|
+
anyOk = true;
|
|
96
|
+
this.connectedAgents.add(agentId);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
anyOk = await this.client.ping();
|
|
101
|
+
if (anyOk) this.connectedAgents = null;
|
|
102
|
+
}
|
|
103
|
+
if (anyOk !== this.client.connected) {
|
|
104
|
+
this.client.connected = anyOk;
|
|
105
|
+
this.emit('status', { connected: anyOk, enabled: this.enabled });
|
|
106
|
+
}
|
|
107
|
+
if (anyOk !== this.connected) {
|
|
108
|
+
this.connected = anyOk;
|
|
109
|
+
const agentCount = this.connectedAgents ? this.connectedAgents.size : (anyOk ? 1 : 0);
|
|
110
|
+
this.addLog(anyOk ? `🟢 健康检查通过 (${agentCount} 个 Agent 在线)` : '🔴 健康检查失败(所有 Gateway 离线)');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 获取状态
|
|
116
|
+
*/
|
|
117
|
+
getStatus() {
|
|
118
|
+
return {
|
|
119
|
+
connected: this.connected,
|
|
120
|
+
enabled: this.enabled,
|
|
121
|
+
clientReady: this.client?.connected || false,
|
|
122
|
+
logs: this.logs.slice()
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 确保 Hermes gateway 在运行,如未运行则自动启动
|
|
128
|
+
*/
|
|
129
|
+
async _ensureGatewayRunning(agentId) {
|
|
130
|
+
// 检查 API Key 是否已配置
|
|
131
|
+
if (!this.options?.apiKey) {
|
|
132
|
+
this.addLog(`❌ API Key 未配置,请先到「设置 → 网关连接管理 → Hermes 连接管理」中点击「一键配置」`);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const port = this.client._agentPort(agentId);
|
|
136
|
+
// 先检查是否已经在运行
|
|
137
|
+
const alreadyRunning = await this.client.ping(agentId);
|
|
138
|
+
if (alreadyRunning) {
|
|
139
|
+
if (!this.connected) {
|
|
140
|
+
this.connected = true;
|
|
141
|
+
this.client.connected = true;
|
|
142
|
+
this.addLog(`🟢 Gateway 已连接 ${agentId}`);
|
|
143
|
+
this.emit('status', { connected: true, enabled: this.enabled });
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
this.addLog(`🔧 gateway 未运行,启动 profile=${agentId} port=${port}...`);
|
|
148
|
+
try {
|
|
149
|
+
const cleanEnv = { ...process.env, HTTPS_PROXY: '', HTTP_PROXY: '' };
|
|
150
|
+
spawn('hermes', ['--profile', agentId, 'gateway', 'run', '--replace'], {
|
|
151
|
+
stdio: 'ignore', windowsHide: true, detached: true, env: cleanEnv
|
|
152
|
+
}).on('error', (err) => {
|
|
153
|
+
this.addLog(`❌ gateway 进程启动失败 (${agentId}): ${err.message}`);
|
|
154
|
+
}).unref();
|
|
155
|
+
// 等待就绪(最多 30s)
|
|
156
|
+
for (let i = 0; i < 30; i++) {
|
|
157
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
158
|
+
const ok = await this.client.ping(agentId);
|
|
159
|
+
if (ok) {
|
|
160
|
+
this.connected = true;
|
|
161
|
+
this.client.connected = true;
|
|
162
|
+
this.addLog(`✅ gateway 已就绪 ${agentId} port=${port}`);
|
|
163
|
+
this.emit('status', { connected: true, enabled: this.enabled });
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
this.addLog(`❌ gateway 启动超时 ${agentId}`);
|
|
168
|
+
return false;
|
|
169
|
+
} catch (e) {
|
|
170
|
+
this.addLog(`❌ gateway 启动失败 ${agentId}: ${e.message}`);
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 发送访客消息到 Hermes agent(走 API Server)
|
|
177
|
+
* sessionKey 格式: hermes:{agentId}:{visitorId}
|
|
178
|
+
*/
|
|
179
|
+
async sendToSession(sessionKey, message, extraData = null) {
|
|
180
|
+
const parts = sessionKey.split(':');
|
|
181
|
+
if (parts.length < 3 || parts[0] !== 'hermes') {
|
|
182
|
+
throw new Error(`无效的 Hermes sessionKey: ${sessionKey}`);
|
|
183
|
+
}
|
|
184
|
+
const agentId = parts[1];
|
|
185
|
+
const visitorId = parts.slice(2).join(':');
|
|
186
|
+
|
|
187
|
+
this.addLog(`📤 转发消息 ${agentId} (visitor=${visitorId.substring(0, 12)}...)`);
|
|
188
|
+
|
|
189
|
+
// 构造结构化 JSON
|
|
190
|
+
const structuredMsg = JSON.stringify({
|
|
191
|
+
type: 'message',
|
|
192
|
+
content: message,
|
|
193
|
+
fromUid: visitorId,
|
|
194
|
+
channelId: extraData?.channelId || visitorId,
|
|
195
|
+
channelType: extraData?.channelType ?? 1,
|
|
196
|
+
contentType: extraData?.contentType ?? 1,
|
|
197
|
+
messageId: extraData?.messageId || '',
|
|
198
|
+
timestamp: extraData?.timestamp || Math.floor(Date.now() / 1000)
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// 自动启动 gateway
|
|
202
|
+
const justStarted = !this.connected;
|
|
203
|
+
await this._ensureGatewayRunning(agentId);
|
|
204
|
+
if (!this.connected) return;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const result = await this.client.chat(agentId, visitorId, structuredMsg);
|
|
208
|
+
const replyLen = (result.reply || '').length;
|
|
209
|
+
const replyPreview = (result.reply || '').substring(0, 120).replace(/\n/g, '\\n');
|
|
210
|
+
this.addLog(`📥 收到回复 ${agentId} (${replyLen} 字) 内容="${replyPreview}"`);
|
|
211
|
+
console.log(`[HermesHandler] 完整回复 ${agentId}:`, result.reply);
|
|
212
|
+
this.emit('agent.reply', {
|
|
213
|
+
agentId,
|
|
214
|
+
visitorId,
|
|
215
|
+
content: result.reply,
|
|
216
|
+
sessionKey
|
|
217
|
+
});
|
|
218
|
+
} catch (err) {
|
|
219
|
+
// 刚启动的 gateway 可能因 --replace 切换窗口而短暂不可用,等 2s 重试
|
|
220
|
+
if (justStarted && (err.message?.includes('ECONNRESET') || err.message?.includes('ECONNREFUSED'))) {
|
|
221
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
222
|
+
try {
|
|
223
|
+
const result = await this.client.chat(agentId, visitorId, structuredMsg);
|
|
224
|
+
const replyLen2 = (result.reply || '').length;
|
|
225
|
+
const replyPrev2 = (result.reply || '').substring(0, 120).replace(/\n/g, '\\n');
|
|
226
|
+
this.addLog(`📥 收到回复 ${agentId} (重试, ${replyLen2} 字) 内容="${replyPrev2}"`);
|
|
227
|
+
console.log(`[HermesHandler] 完整回复(重试) ${agentId}:`, result.reply);
|
|
228
|
+
this.emit('agent.reply', { agentId, visitorId, content: result.reply, sessionKey });
|
|
229
|
+
return;
|
|
230
|
+
} catch (retryErr) {}
|
|
231
|
+
}
|
|
232
|
+
this.addLog(`❌ chat 失败 ${agentId}: ${err.message}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 注入系统消息到 Hermes agent 会话(走 API Server)
|
|
238
|
+
* 用于支付通知、主人回复等场景
|
|
239
|
+
*/
|
|
240
|
+
async steer(sessionKey, content) {
|
|
241
|
+
const parts = sessionKey.split(':');
|
|
242
|
+
if (parts.length < 3 || parts[0] !== 'hermes') {
|
|
243
|
+
throw new Error(`无效的 Hermes sessionKey: ${sessionKey}`);
|
|
244
|
+
}
|
|
245
|
+
const agentId = parts[1];
|
|
246
|
+
const visitorId = parts.slice(2).join(':');
|
|
247
|
+
|
|
248
|
+
this.addLog(`📝 注入系统消息 ${agentId}`);
|
|
249
|
+
|
|
250
|
+
// 自动启动 gateway
|
|
251
|
+
const justStarted = !this.connected;
|
|
252
|
+
await this._ensureGatewayRunning(agentId);
|
|
253
|
+
if (!this.connected) return;
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const result = await this.client.steer(agentId, visitorId, content);
|
|
257
|
+
this.addLog(`✅ steer 完成 ${agentId} (回复 ${(result.output || '').length} 字)`);
|
|
258
|
+
return result;
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (justStarted && (err.message?.includes('ECONNRESET') || err.message?.includes('ECONNREFUSED'))) {
|
|
261
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
262
|
+
try {
|
|
263
|
+
const result = await this.client.steer(agentId, visitorId, content);
|
|
264
|
+
this.addLog(`✅ steer 完成 ${agentId} (重试)`);
|
|
265
|
+
return result;
|
|
266
|
+
} catch (retryErr) {}
|
|
267
|
+
}
|
|
268
|
+
this.addLog(`❌ steer 失败 ${agentId}: ${err.message}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 清理资源
|
|
274
|
+
*/
|
|
275
|
+
async destroy() {
|
|
276
|
+
this._destroyed = true;
|
|
277
|
+
this.enabled = false;
|
|
278
|
+
this.connected = false;
|
|
279
|
+
if (this.client) {
|
|
280
|
+
this.client.destroy();
|
|
281
|
+
this.client = null;
|
|
282
|
+
}
|
|
283
|
+
this.removeAllListeners();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = HermesHandler;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenClaw CLI 消息处理器
|
|
5
|
+
* 使用 CLI 命令调用 OpenClaw,无需 Gateway HTTP 配置
|
|
6
|
+
*/
|
|
7
|
+
class OpenClawCLIHandler {
|
|
8
|
+
constructor(database, mainWindow) {
|
|
9
|
+
this.db = database;
|
|
10
|
+
this.mainWindow = mainWindow;
|
|
11
|
+
this.enabled = false;
|
|
12
|
+
this.processingChannels = new Set();
|
|
13
|
+
this.agentName = 'main'; // 默认使用 main agent,可改成 voko
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
setEnabled(enabled) {
|
|
17
|
+
this.enabled = enabled;
|
|
18
|
+
console.log('[OpenClaw CLI] 自动回复:', enabled ? '已启用' : '已禁用');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async onNewMessage(channelId, messageContent) {
|
|
22
|
+
if (!this.enabled) return;
|
|
23
|
+
if (this.processingChannels.has(channelId)) return;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
this.processingChannels.add(channelId);
|
|
27
|
+
console.log(`[OpenClaw CLI] 开始处理 - Channel: ${channelId}, Content: ${messageContent?.substring(0, 50)}`);
|
|
28
|
+
|
|
29
|
+
// 调用 OpenClaw CLI 生成回复
|
|
30
|
+
console.log('[OpenClaw CLI] 调用: openclaw run --agent', this.agentName);
|
|
31
|
+
const reply = await this.callOpenClawCLI(messageContent);
|
|
32
|
+
console.log(`[OpenClaw CLI] 收到回复: ${reply?.substring(0, 100)}`);
|
|
33
|
+
|
|
34
|
+
// 发送回复到 VOKO IM
|
|
35
|
+
await this.sendReply(channelId, reply);
|
|
36
|
+
console.log('[OpenClaw CLI] 回复发送成功');
|
|
37
|
+
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error('[OpenClaw CLI] 处理失败:', err.message);
|
|
40
|
+
} finally {
|
|
41
|
+
this.processingChannels.delete(channelId);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 调用 OpenClaw CLI 命令
|
|
46
|
+
callOpenClawCLI(messageContent) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const prompt = `你是 VOKO IM 智能助手。用户发来消息:"${messageContent}"
|
|
49
|
+
|
|
50
|
+
请生成友好、专业的回复(100字以内)。只输出回复内容,不要有任何前缀或解释。`;
|
|
51
|
+
|
|
52
|
+
console.log('[OpenClaw CLI] 执行命令...');
|
|
53
|
+
|
|
54
|
+
const child = spawn('openclaw', [
|
|
55
|
+
'run',
|
|
56
|
+
'--agent', this.agentName,
|
|
57
|
+
'--message', prompt
|
|
58
|
+
], {
|
|
59
|
+
timeout: 60000, // 60秒超时
|
|
60
|
+
windowsHide: true // 隐藏命令行窗口
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let output = '';
|
|
64
|
+
let errorOutput = '';
|
|
65
|
+
|
|
66
|
+
child.stdout.on('data', (data) => {
|
|
67
|
+
output += data.toString();
|
|
68
|
+
console.log('[OpenClaw CLI stdout]:', data.toString().substring(0, 100));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
child.stderr.on('data', (data) => {
|
|
72
|
+
errorOutput += data.toString();
|
|
73
|
+
console.log('[OpenClaw CLI stderr]:', data.toString().substring(0, 100));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
child.on('close', (code) => {
|
|
77
|
+
console.log(`[OpenClaw CLI] 进程退出码: ${code}`);
|
|
78
|
+
if (code === 0) {
|
|
79
|
+
resolve(output.trim() || '收到你的消息');
|
|
80
|
+
} else {
|
|
81
|
+
reject(new Error(`CLI 失败,退出码: ${code}, 错误: ${errorOutput}`));
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
child.on('error', (err) => {
|
|
86
|
+
console.error('[OpenClaw CLI] 启动失败:', err.message);
|
|
87
|
+
if (err.code === 'ENOENT') {
|
|
88
|
+
reject(new Error('找不到 openclaw 命令,请检查 PATH 或安装 OpenClaw'));
|
|
89
|
+
} else {
|
|
90
|
+
reject(err);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 发送回复到 VOKO IM
|
|
97
|
+
sendReply(channelId, content) {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const http = require('http');
|
|
100
|
+
const postData = JSON.stringify({
|
|
101
|
+
toUid: channelId,
|
|
102
|
+
content,
|
|
103
|
+
channelType: 1
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const req = http.request({
|
|
107
|
+
hostname: 'localhost',
|
|
108
|
+
port: 3002,
|
|
109
|
+
path: '/api/send-message',
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: {
|
|
112
|
+
'Content-Type': 'application/json',
|
|
113
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
114
|
+
}
|
|
115
|
+
}, (res) => {
|
|
116
|
+
let data = '';
|
|
117
|
+
res.on('data', chunk => data += chunk);
|
|
118
|
+
res.on('end', () => {
|
|
119
|
+
console.log(`[OpenClaw Send] 状态: ${res.statusCode}`);
|
|
120
|
+
resolve(data);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
req.on('error', reject);
|
|
125
|
+
req.write(postData);
|
|
126
|
+
req.end();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = OpenClawCLIHandler;
|