aicq-chat-plugin 2.6.7 → 3.1.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/README.md +40 -44
- package/SKILL.md +27 -19
- package/cli.js +77 -211
- package/index.js +343 -621
- package/openclaw.plugin.json +45 -33
- package/package.json +12 -5
- package/postinstall.js +17 -346
- package/public/index.html +4 -0
- package/setup-entry.js +61 -0
- package/src/channel.js +163 -0
- package/src/ui-routes.js +469 -0
- package/extension.js +0 -204
package/src/channel.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AICQ Channel Plugin — Core Channel Logic
|
|
3
|
+
*
|
|
4
|
+
* Wraps existing lib/ modules (identity, server-client, handshake, chat, database)
|
|
5
|
+
* into the OpenClaw Channel plugin interface via createChatChannelPlugin.
|
|
6
|
+
*
|
|
7
|
+
* Architecture: In-process Channel (no sidecar, no independent port)
|
|
8
|
+
*/
|
|
9
|
+
const { encryptMessage, decryptMessage, deriveSessionKey, computeFingerprint } = require('../lib/crypto');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create the AICQ channel plugin
|
|
13
|
+
* @param {Object} ctx - Plugin context with managers and config
|
|
14
|
+
*/
|
|
15
|
+
function createAicqChannel(ctx) {
|
|
16
|
+
const { db, identity, serverClient, handshake, chat, dataDir, serverUrl } = ctx;
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
// ── Account Resolution ──
|
|
20
|
+
resolveAccount: async (agentId) => {
|
|
21
|
+
// Use OpenClaw agent ID directly as AICQ account ID
|
|
22
|
+
let agentIdentity = identity.loadAgent(agentId);
|
|
23
|
+
if (!agentIdentity) {
|
|
24
|
+
agentIdentity = identity.createAgent(agentId, `agent-${agentId.slice(0, 8)}`);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
accountId: agentId,
|
|
28
|
+
displayName: agentIdentity.nickname || `agent-${agentId.slice(0, 8)}`,
|
|
29
|
+
metadata: {
|
|
30
|
+
publicKey: agentIdentity.signing_public_key,
|
|
31
|
+
exchangePublicKey: agentIdentity.exchange_public_key,
|
|
32
|
+
fingerprint: agentIdentity.fingerprint,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// ── DM Security Policy ──
|
|
38
|
+
security: {
|
|
39
|
+
dm: {
|
|
40
|
+
allowFrom: async (accountId, peerId) => {
|
|
41
|
+
// Only friends in the contact list can send DMs
|
|
42
|
+
return db.isFriend ? db.isFriend(accountId, peerId) : !!db.getFriend(accountId, peerId);
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// ── Friend Pairing ──
|
|
48
|
+
pairing: {
|
|
49
|
+
text: async (accountId) => {
|
|
50
|
+
try {
|
|
51
|
+
await serverClient.ensureAuth(accountId);
|
|
52
|
+
const result = await handshake.generateFriendCode(accountId);
|
|
53
|
+
const code = result.number;
|
|
54
|
+
return {
|
|
55
|
+
code,
|
|
56
|
+
instructions: `Share this pairing code with the other party: ${code}. They can add you using the chat-friend tool's add action.`,
|
|
57
|
+
};
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// Fallback: use public key prefix as pairing code
|
|
60
|
+
const info = identity.getInfo(accountId);
|
|
61
|
+
const code = info ? info.exchange_public_key.slice(0, 16) : 'error';
|
|
62
|
+
return {
|
|
63
|
+
code,
|
|
64
|
+
instructions: `Share this pairing code with the other party: ${code}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
verify: async (accountId, peerCode) => {
|
|
69
|
+
try {
|
|
70
|
+
const result = await handshake.addFriendByCode(accountId, peerCode);
|
|
71
|
+
return { success: true, peerId: result.peer_id || result.friend_id || peerCode };
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return { success: false, error: e.message };
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// ── Inbound Message Processing ──
|
|
79
|
+
inbound: {
|
|
80
|
+
onText: async (message) => {
|
|
81
|
+
const { toAccountId, fromPeerId, encryptedContent } = message;
|
|
82
|
+
|
|
83
|
+
// Try to decrypt if we have a session key
|
|
84
|
+
let content = encryptedContent || message.content || message.payload || '';
|
|
85
|
+
const session = db.loadSession(toAccountId, fromPeerId);
|
|
86
|
+
if (session && session.session_key && typeof content === 'string') {
|
|
87
|
+
try {
|
|
88
|
+
content = decryptMessage(content, session.session_key);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
// Might be plaintext, keep as is
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
text: typeof content === 'string' ? content : JSON.stringify(content),
|
|
96
|
+
metadata: {
|
|
97
|
+
fromPeerId,
|
|
98
|
+
timestamp: message.timestamp,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
onMedia: async (message) => {
|
|
103
|
+
const keys = identity.loadAgent(message.toAccountId);
|
|
104
|
+
let content = message.encryptedContent || message.content || '';
|
|
105
|
+
const session = db.loadSession(message.toAccountId, message.fromPeerId);
|
|
106
|
+
if (session && session.session_key && typeof content === 'string') {
|
|
107
|
+
try {
|
|
108
|
+
content = decryptMessage(content, session.session_key);
|
|
109
|
+
} catch (e) {}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
mediaUrl: content,
|
|
113
|
+
mediaType: message.mediaType || 'file',
|
|
114
|
+
metadata: { fromPeerId: message.fromPeerId },
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// ── Outbound Message Processing ──
|
|
120
|
+
outbound: {
|
|
121
|
+
sendText: async (fromAccountId, toPeerId, text) => {
|
|
122
|
+
const result = await chat.sendMessage(fromAccountId, toPeerId, text, { isGroup: false });
|
|
123
|
+
return result;
|
|
124
|
+
},
|
|
125
|
+
sendMedia: async (fromAccountId, toPeerId, mediaUrl, mediaType) => {
|
|
126
|
+
const result = await chat.sendMessage(fromAccountId, toPeerId, mediaUrl, {
|
|
127
|
+
type: mediaType || 'file',
|
|
128
|
+
isGroup: false,
|
|
129
|
+
});
|
|
130
|
+
return result;
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// ── Lifecycle ──
|
|
135
|
+
lifecycle: {
|
|
136
|
+
onAccountCreate: async (accountId) => {
|
|
137
|
+
let agentIdentity = identity.loadAgent(accountId);
|
|
138
|
+
if (!agentIdentity) {
|
|
139
|
+
agentIdentity = identity.createAgent(accountId, `agent-${accountId.slice(0, 8)}`);
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
await serverClient.start(accountId);
|
|
143
|
+
} catch (e) {
|
|
144
|
+
console.error('[AICQ Channel] Server connection failed for account:', accountId, e.message);
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
onAccountDelete: async (accountId) => {
|
|
148
|
+
try {
|
|
149
|
+
serverClient.disconnect();
|
|
150
|
+
} catch (e) {}
|
|
151
|
+
identity.deleteAgent(accountId);
|
|
152
|
+
},
|
|
153
|
+
onShutdown: async () => {
|
|
154
|
+
try {
|
|
155
|
+
serverClient.stop();
|
|
156
|
+
} catch (e) {}
|
|
157
|
+
console.log('[AICQ Channel] Shutdown complete');
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = { createAicqChannel };
|
package/src/ui-routes.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AICQ Channel Plugin — Gateway HTTP Routes
|
|
3
|
+
*
|
|
4
|
+
* Provides HTTP route handlers for the OpenClaw Gateway.
|
|
5
|
+
* These routes serve the SPA UI and REST API endpoints.
|
|
6
|
+
*
|
|
7
|
+
* Routes are served via Gateway HTTP, not an independent Express server.
|
|
8
|
+
* Prefix: /plugins/aicq-chat/
|
|
9
|
+
*/
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const QRCode = require('qrcode');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create UI route handlers
|
|
16
|
+
* @param {Object} ctx - Plugin context with managers
|
|
17
|
+
*/
|
|
18
|
+
function createUiRoutes(ctx) {
|
|
19
|
+
const { db, identity, serverClient, handshake, chat, dataDir } = ctx;
|
|
20
|
+
const UPLOADS_DIR = path.join(dataDir, 'uploads');
|
|
21
|
+
|
|
22
|
+
// Ensure uploads directory exists
|
|
23
|
+
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Helper to get current agent ID
|
|
27
|
+
*/
|
|
28
|
+
function getAgentId(req) {
|
|
29
|
+
return req.query?.agent_id || req.body?.agent_id || (identity.listAgents()[0]?.agent_id);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register all routes on an Express app or router
|
|
34
|
+
* This is called by the Gateway to mount the routes
|
|
35
|
+
*/
|
|
36
|
+
function registerRoutes(app) {
|
|
37
|
+
// ── Serve SPA static files ────────────────────────────────────
|
|
38
|
+
const publicDir = path.join(__dirname, '..', 'public');
|
|
39
|
+
app.use('/plugins/aicq-chat/ui', (req, res, next) => {
|
|
40
|
+
// Serve static files from public/
|
|
41
|
+
const filePath = path.join(publicDir, req.path === '/' ? 'index.html' : req.path);
|
|
42
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
43
|
+
res.sendFile(filePath);
|
|
44
|
+
} else {
|
|
45
|
+
// SPA fallback: serve index.html for all unknown routes
|
|
46
|
+
res.sendFile(path.join(publicDir, 'index.html'));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ── API Routes ────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
// Status
|
|
53
|
+
app.get('/plugins/aicq-chat/api/status', (req, res) => {
|
|
54
|
+
res.json({
|
|
55
|
+
status: 'running',
|
|
56
|
+
version: '3.0.0',
|
|
57
|
+
architecture: 'channel',
|
|
58
|
+
connected: serverClient.connected,
|
|
59
|
+
serverUrl: ctx.serverUrl,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Agents
|
|
64
|
+
app.get('/plugins/aicq-chat/api/agents', (req, res) => {
|
|
65
|
+
res.json({ agents: identity.listAgents() });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
app.post('/plugins/aicq-chat/api/agents', async (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const { agent_id, nickname } = req.body;
|
|
71
|
+
if (!agent_id) return res.status(400).json({ error: 'agent_id is required' });
|
|
72
|
+
const agent = identity.createAgent(agent_id, nickname);
|
|
73
|
+
try {
|
|
74
|
+
await serverClient.start(agent_id);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
console.error('[AICQ] Server registration failed:', e.message);
|
|
77
|
+
}
|
|
78
|
+
res.json({ success: true, agent });
|
|
79
|
+
} catch (e) {
|
|
80
|
+
res.status(500).json({ error: e.message });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
app.delete('/plugins/aicq-chat/api/agents/:id', (req, res) => {
|
|
85
|
+
identity.deleteAgent(req.params.id);
|
|
86
|
+
res.json({ success: true });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Friends
|
|
90
|
+
app.get('/plugins/aicq-chat/api/friends', (req, res) => {
|
|
91
|
+
const agentId = getAgentId(req);
|
|
92
|
+
res.json({ friends: db.listFriends(agentId) });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
app.post('/plugins/aicq-chat/api/friends/add', async (req, res) => {
|
|
96
|
+
try {
|
|
97
|
+
const { temp_number, friend_code, agent_id } = req.body;
|
|
98
|
+
const agentId = agent_id || getAgentId(req);
|
|
99
|
+
const code = temp_number || friend_code;
|
|
100
|
+
if (!code) return res.status(400).json({ error: 'temp_number or friend_code is required' });
|
|
101
|
+
const result = await handshake.addFriendByCode(agentId, code);
|
|
102
|
+
res.json({ success: true, result });
|
|
103
|
+
} catch (e) {
|
|
104
|
+
res.status(500).json({ error: e.message });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
app.delete('/plugins/aicq-chat/api/friends/:id', async (req, res) => {
|
|
109
|
+
try {
|
|
110
|
+
const agentId = getAgentId(req);
|
|
111
|
+
db.removeFriend(agentId, req.params.id);
|
|
112
|
+
try { await serverClient.removeFriend(req.params.id); } catch (e) {}
|
|
113
|
+
res.json({ success: true });
|
|
114
|
+
} catch (e) {
|
|
115
|
+
res.status(500).json({ error: e.message });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
app.get('/plugins/aicq-chat/api/friends/requests', async (req, res) => {
|
|
120
|
+
try {
|
|
121
|
+
const agentId = getAgentId(req);
|
|
122
|
+
let serverRequests = [];
|
|
123
|
+
try {
|
|
124
|
+
await serverClient.ensureAuth(agentId);
|
|
125
|
+
const result = await serverClient.listFriendRequests();
|
|
126
|
+
serverRequests = result.sent || [];
|
|
127
|
+
serverRequests = serverRequests.concat(result.received || []);
|
|
128
|
+
} catch (e) {}
|
|
129
|
+
const localRequests = db.getPendingRequests(agentId);
|
|
130
|
+
res.json({ requests: [...localRequests, ...serverRequests] });
|
|
131
|
+
} catch (e) {
|
|
132
|
+
res.status(500).json({ error: e.message });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
app.post('/plugins/aicq-chat/api/friends/requests/:id/accept', async (req, res) => {
|
|
137
|
+
try {
|
|
138
|
+
const agentId = getAgentId(req);
|
|
139
|
+
const result = await handshake.acceptRequest(agentId, req.params.id);
|
|
140
|
+
res.json(result);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
res.status(500).json({ error: e.message });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
app.post('/plugins/aicq-chat/api/friends/requests/:id/reject', async (req, res) => {
|
|
147
|
+
try {
|
|
148
|
+
const agentId = getAgentId(req);
|
|
149
|
+
const result = await handshake.rejectRequest(agentId, req.params.id);
|
|
150
|
+
res.json(result);
|
|
151
|
+
} catch (e) {
|
|
152
|
+
res.status(500).json({ error: e.message });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Groups
|
|
157
|
+
app.get('/plugins/aicq-chat/api/groups', (req, res) => {
|
|
158
|
+
const agentId = getAgentId(req);
|
|
159
|
+
res.json({ groups: db.listGroups(agentId) });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
app.post('/plugins/aicq-chat/api/groups', async (req, res) => {
|
|
163
|
+
try {
|
|
164
|
+
const agentId = getAgentId(req);
|
|
165
|
+
const { name, description } = req.body;
|
|
166
|
+
if (!name) return res.status(400).json({ error: 'name is required' });
|
|
167
|
+
await serverClient.ensureAuth(agentId);
|
|
168
|
+
const result = await serverClient.createGroup(name, description);
|
|
169
|
+
if (result.id) {
|
|
170
|
+
db.addGroup({
|
|
171
|
+
agent_id: agentId,
|
|
172
|
+
id: result.id,
|
|
173
|
+
name,
|
|
174
|
+
owner_id: agentId,
|
|
175
|
+
members_json: result.members || '[]',
|
|
176
|
+
description: description || '',
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
res.json({ success: true, group: result });
|
|
180
|
+
} catch (e) {
|
|
181
|
+
res.status(500).json({ error: e.message });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
app.post('/plugins/aicq-chat/api/groups/:id/join', async (req, res) => {
|
|
186
|
+
try {
|
|
187
|
+
const agentId = getAgentId(req);
|
|
188
|
+
await serverClient.ensureAuth(agentId);
|
|
189
|
+
const result = await serverClient.inviteGroupMember(req.params.id, agentId);
|
|
190
|
+
res.json({ success: true, result });
|
|
191
|
+
} catch (e) {
|
|
192
|
+
res.status(500).json({ error: e.message });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
app.get('/plugins/aicq-chat/api/groups/:id/messages', async (req, res) => {
|
|
197
|
+
try {
|
|
198
|
+
const agentId = getAgentId(req);
|
|
199
|
+
const limit = parseInt(req.query.limit || '50', 10);
|
|
200
|
+
const before = req.query.before || null;
|
|
201
|
+
try {
|
|
202
|
+
await serverClient.ensureAuth(agentId);
|
|
203
|
+
const result = await serverClient.getGroupMessages(req.params.id, limit, before);
|
|
204
|
+
if (result.messages && result.messages.length > 0) {
|
|
205
|
+
return res.json({ messages: result.messages });
|
|
206
|
+
}
|
|
207
|
+
} catch (e) {}
|
|
208
|
+
const messages = db.getChatHistory(agentId, req.params.id, { limit, before });
|
|
209
|
+
res.json({ messages });
|
|
210
|
+
} catch (e) {
|
|
211
|
+
res.status(500).json({ error: e.message });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
app.put('/plugins/aicq-chat/api/groups/:id/silent', (req, res) => {
|
|
216
|
+
const agentId = getAgentId(req);
|
|
217
|
+
const { silent } = req.body;
|
|
218
|
+
db.setGroupSilentMode(agentId, req.params.id, !!silent);
|
|
219
|
+
res.json({ success: true, silent: !!silent });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Chat
|
|
223
|
+
app.get('/plugins/aicq-chat/api/chat/:targetId', (req, res) => {
|
|
224
|
+
const agentId = getAgentId(req);
|
|
225
|
+
const limit = parseInt(req.query.limit || '50', 10);
|
|
226
|
+
const before = req.query.before || null;
|
|
227
|
+
const messages = db.getChatHistory(agentId, req.params.targetId, { limit, before });
|
|
228
|
+
res.json({ messages });
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
app.post('/plugins/aicq-chat/api/chat/send', async (req, res) => {
|
|
232
|
+
try {
|
|
233
|
+
const { agent_id, targetId, content, type, isGroup, mentions, file_url, file_name } = req.body;
|
|
234
|
+
const agentId = agent_id || getAgentId(req);
|
|
235
|
+
if (!targetId || !content) return res.status(400).json({ error: 'targetId and content are required' });
|
|
236
|
+
const msg = await chat.sendMessage(agentId, targetId, content, {
|
|
237
|
+
type: type || 'text',
|
|
238
|
+
isGroup: !!isGroup,
|
|
239
|
+
mentions: mentions || [],
|
|
240
|
+
file_url,
|
|
241
|
+
file_name,
|
|
242
|
+
});
|
|
243
|
+
res.json({ success: true, message: msg });
|
|
244
|
+
} catch (e) {
|
|
245
|
+
res.status(500).json({ error: e.message });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
app.delete('/plugins/aicq-chat/api/chat/:messageId', (req, res) => {
|
|
250
|
+
const agentId = getAgentId(req);
|
|
251
|
+
db.deleteMessage(agentId, req.params.messageId);
|
|
252
|
+
res.json({ success: true });
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Streaming endpoints
|
|
256
|
+
app.post('/plugins/aicq-chat/api/chat/stream-chunk', (req, res) => {
|
|
257
|
+
try {
|
|
258
|
+
const { targetId, friend_id, chunk_type, chunkType, data } = req.body;
|
|
259
|
+
const streamTarget = targetId || friend_id;
|
|
260
|
+
if (!streamTarget) return res.status(400).json({ error: 'targetId or friend_id is required' });
|
|
261
|
+
if (!data) return res.status(400).json({ error: 'data is required' });
|
|
262
|
+
const type = chunk_type || chunkType || 'text';
|
|
263
|
+
const ALLOWED_CHUNK_TYPES = ['text', 'reasoning', 'thinking', 'clear_text', 'tool_call', 'tool_result'];
|
|
264
|
+
if (!ALLOWED_CHUNK_TYPES.includes(type)) {
|
|
265
|
+
return res.status(400).json({ error: `Invalid chunk_type: ${type}. Allowed: ${ALLOWED_CHUNK_TYPES.join(', ')}` });
|
|
266
|
+
}
|
|
267
|
+
const sent = serverClient.sendWS({
|
|
268
|
+
type: 'stream_chunk',
|
|
269
|
+
to: streamTarget,
|
|
270
|
+
chunkType: type,
|
|
271
|
+
data: data,
|
|
272
|
+
});
|
|
273
|
+
if (!sent) return res.status(503).json({ error: 'Not connected to server', success: false });
|
|
274
|
+
res.json({ success: true });
|
|
275
|
+
} catch (e) {
|
|
276
|
+
res.status(500).json({ error: e.message });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
app.post('/plugins/aicq-chat/api/chat/stream-end', (req, res) => {
|
|
281
|
+
try {
|
|
282
|
+
const { targetId, friend_id, message_id, messageId } = req.body;
|
|
283
|
+
const streamTarget = targetId || friend_id;
|
|
284
|
+
if (!streamTarget) return res.status(400).json({ error: 'targetId or friend_id is required' });
|
|
285
|
+
const msgId = message_id || messageId || ('msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6));
|
|
286
|
+
const sent = serverClient.sendWS({
|
|
287
|
+
type: 'stream_end',
|
|
288
|
+
to: streamTarget,
|
|
289
|
+
messageId: msgId,
|
|
290
|
+
});
|
|
291
|
+
if (!sent) return res.status(503).json({ error: 'Not connected to server', success: false });
|
|
292
|
+
res.json({ success: true, messageId: msgId });
|
|
293
|
+
} catch (e) {
|
|
294
|
+
res.status(500).json({ error: e.message });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// File upload
|
|
299
|
+
const multer = require('multer');
|
|
300
|
+
const upload = multer({
|
|
301
|
+
storage: multer.memoryStorage(),
|
|
302
|
+
limits: { fileSize: 50 * 1024 * 1024 },
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
app.post('/plugins/aicq-chat/api/upload', upload.single('file'), async (req, res) => {
|
|
306
|
+
try {
|
|
307
|
+
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
|
|
308
|
+
const agentId = getAgentId(req);
|
|
309
|
+
const targetId = req.body.targetId;
|
|
310
|
+
const isGroup = req.body.isGroup === 'true' || req.body.isGroup === '1';
|
|
311
|
+
const msg = await chat.handleFileUpload(agentId, targetId, req.file, isGroup);
|
|
312
|
+
res.json({ success: true, message: msg });
|
|
313
|
+
} catch (e) {
|
|
314
|
+
res.status(500).json({ error: e.message });
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
app.get('/plugins/aicq-chat/api/files/:fileId', (req, res) => {
|
|
319
|
+
const filePath = path.join(UPLOADS_DIR, req.params.fileId);
|
|
320
|
+
if (fs.existsSync(filePath)) {
|
|
321
|
+
res.sendFile(filePath);
|
|
322
|
+
} else {
|
|
323
|
+
res.status(404).json({ error: 'File not found' });
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Identity
|
|
328
|
+
app.get('/plugins/aicq-chat/api/identity', (req, res) => {
|
|
329
|
+
const agentId = getAgentId(req);
|
|
330
|
+
res.json(identity.getInfo(agentId) || {});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
app.post('/plugins/aicq-chat/api/identity/nickname', (req, res) => {
|
|
334
|
+
const { agent_id, nickname } = req.body;
|
|
335
|
+
const agentId = agent_id || getAgentId(req);
|
|
336
|
+
identity.updateNickname(agentId, nickname);
|
|
337
|
+
res.json({ success: true });
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
app.post('/plugins/aicq-chat/api/identity/friend-code', async (req, res) => {
|
|
341
|
+
try {
|
|
342
|
+
const agentId = req.body.agent_id || getAgentId(req);
|
|
343
|
+
await serverClient.ensureAuth(agentId);
|
|
344
|
+
const result = await handshake.generateFriendCode(agentId);
|
|
345
|
+
res.json({ success: true, code: result.number, expires_at: result.expiresAt || result.expires_at });
|
|
346
|
+
} catch (e) {
|
|
347
|
+
res.status(500).json({ error: e.message });
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
app.get('/plugins/aicq-chat/api/identity/qr', async (req, res) => {
|
|
352
|
+
try {
|
|
353
|
+
const agentId = getAgentId(req);
|
|
354
|
+
const info = identity.getInfo(agentId);
|
|
355
|
+
if (!info) return res.status(404).json({ error: 'Agent not found' });
|
|
356
|
+
const qrData = JSON.stringify({
|
|
357
|
+
type: 'aicq-friend',
|
|
358
|
+
agent_id: info.agent_id,
|
|
359
|
+
public_key: info.signing_public_key,
|
|
360
|
+
exchange_public_key: info.exchange_public_key,
|
|
361
|
+
fingerprint: info.fingerprint,
|
|
362
|
+
});
|
|
363
|
+
const qrImage = await QRCode.toDataURL(qrData);
|
|
364
|
+
res.json({ qr: qrImage, data: qrData, info });
|
|
365
|
+
} catch (e) {
|
|
366
|
+
res.status(500).json({ error: e.message });
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
app.post('/plugins/aicq-chat/api/identity/rotate-keys', (req, res) => {
|
|
371
|
+
try {
|
|
372
|
+
const agentId = req.body.agent_id || getAgentId(req);
|
|
373
|
+
identity.rotateKeys(agentId);
|
|
374
|
+
res.json({ success: true, info: identity.getInfo(agentId) });
|
|
375
|
+
} catch (e) {
|
|
376
|
+
res.status(500).json({ error: e.message });
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
app.get('/plugins/aicq-chat/api/identity/keys', (req, res) => {
|
|
381
|
+
const agentId = getAgentId(req);
|
|
382
|
+
const info = identity.loadAgent(agentId);
|
|
383
|
+
if (!info) return res.status(404).json({ error: 'Agent not found' });
|
|
384
|
+
res.json({
|
|
385
|
+
agent_id: info.agent_id,
|
|
386
|
+
nickname: info.nickname,
|
|
387
|
+
signing_public_key: info.signing_public_key,
|
|
388
|
+
exchange_public_key: info.exchange_public_key,
|
|
389
|
+
signing_secret_key: info.signing_secret_key,
|
|
390
|
+
exchange_secret_key: info.exchange_secret_key,
|
|
391
|
+
fingerprint: info.fingerprint,
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Sync endpoint
|
|
396
|
+
app.post('/plugins/aicq-chat/api/sync', async (req, res) => {
|
|
397
|
+
try {
|
|
398
|
+
const agentId = req.body.agent_id || getAgentId(req);
|
|
399
|
+
await serverClient.ensureAuth(agentId);
|
|
400
|
+
// Sync friends
|
|
401
|
+
const friendResult = await serverClient.listFriends();
|
|
402
|
+
if (friendResult.friends) {
|
|
403
|
+
for (const f of friendResult.friends) {
|
|
404
|
+
const existing = db.getFriend(agentId, f.id);
|
|
405
|
+
if (!existing) {
|
|
406
|
+
db.addFriend({
|
|
407
|
+
agent_id: agentId,
|
|
408
|
+
id: f.id,
|
|
409
|
+
public_key: f.public_key || f.publicKey || '',
|
|
410
|
+
fingerprint: f.fingerprint || '',
|
|
411
|
+
friend_type: f.type || f.friend_type || 'ai',
|
|
412
|
+
ai_name: f.agent_name || f.ai_name || f.displayName || '',
|
|
413
|
+
});
|
|
414
|
+
} else {
|
|
415
|
+
db.updateFriendOnline(agentId, f.id, f.is_online || f.isOnline || false);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// Sync groups
|
|
420
|
+
const groupResult = await serverClient.listGroups();
|
|
421
|
+
if (groupResult.groups) {
|
|
422
|
+
for (const g of groupResult.groups) {
|
|
423
|
+
db.addGroup({
|
|
424
|
+
agent_id: agentId,
|
|
425
|
+
id: g.id,
|
|
426
|
+
name: g.name,
|
|
427
|
+
owner_id: g.owner_id || g.ownerId || '',
|
|
428
|
+
members_json: g.members || g.members_json || '[]',
|
|
429
|
+
description: g.description || '',
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
res.json({ success: true });
|
|
434
|
+
} catch (e) {
|
|
435
|
+
res.status(500).json({ error: e.message });
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Gateway proxy endpoint (for backward compatibility)
|
|
440
|
+
app.post('/plugins/aicq-chat/api/gateway', async (req, res) => {
|
|
441
|
+
try {
|
|
442
|
+
const { method, kwargs } = req.body;
|
|
443
|
+
if (!method) return res.status(400).json({ error: 'method is required' });
|
|
444
|
+
// Import handleGateway from index.js
|
|
445
|
+
const { handleGateway } = require('../index');
|
|
446
|
+
const result = await handleGateway(method, kwargs);
|
|
447
|
+
res.json(result);
|
|
448
|
+
} catch (e) {
|
|
449
|
+
res.status(500).json({ error: e.message });
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
registerRoutes,
|
|
456
|
+
// Also export as a map of route handlers for Gateway HTTP registration
|
|
457
|
+
routes: {
|
|
458
|
+
'GET /plugins/aicq-chat/ui/*': 'static:public',
|
|
459
|
+
'GET /plugins/aicq-chat/api/status': 'api:status',
|
|
460
|
+
'GET /plugins/aicq-chat/api/friends': 'api:friends.list',
|
|
461
|
+
'POST /plugins/aicq-chat/api/friends/add': 'api:friends.add',
|
|
462
|
+
'DELETE /plugins/aicq-chat/api/friends/:id': 'api:friends.remove',
|
|
463
|
+
'GET /plugins/aicq-chat/api/messages': 'api:messages',
|
|
464
|
+
'POST /plugins/aicq-chat/api/chat/send': 'api:chat.send',
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
module.exports = { createUiRoutes };
|