aicq-chat-plugin 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +69 -0
- package/lib/chat.js +42 -0
- package/openclaw.plugin.json +3 -1
- package/package.json +1 -1
- package/postinstall.js +178 -17
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/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "aicq-chat",
|
|
3
3
|
"name": "AICQ Encrypted Chat",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.5.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
package/postinstall.js
CHANGED
|
@@ -2,26 +2,187 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* AICQ Chat Plugin — Post-install script
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Automatically installs the plugin into OpenClaw's plugins directory.
|
|
6
|
+
* OpenClaw requires plugins to be in ~/.openclaw/plugins/<plugin-id>/
|
|
7
|
+
* with an openclaw.plugin.json file to recognize them.
|
|
6
8
|
*/
|
|
7
9
|
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
const PLUGIN_ID = 'aicq-chat';
|
|
16
|
+
const PLUGIN_DIR = path.resolve(__dirname);
|
|
17
|
+
|
|
18
|
+
// ── Find OpenClaw installation ──────────────────────────────────────
|
|
19
|
+
function findOpenClawDir() {
|
|
20
|
+
const candidates = [
|
|
21
|
+
path.join(os.homedir(), '.openclaw'),
|
|
22
|
+
path.join(os.homedir(), 'openclaw'),
|
|
23
|
+
path.join(os.homedir(), '.config', 'openclaw'),
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// Check environment variable
|
|
27
|
+
if (process.env.OPENCLAW_HOME) {
|
|
28
|
+
candidates.unshift(process.env.OPENCLAW_HOME);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const dir of candidates) {
|
|
32
|
+
if (fs.existsSync(dir)) {
|
|
33
|
+
return dir;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Copy plugin files to OpenClaw plugins directory ─────────────────
|
|
40
|
+
function installToOpenClaw(openclawDir) {
|
|
41
|
+
const pluginsDir = path.join(openclawDir, 'plugins');
|
|
42
|
+
const targetDir = path.join(pluginsDir, PLUGIN_ID);
|
|
43
|
+
|
|
44
|
+
// Create plugins directory if needed
|
|
45
|
+
if (!fs.existsSync(pluginsDir)) {
|
|
46
|
+
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Files to copy (from package.json "files" field)
|
|
50
|
+
const filesToCopy = [
|
|
51
|
+
'index.js',
|
|
52
|
+
'cli.js',
|
|
53
|
+
'postinstall.js',
|
|
54
|
+
'openclaw.plugin.json',
|
|
55
|
+
'package.json',
|
|
56
|
+
'README.md',
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const dirsToCopy = [
|
|
60
|
+
'lib',
|
|
61
|
+
'public',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// Remove old installation if exists
|
|
65
|
+
if (fs.existsSync(targetDir)) {
|
|
66
|
+
console.log(`[AICQ] Updating existing plugin at ${targetDir}`);
|
|
67
|
+
// Don't remove the entire dir — preserve node_modules and data
|
|
68
|
+
} else {
|
|
69
|
+
console.log(`[AICQ] Installing plugin to ${targetDir}`);
|
|
70
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Copy individual files
|
|
74
|
+
for (const file of filesToCopy) {
|
|
75
|
+
const src = path.join(PLUGIN_DIR, file);
|
|
76
|
+
const dest = path.join(targetDir, file);
|
|
77
|
+
if (fs.existsSync(src)) {
|
|
78
|
+
fs.copyFileSync(src, dest);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Copy directories
|
|
83
|
+
for (const dir of dirsToCopy) {
|
|
84
|
+
const src = path.join(PLUGIN_DIR, dir);
|
|
85
|
+
const dest = path.join(targetDir, dir);
|
|
86
|
+
if (fs.existsSync(src)) {
|
|
87
|
+
// Remove old and copy fresh
|
|
88
|
+
if (fs.existsSync(dest)) {
|
|
89
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
copyDirRecursive(src, dest);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Install npm dependencies in the target directory
|
|
96
|
+
console.log('[AICQ] Installing plugin dependencies...');
|
|
97
|
+
try {
|
|
98
|
+
execSync('npm install --production', {
|
|
99
|
+
cwd: targetDir,
|
|
100
|
+
stdio: 'pipe',
|
|
101
|
+
timeout: 120000,
|
|
102
|
+
});
|
|
103
|
+
console.log('[AICQ] Dependencies installed.');
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.log('[AICQ] Warning: npm install failed. You may need to run it manually:');
|
|
106
|
+
console.log(` cd ${targetDir} && npm install`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return targetDir;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Recursively copy a directory ────────────────────────────────────
|
|
113
|
+
function copyDirRecursive(src, dest) {
|
|
114
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
115
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
116
|
+
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
const srcPath = path.join(src, entry.name);
|
|
119
|
+
const destPath = path.join(dest, entry.name);
|
|
120
|
+
|
|
121
|
+
if (entry.isDirectory()) {
|
|
122
|
+
// Skip node_modules — will be installed fresh
|
|
123
|
+
if (entry.name === 'node_modules') continue;
|
|
124
|
+
copyDirRecursive(srcPath, destPath);
|
|
125
|
+
} else {
|
|
126
|
+
fs.copyFileSync(srcPath, destPath);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Main ────────────────────────────────────────────────────────────
|
|
8
132
|
console.log('');
|
|
9
133
|
console.log(' ╔══════════════════════════════════════════════╗');
|
|
10
|
-
console.log(' ║ AICQ Chat Plugin
|
|
11
|
-
console.log(' ╠══════════════════════════════════════════════╣');
|
|
12
|
-
console.log(' ║ ║');
|
|
13
|
-
console.log(' ║ Start the plugin: ║');
|
|
14
|
-
console.log(' ║ npx aicq-plugin ║');
|
|
15
|
-
console.log(' ║ ║');
|
|
16
|
-
console.log(' ║ Or install globally: ║');
|
|
17
|
-
console.log(' ║ npm install -g aicq-chat-plugin ║');
|
|
18
|
-
console.log(' ║ aicq-plugin ║');
|
|
19
|
-
console.log(' ║ ║');
|
|
20
|
-
console.log(' ║ Options: ║');
|
|
21
|
-
console.log(' ║ --port <port> Server port (6109) ║');
|
|
22
|
-
console.log(' ║ --server <url> AICQ server URL ║');
|
|
23
|
-
console.log(' ║ ║');
|
|
24
|
-
console.log(' ║ Chat UI: http://localhost:6109 ║');
|
|
25
|
-
console.log(' ║ Docs: https://aicq.online ║');
|
|
134
|
+
console.log(' ║ AICQ Chat Plugin — Installing... ║');
|
|
26
135
|
console.log(' ╚══════════════════════════════════════════════╝');
|
|
27
136
|
console.log('');
|
|
137
|
+
|
|
138
|
+
// Try to auto-install into OpenClaw
|
|
139
|
+
const openclawDir = findOpenClawDir();
|
|
140
|
+
|
|
141
|
+
if (openclawDir) {
|
|
142
|
+
console.log(`[AICQ] Found OpenClaw at: ${openclawDir}`);
|
|
143
|
+
try {
|
|
144
|
+
const installedDir = installToOpenClaw(openclawDir);
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log(' ╔══════════════════════════════════════════════╗');
|
|
147
|
+
console.log(' ║ AICQ Plugin Installed Successfully! ║');
|
|
148
|
+
console.log(' ╠══════════════════════════════════════════════╣');
|
|
149
|
+
console.log(' ║ ║');
|
|
150
|
+
console.log(' ║ Plugin installed to: ║');
|
|
151
|
+
console.log(` ║ ${installedDir}`);
|
|
152
|
+
console.log(' ║ ║');
|
|
153
|
+
console.log(' ║ Restart OpenClaw to activate the plugin. ║');
|
|
154
|
+
console.log(' ║ ║');
|
|
155
|
+
console.log(' ║ Chat UI: http://localhost:6109 ║');
|
|
156
|
+
console.log(' ║ Docs: https://aicq.online ║');
|
|
157
|
+
console.log(' ╚══════════════════════════════════════════════╝');
|
|
158
|
+
console.log('');
|
|
159
|
+
} catch (e) {
|
|
160
|
+
console.error('[AICQ] Auto-install failed:', e.message);
|
|
161
|
+
console.log('');
|
|
162
|
+
printManualInstructions();
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
console.log('[AICQ] OpenClaw not found — skipping auto-install.');
|
|
166
|
+
console.log('');
|
|
167
|
+
printManualInstructions();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function printManualInstructions() {
|
|
171
|
+
console.log(' ╔══════════════════════════════════════════════╗');
|
|
172
|
+
console.log(' ║ AICQ Chat Plugin Installed! ║');
|
|
173
|
+
console.log(' ╠══════════════════════════════════════════════╣');
|
|
174
|
+
console.log(' ║ ║');
|
|
175
|
+
console.log(' ║ Manual install to OpenClaw: ║');
|
|
176
|
+
console.log(' ║ mkdir -p ~/.openclaw/plugins/aicq-chat ║');
|
|
177
|
+
console.log(' ║ cp -r ' + PLUGIN_DIR + '/* ~/.openclaw/plugins/aicq-chat/ ║');
|
|
178
|
+
console.log(' ║ cd ~/.openclaw/plugins/aicq-chat ║');
|
|
179
|
+
console.log(' ║ npm install ║');
|
|
180
|
+
console.log(' ║ ║');
|
|
181
|
+
console.log(' ║ Or start standalone: ║');
|
|
182
|
+
console.log(' ║ npx aicq-plugin ║');
|
|
183
|
+
console.log(' ║ ║');
|
|
184
|
+
console.log(' ║ Chat UI: http://localhost:6109 ║');
|
|
185
|
+
console.log(' ║ Docs: https://aicq.online ║');
|
|
186
|
+
console.log(' ╚══════════════════════════════════════════════╝');
|
|
187
|
+
console.log('');
|
|
188
|
+
}
|