aicq-chat-plugin 2.5.9 → 2.6.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/extension.js +1 -1
- package/index.js +573 -568
- package/lib/database.js +205 -85
- package/openclaw.plugin.json +2 -2
- package/package.json +9 -9
- package/postinstall.js +1 -1
package/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AICQ Chat Plugin — Main Entry Point
|
|
3
3
|
* OpenClaw sidecar plugin providing E2EE chat UI
|
|
4
|
+
*
|
|
5
|
+
* Uses sql.js (pure WASM SQLite) instead of better-sqlite3
|
|
6
|
+
* to avoid native C++ compilation issues.
|
|
4
7
|
*/
|
|
5
8
|
const express = require('express');
|
|
6
9
|
const multer = require('multer');
|
|
@@ -9,10 +12,6 @@ const fs = require('fs');
|
|
|
9
12
|
const os = require('os');
|
|
10
13
|
const QRCode = require('qrcode');
|
|
11
14
|
const PluginDatabase = require('./lib/database');
|
|
12
|
-
const IdentityManager = require('./lib/identity');
|
|
13
|
-
const ServerClient = require('./lib/server-client');
|
|
14
|
-
const HandshakeManager = require('./lib/handshake');
|
|
15
|
-
const ChatManager = require('./lib/chat');
|
|
16
15
|
|
|
17
16
|
// ─── Configuration ──────────────────────────────────────────────────
|
|
18
17
|
const PORT = parseInt(process.env.AICQ_PORT || '6109', 10);
|
|
@@ -23,640 +22,646 @@ const UPLOADS_DIR = path.join(DATA_DIR, 'uploads');
|
|
|
23
22
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
24
23
|
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
25
24
|
|
|
26
|
-
// ───
|
|
27
|
-
|
|
28
|
-
const identity = new IdentityManager(db);
|
|
29
|
-
const serverClient = new ServerClient(identity, db, SERVER_URL);
|
|
30
|
-
const handshake = new HandshakeManager(identity, serverClient, db);
|
|
31
|
-
const chat = new ChatManager(identity, serverClient, db, UPLOADS_DIR);
|
|
32
|
-
|
|
33
|
-
// Auto-create a default agent if none exists
|
|
34
|
-
const agents = identity.listAgents();
|
|
35
|
-
let currentAgentId = null;
|
|
36
|
-
if (agents.length === 0) {
|
|
37
|
-
const defaultAgent = identity.createAgent('agent-' + Date.now(), '默认Agent');
|
|
38
|
-
currentAgentId = defaultAgent.agent_id;
|
|
39
|
-
console.log('[AICQ] Created default agent:', currentAgentId);
|
|
40
|
-
} else {
|
|
41
|
-
currentAgentId = agents[0].agent_id;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Connect to server in background
|
|
25
|
+
// ─── Async bootstrap ────────────────────────────────────────────────
|
|
26
|
+
// sql.js requires async init, so we wrap the entire app setup
|
|
45
27
|
(async () => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
28
|
+
// Initialize database (async — loads WASM + opens/creates DB file)
|
|
29
|
+
const db = new PluginDatabase(DATA_DIR);
|
|
30
|
+
await db.init();
|
|
31
|
+
console.log('[AICQ] Database initialized');
|
|
32
|
+
|
|
33
|
+
// Lazy-load modules that depend on db
|
|
34
|
+
const IdentityManager = require('./lib/identity');
|
|
35
|
+
const ServerClient = require('./lib/server-client');
|
|
36
|
+
const HandshakeManager = require('./lib/handshake');
|
|
37
|
+
const ChatManager = require('./lib/chat');
|
|
38
|
+
|
|
39
|
+
const identity = new IdentityManager(db);
|
|
40
|
+
const serverClient = new ServerClient(identity, db, SERVER_URL);
|
|
41
|
+
const handshake = new HandshakeManager(identity, serverClient, db);
|
|
42
|
+
const chat = new ChatManager(identity, serverClient, db, UPLOADS_DIR);
|
|
43
|
+
|
|
44
|
+
// Auto-create a default agent if none exists
|
|
45
|
+
const agents = identity.listAgents();
|
|
46
|
+
let currentAgentId = null;
|
|
47
|
+
if (agents.length === 0) {
|
|
48
|
+
const defaultAgent = identity.createAgent('agent-' + Date.now(), '默认Agent');
|
|
49
|
+
currentAgentId = defaultAgent.agent_id;
|
|
50
|
+
console.log('[AICQ] Created default agent:', currentAgentId);
|
|
51
|
+
} else {
|
|
52
|
+
currentAgentId = agents[0].agent_id;
|
|
53
53
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
54
|
+
|
|
55
|
+
// Connect to server in background
|
|
56
|
+
(async () => {
|
|
57
|
+
try {
|
|
58
|
+
await serverClient.start(currentAgentId);
|
|
59
|
+
await syncFriendsFromServer(currentAgentId);
|
|
60
|
+
await syncGroupsFromServer(currentAgentId);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error('[AICQ] Initial server connection failed:', e.message);
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
65
|
+
|
|
66
|
+
// Periodic cleanup + save
|
|
67
|
+
setInterval(() => db.cleanup(), 3600000);
|
|
68
|
+
|
|
69
|
+
// ─── Helper: get current agent ID ──────────────────────────────────
|
|
70
|
+
function getAgentId(req) {
|
|
71
|
+
return req.query.agent_id || req.body?.agent_id || currentAgentId;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Sync friends/groups from server ────────────────────────────────
|
|
75
|
+
async function syncFriendsFromServer(agentId) {
|
|
76
|
+
try {
|
|
77
|
+
await serverClient.ensureAuth(agentId);
|
|
78
|
+
const result = await serverClient.listFriends();
|
|
79
|
+
if (result.friends) {
|
|
80
|
+
for (const f of result.friends) {
|
|
81
|
+
const existing = db.getFriend(agentId, f.id);
|
|
82
|
+
if (!existing) {
|
|
83
|
+
db.addFriend({
|
|
84
|
+
agent_id: agentId,
|
|
85
|
+
id: f.id,
|
|
86
|
+
public_key: f.public_key || f.publicKey || '',
|
|
87
|
+
fingerprint: f.fingerprint || '',
|
|
88
|
+
friend_type: f.type || f.friend_type || 'ai',
|
|
89
|
+
ai_name: f.agent_name || f.ai_name || f.displayName || '',
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
db.updateFriendOnline(agentId, f.id, f.is_online || f.isOnline || false);
|
|
93
|
+
}
|
|
83
94
|
}
|
|
84
95
|
}
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.error('[AICQ] Sync friends failed:', e.message);
|
|
85
98
|
}
|
|
86
|
-
} catch (e) {
|
|
87
|
-
console.error('[AICQ] Sync friends failed:', e.message);
|
|
88
99
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
100
|
+
|
|
101
|
+
async function syncGroupsFromServer(agentId) {
|
|
102
|
+
try {
|
|
103
|
+
await serverClient.ensureAuth(agentId);
|
|
104
|
+
const result = await serverClient.listGroups();
|
|
105
|
+
if (result.groups) {
|
|
106
|
+
for (const g of result.groups) {
|
|
107
|
+
db.addGroup({
|
|
108
|
+
agent_id: agentId,
|
|
109
|
+
id: g.id,
|
|
110
|
+
name: g.name,
|
|
111
|
+
owner_id: g.owner_id || g.ownerId || '',
|
|
112
|
+
members_json: g.members || g.members_json || '[]',
|
|
113
|
+
description: g.description || '',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
105
116
|
}
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.error('[AICQ] Sync groups failed:', e.message);
|
|
106
119
|
}
|
|
107
|
-
} catch (e) {
|
|
108
|
-
console.error('[AICQ] Sync groups failed:', e.message);
|
|
109
120
|
}
|
|
110
|
-
}
|
|
111
121
|
|
|
112
|
-
// ─── Express App ────────────────────────────────────────────────────
|
|
113
|
-
const app = express();
|
|
114
|
-
app.use(express.json());
|
|
115
|
-
app.use(express.urlencoded({ extended: true }));
|
|
122
|
+
// ─── Express App ────────────────────────────────────────────────────
|
|
123
|
+
const app = express();
|
|
124
|
+
app.use(express.json());
|
|
125
|
+
app.use(express.urlencoded({ extended: true }));
|
|
116
126
|
|
|
117
|
-
const upload = multer({
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
});
|
|
127
|
+
const upload = multer({
|
|
128
|
+
storage: multer.memoryStorage(),
|
|
129
|
+
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB
|
|
130
|
+
});
|
|
121
131
|
|
|
122
|
-
// ─── Serve SPA ──────────────────────────────────────────────────────
|
|
123
|
-
app.use(express.static(path.join(__dirname, 'public')));
|
|
132
|
+
// ─── Serve SPA ──────────────────────────────────────────────────────
|
|
133
|
+
app.use(express.static(path.join(__dirname, 'public')));
|
|
124
134
|
|
|
125
|
-
// ─── API Routes ─────────────────────────────────────────────────────
|
|
135
|
+
// ─── API Routes ─────────────────────────────────────────────────────
|
|
126
136
|
|
|
127
|
-
// Status
|
|
128
|
-
app.get('/api/status', (req, res) => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
// Status
|
|
138
|
+
app.get('/api/status', (req, res) => {
|
|
139
|
+
res.json({
|
|
140
|
+
status: 'running',
|
|
141
|
+
version: '2.6.0',
|
|
142
|
+
connected: serverClient.connected,
|
|
143
|
+
currentAgent: currentAgentId,
|
|
144
|
+
serverUrl: SERVER_URL,
|
|
145
|
+
});
|
|
135
146
|
});
|
|
136
|
-
});
|
|
137
147
|
|
|
138
|
-
// Agents
|
|
139
|
-
app.get('/api/agents', (req, res) => {
|
|
140
|
-
|
|
141
|
-
});
|
|
148
|
+
// Agents
|
|
149
|
+
app.get('/api/agents', (req, res) => {
|
|
150
|
+
res.json({ agents: identity.listAgents() });
|
|
151
|
+
});
|
|
142
152
|
|
|
143
|
-
app.post('/api/agents', async (req, res) => {
|
|
144
|
-
try {
|
|
145
|
-
const { agent_id, nickname } = req.body;
|
|
146
|
-
if (!agent_id) return res.status(400).json({ error: 'agent_id is required' });
|
|
147
|
-
const agent = identity.createAgent(agent_id, nickname);
|
|
148
|
-
currentAgentId = agent_id;
|
|
149
|
-
// Register on server
|
|
153
|
+
app.post('/api/agents', async (req, res) => {
|
|
150
154
|
try {
|
|
151
|
-
|
|
155
|
+
const { agent_id, nickname } = req.body;
|
|
156
|
+
if (!agent_id) return res.status(400).json({ error: 'agent_id is required' });
|
|
157
|
+
const agent = identity.createAgent(agent_id, nickname);
|
|
158
|
+
currentAgentId = agent_id;
|
|
159
|
+
try {
|
|
160
|
+
await serverClient.start(agent_id);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
console.error('Server registration failed:', e.message);
|
|
163
|
+
}
|
|
164
|
+
res.json({ success: true, agent });
|
|
152
165
|
} catch (e) {
|
|
153
|
-
|
|
166
|
+
res.status(500).json({ error: e.message });
|
|
154
167
|
}
|
|
155
|
-
|
|
156
|
-
} catch (e) {
|
|
157
|
-
res.status(500).json({ error: e.message });
|
|
158
|
-
}
|
|
159
|
-
});
|
|
168
|
+
});
|
|
160
169
|
|
|
161
|
-
app.delete('/api/agents/:id', (req, res) => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
});
|
|
170
|
+
app.delete('/api/agents/:id', (req, res) => {
|
|
171
|
+
identity.deleteAgent(req.params.id);
|
|
172
|
+
if (currentAgentId === req.params.id) {
|
|
173
|
+
const remaining = identity.listAgents();
|
|
174
|
+
currentAgentId = remaining.length > 0 ? remaining[0].agent_id : null;
|
|
175
|
+
}
|
|
176
|
+
res.json({ success: true });
|
|
177
|
+
});
|
|
169
178
|
|
|
170
|
-
app.post('/api/agents/switch', async (req, res) => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
});
|
|
179
|
+
app.post('/api/agents/switch', async (req, res) => {
|
|
180
|
+
try {
|
|
181
|
+
const { agent_id } = req.body;
|
|
182
|
+
if (!agent_id) return res.status(400).json({ error: 'agent_id is required' });
|
|
183
|
+
currentAgentId = agent_id;
|
|
184
|
+
await serverClient.switchAgent(agent_id);
|
|
185
|
+
await syncFriendsFromServer(agent_id);
|
|
186
|
+
await syncGroupsFromServer(agent_id);
|
|
187
|
+
res.json({ success: true, agent_id });
|
|
188
|
+
} catch (e) {
|
|
189
|
+
res.status(500).json({ error: e.message });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
183
192
|
|
|
184
|
-
// Friends
|
|
185
|
-
app.get('/api/friends', (req, res) => {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
});
|
|
193
|
+
// Friends
|
|
194
|
+
app.get('/api/friends', (req, res) => {
|
|
195
|
+
const agentId = getAgentId(req);
|
|
196
|
+
res.json({ friends: db.listFriends(agentId) });
|
|
197
|
+
});
|
|
189
198
|
|
|
190
|
-
app.post('/api/friends/add', async (req, res) => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
});
|
|
199
|
+
app.post('/api/friends/add', async (req, res) => {
|
|
200
|
+
try {
|
|
201
|
+
const { temp_number, friend_code, agent_id } = req.body;
|
|
202
|
+
const agentId = agent_id || currentAgentId;
|
|
203
|
+
const code = temp_number || friend_code;
|
|
204
|
+
if (!code) return res.status(400).json({ error: 'temp_number or friend_code is required' });
|
|
205
|
+
const result = await handshake.addFriendByCode(agentId, code);
|
|
206
|
+
res.json({ success: true, result });
|
|
207
|
+
} catch (e) {
|
|
208
|
+
res.status(500).json({ error: e.message });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
202
211
|
|
|
203
|
-
app.delete('/api/friends/:id', async (req, res) => {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
});
|
|
212
|
+
app.delete('/api/friends/:id', async (req, res) => {
|
|
213
|
+
try {
|
|
214
|
+
const agentId = getAgentId(req);
|
|
215
|
+
db.removeFriend(agentId, req.params.id);
|
|
216
|
+
try { await serverClient.removeFriend(req.params.id); } catch (e) {}
|
|
217
|
+
res.json({ success: true });
|
|
218
|
+
} catch (e) {
|
|
219
|
+
res.status(500).json({ error: e.message });
|
|
220
|
+
}
|
|
221
|
+
});
|
|
213
222
|
|
|
214
|
-
app.get('/api/friends/requests', async (req, res) => {
|
|
215
|
-
try {
|
|
216
|
-
const agentId = getAgentId(req);
|
|
217
|
-
// Get from server
|
|
218
|
-
let serverRequests = [];
|
|
223
|
+
app.get('/api/friends/requests', async (req, res) => {
|
|
219
224
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
})
|
|
225
|
+
const agentId = getAgentId(req);
|
|
226
|
+
let serverRequests = [];
|
|
227
|
+
try {
|
|
228
|
+
await serverClient.ensureAuth(agentId);
|
|
229
|
+
const result = await serverClient.listFriendRequests();
|
|
230
|
+
serverRequests = result.sent || [];
|
|
231
|
+
serverRequests = serverRequests.concat(result.received || []);
|
|
232
|
+
} catch (e) {}
|
|
233
|
+
const localRequests = db.getPendingRequests(agentId);
|
|
234
|
+
res.json({ requests: [...localRequests, ...serverRequests] });
|
|
235
|
+
} catch (e) {
|
|
236
|
+
res.status(500).json({ error: e.message });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
231
239
|
|
|
232
|
-
app.post('/api/friends/requests/:id/accept', async (req, res) => {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
});
|
|
240
|
+
app.post('/api/friends/requests/:id/accept', async (req, res) => {
|
|
241
|
+
try {
|
|
242
|
+
const agentId = getAgentId(req);
|
|
243
|
+
const result = await handshake.acceptRequest(agentId, req.params.id);
|
|
244
|
+
res.json(result);
|
|
245
|
+
} catch (e) {
|
|
246
|
+
res.status(500).json({ error: e.message });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
app.post('/api/friends/requests/:id/reject', async (req, res) => {
|
|
251
|
+
try {
|
|
252
|
+
const agentId = getAgentId(req);
|
|
253
|
+
const result = await handshake.rejectRequest(agentId, req.params.id);
|
|
254
|
+
res.json(result);
|
|
255
|
+
} catch (e) {
|
|
256
|
+
res.status(500).json({ error: e.message });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
241
259
|
|
|
242
|
-
|
|
243
|
-
|
|
260
|
+
// Groups
|
|
261
|
+
app.get('/api/groups', (req, res) => {
|
|
244
262
|
const agentId = getAgentId(req);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
} catch (e) {
|
|
248
|
-
res.status(500).json({ error: e.message });
|
|
249
|
-
}
|
|
250
|
-
});
|
|
263
|
+
res.json({ groups: db.listGroups(agentId) });
|
|
264
|
+
});
|
|
251
265
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
});
|
|
266
|
+
app.post('/api/groups', async (req, res) => {
|
|
267
|
+
try {
|
|
268
|
+
const agentId = getAgentId(req);
|
|
269
|
+
const { name, description } = req.body;
|
|
270
|
+
if (!name) return res.status(400).json({ error: 'name is required' });
|
|
271
|
+
await serverClient.ensureAuth(agentId);
|
|
272
|
+
const result = await serverClient.createGroup(name, description);
|
|
273
|
+
if (result.id) {
|
|
274
|
+
db.addGroup({
|
|
275
|
+
agent_id: agentId,
|
|
276
|
+
id: result.id,
|
|
277
|
+
name,
|
|
278
|
+
owner_id: agentId,
|
|
279
|
+
members_json: result.members || '[]',
|
|
280
|
+
description: description || '',
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
res.json({ success: true, group: result });
|
|
284
|
+
} catch (e) {
|
|
285
|
+
res.status(500).json({ error: e.message });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
257
288
|
|
|
258
|
-
app.post('/api/groups', async (req, res) => {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
db.addGroup({
|
|
267
|
-
agent_id: agentId,
|
|
268
|
-
id: result.id,
|
|
269
|
-
name,
|
|
270
|
-
owner_id: agentId,
|
|
271
|
-
members_json: result.members || '[]',
|
|
272
|
-
description: description || '',
|
|
273
|
-
});
|
|
289
|
+
app.post('/api/groups/:id/join', async (req, res) => {
|
|
290
|
+
try {
|
|
291
|
+
const agentId = getAgentId(req);
|
|
292
|
+
await serverClient.ensureAuth(agentId);
|
|
293
|
+
const result = await serverClient.inviteGroupMember(req.params.id, agentId);
|
|
294
|
+
res.json({ success: true, result });
|
|
295
|
+
} catch (e) {
|
|
296
|
+
res.status(500).json({ error: e.message });
|
|
274
297
|
}
|
|
275
|
-
|
|
276
|
-
} catch (e) {
|
|
277
|
-
res.status(500).json({ error: e.message });
|
|
278
|
-
}
|
|
279
|
-
});
|
|
298
|
+
});
|
|
280
299
|
|
|
281
|
-
app.
|
|
282
|
-
|
|
300
|
+
app.get('/api/groups/:id/messages', async (req, res) => {
|
|
301
|
+
try {
|
|
302
|
+
const agentId = getAgentId(req);
|
|
303
|
+
const limit = parseInt(req.query.limit || '50', 10);
|
|
304
|
+
const before = req.query.before || null;
|
|
305
|
+
try {
|
|
306
|
+
await serverClient.ensureAuth(agentId);
|
|
307
|
+
const result = await serverClient.getGroupMessages(req.params.id, limit, before);
|
|
308
|
+
if (result.messages && result.messages.length > 0) {
|
|
309
|
+
return res.json({ messages: result.messages });
|
|
310
|
+
}
|
|
311
|
+
} catch (e) {}
|
|
312
|
+
const messages = db.getChatHistory(agentId, req.params.id, { limit, before });
|
|
313
|
+
res.json({ messages });
|
|
314
|
+
} catch (e) {
|
|
315
|
+
res.status(500).json({ error: e.message });
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
app.put('/api/groups/:id/silent', (req, res) => {
|
|
283
320
|
const agentId = getAgentId(req);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
res.json({ success: true,
|
|
287
|
-
}
|
|
288
|
-
res.status(500).json({ error: e.message });
|
|
289
|
-
}
|
|
290
|
-
});
|
|
321
|
+
const { silent } = req.body;
|
|
322
|
+
db.setGroupSilentMode(agentId, req.params.id, !!silent);
|
|
323
|
+
res.json({ success: true, silent: !!silent });
|
|
324
|
+
});
|
|
291
325
|
|
|
292
|
-
|
|
293
|
-
|
|
326
|
+
// Chat
|
|
327
|
+
app.get('/api/chat/:targetId', (req, res) => {
|
|
294
328
|
const agentId = getAgentId(req);
|
|
295
329
|
const limit = parseInt(req.query.limit || '50', 10);
|
|
296
330
|
const before = req.query.before || null;
|
|
297
|
-
|
|
298
|
-
try {
|
|
299
|
-
await serverClient.ensureAuth(agentId);
|
|
300
|
-
const result = await serverClient.getGroupMessages(req.params.id, limit, before);
|
|
301
|
-
if (result.messages && result.messages.length > 0) {
|
|
302
|
-
return res.json({ messages: result.messages });
|
|
303
|
-
}
|
|
304
|
-
} catch (e) {}
|
|
305
|
-
// Fallback to local
|
|
306
|
-
const messages = db.getChatHistory(agentId, req.params.id, { limit, before });
|
|
331
|
+
const messages = db.getChatHistory(agentId, req.params.targetId, { limit, before });
|
|
307
332
|
res.json({ messages });
|
|
308
|
-
}
|
|
309
|
-
res.status(500).json({ error: e.message });
|
|
310
|
-
}
|
|
311
|
-
});
|
|
333
|
+
});
|
|
312
334
|
|
|
313
|
-
app.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
335
|
+
app.post('/api/chat/send', async (req, res) => {
|
|
336
|
+
try {
|
|
337
|
+
const { agent_id, targetId, content, type, isGroup, mentions, file_url, file_name } = req.body;
|
|
338
|
+
const agentId = agent_id || currentAgentId;
|
|
339
|
+
if (!targetId || !content) return res.status(400).json({ error: 'targetId and content are required' });
|
|
340
|
+
const msg = await chat.sendMessage(agentId, targetId, content, {
|
|
341
|
+
type: type || 'text',
|
|
342
|
+
isGroup: !!isGroup,
|
|
343
|
+
mentions: mentions || [],
|
|
344
|
+
file_url,
|
|
345
|
+
file_name,
|
|
346
|
+
});
|
|
347
|
+
res.json({ success: true, message: msg });
|
|
348
|
+
} catch (e) {
|
|
349
|
+
res.status(500).json({ error: e.message });
|
|
350
|
+
}
|
|
351
|
+
});
|
|
319
352
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const messages = db.getChatHistory(agentId, req.params.targetId, { limit, before });
|
|
326
|
-
res.json({ messages });
|
|
327
|
-
});
|
|
353
|
+
app.delete('/api/chat/:messageId', (req, res) => {
|
|
354
|
+
const agentId = getAgentId(req);
|
|
355
|
+
db.deleteMessage(agentId, req.params.messageId);
|
|
356
|
+
res.json({ success: true });
|
|
357
|
+
});
|
|
328
358
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
});
|
|
359
|
+
// Streaming endpoints
|
|
360
|
+
app.post('/api/chat/stream-chunk', (req, res) => {
|
|
361
|
+
try {
|
|
362
|
+
const { targetId, friend_id, chunk_type, chunkType, data } = req.body;
|
|
363
|
+
const streamTarget = targetId || friend_id;
|
|
364
|
+
if (!streamTarget) return res.status(400).json({ error: 'targetId or friend_id is required' });
|
|
365
|
+
if (!data) return res.status(400).json({ error: 'data is required' });
|
|
366
|
+
const type = chunk_type || chunkType || 'text';
|
|
367
|
+
if (!['text', 'reasoning', 'tool_call', 'tool_result'].includes(type)) {
|
|
368
|
+
return res.status(400).json({ error: `Invalid chunk_type: ${type}` });
|
|
369
|
+
}
|
|
370
|
+
const sent = serverClient.sendWS({
|
|
371
|
+
type: 'stream_chunk',
|
|
372
|
+
to: streamTarget,
|
|
373
|
+
chunkType: type,
|
|
374
|
+
data: data,
|
|
375
|
+
});
|
|
376
|
+
if (!sent) return res.status(503).json({ error: 'Not connected to server', success: false });
|
|
377
|
+
res.json({ success: true });
|
|
378
|
+
} catch (e) {
|
|
379
|
+
res.status(500).json({ error: e.message });
|
|
380
|
+
}
|
|
381
|
+
});
|
|
346
382
|
|
|
347
|
-
app.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
});
|
|
383
|
+
app.post('/api/chat/stream-end', (req, res) => {
|
|
384
|
+
try {
|
|
385
|
+
const { targetId, friend_id, message_id, messageId } = req.body;
|
|
386
|
+
const streamTarget = targetId || friend_id;
|
|
387
|
+
if (!streamTarget) return res.status(400).json({ error: 'targetId or friend_id is required' });
|
|
388
|
+
const msgId = message_id || messageId || ('msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6));
|
|
389
|
+
const sent = serverClient.sendWS({
|
|
390
|
+
type: 'stream_end',
|
|
391
|
+
to: streamTarget,
|
|
392
|
+
messageId: msgId,
|
|
393
|
+
});
|
|
394
|
+
if (!sent) return res.status(503).json({ error: 'Not connected to server', success: false });
|
|
395
|
+
res.json({ success: true, messageId: msgId });
|
|
396
|
+
} catch (e) {
|
|
397
|
+
res.status(500).json({ error: e.message });
|
|
398
|
+
}
|
|
399
|
+
});
|
|
352
400
|
|
|
353
|
-
//
|
|
354
|
-
app.post('/api/
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
401
|
+
// File upload
|
|
402
|
+
app.post('/api/upload', upload.single('file'), async (req, res) => {
|
|
403
|
+
try {
|
|
404
|
+
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
|
|
405
|
+
const agentId = getAgentId(req);
|
|
406
|
+
const targetId = req.body.targetId;
|
|
407
|
+
const isGroup = req.body.isGroup === 'true' || req.body.isGroup === '1';
|
|
408
|
+
const msg = await chat.handleFileUpload(agentId, targetId, req.file, isGroup);
|
|
409
|
+
res.json({ success: true, message: msg });
|
|
410
|
+
} catch (e) {
|
|
411
|
+
res.status(500).json({ error: e.message });
|
|
363
412
|
}
|
|
364
|
-
|
|
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
|
-
});
|
|
413
|
+
});
|
|
376
414
|
|
|
377
|
-
app.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
});
|
|
415
|
+
app.get('/api/files/:fileId', (req, res) => {
|
|
416
|
+
const filePath = path.join(UPLOADS_DIR, req.params.fileId);
|
|
417
|
+
if (fs.existsSync(filePath)) {
|
|
418
|
+
res.sendFile(filePath);
|
|
419
|
+
} else {
|
|
420
|
+
res.status(404).json({ error: 'File not found' });
|
|
421
|
+
}
|
|
422
|
+
});
|
|
394
423
|
|
|
395
|
-
//
|
|
396
|
-
app.
|
|
397
|
-
try {
|
|
398
|
-
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
|
|
424
|
+
// Identity
|
|
425
|
+
app.get('/api/identity', (req, res) => {
|
|
399
426
|
const agentId = getAgentId(req);
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const msg = await chat.handleFileUpload(agentId, targetId, req.file, isGroup);
|
|
403
|
-
res.json({ success: true, message: msg });
|
|
404
|
-
} catch (e) {
|
|
405
|
-
res.status(500).json({ error: e.message });
|
|
406
|
-
}
|
|
407
|
-
});
|
|
427
|
+
res.json(identity.getInfo(agentId) || {});
|
|
428
|
+
});
|
|
408
429
|
|
|
409
|
-
app.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
});
|
|
430
|
+
app.post('/api/identity/nickname', (req, res) => {
|
|
431
|
+
const { agent_id, nickname } = req.body;
|
|
432
|
+
const agentId = agent_id || currentAgentId;
|
|
433
|
+
identity.updateNickname(agentId, nickname);
|
|
434
|
+
res.json({ success: true });
|
|
435
|
+
});
|
|
417
436
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
437
|
+
app.post('/api/identity/friend-code', async (req, res) => {
|
|
438
|
+
try {
|
|
439
|
+
const agentId = req.body.agent_id || currentAgentId;
|
|
440
|
+
await serverClient.ensureAuth(agentId);
|
|
441
|
+
const result = await handshake.generateFriendCode(agentId);
|
|
442
|
+
res.json({ success: true, code: result.number, expires_at: result.expiresAt || result.expires_at });
|
|
443
|
+
} catch (e) {
|
|
444
|
+
res.status(500).json({ error: e.message });
|
|
445
|
+
}
|
|
446
|
+
});
|
|
423
447
|
|
|
424
|
-
app.
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
448
|
+
app.get('/api/identity/qr', async (req, res) => {
|
|
449
|
+
try {
|
|
450
|
+
const agentId = getAgentId(req);
|
|
451
|
+
const info = identity.getInfo(agentId);
|
|
452
|
+
if (!info) return res.status(404).json({ error: 'Agent not found' });
|
|
453
|
+
const qrData = JSON.stringify({
|
|
454
|
+
type: 'aicq-friend',
|
|
455
|
+
agent_id: info.agent_id,
|
|
456
|
+
public_key: info.signing_public_key,
|
|
457
|
+
exchange_public_key: info.exchange_public_key,
|
|
458
|
+
fingerprint: info.fingerprint,
|
|
459
|
+
});
|
|
460
|
+
const qrImage = await QRCode.toDataURL(qrData);
|
|
461
|
+
res.json({ qr: qrImage, data: qrData, info });
|
|
462
|
+
} catch (e) {
|
|
463
|
+
res.status(500).json({ error: e.message });
|
|
464
|
+
}
|
|
465
|
+
});
|
|
430
466
|
|
|
431
|
-
app.post('/api/identity/
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
|
|
467
|
+
app.post('/api/identity/rotate-keys', (req, res) => {
|
|
468
|
+
try {
|
|
469
|
+
const agentId = req.body.agent_id || currentAgentId;
|
|
470
|
+
const newInfo = identity.rotateKeys(agentId);
|
|
471
|
+
res.json({ success: true, info: identity.getInfo(agentId) });
|
|
472
|
+
} catch (e) {
|
|
473
|
+
res.status(500).json({ error: e.message });
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Avatar upload
|
|
478
|
+
const avatarUpload = multer({
|
|
479
|
+
storage: multer.memoryStorage(),
|
|
480
|
+
limits: { fileSize: 2 * 1024 * 1024 }, // 2MB max
|
|
481
|
+
fileFilter: (req, file, cb) => {
|
|
482
|
+
if (file.mimetype && file.mimetype.startsWith('image/')) {
|
|
483
|
+
cb(null, true);
|
|
484
|
+
} else {
|
|
485
|
+
cb(new Error('Only image files are allowed'));
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
});
|
|
441
489
|
|
|
442
|
-
app.
|
|
443
|
-
|
|
490
|
+
app.post('/api/identity/avatar', avatarUpload.single('avatar'), async (req, res) => {
|
|
491
|
+
try {
|
|
492
|
+
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
|
|
493
|
+
const agentId = req.body.agent_id || currentAgentId;
|
|
494
|
+
|
|
495
|
+
const avatarsDir = path.join(DATA_DIR, 'avatars');
|
|
496
|
+
fs.mkdirSync(avatarsDir, { recursive: true });
|
|
497
|
+
const ext = req.file.mimetype.split('/')[1] || 'png';
|
|
498
|
+
const avatarId = Date.now() + '-' + Math.random().toString(36).slice(2, 8);
|
|
499
|
+
const filename = `${avatarId}.${ext}`;
|
|
500
|
+
const filePath = path.join(avatarsDir, filename);
|
|
501
|
+
fs.writeFileSync(filePath, req.file.buffer);
|
|
502
|
+
|
|
503
|
+
const avatarUrl = `/api/identity/avatars/${filename}`;
|
|
504
|
+
identity.updateAvatar(agentId, avatarUrl);
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
await serverClient.ensureAuth(agentId);
|
|
508
|
+
const FormData = (await import('form-data')).default;
|
|
509
|
+
const form = new FormData();
|
|
510
|
+
form.append('avatar', req.file.buffer, {
|
|
511
|
+
filename: req.file.originalname || 'avatar.' + ext,
|
|
512
|
+
contentType: req.file.mimetype,
|
|
513
|
+
});
|
|
514
|
+
const fetch = (await import('node-fetch')).default;
|
|
515
|
+
const serverUrl = SERVER_URL + '/api/v1/accounts/avatar';
|
|
516
|
+
const serverResp = await fetch(serverUrl, {
|
|
517
|
+
method: 'POST',
|
|
518
|
+
body: form,
|
|
519
|
+
headers: {
|
|
520
|
+
...form.getHeaders(),
|
|
521
|
+
'Authorization': 'Bearer ' + serverClient.getAccessToken(agentId),
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
if (serverResp.ok) {
|
|
525
|
+
const serverData = await serverResp.json();
|
|
526
|
+
if (serverData.avatar) {
|
|
527
|
+
identity.updateAvatar(agentId, serverData.avatar);
|
|
528
|
+
return res.json({ success: true, avatar: serverData.avatar });
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
} catch (e) {
|
|
532
|
+
console.error('[AICQ] Server avatar upload failed:', e.message);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
res.json({ success: true, avatar: avatarUrl });
|
|
536
|
+
} catch (e) {
|
|
537
|
+
res.status(500).json({ error: e.message });
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
app.get('/api/identity/avatars/:filename', (req, res) => {
|
|
542
|
+
const filename = req.params.filename;
|
|
543
|
+
if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
|
|
544
|
+
return res.status(400).json({ error: 'Invalid filename' });
|
|
545
|
+
}
|
|
546
|
+
const filePath = path.join(DATA_DIR, 'avatars', filename);
|
|
547
|
+
if (fs.existsSync(filePath)) {
|
|
548
|
+
res.sendFile(filePath);
|
|
549
|
+
} else {
|
|
550
|
+
res.status(404).json({ error: 'Avatar not found' });
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
app.get('/api/identity/keys', (req, res) => {
|
|
444
555
|
const agentId = getAgentId(req);
|
|
445
|
-
const info = identity.
|
|
556
|
+
const info = identity.loadAgent(agentId);
|
|
446
557
|
if (!info) return res.status(404).json({ error: 'Agent not found' });
|
|
447
|
-
|
|
448
|
-
type: 'aicq-friend',
|
|
558
|
+
res.json({
|
|
449
559
|
agent_id: info.agent_id,
|
|
450
|
-
|
|
560
|
+
nickname: info.nickname,
|
|
561
|
+
signing_public_key: info.signing_public_key,
|
|
451
562
|
exchange_public_key: info.exchange_public_key,
|
|
563
|
+
signing_secret_key: info.signing_secret_key,
|
|
564
|
+
exchange_secret_key: info.exchange_secret_key,
|
|
452
565
|
fingerprint: info.fingerprint,
|
|
453
566
|
});
|
|
454
|
-
|
|
455
|
-
res.json({ qr: qrImage, data: qrData, info });
|
|
456
|
-
} catch (e) {
|
|
457
|
-
res.status(500).json({ error: e.message });
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
app.post('/api/identity/rotate-keys', (req, res) => {
|
|
462
|
-
try {
|
|
463
|
-
const agentId = req.body.agent_id || currentAgentId;
|
|
464
|
-
const newInfo = identity.rotateKeys(agentId);
|
|
465
|
-
res.json({ success: true, info: identity.getInfo(agentId) });
|
|
466
|
-
} catch (e) {
|
|
467
|
-
res.status(500).json({ error: e.message });
|
|
468
|
-
}
|
|
469
|
-
});
|
|
567
|
+
});
|
|
470
568
|
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
|
|
569
|
+
// Sync endpoint
|
|
570
|
+
app.post('/api/sync', async (req, res) => {
|
|
571
|
+
try {
|
|
572
|
+
const agentId = req.body.agent_id || currentAgentId;
|
|
573
|
+
await syncFriendsFromServer(agentId);
|
|
574
|
+
await syncGroupsFromServer(agentId);
|
|
575
|
+
res.json({ success: true });
|
|
576
|
+
} catch (e) {
|
|
577
|
+
res.status(500).json({ error: e.message });
|
|
480
578
|
}
|
|
481
|
-
}
|
|
482
|
-
});
|
|
579
|
+
});
|
|
483
580
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
|
|
487
|
-
const agentId = req.body.agent_id || currentAgentId;
|
|
488
|
-
|
|
489
|
-
// Save avatar locally
|
|
490
|
-
const avatarsDir = path.join(DATA_DIR, 'avatars');
|
|
491
|
-
fs.mkdirSync(avatarsDir, { recursive: true });
|
|
492
|
-
const ext = req.file.mimetype.split('/')[1] || 'png';
|
|
493
|
-
const avatarId = Date.now() + '-' + Math.random().toString(36).slice(2, 8);
|
|
494
|
-
const filename = `${avatarId}.${ext}`;
|
|
495
|
-
const filePath = path.join(avatarsDir, filename);
|
|
496
|
-
fs.writeFileSync(filePath, req.file.buffer);
|
|
497
|
-
|
|
498
|
-
// Update local identity
|
|
499
|
-
const avatarUrl = `/api/identity/avatars/${filename}`;
|
|
500
|
-
identity.updateAvatar(agentId, avatarUrl);
|
|
501
|
-
|
|
502
|
-
// Try to upload to server too
|
|
581
|
+
// ─── Gateway Proxy Endpoint (for extension.js) ──────────────────────
|
|
582
|
+
app.post('/api/gateway', async (req, res) => {
|
|
503
583
|
try {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
filename: req.file.originalname || 'avatar.' + ext,
|
|
509
|
-
contentType: req.file.mimetype,
|
|
510
|
-
});
|
|
511
|
-
// Use node-fetch to upload to server
|
|
512
|
-
const fetch = (await import('node-fetch')).default;
|
|
513
|
-
const serverUrl = SERVER_URL + '/api/v1/accounts/avatar';
|
|
514
|
-
const serverResp = await fetch(serverUrl, {
|
|
515
|
-
method: 'POST',
|
|
516
|
-
body: form,
|
|
517
|
-
headers: {
|
|
518
|
-
...form.getHeaders(),
|
|
519
|
-
'Authorization': 'Bearer ' + serverClient.getAccessToken(agentId),
|
|
520
|
-
},
|
|
521
|
-
});
|
|
522
|
-
if (serverResp.ok) {
|
|
523
|
-
const serverData = await serverResp.json();
|
|
524
|
-
if (serverData.avatar) {
|
|
525
|
-
identity.updateAvatar(agentId, serverData.avatar);
|
|
526
|
-
return res.json({ success: true, avatar: serverData.avatar });
|
|
527
|
-
}
|
|
528
|
-
}
|
|
584
|
+
const { method, kwargs } = req.body;
|
|
585
|
+
if (!method) return res.status(400).json({ error: 'method is required' });
|
|
586
|
+
const result = await handleGatewayCall(method, kwargs);
|
|
587
|
+
res.json(result);
|
|
529
588
|
} catch (e) {
|
|
530
|
-
|
|
589
|
+
res.status(500).json({ error: e.message });
|
|
531
590
|
}
|
|
532
|
-
|
|
533
|
-
res.json({ success: true, avatar: avatarUrl });
|
|
534
|
-
} catch (e) {
|
|
535
|
-
res.status(500).json({ error: e.message });
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
app.get('/api/identity/avatars/:filename', (req, res) => {
|
|
540
|
-
const filename = req.params.filename;
|
|
541
|
-
// Security: prevent directory traversal
|
|
542
|
-
if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
|
|
543
|
-
return res.status(400).json({ error: 'Invalid filename' });
|
|
544
|
-
}
|
|
545
|
-
const filePath = path.join(DATA_DIR, 'avatars', filename);
|
|
546
|
-
if (fs.existsSync(filePath)) {
|
|
547
|
-
res.sendFile(filePath);
|
|
548
|
-
} else {
|
|
549
|
-
res.status(404).json({ error: 'Avatar not found' });
|
|
550
|
-
}
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
app.get('/api/identity/keys', (req, res) => {
|
|
554
|
-
const agentId = getAgentId(req);
|
|
555
|
-
const info = identity.loadAgent(agentId);
|
|
556
|
-
if (!info) return res.status(404).json({ error: 'Agent not found' });
|
|
557
|
-
res.json({
|
|
558
|
-
agent_id: info.agent_id,
|
|
559
|
-
nickname: info.nickname,
|
|
560
|
-
signing_public_key: info.signing_public_key,
|
|
561
|
-
exchange_public_key: info.exchange_public_key,
|
|
562
|
-
signing_secret_key: info.signing_secret_key,
|
|
563
|
-
exchange_secret_key: info.exchange_secret_key,
|
|
564
|
-
fingerprint: info.fingerprint,
|
|
565
591
|
});
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
// Sync endpoint
|
|
569
|
-
app.post('/api/sync', async (req, res) => {
|
|
570
|
-
try {
|
|
571
|
-
const agentId = req.body.agent_id || currentAgentId;
|
|
572
|
-
await syncFriendsFromServer(agentId);
|
|
573
|
-
await syncGroupsFromServer(agentId);
|
|
574
|
-
res.json({ success: true });
|
|
575
|
-
} catch (e) {
|
|
576
|
-
res.status(500).json({ error: e.message });
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
// ─── Gateway Proxy Endpoint (for extension.js) ──────────────────────
|
|
581
|
-
app.post('/api/gateway', async (req, res) => {
|
|
582
|
-
try {
|
|
583
|
-
const { method, kwargs } = req.body;
|
|
584
|
-
if (!method) return res.status(400).json({ error: 'method is required' });
|
|
585
|
-
const result = await handleGatewayCall(method, kwargs);
|
|
586
|
-
res.json(result);
|
|
587
|
-
} catch (e) {
|
|
588
|
-
res.status(500).json({ error: e.message });
|
|
589
|
-
}
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
// ─── Start Server ───────────────────────────────────────────────────
|
|
593
|
-
app.listen(PORT, '0.0.0.0', () => {
|
|
594
|
-
console.log(`[AICQ Plugin] Running on http://0.0.0.0:${PORT}`);
|
|
595
|
-
console.log(`[AICQ Plugin] Server: ${SERVER_URL}`);
|
|
596
|
-
console.log(`[AICQ Plugin] Data dir: ${DATA_DIR}`);
|
|
597
|
-
});
|
|
598
592
|
|
|
599
|
-
// ───
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
process.send({ type: 'gateway_response', id: msg.id, error: err.message });
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
});
|
|
593
|
+
// ─── Start Server ───────────────────────────────────────────────────
|
|
594
|
+
app.listen(PORT, '0.0.0.0', () => {
|
|
595
|
+
console.log(`[AICQ Plugin] Running on http://0.0.0.0:${PORT}`);
|
|
596
|
+
console.log(`[AICQ Plugin] Server: ${SERVER_URL}`);
|
|
597
|
+
console.log(`[AICQ Plugin] Data dir: ${DATA_DIR}`);
|
|
598
|
+
});
|
|
609
599
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
return await handshake.addFriendByCode(currentAgentId, kwargs.temp_number);
|
|
618
|
-
case 'aicq.friends.remove':
|
|
619
|
-
db.removeFriend(currentAgentId, kwargs.friend_id);
|
|
620
|
-
return { success: true };
|
|
621
|
-
case 'aicq.friends.requests':
|
|
622
|
-
return { requests: db.getPendingRequests(currentAgentId) };
|
|
623
|
-
case 'aicq.identity.info':
|
|
624
|
-
return identity.getInfo(currentAgentId) || {};
|
|
625
|
-
case 'aicq.agent.create':
|
|
626
|
-
identity.createAgent(kwargs.agent_id, kwargs.nickname);
|
|
627
|
-
return { success: true };
|
|
628
|
-
case 'aicq.chat.send':
|
|
629
|
-
return await chat.sendMessage(currentAgentId, kwargs.targetId, kwargs.content, { isGroup: kwargs.isGroup });
|
|
630
|
-
case 'aicq.chat.history':
|
|
631
|
-
return { messages: db.getChatHistory(currentAgentId, kwargs.targetId, { limit: kwargs.limit || 50 }) };
|
|
632
|
-
case 'aicq.chat.streamChunk': {
|
|
633
|
-
if (!kwargs.friend_id && !kwargs.targetId) return { error: 'friend_id or targetId is required' };
|
|
634
|
-
if (!kwargs.data) return { error: 'data is required' };
|
|
635
|
-
const chunkType = kwargs.chunk_type || kwargs.chunkType || 'text';
|
|
636
|
-
if (!['text', 'reasoning', 'tool_call', 'tool_result'].includes(chunkType)) return { error: `Invalid chunk_type: ${chunkType}` };
|
|
637
|
-
const streamTarget = kwargs.friend_id || kwargs.targetId;
|
|
638
|
-
const sent = serverClient.sendWS({
|
|
639
|
-
type: 'stream_chunk',
|
|
640
|
-
to: streamTarget,
|
|
641
|
-
chunkType: chunkType,
|
|
642
|
-
data: kwargs.data,
|
|
600
|
+
// ─── OpenClaw Gateway Integration ───────────────────────────────────
|
|
601
|
+
process.on('message', (msg) => {
|
|
602
|
+
if (msg.type === 'gateway_call') {
|
|
603
|
+
handleGatewayCall(msg.method, msg.kwargs).then(result => {
|
|
604
|
+
process.send({ type: 'gateway_response', id: msg.id, result });
|
|
605
|
+
}).catch(err => {
|
|
606
|
+
process.send({ type: 'gateway_response', id: msg.id, error: err.message });
|
|
643
607
|
});
|
|
644
|
-
if (!sent) return { error: 'Not connected to server', success: false };
|
|
645
|
-
return { success: true };
|
|
646
608
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
async function handleGatewayCall(method, kwargs = {}) {
|
|
612
|
+
switch (method) {
|
|
613
|
+
case 'aicq.status':
|
|
614
|
+
return { state: serverClient.connected ? 'connected' : 'disconnected', agent_id: currentAgentId, version: '2.6.0' };
|
|
615
|
+
case 'aicq.friends.list':
|
|
616
|
+
return { friends: db.listFriends(currentAgentId) };
|
|
617
|
+
case 'aicq.friends.add':
|
|
618
|
+
return await handshake.addFriendByCode(currentAgentId, kwargs.temp_number);
|
|
619
|
+
case 'aicq.friends.remove':
|
|
620
|
+
db.removeFriend(currentAgentId, kwargs.friend_id);
|
|
621
|
+
return { success: true };
|
|
622
|
+
case 'aicq.friends.requests':
|
|
623
|
+
return { requests: db.getPendingRequests(currentAgentId) };
|
|
624
|
+
case 'aicq.identity.info':
|
|
625
|
+
return identity.getInfo(currentAgentId) || {};
|
|
626
|
+
case 'aicq.agent.create':
|
|
627
|
+
identity.createAgent(kwargs.agent_id, kwargs.nickname);
|
|
628
|
+
return { success: true };
|
|
629
|
+
case 'aicq.chat.send':
|
|
630
|
+
return await chat.sendMessage(currentAgentId, kwargs.targetId, kwargs.content, { isGroup: kwargs.isGroup });
|
|
631
|
+
case 'aicq.chat.history':
|
|
632
|
+
return { messages: db.getChatHistory(currentAgentId, kwargs.targetId, { limit: kwargs.limit || 50 }) };
|
|
633
|
+
case 'aicq.chat.streamChunk': {
|
|
634
|
+
if (!kwargs.friend_id && !kwargs.targetId) return { error: 'friend_id or targetId is required' };
|
|
635
|
+
if (!kwargs.data) return { error: 'data is required' };
|
|
636
|
+
const chunkType = kwargs.chunk_type || kwargs.chunkType || 'text';
|
|
637
|
+
if (!['text', 'reasoning', 'tool_call', 'tool_result'].includes(chunkType)) return { error: `Invalid chunk_type: ${chunkType}` };
|
|
638
|
+
const streamTarget = kwargs.friend_id || kwargs.targetId;
|
|
639
|
+
const sent = serverClient.sendWS({
|
|
640
|
+
type: 'stream_chunk',
|
|
641
|
+
to: streamTarget,
|
|
642
|
+
chunkType: chunkType,
|
|
643
|
+
data: kwargs.data,
|
|
644
|
+
});
|
|
645
|
+
if (!sent) return { error: 'Not connected to server', success: false };
|
|
646
|
+
return { success: true };
|
|
647
|
+
}
|
|
648
|
+
case 'aicq.chat.streamEnd': {
|
|
649
|
+
if (!kwargs.friend_id && !kwargs.targetId) return { error: 'friend_id or targetId is required' };
|
|
650
|
+
const endTarget = kwargs.friend_id || kwargs.targetId;
|
|
651
|
+
const msgId = kwargs.message_id || kwargs.messageId || ('msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6));
|
|
652
|
+
const endSent = serverClient.sendWS({
|
|
653
|
+
type: 'stream_end',
|
|
654
|
+
to: endTarget,
|
|
655
|
+
messageId: msgId,
|
|
656
|
+
});
|
|
657
|
+
if (!endSent) return { error: 'Not connected to server', success: false };
|
|
658
|
+
return { success: true, messageId: msgId };
|
|
659
|
+
}
|
|
660
|
+
default:
|
|
661
|
+
return { error: `Unknown method: ${method}` };
|
|
658
662
|
}
|
|
659
|
-
default:
|
|
660
|
-
return { error: `Unknown method: ${method}` };
|
|
661
663
|
}
|
|
662
|
-
}
|
|
664
|
+
})().catch(err => {
|
|
665
|
+
console.error('[AICQ] Fatal startup error:', err);
|
|
666
|
+
process.exit(1);
|
|
667
|
+
});
|