aicq-chat-plugin 2.4.0 → 2.4.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/index.js CHANGED
@@ -350,6 +350,48 @@ app.delete('/api/chat/:messageId', (req, res) => {
350
350
  res.json({ success: true });
351
351
  });
352
352
 
353
+ // Streaming endpoints — for external systems / OpenClaw agent output
354
+ app.post('/api/chat/stream-chunk', (req, res) => {
355
+ try {
356
+ const { targetId, friend_id, chunk_type, chunkType, data } = req.body;
357
+ const streamTarget = targetId || friend_id;
358
+ if (!streamTarget) return res.status(400).json({ error: 'targetId or friend_id is required' });
359
+ if (!data) return res.status(400).json({ error: 'data is required' });
360
+ const type = chunk_type || chunkType || 'text';
361
+ if (!['text', 'reasoning', 'tool_call', 'tool_result'].includes(type)) {
362
+ return res.status(400).json({ error: `Invalid chunk_type: ${type}` });
363
+ }
364
+ const sent = serverClient.sendWS({
365
+ type: 'stream_chunk',
366
+ to: streamTarget,
367
+ chunkType: type,
368
+ data: data,
369
+ });
370
+ if (!sent) return res.status(503).json({ error: 'Not connected to server', success: false });
371
+ res.json({ success: true });
372
+ } catch (e) {
373
+ res.status(500).json({ error: e.message });
374
+ }
375
+ });
376
+
377
+ app.post('/api/chat/stream-end', (req, res) => {
378
+ try {
379
+ const { targetId, friend_id, message_id, messageId } = req.body;
380
+ const streamTarget = targetId || friend_id;
381
+ if (!streamTarget) return res.status(400).json({ error: 'targetId or friend_id is required' });
382
+ const msgId = message_id || messageId || ('msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6));
383
+ const sent = serverClient.sendWS({
384
+ type: 'stream_end',
385
+ to: streamTarget,
386
+ messageId: msgId,
387
+ });
388
+ if (!sent) return res.status(503).json({ error: 'Not connected to server', success: false });
389
+ res.json({ success: true, messageId: msgId });
390
+ } catch (e) {
391
+ res.status(500).json({ error: e.message });
392
+ }
393
+ });
394
+
353
395
  // File upload
354
396
  app.post('/api/upload', upload.single('file'), async (req, res) => {
355
397
  try {
@@ -575,6 +617,33 @@ async function handleGatewayCall(method, kwargs = {}) {
575
617
  return await chat.sendMessage(currentAgentId, kwargs.targetId, kwargs.content, { isGroup: kwargs.isGroup });
576
618
  case 'aicq.chat.history':
577
619
  return { messages: db.getChatHistory(currentAgentId, kwargs.targetId, { limit: kwargs.limit || 50 }) };
620
+ case 'aicq.chat.streamChunk': {
621
+ if (!kwargs.friend_id && !kwargs.targetId) return { error: 'friend_id or targetId is required' };
622
+ if (!kwargs.data) return { error: 'data is required' };
623
+ const chunkType = kwargs.chunk_type || kwargs.chunkType || 'text';
624
+ if (!['text', 'reasoning', 'tool_call', 'tool_result'].includes(chunkType)) return { error: `Invalid chunk_type: ${chunkType}` };
625
+ const streamTarget = kwargs.friend_id || kwargs.targetId;
626
+ const sent = serverClient.sendWS({
627
+ type: 'stream_chunk',
628
+ to: streamTarget,
629
+ chunkType: chunkType,
630
+ data: kwargs.data,
631
+ });
632
+ if (!sent) return { error: 'Not connected to server', success: false };
633
+ return { success: true };
634
+ }
635
+ case 'aicq.chat.streamEnd': {
636
+ if (!kwargs.friend_id && !kwargs.targetId) return { error: 'friend_id or targetId is required' };
637
+ const endTarget = kwargs.friend_id || kwargs.targetId;
638
+ const msgId = kwargs.message_id || kwargs.messageId || ('msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6));
639
+ const endSent = serverClient.sendWS({
640
+ type: 'stream_end',
641
+ to: endTarget,
642
+ messageId: msgId,
643
+ });
644
+ if (!endSent) return { error: 'Not connected to server', success: false };
645
+ return { success: true, messageId: msgId };
646
+ }
578
647
  default:
579
648
  return { error: `Unknown method: ${method}` };
580
649
  }
package/lib/chat.js CHANGED
@@ -21,6 +21,8 @@ class ChatManager {
21
21
  this.server.onMessage('handshake_initiate', (data) => this._handleHandshakeRequest(data));
22
22
  this.server.onMessage('presence', (data) => this._handlePresence(data));
23
23
  this.server.onMessage('file_chunk', (data) => this._handleFileChunk(data));
24
+ this.server.onMessage('stream_chunk', (data) => this._handleStreamChunk(data));
25
+ this.server.onMessage('stream_end', (data) => this._handleStreamEnd(data));
24
26
  }
25
27
 
26
28
  setOnNewMessage(callback) {
@@ -208,6 +210,46 @@ class ChatManager {
208
210
  console.log('[Chat] File chunk from', data.from);
209
211
  }
210
212
 
213
+ _handleStreamChunk(data) {
214
+ // Incoming streaming chunk from another agent
215
+ const agentId = this.server.currentAgentId;
216
+ if (!agentId) return;
217
+
218
+ const fromId = data.from;
219
+ const chunkType = data.chunkType || 'text';
220
+ const chunkData = data.data;
221
+
222
+ // Notify callback so OpenClaw agent can process streaming input
223
+ if (this._onNewMessage) {
224
+ this._onNewMessage({
225
+ type: 'stream_chunk',
226
+ from_id: fromId,
227
+ chunk_type: chunkType,
228
+ data: chunkData,
229
+ });
230
+ }
231
+ console.log('[Chat] Stream chunk from', fromId, 'type:', chunkType);
232
+ }
233
+
234
+ _handleStreamEnd(data) {
235
+ // Incoming stream end signal from another agent
236
+ const agentId = this.server.currentAgentId;
237
+ if (!agentId) return;
238
+
239
+ const fromId = data.from;
240
+ const messageId = data.messageId || '';
241
+
242
+ // Notify callback so OpenClaw agent knows stream is complete
243
+ if (this._onNewMessage) {
244
+ this._onNewMessage({
245
+ type: 'stream_end',
246
+ from_id: fromId,
247
+ message_id: messageId,
248
+ });
249
+ }
250
+ console.log('[Chat] Stream end from', fromId, 'messageId:', messageId);
251
+ }
252
+
211
253
  // ─── Chat History ─────────────────────────────────────────────────
212
254
 
213
255
  getHistory(agentId, targetId, { limit = 50, before = null } = {}) {
package/lib/crypto.js CHANGED
@@ -31,13 +31,7 @@ function generateExchangeKeypair() {
31
31
 
32
32
  function signMessage(message, secretKeyHex) {
33
33
  const secretKey = Buffer.from(secretKeyHex, 'hex');
34
- // If message looks like hex (64 chars), treat as raw bytes to match server's bytes.fromhex()
35
- let messageBytes;
36
- if (/^[0-9a-fA-F]{64}$/.test(message)) {
37
- messageBytes = Buffer.from(message, 'hex');
38
- } else {
39
- messageBytes = naclUtil.decodeUTF8(message);
40
- }
34
+ const messageBytes = naclUtil.decodeUTF8(message);
41
35
  const signature = nacl.sign.detached(messageBytes, secretKey);
42
36
  return Buffer.from(signature).toString('hex');
43
37
  }
@@ -45,13 +39,7 @@ function signMessage(message, secretKeyHex) {
45
39
  function verifySignature(message, signatureHex, publicKeyHex) {
46
40
  try {
47
41
  const publicKey = Buffer.from(publicKeyHex, 'hex');
48
- // If message looks like hex (64 chars), treat as raw bytes to match server
49
- let messageBytes;
50
- if (/^[0-9a-fA-F]{64}$/.test(message)) {
51
- messageBytes = Buffer.from(message, 'hex');
52
- } else {
53
- messageBytes = naclUtil.decodeUTF8(message);
54
- }
42
+ const messageBytes = naclUtil.decodeUTF8(message);
55
43
  const signature = Buffer.from(signatureHex, 'hex');
56
44
  return nacl.sign.detached.verify(messageBytes, signature, publicKey);
57
45
  } catch (e) {
@@ -53,13 +53,9 @@ class ServerClient {
53
53
  public_key: identity.signing_public_key,
54
54
  agent_name: identity.nickname || agentId,
55
55
  });
56
- if (data.access_token || data.accessToken) {
57
- this.jwtToken = data.access_token || data.accessToken;
56
+ if (data.accessToken) {
57
+ this.jwtToken = data.accessToken;
58
58
  this.currentAgentId = agentId;
59
- // Store server-side account ID for WS auth (nodeId must match JWT sub)
60
- if (data.account && data.account.id) {
61
- this.serverAccountId = data.account.id;
62
- }
63
59
  }
64
60
  return data;
65
61
  }
@@ -87,13 +83,9 @@ class ServerClient {
87
83
  challenge,
88
84
  });
89
85
 
90
- if (loginData.access_token || loginData.accessToken) {
91
- this.jwtToken = loginData.access_token || loginData.accessToken;
86
+ if (loginData.accessToken) {
87
+ this.jwtToken = loginData.accessToken;
92
88
  this.currentAgentId = agentId;
93
- // Store server-side account ID for WS auth (nodeId must match JWT sub)
94
- if (loginData.account && loginData.account.id) {
95
- this.serverAccountId = loginData.account.id;
96
- }
97
89
  }
98
90
  return loginData;
99
91
  }
@@ -206,7 +198,7 @@ class ServerClient {
206
198
  console.log('[WS] Connected, sending auth...');
207
199
  this.ws.send(JSON.stringify({
208
200
  type: 'online',
209
- nodeId: this.serverAccountId || this.currentAgentId,
201
+ nodeId: this.currentAgentId,
210
202
  token: this.jwtToken,
211
203
  }));
212
204
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "aicq-chat",
3
3
  "name": "AICQ Encrypted Chat",
4
- "version": "2.1.0",
4
+ "version": "2.2.0",
5
5
  "description": "End-to-end encrypted chat plugin for OpenClaw agents — Node.js implementation with full UI",
6
6
  "entry": "index.js",
7
7
  "activation": {
@@ -25,6 +25,8 @@
25
25
  "aicq.chat.history",
26
26
  "aicq.chat.send",
27
27
  "aicq.chat.delete",
28
+ "aicq.chat.streamChunk",
29
+ "aicq.chat.streamEnd",
28
30
  "aicq.groups.list",
29
31
  "aicq.groups.create",
30
32
  "aicq.groups.join",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicq-chat-plugin",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "AICQ End-to-end Encrypted Chat Plugin for OpenClaw — Full UI with friend management, group chat, file transfer, and AI agent communication",
5
5
  "main": "index.js",
6
6
  "bin": {