openclaw-vchat-plugin 0.0.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.
Files changed (70) hide show
  1. package/bin/openclaw-vchat.js +110 -0
  2. package/dist/commands.d.ts +18 -0
  3. package/dist/commands.d.ts.map +1 -0
  4. package/dist/commands.js +509 -0
  5. package/dist/commands.js.map +1 -0
  6. package/dist/constants.d.ts +14 -0
  7. package/dist/constants.d.ts.map +1 -0
  8. package/dist/constants.js +51 -0
  9. package/dist/constants.js.map +1 -0
  10. package/dist/gateway-client.d.ts +43 -0
  11. package/dist/gateway-client.d.ts.map +1 -0
  12. package/dist/gateway-client.js +623 -0
  13. package/dist/gateway-client.js.map +1 -0
  14. package/dist/group-manager.d.ts +30 -0
  15. package/dist/group-manager.d.ts.map +1 -0
  16. package/dist/group-manager.js +107 -0
  17. package/dist/group-manager.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +382 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/media-handler.d.ts +31 -0
  23. package/dist/media-handler.d.ts.map +1 -0
  24. package/dist/media-handler.js +67 -0
  25. package/dist/media-handler.js.map +1 -0
  26. package/dist/message-handler.d.ts +52 -0
  27. package/dist/message-handler.d.ts.map +1 -0
  28. package/dist/message-handler.js +291 -0
  29. package/dist/message-handler.js.map +1 -0
  30. package/dist/relay-server.d.ts +16 -0
  31. package/dist/relay-server.d.ts.map +1 -0
  32. package/dist/relay-server.js +877 -0
  33. package/dist/relay-server.js.map +1 -0
  34. package/dist/routes/config.routes.d.ts +12 -0
  35. package/dist/routes/config.routes.d.ts.map +1 -0
  36. package/dist/routes/config.routes.js +175 -0
  37. package/dist/routes/config.routes.js.map +1 -0
  38. package/dist/services/config.service.d.ts +57 -0
  39. package/dist/services/config.service.d.ts.map +1 -0
  40. package/dist/services/config.service.js +361 -0
  41. package/dist/services/config.service.js.map +1 -0
  42. package/dist/session-key.d.ts +8 -0
  43. package/dist/session-key.d.ts.map +1 -0
  44. package/dist/session-key.js +28 -0
  45. package/dist/session-key.js.map +1 -0
  46. package/dist/session-manager.d.ts +32 -0
  47. package/dist/session-manager.d.ts.map +1 -0
  48. package/dist/session-manager.js +303 -0
  49. package/dist/session-manager.js.map +1 -0
  50. package/dist/types.d.ts +81 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +3 -0
  53. package/dist/types.js.map +1 -0
  54. package/nginx-proxy.conf +24 -0
  55. package/package.json +51 -0
  56. package/src/commands.ts +499 -0
  57. package/src/constants.ts +49 -0
  58. package/src/gateway-client.ts +648 -0
  59. package/src/group-manager.ts +119 -0
  60. package/src/index.ts +443 -0
  61. package/src/media-handler.ts +70 -0
  62. package/src/message-handler.ts +419 -0
  63. package/src/relay-server.ts +979 -0
  64. package/src/routes/config.routes.ts +144 -0
  65. package/src/services/config.service.ts +398 -0
  66. package/src/session-key.ts +30 -0
  67. package/src/session-manager.ts +374 -0
  68. package/src/types.ts +96 -0
  69. package/start.sh +5 -0
  70. package/tsconfig.json +26 -0
@@ -0,0 +1,877 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createRelayServer = createRelayServer;
7
+ const express_1 = __importDefault(require("express"));
8
+ const cors_1 = __importDefault(require("cors"));
9
+ const multer_1 = __importDefault(require("multer"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const http_1 = __importDefault(require("http"));
13
+ const ws_1 = require("ws");
14
+ const crypto_1 = require("crypto");
15
+ const session_manager_1 = require("./session-manager");
16
+ const message_handler_1 = require("./message-handler");
17
+ const media_handler_1 = require("./media-handler");
18
+ const group_manager_1 = require("./group-manager");
19
+ const commands_1 = require("./commands");
20
+ const constants_1 = require("./constants");
21
+ const session_key_1 = require("./session-key");
22
+ const config_routes_1 = __importDefault(require("./routes/config.routes"));
23
+ function createRequestMemo(ttlMs) {
24
+ const inflight = new Map();
25
+ const cache = new Map();
26
+ return async function memoized(key, loader) {
27
+ const now = Date.now();
28
+ const cached = cache.get(key);
29
+ if (cached && cached.expiresAt > now) {
30
+ return cached.value;
31
+ }
32
+ const pending = inflight.get(key);
33
+ if (pending)
34
+ return pending;
35
+ const task = (async () => {
36
+ try {
37
+ const value = await loader();
38
+ cache.set(key, { value, expiresAt: Date.now() + ttlMs });
39
+ return value;
40
+ }
41
+ finally {
42
+ inflight.delete(key);
43
+ }
44
+ })();
45
+ inflight.set(key, task);
46
+ return task;
47
+ };
48
+ }
49
+ function toBridgeSessionPayload(item) {
50
+ const createdAt = Number(item.createdAt) || Date.now();
51
+ return {
52
+ id: String(item.id || '').trim(),
53
+ title: String(item.title || '').trim() || '新对话',
54
+ lastMessage: String(item.lastMessage || '').trim(),
55
+ lastMessageTime: Number(item.lastMessageTime) || createdAt,
56
+ createdAt,
57
+ agentId: String(item.agentId || '').trim() || 'main',
58
+ };
59
+ }
60
+ function parseTimestamp(value) {
61
+ if (typeof value === 'number' && Number.isFinite(value)) {
62
+ if (value > 1000000000000)
63
+ return value;
64
+ if (value > 1000000000)
65
+ return value * 1000;
66
+ }
67
+ if (typeof value === 'string' && value.trim()) {
68
+ const n = Number(value);
69
+ if (Number.isFinite(n))
70
+ return parseTimestamp(n);
71
+ const d = Date.parse(value);
72
+ if (!Number.isNaN(d))
73
+ return d;
74
+ }
75
+ return 0;
76
+ }
77
+ function pickTimestamp(entry, fallback) {
78
+ return (parseTimestamp(entry?.lastMessageTime) ||
79
+ parseTimestamp(entry?.updatedAt) ||
80
+ parseTimestamp(entry?.lastActiveAt) ||
81
+ parseTimestamp(entry?.lastActivityAt) ||
82
+ parseTimestamp(entry?.timestamp) ||
83
+ parseTimestamp(entry?.createdAt) ||
84
+ fallback);
85
+ }
86
+ function normalizeMessageText(raw) {
87
+ if (typeof raw === 'string')
88
+ return raw;
89
+ if (!raw || typeof raw !== 'object')
90
+ return '';
91
+ if (typeof raw.text === 'string')
92
+ return raw.text;
93
+ if (typeof raw.content === 'string')
94
+ return raw.content;
95
+ if (Array.isArray(raw.content)) {
96
+ return raw.content
97
+ .map((part) => {
98
+ if (typeof part === 'string')
99
+ return part;
100
+ if (part?.type === 'text' && typeof part?.text === 'string')
101
+ return part.text;
102
+ if (typeof part?.text === 'string')
103
+ return part.text;
104
+ return '';
105
+ })
106
+ .filter(Boolean)
107
+ .join('');
108
+ }
109
+ if (raw.message)
110
+ return normalizeMessageText(raw.message);
111
+ return '';
112
+ }
113
+ function getHistoryRoleRaw(row) {
114
+ return String(row?.role || row?.author || row?.sender || row?.message?.role || row?.message?.author || '').trim().toLowerCase();
115
+ }
116
+ function isRenderableHistoryRole(roleRaw) {
117
+ if (!roleRaw)
118
+ return true;
119
+ return roleRaw === 'user' || roleRaw === 'assistant' || roleRaw === 'system';
120
+ }
121
+ function normalizePreviewMessageText(raw) {
122
+ if (!raw || typeof raw !== 'object')
123
+ return normalizeMessageText(raw);
124
+ const roleRaw = getHistoryRoleRaw(raw);
125
+ if (!isRenderableHistoryRole(roleRaw))
126
+ return '';
127
+ return normalizeMessageText(raw);
128
+ }
129
+ function normalizeHistoryMessages(rawPayload, sessionId, limit, before) {
130
+ const payload = rawPayload || {};
131
+ const rows = Array.isArray(payload.messages) ? payload.messages
132
+ : Array.isArray(payload.items) ? payload.items
133
+ : Array.isArray(payload.history) ? payload.history
134
+ : Array.isArray(payload.entries) ? payload.entries
135
+ : [];
136
+ const mapped = rows.map((row) => {
137
+ const roleRaw = getHistoryRoleRaw(row) || 'assistant';
138
+ if (!isRenderableHistoryRole(roleRaw))
139
+ return null;
140
+ const role = roleRaw === 'user' ? 'user' : roleRaw === 'system' ? 'system' : 'assistant';
141
+ const typeRaw = String(row?.type || row?.message?.type || 'text').toLowerCase();
142
+ const type = typeRaw === 'voice' || typeRaw === 'image' || typeRaw === 'system' || typeRaw === 'command'
143
+ ? typeRaw
144
+ : 'text';
145
+ const timestamp = pickTimestamp(row, Date.now());
146
+ return {
147
+ id: String(row?.id || row?.messageId || (0, crypto_1.randomUUID)()),
148
+ sessionId,
149
+ role,
150
+ type,
151
+ content: normalizeMessageText(row),
152
+ mediaUrl: row?.mediaUrl || row?.media_url || row?.message?.mediaUrl || '',
153
+ duration: Number(row?.duration || row?.message?.duration) || undefined,
154
+ timestamp,
155
+ };
156
+ }).filter((m) => Boolean(m && (m.content || m.type !== 'text')));
157
+ const sorted = mapped.sort((a, b) => a.timestamp - b.timestamp);
158
+ const filtered = before ? sorted.filter((m) => m.timestamp < before) : sorted;
159
+ if (filtered.length <= limit)
160
+ return filtered;
161
+ return filtered.slice(filtered.length - limit);
162
+ }
163
+ async function listGatewayWechatSessions(gateway, userId, agentId) {
164
+ const targetUserId = (0, session_key_1.sanitizeWeChatId)(userId, 'unknown');
165
+ const targetAgentId = (0, session_key_1.sanitizeWeChatId)(agentId, '');
166
+ const response = await gateway.call('sessions.list', {});
167
+ const rawSessions = Array.isArray(response?.sessions) ? response.sessions : [];
168
+ const now = Date.now();
169
+ const rows = [];
170
+ for (const row of rawSessions) {
171
+ const key = String(row?.key || row?.sessionKey || row?.id || '').trim().toLowerCase();
172
+ if (!key)
173
+ continue;
174
+ const parsed = (0, session_key_1.parseWeChatDirectThreadSessionKey)(key);
175
+ if (!parsed)
176
+ continue;
177
+ if (parsed.userId !== targetUserId)
178
+ continue;
179
+ if (targetAgentId && parsed.agentId !== targetAgentId)
180
+ continue;
181
+ const lastMessage = normalizePreviewMessageText(row?.lastMessage || row?.preview || row?.summary || '');
182
+ const title = String(row?.title || row?.displayName || row?.name || '').trim();
183
+ const createdAt = parseTimestamp(row?.createdAt) || pickTimestamp(row, now);
184
+ const lastMessageTime = pickTimestamp(row, createdAt || now);
185
+ rows.push({
186
+ id: parsed.sessionId,
187
+ userId: targetUserId,
188
+ agentId: parsed.agentId,
189
+ gatewaySessionKey: key,
190
+ title: title || lastMessage || '新对话',
191
+ lastMessage,
192
+ lastMessageTime,
193
+ createdAt: createdAt || lastMessageTime || now,
194
+ });
195
+ }
196
+ return rows.sort((a, b) => b.lastMessageTime - a.lastMessageTime);
197
+ }
198
+ async function resolveGatewaySessionKey(gateway, userId, sessionId, agentId) {
199
+ const all = await listGatewayWechatSessions(gateway, userId);
200
+ const targetSessionId = (0, session_key_1.sanitizeWeChatId)(sessionId, '');
201
+ const targetAgent = (0, session_key_1.sanitizeWeChatId)(agentId, '');
202
+ const candidates = all.filter(item => item.id === targetSessionId);
203
+ if (candidates.length > 0) {
204
+ const exact = targetAgent ? candidates.find(c => c.agentId === targetAgent) : null;
205
+ const selected = exact || candidates[0];
206
+ return { key: selected.gatewaySessionKey, agentId: selected.agentId, found: true };
207
+ }
208
+ const fallbackAgent = targetAgent || 'main';
209
+ return {
210
+ key: (0, session_key_1.buildWeChatGatewaySessionKey)(userId, sessionId, fallbackAgent),
211
+ agentId: fallbackAgent,
212
+ found: false,
213
+ };
214
+ }
215
+ async function callGatewayHistory(gateway, key, sessionId, limit, before) {
216
+ const attempts = [
217
+ { sessionKey: key, limit, before },
218
+ { key, limit, before },
219
+ { sessionKey: key, limit },
220
+ { key, limit },
221
+ ];
222
+ let lastError = null;
223
+ for (const params of attempts) {
224
+ try {
225
+ const result = await gateway.call('chat.history', params);
226
+ const messages = normalizeHistoryMessages(result, sessionId, limit, before);
227
+ const hasMore = Boolean(result?.hasMore) || messages.length === limit;
228
+ return { messages, hasMore };
229
+ }
230
+ catch (err) {
231
+ lastError = err;
232
+ }
233
+ }
234
+ throw lastError || new Error('Gateway 历史查询失败');
235
+ }
236
+ async function callGatewayDeleteSession(gateway, key) {
237
+ try {
238
+ await gateway.call('sessions.delete', { key, deleteTranscript: true });
239
+ return;
240
+ }
241
+ catch {
242
+ await gateway.call('sessions.delete', { key });
243
+ }
244
+ }
245
+ function ensureBridgeSession(sessionManager, userId, sessionId, agentId, groupId) {
246
+ const normalizedAgentId = (0, session_key_1.sanitizeWeChatId)(agentId, 'main');
247
+ const existingSessionId = String(sessionId || '').trim();
248
+ if (existingSessionId) {
249
+ return sessionManager.ensureSessionBinding(userId, existingSessionId, normalizedAgentId, (0, session_key_1.buildWeChatGatewaySessionKey)(userId, existingSessionId, normalizedAgentId, groupId));
250
+ }
251
+ const created = sessionManager.createSession(userId, undefined, normalizedAgentId);
252
+ return sessionManager.ensureSessionBinding(userId, created.id, normalizedAgentId, (0, session_key_1.buildWeChatGatewaySessionKey)(userId, created.id, normalizedAgentId, groupId));
253
+ }
254
+ /**
255
+ * 内部中继服务器
256
+ * 仅接受来自 wechat-backend 的请求(通过 X-Internal-Secret 验证)
257
+ * 路由前缀: /internal/*
258
+ */
259
+ function createRelayServer(config, gateway) {
260
+ const app = (0, express_1.default)();
261
+ const dataDir = path_1.default.join(config.uploadDir, '..');
262
+ const sessionManager = new session_manager_1.SessionManager(dataDir);
263
+ const messageHandler = new message_handler_1.MessageHandler(config, sessionManager, gateway);
264
+ const mediaHandler = new media_handler_1.MediaHandler(config);
265
+ const groupManager = new group_manager_1.GroupManager(dataDir);
266
+ const memoSessions = createRequestMemo(1000);
267
+ const memoGroups = createRequestMemo(1000);
268
+ const memoHistory = createRequestMemo(800);
269
+ // ===== 中间件 =====
270
+ app.use((0, cors_1.default)());
271
+ app.use(express_1.default.json({ limit: '10mb' }));
272
+ app.use(express_1.default.urlencoded({ extended: true, limit: '10mb' }));
273
+ app.use('/uploads', express_1.default.static(config.uploadDir));
274
+ const upload = (0, multer_1.default)({
275
+ storage: multer_1.default.memoryStorage(),
276
+ limits: { fileSize: config.maxFileSize },
277
+ });
278
+ // ===== 内部鉴权中间件 =====
279
+ function internalAuth(req, res, next) {
280
+ const secret = req.headers['x-internal-secret'];
281
+ if (!config.internalSecret || secret === config.internalSecret) {
282
+ next();
283
+ }
284
+ else {
285
+ res.status(403).json({ error: '内部通信密钥无效' });
286
+ }
287
+ }
288
+ function getRequestId(req) {
289
+ return String(req.headers['x-request-id'] || req.body?.requestId || req.query?.requestId || '').trim();
290
+ }
291
+ function logBridgeRequest(req, action, extra) {
292
+ const requestId = getRequestId(req);
293
+ const userId = String(req.body?.userId || req.query?.userId || req.userId || '').trim();
294
+ const sessionId = String(req.body?.sessionId || req.query?.sessionId || req.params?.id || '').trim();
295
+ const payload = {
296
+ requestId: requestId || '-',
297
+ action,
298
+ method: req.method,
299
+ path: req.path,
300
+ userId: userId || '-',
301
+ sessionId: sessionId || '-',
302
+ ...(extra || {}),
303
+ };
304
+ console.log('[BridgeHTTP]', JSON.stringify(payload));
305
+ }
306
+ // ===== 健康检查(无鉴权) =====
307
+ app.get('/internal/health', (_req, res) => {
308
+ res.json({
309
+ status: 'ok',
310
+ service: 'openclaw-wechat-plugin',
311
+ timestamp: Date.now(),
312
+ gateway: config.openclawGatewayUrl,
313
+ });
314
+ });
315
+ // ===== 所有内部路由需要密钥验证 =====
316
+ const internal = express_1.default.Router();
317
+ internal.use(internalAuth);
318
+ internal.get('/bridge/capabilities', (_req, res) => {
319
+ res.json({
320
+ protocolVersion: constants_1.BRIDGE_PROTOCOL_VERSION,
321
+ pluginVersion: constants_1.BRIDGE_PLUGIN_VERSION,
322
+ capabilities: Array.from(constants_1.BRIDGE_CAPABILITIES),
323
+ bridgeRole: 'gateway-bridge',
324
+ runtime: {
325
+ persistMessagesEnabled: sessionManager.isPersistMessagesEnabled(),
326
+ localFallbackEnabled: Boolean(config.allowLocalFallback),
327
+ },
328
+ });
329
+ });
330
+ // ===== 会话管理 =====
331
+ internal.get('/sessions', async (req, res) => {
332
+ logBridgeRequest(req, 'sessions.list');
333
+ const userId = String(req.query.userId || req.userId || '').trim();
334
+ const agentId = String(req.query.agentId || '').trim();
335
+ if (!userId) {
336
+ res.status(400).json({ error: '缺少 userId' });
337
+ return;
338
+ }
339
+ try {
340
+ const memoKey = `sessions:${userId}:${agentId || '*'}`;
341
+ const payload = await memoSessions(memoKey, async () => {
342
+ const fromGateway = await listGatewayWechatSessions(gateway, userId, agentId || undefined);
343
+ const normalizedAgentId = agentId ? (0, session_key_1.sanitizeWeChatId)(agentId, 'main') : undefined;
344
+ const sessions = fromGateway.map((item) => {
345
+ const local = sessionManager.getSession(item.id, userId);
346
+ sessionManager.upsertSession(userId, item.id, {
347
+ createdAt: local?.createdAt || item.createdAt,
348
+ agentId: item.agentId,
349
+ gatewaySessionKey: item.gatewaySessionKey,
350
+ });
351
+ return toBridgeSessionPayload({
352
+ id: item.id,
353
+ title: item.title || '新对话',
354
+ lastMessage: item.lastMessage || '',
355
+ lastMessageTime: item.lastMessageTime || item.createdAt || Date.now(),
356
+ createdAt: local?.createdAt || item.createdAt,
357
+ agentId: item.agentId,
358
+ });
359
+ }).sort((a, b) => b.lastMessageTime - a.lastMessageTime);
360
+ sessionManager.pruneSessions(userId, sessions.map(item => item.id), normalizedAgentId);
361
+ return {
362
+ sessions,
363
+ source: 'gateway',
364
+ };
365
+ });
366
+ res.json(payload);
367
+ }
368
+ catch (err) {
369
+ const gatewayError = err?.message || 'unknown';
370
+ console.error('[Sessions] Gateway 列表获取失败:', gatewayError);
371
+ if (!config.allowLocalFallback) {
372
+ res.status(502).json({
373
+ error: 'Gateway 会话列表查询失败',
374
+ source: 'gateway-error',
375
+ });
376
+ return;
377
+ }
378
+ console.warn('[Sessions] 已启用本地 fallback,返回本地元数据');
379
+ const local = sessionManager.getSessions(userId)
380
+ .filter(s => !agentId || s.agentId === (0, session_key_1.sanitizeWeChatId)(agentId, 'main'))
381
+ .map(item => toBridgeSessionPayload(item));
382
+ res.json({
383
+ sessions: local,
384
+ source: 'local-fallback',
385
+ });
386
+ }
387
+ });
388
+ internal.post('/sessions', (req, res) => {
389
+ logBridgeRequest(req, 'sessions.create', { agentId: String(req.body.agentId || 'main') });
390
+ const userId = String(req.body.userId || '').trim();
391
+ const agentId = String(req.body.agentId || 'main').trim() || 'main';
392
+ if (!userId) {
393
+ res.status(400).json({ error: '缺少 userId' });
394
+ return;
395
+ }
396
+ const session = ensureBridgeSession(sessionManager, userId, typeof req.body.sessionId === 'string' ? req.body.sessionId : '', agentId);
397
+ res.json(toBridgeSessionPayload({
398
+ ...session,
399
+ title: req.body.title || session.title,
400
+ }));
401
+ });
402
+ internal.delete('/sessions/:id', async (req, res) => {
403
+ logBridgeRequest(req, 'sessions.delete', { targetSessionId: String(req.params.id || '').trim() });
404
+ const userId = String(req.query.userId || req.body.userId || '').trim();
405
+ const agentId = String(req.query.agentId || req.body.agentId || '').trim();
406
+ const sessionId = String(req.params.id || '').trim();
407
+ if (!userId) {
408
+ res.status(400).json({ error: '缺少 userId' });
409
+ return;
410
+ }
411
+ if (!sessionId) {
412
+ res.status(400).json({ error: '缺少 sessionId' });
413
+ return;
414
+ }
415
+ let gatewayDeleted = true;
416
+ let matchedGatewaySessions = 0;
417
+ try {
418
+ const all = await listGatewayWechatSessions(gateway, userId);
419
+ const hit = all.filter(s => s.id === (0, session_key_1.sanitizeWeChatId)(sessionId, ''));
420
+ matchedGatewaySessions = hit.length;
421
+ if (hit.length > 0) {
422
+ for (const item of hit) {
423
+ await callGatewayDeleteSession(gateway, item.gatewaySessionKey);
424
+ }
425
+ }
426
+ else {
427
+ const fallbackKey = (0, session_key_1.buildWeChatGatewaySessionKey)(userId, sessionId, agentId || 'main');
428
+ await callGatewayDeleteSession(gateway, fallbackKey);
429
+ }
430
+ }
431
+ catch (err) {
432
+ console.error('[Sessions] Gateway 删除失败:', err.message);
433
+ gatewayDeleted = false;
434
+ }
435
+ const localDeleted = sessionManager.deleteSession(sessionId, userId);
436
+ res.json({
437
+ ok: localDeleted && (gatewayDeleted || matchedGatewaySessions === 0),
438
+ sessionId,
439
+ localDeleted,
440
+ gatewayDeleted,
441
+ });
442
+ });
443
+ // ===== Agent REST API =====
444
+ const OPENCLAW_AGENTS_DIR = path_1.default.join(process.env.HOME || '/root', '.openclaw', 'agents');
445
+ internal.get('/agents', (_req, res) => {
446
+ try {
447
+ const agents = [];
448
+ if (fs_1.default.existsSync(OPENCLAW_AGENTS_DIR)) {
449
+ const dirs = fs_1.default.readdirSync(OPENCLAW_AGENTS_DIR, { withFileTypes: true });
450
+ for (const d of dirs) {
451
+ if (!d.isDirectory())
452
+ continue;
453
+ const agentId = d.name;
454
+ const identityFile = path_1.default.join(OPENCLAW_AGENTS_DIR, agentId, 'agent', 'identity.json');
455
+ let identity = { name: agentId, emoji: '🤖' };
456
+ try {
457
+ if (fs_1.default.existsSync(identityFile)) {
458
+ identity = { ...identity, ...JSON.parse(fs_1.default.readFileSync(identityFile, 'utf-8')) };
459
+ }
460
+ }
461
+ catch { /* ignore */ }
462
+ agents.push({ id: agentId, name: agentId, identity });
463
+ }
464
+ }
465
+ if (agents.length === 0) {
466
+ agents.push({ id: 'main', name: 'main', identity: { name: 'OpenClaw', emoji: '🤖' } });
467
+ }
468
+ res.json({ agents, defaultId: 'main' });
469
+ }
470
+ catch (err) {
471
+ res.json({ agents: [{ id: 'main', name: 'main', identity: { name: 'OpenClaw', emoji: '🤖' } }], defaultId: 'main' });
472
+ }
473
+ });
474
+ internal.get('/agents/:id/identity', (req, res) => {
475
+ const identityFile = path_1.default.join(OPENCLAW_AGENTS_DIR, req.params.id, 'agent', 'identity.json');
476
+ try {
477
+ if (fs_1.default.existsSync(identityFile)) {
478
+ res.json(JSON.parse(fs_1.default.readFileSync(identityFile, 'utf-8')));
479
+ }
480
+ else {
481
+ res.json({ name: req.params.id, emoji: '🤖' });
482
+ }
483
+ }
484
+ catch {
485
+ res.json({ name: req.params.id, emoji: '🤖' });
486
+ }
487
+ });
488
+ internal.post('/agents', (req, res) => {
489
+ try {
490
+ const { name, emoji } = req.body;
491
+ if (!name) {
492
+ res.status(400).json({ error: '缺少 name' });
493
+ return;
494
+ }
495
+ const agentDir = path_1.default.join(OPENCLAW_AGENTS_DIR, name, 'agent');
496
+ fs_1.default.mkdirSync(agentDir, { recursive: true });
497
+ if (emoji) {
498
+ fs_1.default.writeFileSync(path_1.default.join(agentDir, 'identity.json'), JSON.stringify({ name, emoji }, null, 2), 'utf-8');
499
+ }
500
+ res.json({ id: name, name, identity: { name, emoji: emoji || '🤖' } });
501
+ }
502
+ catch (err) {
503
+ res.status(500).json({ error: err.message });
504
+ }
505
+ });
506
+ internal.put('/agents/:id', (req, res) => {
507
+ try {
508
+ const agentDir = path_1.default.join(OPENCLAW_AGENTS_DIR, req.params.id, 'agent');
509
+ const idFile = path_1.default.join(agentDir, 'identity.json');
510
+ let identity = { name: req.params.id, emoji: '🤖' };
511
+ try {
512
+ if (fs_1.default.existsSync(idFile))
513
+ identity = JSON.parse(fs_1.default.readFileSync(idFile, 'utf-8'));
514
+ }
515
+ catch { }
516
+ Object.assign(identity, req.body);
517
+ fs_1.default.mkdirSync(agentDir, { recursive: true });
518
+ fs_1.default.writeFileSync(idFile, JSON.stringify(identity, null, 2), 'utf-8');
519
+ res.json({ id: req.params.id, identity });
520
+ }
521
+ catch (err) {
522
+ res.status(500).json({ error: err.message });
523
+ }
524
+ });
525
+ internal.delete('/agents/:id', (req, res) => {
526
+ try {
527
+ const agentDir = path_1.default.join(OPENCLAW_AGENTS_DIR, req.params.id);
528
+ if (fs_1.default.existsSync(agentDir)) {
529
+ fs_1.default.rmSync(agentDir, { recursive: true, force: true });
530
+ }
531
+ res.json({ ok: true });
532
+ }
533
+ catch (err) {
534
+ res.status(500).json({ error: err.message });
535
+ }
536
+ });
537
+ // Agent 文件管理
538
+ internal.get('/agents/:id/files', async (req, res) => {
539
+ try {
540
+ const result = await gateway.getAgentFiles(req.params.id);
541
+ res.json(result);
542
+ }
543
+ catch (err) {
544
+ res.status(500).json({ error: err.message });
545
+ }
546
+ });
547
+ internal.get('/agents/:id/files/:filename', async (req, res) => {
548
+ try {
549
+ const result = await gateway.getAgentFile(req.params.id, req.params.filename);
550
+ res.json(result);
551
+ }
552
+ catch (err) {
553
+ res.status(500).json({ error: err.message });
554
+ }
555
+ });
556
+ internal.put('/agents/:id/files/:filename', async (req, res) => {
557
+ try {
558
+ const { content } = req.body;
559
+ await gateway.setAgentFile(req.params.id, req.params.filename, content);
560
+ res.json({ ok: true });
561
+ }
562
+ catch (err) {
563
+ res.status(500).json({ error: err.message });
564
+ }
565
+ });
566
+ // ===== Group API =====
567
+ internal.get('/groups', (_req, res) => {
568
+ memoGroups('groups:list', async () => ({ groups: groupManager.list() }))
569
+ .then(payload => res.json(payload))
570
+ .catch((err) => res.status(500).json({ error: err?.message || '群列表获取失败' }));
571
+ });
572
+ internal.post('/groups', (req, res) => {
573
+ const { name, emoji, agentIds, mode } = req.body;
574
+ if (!name) {
575
+ res.status(400).json({ error: '缺少群名称' });
576
+ return;
577
+ }
578
+ const group = groupManager.create(name, emoji, agentIds, mode);
579
+ res.json(group);
580
+ });
581
+ internal.put('/groups/:id', (req, res) => {
582
+ const group = groupManager.update(req.params.id, req.body);
583
+ group ? res.json(group) : res.status(404).json({ error: '群不存在' });
584
+ });
585
+ internal.delete('/groups/:id', (req, res) => {
586
+ groupManager.delete(req.params.id)
587
+ ? res.json({ ok: true })
588
+ : res.status(404).json({ error: '群不存在' });
589
+ });
590
+ internal.post('/groups/:id/agents', (req, res) => {
591
+ const { agentId } = req.body;
592
+ if (!agentId) {
593
+ res.status(400).json({ error: '缺少 agentId' });
594
+ return;
595
+ }
596
+ const group = groupManager.addAgent(req.params.id, agentId);
597
+ group ? res.json(group) : res.status(404).json({ error: '群不存在' });
598
+ });
599
+ internal.delete('/groups/:id/agents/:agentId', (req, res) => {
600
+ const group = groupManager.removeAgent(req.params.id, req.params.agentId);
601
+ group ? res.json(group) : res.status(404).json({ error: '群不存在' });
602
+ });
603
+ // ===== 聊天 =====
604
+ internal.get('/chat/history', async (req, res) => {
605
+ logBridgeRequest(req, 'chat.history');
606
+ const userId = String(req.query.userId || '').trim();
607
+ const sessionId = String(req.query.sessionId || '').trim();
608
+ const agentId = String(req.query.agentId || '').trim();
609
+ const limit = parseInt(req.query.limit) || 50;
610
+ const before = req.query.before ? parseInt(req.query.before) : undefined;
611
+ if (!userId) {
612
+ res.status(400).json({ error: '缺少 userId' });
613
+ return;
614
+ }
615
+ if (!sessionId) {
616
+ res.status(400).json({ error: '缺少 sessionId' });
617
+ return;
618
+ }
619
+ try {
620
+ const memoKey = `history:${userId}:${agentId || '*'}:${sessionId}:${limit}:${before || 0}`;
621
+ const payload = await memoHistory(memoKey, async () => {
622
+ const resolved = await resolveGatewaySessionKey(gateway, userId, sessionId, agentId || undefined);
623
+ if (!resolved.found) {
624
+ sessionManager.deleteSession(sessionId, userId);
625
+ sessionManager.clearMessages(sessionId);
626
+ return {
627
+ messages: [],
628
+ hasMore: false,
629
+ source: 'gateway',
630
+ sessionMissing: true,
631
+ };
632
+ }
633
+ sessionManager.upsertSession(userId, sessionId, {
634
+ agentId: resolved.agentId,
635
+ gatewaySessionKey: resolved.key,
636
+ });
637
+ const { messages, hasMore } = await callGatewayHistory(gateway, resolved.key, sessionId, limit, before);
638
+ if (messages.length === 0) {
639
+ sessionManager.clearMessages(sessionId);
640
+ }
641
+ return { messages, hasMore, source: 'gateway', sessionMissing: false };
642
+ });
643
+ res.json(payload);
644
+ return;
645
+ }
646
+ catch (err) {
647
+ const gatewayError = err?.message || 'unknown';
648
+ console.error('[History] Gateway 查询失败:', gatewayError);
649
+ if (!config.allowLocalFallback) {
650
+ res.status(502).json({
651
+ error: 'Gateway 历史查询失败',
652
+ source: 'gateway-error',
653
+ sessionMissing: false,
654
+ });
655
+ return;
656
+ }
657
+ console.warn('[History] 已启用本地 fallback,返回本地缓存');
658
+ }
659
+ const messages = sessionManager.getMessages(sessionId, limit, before);
660
+ res.json({
661
+ messages,
662
+ hasMore: messages.length === limit,
663
+ source: 'local-fallback',
664
+ sessionMissing: false,
665
+ });
666
+ });
667
+ internal.post('/chat/send', async (req, res) => {
668
+ logBridgeRequest(req, 'chat.send', {
669
+ route: typeof req.body.commandRoute === 'string' ? req.body.commandRoute : '-',
670
+ routeSource: typeof req.body.routeSource === 'string' ? req.body.routeSource : '-',
671
+ });
672
+ const userId = req.body.userId;
673
+ const { sessionId, content } = req.body;
674
+ const agentId = String(req.body.agentId || 'main');
675
+ if (!userId) {
676
+ res.status(400).json({ error: '缺少 userId' });
677
+ return;
678
+ }
679
+ if (!content) {
680
+ res.status(400).json({ error: '消息内容不能为空' });
681
+ return;
682
+ }
683
+ try {
684
+ const session = ensureBridgeSession(sessionManager, userId, sessionId, agentId);
685
+ const sid = session.id;
686
+ const result = await messageHandler.handleMessage(userId, sid, content, 'text', undefined, undefined, agentId, undefined, {
687
+ commandRoute: typeof req.body.commandRoute === 'string' ? req.body.commandRoute : undefined,
688
+ routeSource: typeof req.body.routeSource === 'string' ? req.body.routeSource : undefined,
689
+ requestId: typeof req.body.requestId === 'string' ? req.body.requestId : undefined,
690
+ });
691
+ res.json(result);
692
+ }
693
+ catch (err) {
694
+ console.error('[Chat] 发送消息失败:', err);
695
+ res.status(500).json({ error: err.message });
696
+ }
697
+ });
698
+ internal.post('/chat/send-voice', upload.single('voice'), async (req, res) => {
699
+ logBridgeRequest(req, 'chat.send-voice');
700
+ const userId = req.body.userId;
701
+ const { sessionId } = req.body;
702
+ const agentId = String(req.body.agentId || 'main');
703
+ const duration = parseFloat(req.body.duration) || 0;
704
+ if (!userId) {
705
+ res.status(400).json({ error: '缺少 userId' });
706
+ return;
707
+ }
708
+ if (!req.file) {
709
+ res.status(400).json({ error: '没有上传语音文件' });
710
+ return;
711
+ }
712
+ try {
713
+ const { url } = mediaHandler.saveVoice(req.file.buffer, req.file.originalname);
714
+ const session = ensureBridgeSession(sessionManager, userId, sessionId, agentId);
715
+ const sid = session.id;
716
+ const textContent = req.body.text || '[语音消息]';
717
+ const result = await messageHandler.handleMessage(userId, sid, textContent, 'voice', url, duration, agentId);
718
+ res.json(result);
719
+ }
720
+ catch (err) {
721
+ console.error('[Chat] 语音消息失败:', err);
722
+ res.status(500).json({ error: err.message });
723
+ }
724
+ });
725
+ internal.post('/chat/send-image', upload.single('image'), async (req, res) => {
726
+ logBridgeRequest(req, 'chat.send-image');
727
+ const userId = req.body.userId;
728
+ const { sessionId } = req.body;
729
+ const agentId = String(req.body.agentId || 'main');
730
+ if (!userId) {
731
+ res.status(400).json({ error: '缺少 userId' });
732
+ return;
733
+ }
734
+ if (!req.file) {
735
+ res.status(400).json({ error: '没有上传图片文件' });
736
+ return;
737
+ }
738
+ try {
739
+ const { url } = mediaHandler.saveImage(req.file.buffer, req.file.originalname);
740
+ const session = ensureBridgeSession(sessionManager, userId, sessionId, agentId);
741
+ const sid = session.id;
742
+ const userMessage = sessionManager.addMessage(sid, 'user', 'image', req.body.description || '请查看这张图片', url);
743
+ res.json({ userMessage, mediaUrl: url, sessionId: sid });
744
+ }
745
+ catch (err) {
746
+ console.error('[Chat] 图片上传失败:', err);
747
+ res.status(500).json({ error: err.message });
748
+ }
749
+ });
750
+ internal.get('/commands', (_req, res) => {
751
+ res.json({ commands: commands_1.BOT_COMMANDS });
752
+ });
753
+ // 配置路由
754
+ internal.use('/config', config_routes_1.default);
755
+ app.use('/internal', internal);
756
+ // ===== WebSocket 服务 =====
757
+ const server = http_1.default.createServer(app);
758
+ const wss = new ws_1.WebSocketServer({ server, path: '/internal/ws' });
759
+ const heartbeatInterval = setInterval(() => {
760
+ wss.clients.forEach((ws) => {
761
+ if (ws.isAlive === false)
762
+ return ws.terminate();
763
+ ws.isAlive = false;
764
+ ws.ping();
765
+ });
766
+ }, 30000);
767
+ wss.on('close', () => clearInterval(heartbeatInterval));
768
+ wss.on('connection', (ws, req) => {
769
+ const wsAny = ws;
770
+ wsAny.isAlive = true;
771
+ wsAny.abortCurrent = null;
772
+ // 从后端代理传来的 header 获取 userId 和密钥验证
773
+ const secret = req.headers['x-internal-secret'];
774
+ const userId = req.headers['x-user-id'];
775
+ if (config.internalSecret && secret !== config.internalSecret) {
776
+ ws.close(4003, '内部通信密钥无效');
777
+ return;
778
+ }
779
+ wsAny.authenticated = true;
780
+ wsAny.userId = userId;
781
+ console.log(`[WS] 内部连接: userId=${userId}`);
782
+ ws.on('pong', () => { wsAny.isAlive = true; });
783
+ ws.on('message', (raw) => {
784
+ wsAny.isAlive = true;
785
+ let msg;
786
+ try {
787
+ msg = JSON.parse(raw.toString());
788
+ }
789
+ catch {
790
+ wsSend(ws, { type: 'error', message: '无效的 JSON' });
791
+ return;
792
+ }
793
+ // userId 来自消息或 header
794
+ if (msg.userId)
795
+ wsAny.userId = msg.userId;
796
+ switch (msg.type) {
797
+ case 'chat':
798
+ handleWsChat(ws, msg, messageHandler, sessionManager);
799
+ break;
800
+ case 'stop':
801
+ if (wsAny.abortCurrent) {
802
+ wsAny.abortCurrent();
803
+ wsAny.abortCurrent = null;
804
+ wsSend(ws, { type: 'stopped' });
805
+ }
806
+ break;
807
+ case 'ping':
808
+ wsSend(ws, { type: 'pong' });
809
+ break;
810
+ default:
811
+ wsSend(ws, { type: 'error', message: `未知消息类型: ${msg.type}` });
812
+ }
813
+ });
814
+ ws.on('close', () => {
815
+ if (wsAny.abortCurrent) {
816
+ wsAny.abortCurrent();
817
+ wsAny.abortCurrent = null;
818
+ }
819
+ });
820
+ ws.on('error', (err) => {
821
+ console.error('[WS] 错误:', err.message);
822
+ });
823
+ });
824
+ return { app, server, sessionManager, wss };
825
+ }
826
+ // ===== WebSocket 处理函数 =====
827
+ function wsSend(ws, data) {
828
+ if (ws.readyState === ws_1.WebSocket.OPEN) {
829
+ ws.send(JSON.stringify(data));
830
+ }
831
+ }
832
+ async function handleWsChat(ws, msg, messageHandler, sessionManager) {
833
+ const wsAny = ws;
834
+ if (!wsAny.authenticated || !wsAny.userId) {
835
+ wsSend(ws, { type: 'error', message: '未认证' });
836
+ return;
837
+ }
838
+ const { sessionId, content, messageType, agentId, groupId, mediaUrl } = msg;
839
+ if (!content) {
840
+ wsSend(ws, { type: 'error', message: '消息内容不能为空' });
841
+ return;
842
+ }
843
+ const userId = wsAny.userId;
844
+ const selectedAgentId = String(agentId || 'main');
845
+ const session = ensureBridgeSession(sessionManager, userId, sessionId, selectedAgentId, groupId);
846
+ const sid = session.id;
847
+ const type = messageType || 'text';
848
+ if (wsAny.abortCurrent) {
849
+ wsAny.abortCurrent();
850
+ }
851
+ wsSend(ws, { type: 'start', sessionId: sid, agentId: selectedAgentId });
852
+ const abort = messageHandler.handleMessageStream(userId, sid, content, type, {
853
+ onThinking: (text) => {
854
+ wsSend(ws, { type: 'thinking', text, agentId: selectedAgentId });
855
+ },
856
+ onChunk: (text) => {
857
+ wsSend(ws, { type: 'chunk', text, agentId: selectedAgentId });
858
+ },
859
+ onDone: (userMessage, assistantMessage) => {
860
+ wsAny.abortCurrent = null;
861
+ wsSend(ws, {
862
+ type: 'done', userMessage, assistantMessage,
863
+ sessionId: sid, agentId: selectedAgentId
864
+ });
865
+ },
866
+ onError: (error) => {
867
+ wsAny.abortCurrent = null;
868
+ wsSend(ws, { type: 'error', message: error, agentId: selectedAgentId });
869
+ },
870
+ }, selectedAgentId, mediaUrl, undefined, groupId, {
871
+ commandRoute: typeof msg.commandRoute === 'string' ? msg.commandRoute : undefined,
872
+ routeSource: typeof msg.routeSource === 'string' ? msg.routeSource : undefined,
873
+ requestId: typeof msg.requestId === 'string' ? msg.requestId : undefined,
874
+ });
875
+ wsAny.abortCurrent = abort;
876
+ }
877
+ //# sourceMappingURL=relay-server.js.map