aicq-chat-plugin 2.5.9 → 2.6.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/README.md +11 -6
- package/SKILL.md +11 -6
- package/cli.js +29 -19
- package/extension.js +1 -1
- package/index.js +578 -569
- package/lib/chat.js +0 -0
- package/lib/crypto.js +0 -0
- package/lib/database.js +205 -85
- package/lib/file-transfer.js +0 -0
- package/lib/handshake.js +0 -0
- package/lib/identity.js +0 -0
- package/lib/server-client.js +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +12 -12
- package/postinstall.js +15 -5
- package/public/favicon.ico +0 -0
- package/public/icon-16.png +0 -0
- package/public/icon-32.png +0 -0
- package/public/index.html +84 -56
- package/public/logo-512.png +0 -0
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,654 +12,660 @@ 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);
|
|
19
|
-
const SERVER_URL = process.env.AICQ_SERVER_URL || '
|
|
18
|
+
const SERVER_URL = process.env.AICQ_SERVER_URL || 'https://aicq.online';
|
|
20
19
|
const DATA_DIR = process.env.AICQ_DATA_DIR || path.join(os.homedir(), '.aicq-plugin');
|
|
21
20
|
const UPLOADS_DIR = path.join(DATA_DIR, 'uploads');
|
|
22
21
|
|
|
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
|
+
// Allowed chunk types — extended to include thinking and clear_text
|
|
368
|
+
const ALLOWED_CHUNK_TYPES = ['text', 'reasoning', 'thinking', 'clear_text', 'tool_call', 'tool_result'];
|
|
369
|
+
if (!ALLOWED_CHUNK_TYPES.includes(type)) {
|
|
370
|
+
return res.status(400).json({ error: `Invalid chunk_type: ${type}. Allowed: ${ALLOWED_CHUNK_TYPES.join(', ')}` });
|
|
371
|
+
}
|
|
372
|
+
const sent = serverClient.sendWS({
|
|
373
|
+
type: 'stream_chunk',
|
|
374
|
+
to: streamTarget,
|
|
375
|
+
chunkType: type,
|
|
376
|
+
data: data,
|
|
377
|
+
});
|
|
378
|
+
if (!sent) return res.status(503).json({ error: 'Not connected to server', success: false });
|
|
379
|
+
res.json({ success: true });
|
|
380
|
+
} catch (e) {
|
|
381
|
+
res.status(500).json({ error: e.message });
|
|
382
|
+
}
|
|
383
|
+
});
|
|
346
384
|
|
|
347
|
-
app.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
});
|
|
385
|
+
app.post('/api/chat/stream-end', (req, res) => {
|
|
386
|
+
try {
|
|
387
|
+
const { targetId, friend_id, message_id, messageId } = req.body;
|
|
388
|
+
const streamTarget = targetId || friend_id;
|
|
389
|
+
if (!streamTarget) return res.status(400).json({ error: 'targetId or friend_id is required' });
|
|
390
|
+
const msgId = message_id || messageId || ('msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6));
|
|
391
|
+
const sent = serverClient.sendWS({
|
|
392
|
+
type: 'stream_end',
|
|
393
|
+
to: streamTarget,
|
|
394
|
+
messageId: msgId,
|
|
395
|
+
});
|
|
396
|
+
if (!sent) return res.status(503).json({ error: 'Not connected to server', success: false });
|
|
397
|
+
res.json({ success: true, messageId: msgId });
|
|
398
|
+
} catch (e) {
|
|
399
|
+
res.status(500).json({ error: e.message });
|
|
400
|
+
}
|
|
401
|
+
});
|
|
352
402
|
|
|
353
|
-
//
|
|
354
|
-
app.post('/api/
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
403
|
+
// File upload
|
|
404
|
+
app.post('/api/upload', upload.single('file'), async (req, res) => {
|
|
405
|
+
try {
|
|
406
|
+
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
|
|
407
|
+
const agentId = getAgentId(req);
|
|
408
|
+
const targetId = req.body.targetId;
|
|
409
|
+
const isGroup = req.body.isGroup === 'true' || req.body.isGroup === '1';
|
|
410
|
+
const msg = await chat.handleFileUpload(agentId, targetId, req.file, isGroup);
|
|
411
|
+
res.json({ success: true, message: msg });
|
|
412
|
+
} catch (e) {
|
|
413
|
+
res.status(500).json({ error: e.message });
|
|
363
414
|
}
|
|
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
|
-
});
|
|
415
|
+
});
|
|
376
416
|
|
|
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
|
-
});
|
|
417
|
+
app.get('/api/files/:fileId', (req, res) => {
|
|
418
|
+
const filePath = path.join(UPLOADS_DIR, req.params.fileId);
|
|
419
|
+
if (fs.existsSync(filePath)) {
|
|
420
|
+
res.sendFile(filePath);
|
|
421
|
+
} else {
|
|
422
|
+
res.status(404).json({ error: 'File not found' });
|
|
423
|
+
}
|
|
424
|
+
});
|
|
394
425
|
|
|
395
|
-
//
|
|
396
|
-
app.
|
|
397
|
-
try {
|
|
398
|
-
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
|
|
426
|
+
// Identity
|
|
427
|
+
app.get('/api/identity', (req, res) => {
|
|
399
428
|
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
|
-
});
|
|
429
|
+
res.json(identity.getInfo(agentId) || {});
|
|
430
|
+
});
|
|
408
431
|
|
|
409
|
-
app.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
});
|
|
432
|
+
app.post('/api/identity/nickname', (req, res) => {
|
|
433
|
+
const { agent_id, nickname } = req.body;
|
|
434
|
+
const agentId = agent_id || currentAgentId;
|
|
435
|
+
identity.updateNickname(agentId, nickname);
|
|
436
|
+
res.json({ success: true });
|
|
437
|
+
});
|
|
417
438
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
439
|
+
app.post('/api/identity/friend-code', async (req, res) => {
|
|
440
|
+
try {
|
|
441
|
+
const agentId = req.body.agent_id || currentAgentId;
|
|
442
|
+
await serverClient.ensureAuth(agentId);
|
|
443
|
+
const result = await handshake.generateFriendCode(agentId);
|
|
444
|
+
res.json({ success: true, code: result.number, expires_at: result.expiresAt || result.expires_at });
|
|
445
|
+
} catch (e) {
|
|
446
|
+
res.status(500).json({ error: e.message });
|
|
447
|
+
}
|
|
448
|
+
});
|
|
423
449
|
|
|
424
|
-
app.
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
450
|
+
app.get('/api/identity/qr', async (req, res) => {
|
|
451
|
+
try {
|
|
452
|
+
const agentId = getAgentId(req);
|
|
453
|
+
const info = identity.getInfo(agentId);
|
|
454
|
+
if (!info) return res.status(404).json({ error: 'Agent not found' });
|
|
455
|
+
const qrData = JSON.stringify({
|
|
456
|
+
type: 'aicq-friend',
|
|
457
|
+
agent_id: info.agent_id,
|
|
458
|
+
public_key: info.signing_public_key,
|
|
459
|
+
exchange_public_key: info.exchange_public_key,
|
|
460
|
+
fingerprint: info.fingerprint,
|
|
461
|
+
});
|
|
462
|
+
const qrImage = await QRCode.toDataURL(qrData);
|
|
463
|
+
res.json({ qr: qrImage, data: qrData, info });
|
|
464
|
+
} catch (e) {
|
|
465
|
+
res.status(500).json({ error: e.message });
|
|
466
|
+
}
|
|
467
|
+
});
|
|
430
468
|
|
|
431
|
-
app.post('/api/identity/
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
|
|
469
|
+
app.post('/api/identity/rotate-keys', (req, res) => {
|
|
470
|
+
try {
|
|
471
|
+
const agentId = req.body.agent_id || currentAgentId;
|
|
472
|
+
const newInfo = identity.rotateKeys(agentId);
|
|
473
|
+
res.json({ success: true, info: identity.getInfo(agentId) });
|
|
474
|
+
} catch (e) {
|
|
475
|
+
res.status(500).json({ error: e.message });
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Avatar upload
|
|
480
|
+
const avatarUpload = multer({
|
|
481
|
+
storage: multer.memoryStorage(),
|
|
482
|
+
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB max (client should resize before uploading)
|
|
483
|
+
fileFilter: (req, file, cb) => {
|
|
484
|
+
if (file.mimetype && file.mimetype.startsWith('image/')) {
|
|
485
|
+
cb(null, true);
|
|
486
|
+
} else {
|
|
487
|
+
cb(new Error('Only image files are allowed'));
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
});
|
|
441
491
|
|
|
442
|
-
app.
|
|
443
|
-
|
|
492
|
+
app.post('/api/identity/avatar', avatarUpload.single('avatar'), async (req, res) => {
|
|
493
|
+
try {
|
|
494
|
+
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
|
|
495
|
+
const agentId = req.body.agent_id || currentAgentId;
|
|
496
|
+
|
|
497
|
+
const avatarsDir = path.join(DATA_DIR, 'avatars');
|
|
498
|
+
fs.mkdirSync(avatarsDir, { recursive: true });
|
|
499
|
+
const ext = req.file.mimetype.split('/')[1] || 'png';
|
|
500
|
+
const avatarId = Date.now() + '-' + Math.random().toString(36).slice(2, 8);
|
|
501
|
+
const filename = `${avatarId}.${ext}`;
|
|
502
|
+
const filePath = path.join(avatarsDir, filename);
|
|
503
|
+
fs.writeFileSync(filePath, req.file.buffer);
|
|
504
|
+
|
|
505
|
+
const avatarUrl = `/api/identity/avatars/${filename}`;
|
|
506
|
+
identity.updateAvatar(agentId, avatarUrl);
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
await serverClient.ensureAuth(agentId);
|
|
510
|
+
const FormData = (await import('form-data')).default;
|
|
511
|
+
const form = new FormData();
|
|
512
|
+
form.append('avatar', req.file.buffer, {
|
|
513
|
+
filename: req.file.originalname || 'avatar.' + ext,
|
|
514
|
+
contentType: req.file.mimetype,
|
|
515
|
+
});
|
|
516
|
+
const fetch = (await import('node-fetch')).default;
|
|
517
|
+
const serverUrl = SERVER_URL + '/api/v1/accounts/avatar';
|
|
518
|
+
const serverResp = await fetch(serverUrl, {
|
|
519
|
+
method: 'POST',
|
|
520
|
+
body: form,
|
|
521
|
+
headers: {
|
|
522
|
+
...form.getHeaders(),
|
|
523
|
+
'Authorization': 'Bearer ' + serverClient.getAccessToken(agentId),
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
if (serverResp.ok) {
|
|
527
|
+
const serverData = await serverResp.json();
|
|
528
|
+
if (serverData.avatar) {
|
|
529
|
+
identity.updateAvatar(agentId, serverData.avatar);
|
|
530
|
+
return res.json({ success: true, avatar: serverData.avatar });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
} catch (e) {
|
|
534
|
+
console.error('[AICQ] Server avatar upload failed:', e.message);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
res.json({ success: true, avatar: avatarUrl });
|
|
538
|
+
} catch (e) {
|
|
539
|
+
res.status(500).json({ error: e.message });
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
app.get('/api/identity/avatars/:filename', (req, res) => {
|
|
544
|
+
const filename = req.params.filename;
|
|
545
|
+
if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
|
|
546
|
+
return res.status(400).json({ error: 'Invalid filename' });
|
|
547
|
+
}
|
|
548
|
+
const filePath = path.join(DATA_DIR, 'avatars', filename);
|
|
549
|
+
if (fs.existsSync(filePath)) {
|
|
550
|
+
res.sendFile(filePath);
|
|
551
|
+
} else {
|
|
552
|
+
res.status(404).json({ error: 'Avatar not found' });
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
app.get('/api/identity/keys', (req, res) => {
|
|
444
557
|
const agentId = getAgentId(req);
|
|
445
|
-
const info = identity.
|
|
558
|
+
const info = identity.loadAgent(agentId);
|
|
446
559
|
if (!info) return res.status(404).json({ error: 'Agent not found' });
|
|
447
|
-
|
|
448
|
-
type: 'aicq-friend',
|
|
560
|
+
res.json({
|
|
449
561
|
agent_id: info.agent_id,
|
|
450
|
-
|
|
562
|
+
nickname: info.nickname,
|
|
563
|
+
signing_public_key: info.signing_public_key,
|
|
451
564
|
exchange_public_key: info.exchange_public_key,
|
|
565
|
+
signing_secret_key: info.signing_secret_key,
|
|
566
|
+
exchange_secret_key: info.exchange_secret_key,
|
|
452
567
|
fingerprint: info.fingerprint,
|
|
453
568
|
});
|
|
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
|
-
});
|
|
569
|
+
});
|
|
470
570
|
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
|
|
571
|
+
// Sync endpoint
|
|
572
|
+
app.post('/api/sync', async (req, res) => {
|
|
573
|
+
try {
|
|
574
|
+
const agentId = req.body.agent_id || currentAgentId;
|
|
575
|
+
await syncFriendsFromServer(agentId);
|
|
576
|
+
await syncGroupsFromServer(agentId);
|
|
577
|
+
res.json({ success: true });
|
|
578
|
+
} catch (e) {
|
|
579
|
+
res.status(500).json({ error: e.message });
|
|
480
580
|
}
|
|
481
|
-
}
|
|
482
|
-
});
|
|
581
|
+
});
|
|
483
582
|
|
|
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
|
|
583
|
+
// ─── Gateway Proxy Endpoint (for extension.js) ──────────────────────
|
|
584
|
+
app.post('/api/gateway', async (req, res) => {
|
|
503
585
|
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
|
-
}
|
|
586
|
+
const { method, kwargs } = req.body;
|
|
587
|
+
if (!method) return res.status(400).json({ error: 'method is required' });
|
|
588
|
+
const result = await handleGatewayCall(method, kwargs);
|
|
589
|
+
res.json(result);
|
|
529
590
|
} catch (e) {
|
|
530
|
-
|
|
591
|
+
res.status(500).json({ error: e.message });
|
|
531
592
|
}
|
|
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
593
|
});
|
|
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
594
|
|
|
599
|
-
// ───
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
process.send({ type: 'gateway_response', id: msg.id, error: err.message });
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
});
|
|
595
|
+
// ─── Start Server ───────────────────────────────────────────────────
|
|
596
|
+
app.listen(PORT, '0.0.0.0', () => {
|
|
597
|
+
console.log(`[AICQ Plugin] Running on http://0.0.0.0:${PORT}`);
|
|
598
|
+
console.log(`[AICQ Plugin] Server: ${SERVER_URL}`);
|
|
599
|
+
console.log(`[AICQ Plugin] Data dir: ${DATA_DIR}`);
|
|
600
|
+
});
|
|
609
601
|
|
|
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,
|
|
602
|
+
// ─── OpenClaw Gateway Integration ───────────────────────────────────
|
|
603
|
+
process.on('message', (msg) => {
|
|
604
|
+
if (msg.type === 'gateway_call') {
|
|
605
|
+
handleGatewayCall(msg.method, msg.kwargs).then(result => {
|
|
606
|
+
process.send({ type: 'gateway_response', id: msg.id, result });
|
|
607
|
+
}).catch(err => {
|
|
608
|
+
process.send({ type: 'gateway_response', id: msg.id, error: err.message });
|
|
643
609
|
});
|
|
644
|
-
if (!sent) return { error: 'Not connected to server', success: false };
|
|
645
|
-
return { success: true };
|
|
646
610
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
async function handleGatewayCall(method, kwargs = {}) {
|
|
614
|
+
switch (method) {
|
|
615
|
+
case 'aicq.status':
|
|
616
|
+
return { state: serverClient.connected ? 'connected' : 'disconnected', agent_id: currentAgentId, version: '2.6.0' };
|
|
617
|
+
case 'aicq.friends.list':
|
|
618
|
+
return { friends: db.listFriends(currentAgentId) };
|
|
619
|
+
case 'aicq.friends.add':
|
|
620
|
+
return await handshake.addFriendByCode(currentAgentId, kwargs.temp_number);
|
|
621
|
+
case 'aicq.friends.remove':
|
|
622
|
+
db.removeFriend(currentAgentId, kwargs.friend_id);
|
|
623
|
+
return { success: true };
|
|
624
|
+
case 'aicq.friends.requests':
|
|
625
|
+
return { requests: db.getPendingRequests(currentAgentId) };
|
|
626
|
+
case 'aicq.identity.info':
|
|
627
|
+
return identity.getInfo(currentAgentId) || {};
|
|
628
|
+
case 'aicq.agent.create':
|
|
629
|
+
identity.createAgent(kwargs.agent_id, kwargs.nickname);
|
|
630
|
+
return { success: true };
|
|
631
|
+
case 'aicq.chat.send':
|
|
632
|
+
return await chat.sendMessage(currentAgentId, kwargs.targetId, kwargs.content, { isGroup: kwargs.isGroup });
|
|
633
|
+
case 'aicq.chat.history':
|
|
634
|
+
return { messages: db.getChatHistory(currentAgentId, kwargs.targetId, { limit: kwargs.limit || 50 }) };
|
|
635
|
+
case 'aicq.chat.streamChunk': {
|
|
636
|
+
if (!kwargs.friend_id && !kwargs.targetId) return { error: 'friend_id or targetId is required' };
|
|
637
|
+
if (!kwargs.data) return { error: 'data is required' };
|
|
638
|
+
const chunkType = kwargs.chunk_type || kwargs.chunkType || 'text';
|
|
639
|
+
// Allowed chunk types — extended to include thinking and clear_text
|
|
640
|
+
const ALLOWED_CHUNK_TYPES = ['text', 'reasoning', 'thinking', 'clear_text', 'tool_call', 'tool_result'];
|
|
641
|
+
if (!ALLOWED_CHUNK_TYPES.includes(chunkType)) return { error: `Invalid chunk_type: ${chunkType}. Allowed: ${ALLOWED_CHUNK_TYPES.join(', ')}` };
|
|
642
|
+
const streamTarget = kwargs.friend_id || kwargs.targetId;
|
|
643
|
+
const sent = serverClient.sendWS({
|
|
644
|
+
type: 'stream_chunk',
|
|
645
|
+
to: streamTarget,
|
|
646
|
+
chunkType: chunkType,
|
|
647
|
+
data: kwargs.data,
|
|
648
|
+
});
|
|
649
|
+
if (!sent) return { error: 'Not connected to server', success: false };
|
|
650
|
+
return { success: true };
|
|
651
|
+
}
|
|
652
|
+
case 'aicq.chat.streamEnd': {
|
|
653
|
+
if (!kwargs.friend_id && !kwargs.targetId) return { error: 'friend_id or targetId is required' };
|
|
654
|
+
const endTarget = kwargs.friend_id || kwargs.targetId;
|
|
655
|
+
const msgId = kwargs.message_id || kwargs.messageId || ('msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6));
|
|
656
|
+
const endSent = serverClient.sendWS({
|
|
657
|
+
type: 'stream_end',
|
|
658
|
+
to: endTarget,
|
|
659
|
+
messageId: msgId,
|
|
660
|
+
});
|
|
661
|
+
if (!endSent) return { error: 'Not connected to server', success: false };
|
|
662
|
+
return { success: true, messageId: msgId };
|
|
663
|
+
}
|
|
664
|
+
default:
|
|
665
|
+
return { error: `Unknown method: ${method}` };
|
|
658
666
|
}
|
|
659
|
-
default:
|
|
660
|
-
return { error: `Unknown method: ${method}` };
|
|
661
667
|
}
|
|
662
|
-
}
|
|
668
|
+
})().catch(err => {
|
|
669
|
+
console.error('[AICQ] Fatal startup error:', err);
|
|
670
|
+
process.exit(1);
|
|
671
|
+
});
|