openclaw-vchat-plugin 0.0.7 → 0.0.9

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 (53) hide show
  1. package/dist/commands.d.ts +1 -0
  2. package/dist/commands.d.ts.map +1 -1
  3. package/dist/commands.js +37 -62
  4. package/dist/commands.js.map +1 -1
  5. package/dist/constants.d.ts +1 -1
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/constants.js +3 -1
  8. package/dist/constants.js.map +1 -1
  9. package/dist/group-event-store.d.ts +47 -0
  10. package/dist/group-event-store.d.ts.map +1 -0
  11. package/dist/group-event-store.js +145 -0
  12. package/dist/group-event-store.js.map +1 -0
  13. package/dist/index.js +74 -8
  14. package/dist/index.js.map +1 -1
  15. package/dist/message-handler.d.ts.map +1 -1
  16. package/dist/message-handler.js +11 -4
  17. package/dist/message-handler.js.map +1 -1
  18. package/dist/plugin-update-runner.d.ts +2 -0
  19. package/dist/plugin-update-runner.d.ts.map +1 -0
  20. package/dist/plugin-update-runner.js +179 -0
  21. package/dist/plugin-update-runner.js.map +1 -0
  22. package/dist/relay-server.d.ts +2 -0
  23. package/dist/relay-server.d.ts.map +1 -1
  24. package/dist/relay-server.js +268 -8
  25. package/dist/relay-server.js.bak-self-send-filter-20260310 +1028 -0
  26. package/dist/relay-server.js.map +1 -1
  27. package/dist/routes/config.routes.d.ts.map +1 -1
  28. package/dist/routes/config.routes.js +53 -0
  29. package/dist/routes/config.routes.js.map +1 -1
  30. package/dist/services/config.service.d.ts +8 -0
  31. package/dist/services/config.service.d.ts.map +1 -1
  32. package/dist/services/config.service.js +200 -2
  33. package/dist/services/config.service.js.map +1 -1
  34. package/dist/services/plugin-update.service.d.ts +24 -0
  35. package/dist/services/plugin-update.service.d.ts.map +1 -0
  36. package/dist/services/plugin-update.service.js +308 -0
  37. package/dist/services/plugin-update.service.js.map +1 -0
  38. package/dist/services/skills.service.d.ts +33 -0
  39. package/dist/services/skills.service.d.ts.map +1 -0
  40. package/dist/services/skills.service.js +177 -0
  41. package/dist/services/skills.service.js.map +1 -0
  42. package/package.json +1 -1
  43. package/src/commands.ts +38 -61
  44. package/src/constants.ts +4 -1
  45. package/src/group-event-store.ts +204 -0
  46. package/src/index.ts +72 -8
  47. package/src/message-handler.ts +19 -5
  48. package/src/plugin-update-runner.ts +217 -0
  49. package/src/relay-server.ts +292 -8
  50. package/src/routes/config.routes.ts +56 -0
  51. package/src/services/config.service.ts +223 -3
  52. package/src/services/plugin-update.service.ts +335 -0
  53. package/src/services/skills.service.ts +175 -0
@@ -10,16 +10,20 @@ const multer_1 = __importDefault(require("multer"));
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const fs_1 = __importDefault(require("fs"));
12
12
  const http_1 = __importDefault(require("http"));
13
+ const child_process_1 = require("child_process");
13
14
  const ws_1 = require("ws");
14
15
  const crypto_1 = require("crypto");
15
16
  const session_manager_1 = require("./session-manager");
16
17
  const message_handler_1 = require("./message-handler");
17
18
  const media_handler_1 = require("./media-handler");
18
19
  const group_manager_1 = require("./group-manager");
20
+ const group_event_store_1 = require("./group-event-store");
19
21
  const commands_1 = require("./commands");
20
22
  const constants_1 = require("./constants");
21
23
  const session_key_1 = require("./session-key");
22
24
  const config_routes_1 = __importDefault(require("./routes/config.routes"));
25
+ const skills_service_1 = require("./services/skills.service");
26
+ const plugin_update_service_1 = require("./services/plugin-update.service");
23
27
  function createRequestMemo(ttlMs) {
24
28
  const inflight = new Map();
25
29
  const cache = new Map();
@@ -55,6 +59,7 @@ function toBridgeSessionPayload(item) {
55
59
  lastMessageTime: Number(item.lastMessageTime) || createdAt,
56
60
  createdAt,
57
61
  agentId: String(item.agentId || '').trim() || 'main',
62
+ model: String(item.model || '').trim(),
58
63
  };
59
64
  }
60
65
  function parseTimestamp(value) {
@@ -128,15 +133,63 @@ function isRenderableHistoryRole(roleRaw) {
128
133
  return true;
129
134
  return roleRaw === 'user' || roleRaw === 'assistant' || roleRaw === 'system';
130
135
  }
131
- function normalizePreviewMessageText(raw) {
136
+ function getHistoryProvenance(row) {
137
+ if (!row || typeof row !== 'object')
138
+ return null;
139
+ return row?.provenance || row?.message?.provenance || null;
140
+ }
141
+ function shouldHideHistoryRow(row, currentSessionKey) {
142
+ const provenance = getHistoryProvenance(row);
143
+ if (!provenance || typeof provenance !== 'object')
144
+ return false;
145
+ const kind = String(provenance?.kind || '').trim().toLowerCase();
146
+ if (kind !== 'inter_session')
147
+ return false;
148
+ const sourceTool = String(provenance?.sourceTool || '').trim().toLowerCase();
149
+ const sourceSessionKey = String(provenance?.sourceSessionKey || '').trim().toLowerCase();
150
+ const targetSessionKey = String(currentSessionKey || '').trim().toLowerCase();
151
+ // Hide self-routed relay messages. They are persisted by OpenClaw with user role,
152
+ // but should not be rendered as human-authored bubbles in the WeChat UI.
153
+ return sourceTool === 'sessions_send' && Boolean(targetSessionKey) && sourceSessionKey === targetSessionKey;
154
+ }
155
+ function normalizePreviewMessageText(raw, currentSessionKey) {
132
156
  if (!raw || typeof raw !== 'object')
133
157
  return normalizeMessageText(raw);
134
158
  const roleRaw = getHistoryRoleRaw(raw);
135
159
  if (!isRenderableHistoryRole(roleRaw))
136
160
  return '';
161
+ if (shouldHideHistoryRow(raw, currentSessionKey))
162
+ return '';
137
163
  return normalizeMessageText(raw);
138
164
  }
139
- function normalizeHistoryMessages(rawPayload, sessionId, limit, before) {
165
+ function isInternalSessionTitle(raw) {
166
+ const title = String(raw || '').trim();
167
+ if (!title)
168
+ return true;
169
+ const lower = title.toLowerCase();
170
+ if (lower === 'openclaw-wechat-plugin'
171
+ || lower === 'openclaw-vchat-plugin'
172
+ || lower === 'openclaw-vchat'
173
+ || lower === 'openclaw-tui') {
174
+ return true;
175
+ }
176
+ return (lower.startsWith('webchat:')
177
+ || lower.startsWith('agent:')
178
+ || lower.includes('wechat:direct:')
179
+ || lower.includes('g-agent-')
180
+ || /^[a-f0-9-]{24,}$/i.test(title));
181
+ }
182
+ function pickVisibleSessionTitle(row, lastMessage) {
183
+ const candidates = [row?.title, row?.displayName, row?.name];
184
+ for (const candidate of candidates) {
185
+ const title = String(candidate || '').trim();
186
+ if (!title || isInternalSessionTitle(title))
187
+ continue;
188
+ return title;
189
+ }
190
+ return String(lastMessage || '').trim() || '新对话';
191
+ }
192
+ function normalizeHistoryMessages(rawPayload, sessionId, limit, before, currentSessionKey) {
140
193
  const payload = rawPayload || {};
141
194
  const rows = Array.isArray(payload.messages) ? payload.messages
142
195
  : Array.isArray(payload.items) ? payload.items
@@ -144,6 +197,8 @@ function normalizeHistoryMessages(rawPayload, sessionId, limit, before) {
144
197
  : Array.isArray(payload.entries) ? payload.entries
145
198
  : [];
146
199
  const mapped = rows.map((row) => {
200
+ if (shouldHideHistoryRow(row, currentSessionKey))
201
+ return null;
147
202
  const roleRaw = getHistoryRoleRaw(row) || 'assistant';
148
203
  if (!isRenderableHistoryRole(roleRaw))
149
204
  return null;
@@ -170,12 +225,91 @@ function normalizeHistoryMessages(rawPayload, sessionId, limit, before) {
170
225
  return filtered;
171
226
  return filtered.slice(filtered.length - limit);
172
227
  }
228
+ function readConfiguredPrimaryModel(gateway) {
229
+ try {
230
+ const raw = gateway.readConfig();
231
+ const agents = Array.isArray(raw?.agents?.list) ? raw.agents.list : [];
232
+ const mainAgent = agents.find((item) => String(item?.id || '').trim() === 'main');
233
+ const candidates = [
234
+ mainAgent?.model,
235
+ raw?.agents?.defaults?.model?.primary,
236
+ raw?.agents?.defaults?.primary,
237
+ raw?.defaults?.primary,
238
+ raw?.model,
239
+ ];
240
+ for (const candidate of candidates) {
241
+ const value = String(candidate || '').trim();
242
+ if (value)
243
+ return value;
244
+ }
245
+ }
246
+ catch {
247
+ // ignore
248
+ }
249
+ return '';
250
+ }
251
+ function readOpenClawVersion() {
252
+ try {
253
+ const value = (0, child_process_1.execFileSync)('openclaw', ['--version'], {
254
+ encoding: 'utf8',
255
+ timeout: 3000,
256
+ stdio: ['ignore', 'pipe', 'ignore'],
257
+ });
258
+ return String(value || '').trim();
259
+ }
260
+ catch {
261
+ return '';
262
+ }
263
+ }
264
+ async function getOpenClawSummary(gateway) {
265
+ const version = readOpenClawVersion();
266
+ try {
267
+ const status = await gateway.call('status', {});
268
+ const recent = status?.sessions?.recent?.[0];
269
+ const model = String(status?.sessions?.defaults?.model
270
+ || recent?.model
271
+ || readConfiguredPrimaryModel(gateway)
272
+ || '').trim();
273
+ return {
274
+ online: true,
275
+ version,
276
+ model,
277
+ };
278
+ }
279
+ catch (err) {
280
+ return {
281
+ online: false,
282
+ version,
283
+ model: readConfiguredPrimaryModel(gateway),
284
+ error: err?.message || 'gateway-unavailable',
285
+ };
286
+ }
287
+ }
288
+ function buildGatewayRecentModelHints(statusPayload) {
289
+ const hints = new Map();
290
+ const recent = Array.isArray(statusPayload?.sessions?.recent) ? statusPayload.sessions.recent : [];
291
+ for (const row of recent) {
292
+ const model = String(row?.model || '').trim();
293
+ if (!model)
294
+ continue;
295
+ const key = String(row?.key || row?.sessionKey || '').trim().toLowerCase();
296
+ if (key) {
297
+ hints.set(key, model);
298
+ const parsed = (0, session_key_1.parseWeChatDirectThreadSessionKey)(key);
299
+ if (parsed) {
300
+ hints.set(`${parsed.userId}:${parsed.agentId}:${parsed.sessionId}`, model);
301
+ }
302
+ }
303
+ }
304
+ return hints;
305
+ }
173
306
  async function listGatewayWechatSessions(gateway, userId, agentId) {
174
307
  const targetUserId = (0, session_key_1.sanitizeWeChatId)(userId, 'unknown');
175
308
  const targetAgentId = (0, session_key_1.sanitizeWeChatId)(agentId, '');
176
309
  const response = await gateway.call('sessions.list', {});
177
310
  const rawSessions = Array.isArray(response?.sessions) ? response.sessions : [];
178
311
  const now = Date.now();
312
+ let recentModelHints = null;
179
313
  const rows = [];
180
314
  for (const row of rawSessions) {
181
315
  const key = String(row?.key || row?.sessionKey || row?.id || '').trim().toLowerCase();
@@ -188,19 +322,37 @@ async function listGatewayWechatSessions(gateway, userId, agentId) {
188
322
  continue;
189
323
  if (targetAgentId && parsed.agentId !== targetAgentId)
190
324
  continue;
191
- const lastMessage = normalizePreviewMessageText(row?.lastMessage || row?.preview || row?.summary || '');
192
- const title = String(row?.title || row?.displayName || row?.name || '').trim();
325
+ const lastMessage = normalizePreviewMessageText(row?.lastMessage || row?.preview || row?.summary || '', key);
326
+ const title = pickVisibleSessionTitle(row, lastMessage);
193
327
  const createdAt = parseTimestamp(row?.createdAt) || pickTimestamp(row, now);
194
328
  const lastMessageTime = pickTimestamp(row, createdAt || now);
329
+ const modelProvider = String(row?.modelProvider || '').trim();
330
+ const modelId = String(row?.model || '').trim();
331
+ let model = modelProvider && modelId ? `${modelProvider}/${modelId}` : modelId;
332
+ if (!model) {
333
+ if (!recentModelHints) {
334
+ try {
335
+ const status = await gateway.call('status', {});
336
+ recentModelHints = buildGatewayRecentModelHints(status);
337
+ }
338
+ catch {
339
+ recentModelHints = new Map();
340
+ }
341
+ }
342
+ model = recentModelHints.get(key)
343
+ || recentModelHints.get(`${parsed.userId}:${parsed.agentId}:${parsed.sessionId}`)
344
+ || '';
345
+ }
195
346
  rows.push({
196
347
  id: parsed.sessionId,
197
348
  userId: targetUserId,
198
349
  agentId: parsed.agentId,
199
350
  gatewaySessionKey: key,
200
- title: title || lastMessage || '新对话',
351
+ title,
201
352
  lastMessage,
202
353
  lastMessageTime,
203
354
  createdAt: createdAt || lastMessageTime || now,
355
+ model,
204
356
  });
205
357
  }
206
358
  return rows.sort((a, b) => b.lastMessageTime - a.lastMessageTime);
@@ -233,7 +385,7 @@ async function callGatewayHistory(gateway, key, sessionId, limit, before) {
233
385
  for (const params of attempts) {
234
386
  try {
235
387
  const result = await gateway.call('chat.history', params);
236
- const messages = normalizeHistoryMessages(result, sessionId, limit, before);
388
+ const messages = normalizeHistoryMessages(result, sessionId, limit, before, key);
237
389
  const hasMore = Boolean(result?.hasMore) || messages.length === limit;
238
390
  return { messages, hasMore };
239
391
  }
@@ -270,11 +422,13 @@ function createRelayServer(config, gateway) {
270
422
  const app = (0, express_1.default)();
271
423
  const dataDir = path_1.default.join(config.uploadDir, '..');
272
424
  const sessionManager = new session_manager_1.SessionManager(dataDir);
425
+ const groupEventStore = new group_event_store_1.GroupEventStore(dataDir);
273
426
  const messageHandler = new message_handler_1.MessageHandler(config, sessionManager, gateway);
274
427
  const mediaHandler = new media_handler_1.MediaHandler(config);
275
428
  const groupManager = new group_manager_1.GroupManager(dataDir);
276
429
  const memoSessions = createRequestMemo(1000);
277
430
  const memoGroups = createRequestMemo(1000);
431
+ const memoSkills = createRequestMemo(1500);
278
432
  const memoHistory = createRequestMemo(800);
279
433
  // ===== 中间件 =====
280
434
  app.use((0, cors_1.default)());
@@ -325,12 +479,14 @@ function createRelayServer(config, gateway) {
325
479
  // ===== 所有内部路由需要密钥验证 =====
326
480
  const internal = express_1.default.Router();
327
481
  internal.use(internalAuth);
328
- internal.get('/bridge/capabilities', (_req, res) => {
482
+ internal.get('/bridge/capabilities', async (_req, res) => {
483
+ const openclaw = await getOpenClawSummary(gateway);
329
484
  res.json({
330
485
  protocolVersion: constants_1.BRIDGE_PROTOCOL_VERSION,
331
486
  pluginVersion: constants_1.BRIDGE_PLUGIN_VERSION,
332
487
  capabilities: Array.from(constants_1.BRIDGE_CAPABILITIES),
333
488
  bridgeRole: 'gateway-bridge',
489
+ openclaw,
334
490
  runtime: {
335
491
  persistMessagesEnabled: sessionManager.isPersistMessagesEnabled(),
336
492
  localFallbackEnabled: Boolean(config.allowLocalFallback),
@@ -365,6 +521,7 @@ function createRelayServer(config, gateway) {
365
521
  lastMessageTime: item.lastMessageTime || item.createdAt || Date.now(),
366
522
  createdAt: local?.createdAt || item.createdAt,
367
523
  agentId: item.agentId,
524
+ model: item.model,
368
525
  });
369
526
  }).sort((a, b) => b.lastMessageTime - a.lastMessageTime);
370
527
  sessionManager.pruneSessions(userId, sessions.map(item => item.id), normalizedAgentId);
@@ -579,6 +736,19 @@ function createRelayServer(config, gateway) {
579
736
  .then(payload => res.json(payload))
580
737
  .catch((err) => res.status(500).json({ error: err?.message || '群列表获取失败' }));
581
738
  });
739
+ internal.get('/groups/:id/history', (req, res) => {
740
+ try {
741
+ const limit = parseInt(String(req.query.limit || ''), 10) || 100;
742
+ const before = req.query.before ? parseInt(String(req.query.before), 10) : undefined;
743
+ res.json({
744
+ messages: groupEventStore.list(req.params.id, limit, before),
745
+ source: 'local-group-events',
746
+ });
747
+ }
748
+ catch (err) {
749
+ res.status(500).json({ error: err?.message || '群历史获取失败' });
750
+ }
751
+ });
582
752
  internal.post('/groups', (req, res) => {
583
753
  const { name, emoji, agentIds, mode } = req.body;
584
754
  if (!name) {
@@ -610,6 +780,96 @@ function createRelayServer(config, gateway) {
610
780
  const group = groupManager.removeAgent(req.params.id, req.params.agentId);
611
781
  group ? res.json(group) : res.status(404).json({ error: '群不存在' });
612
782
  });
783
+ // ===== Skills API =====
784
+ internal.get('/skills', async (req, res) => {
785
+ try {
786
+ const agentId = typeof req.query.agentId === 'string' ? req.query.agentId : '';
787
+ const memoKey = `skills:${String(agentId || '*').trim() || '*'}`;
788
+ const payload = await memoSkills(memoKey, async () => {
789
+ const status = await (0, skills_service_1.getSkillsStatus)(gateway, agentId);
790
+ return {
791
+ ...status,
792
+ source: 'gateway',
793
+ };
794
+ });
795
+ res.json(payload);
796
+ }
797
+ catch (err) {
798
+ res.status(500).json({ error: err?.message || '获取 Skills 列表失败' });
799
+ }
800
+ });
801
+ internal.post('/skills/install', async (req, res) => {
802
+ try {
803
+ const result = await (0, skills_service_1.installSkill)(gateway, {
804
+ name: typeof req.body?.name === 'string' ? req.body.name : '',
805
+ installId: typeof req.body?.installId === 'string' ? req.body.installId : '',
806
+ timeoutMs: req.body?.timeoutMs,
807
+ });
808
+ res.json(result);
809
+ }
810
+ catch (err) {
811
+ res.status(500).json({ error: err?.message || '安装 Skill 失败' });
812
+ }
813
+ });
814
+ internal.post('/skills/:skillKey/update', async (req, res) => {
815
+ try {
816
+ const skillKey = String(req.params.skillKey || '').trim();
817
+ if (!skillKey) {
818
+ res.status(400).json({ error: '缺少 skillKey' });
819
+ return;
820
+ }
821
+ const payload = {
822
+ enabled: typeof req.body?.enabled === 'boolean' ? req.body.enabled : undefined,
823
+ apiKey: typeof req.body?.apiKey === 'string' ? req.body.apiKey : undefined,
824
+ env: req.body?.env && typeof req.body.env === 'object' ? req.body.env : undefined,
825
+ };
826
+ try {
827
+ const result = await (0, skills_service_1.updateSkill)(gateway, skillKey, payload);
828
+ res.json(result);
829
+ }
830
+ catch (err) {
831
+ if (typeof payload.enabled !== 'boolean')
832
+ throw err;
833
+ const fallback = (0, skills_service_1.setSkillEnabledLocally)(gateway, skillKey, payload.enabled);
834
+ res.json({
835
+ ...fallback,
836
+ source: 'local-config',
837
+ warning: err?.message || 'Gateway skills.update 失败,已改为本地配置写入',
838
+ });
839
+ }
840
+ }
841
+ catch (err) {
842
+ res.status(500).json({ error: err?.message || '更新 Skill 状态失败' });
843
+ }
844
+ });
845
+ // ===== Plugin update =====
846
+ internal.get('/plugin/update/info', async (_req, res) => {
847
+ try {
848
+ const info = await (0, plugin_update_service_1.checkPluginUpdateInfo)();
849
+ res.json(info);
850
+ }
851
+ catch (err) {
852
+ res.status(500).json({ error: err?.message || '检查插件更新失败' });
853
+ }
854
+ });
855
+ internal.get('/plugin/update/status', (_req, res) => {
856
+ try {
857
+ res.json((0, plugin_update_service_1.readPluginUpdateState)());
858
+ }
859
+ catch (err) {
860
+ res.status(500).json({ error: err?.message || '获取插件更新状态失败' });
861
+ }
862
+ });
863
+ internal.post('/plugin/update/start', async (req, res) => {
864
+ try {
865
+ const targetVersion = typeof req.body?.version === 'string' ? req.body.version : '';
866
+ const result = await (0, plugin_update_service_1.startPluginUpdate)(targetVersion);
867
+ res.json(result);
868
+ }
869
+ catch (err) {
870
+ res.status(400).json({ error: err?.message || '启动插件更新失败' });
871
+ }
872
+ });
613
873
  // ===== 聊天 =====
614
874
  internal.get('/chat/history', async (req, res) => {
615
875
  logBridgeRequest(req, 'chat.history');
@@ -853,7 +1113,7 @@ function createRelayServer(config, gateway) {
853
1113
  console.error('[WS] 错误:', err.message);
854
1114
  });
855
1115
  });
856
- return { app, server, sessionManager, wss };
1116
+ return { app, server, sessionManager, groupEventStore, wss };
857
1117
  }
858
1118
  // ===== WebSocket 处理函数 =====
859
1119
  function wsSend(ws, data) {