aicq-chat-plugin 3.8.1 → 3.9.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 +80 -80
- package/SKILL.md +78 -78
- package/cli.cjs +356 -356
- package/index.js +417 -375
- package/lib/chat.js +854 -749
- package/lib/crypto.js +168 -168
- package/lib/database.js +455 -455
- package/lib/file-transfer.js +266 -266
- package/lib/handshake.js +147 -147
- package/lib/identity.js +165 -165
- package/lib/package.json +3 -3
- package/lib/server-client.js +380 -337
- package/openclaw.plugin.json +170 -168
- package/package.json +87 -87
- package/postinstall.cjs +27 -27
- package/public/favicon.ico +0 -0
- package/public/icon-16.png +0 -0
- package/public/icon-32.png +0 -0
- package/public/index.html +1468 -1468
- package/public/logo-512.png +0 -0
- package/setup-entry.js +14 -14
- package/src/channel.js +616 -613
- package/src/ui-routes.js +647 -594
package/lib/file-transfer.js
CHANGED
|
@@ -1,266 +1,266 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AICQ File Transfer
|
|
3
|
-
* ====================
|
|
4
|
-
* Handles chunked file upload/download via WebSocket.
|
|
5
|
-
* Files are encrypted with session keys before transmission.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const crypto = require('./crypto');
|
|
11
|
-
const { v4: uuidv4 } = require('uuid');
|
|
12
|
-
const { isoNow } = require('./database');
|
|
13
|
-
|
|
14
|
-
const CHUNK_SIZE = 65536; // 64KB chunks
|
|
15
|
-
const UPLOADS_DIR = path.join(__dirname, '..', 'uploads');
|
|
16
|
-
|
|
17
|
-
class FileTransferManager {
|
|
18
|
-
constructor(db, identity, serverClient) {
|
|
19
|
-
this.db = db;
|
|
20
|
-
this.identity = identity;
|
|
21
|
-
this.serverClient = serverClient;
|
|
22
|
-
|
|
23
|
-
// In-progress transfers: fileId -> { chunks, meta }
|
|
24
|
-
this._incoming = new Map();
|
|
25
|
-
|
|
26
|
-
// Ensure uploads directory exists
|
|
27
|
-
if (!fs.existsSync(UPLOADS_DIR)) {
|
|
28
|
-
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Upload a file to a friend.
|
|
34
|
-
* Reads the file, encrypts chunks, and sends via WebSocket.
|
|
35
|
-
*/
|
|
36
|
-
uploadFile(targetId, filePath, { isGroup = false } = {}) {
|
|
37
|
-
const agentId = this.identity.currentAgentId;
|
|
38
|
-
if (!agentId) throw new Error('No agent selected');
|
|
39
|
-
|
|
40
|
-
const identity = this.identity.getCurrent();
|
|
41
|
-
if (!identity) throw new Error('No identity');
|
|
42
|
-
|
|
43
|
-
const fileId = uuidv4();
|
|
44
|
-
const fileName = path.basename(filePath);
|
|
45
|
-
const fileBuffer = fs.readFileSync(filePath);
|
|
46
|
-
const fileSize = fileBuffer.length;
|
|
47
|
-
const totalChunks = Math.ceil(fileSize / CHUNK_SIZE);
|
|
48
|
-
|
|
49
|
-
// Get session key for encryption
|
|
50
|
-
let sessionKey = null;
|
|
51
|
-
if (!isGroup) {
|
|
52
|
-
const session = this.db.loadSession(agentId, targetId);
|
|
53
|
-
if (session) {
|
|
54
|
-
sessionKey = session.session_key;
|
|
55
|
-
} else {
|
|
56
|
-
const friend = this.db.getFriend(agentId, targetId);
|
|
57
|
-
if (friend && friend.public_key) {
|
|
58
|
-
sessionKey = crypto.sha256Hex(crypto.computeSharedSecret(identity.exchangeSecretKey, friend.public_key));
|
|
59
|
-
this.db.saveSession(agentId, targetId, sessionKey);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Save file locally
|
|
65
|
-
const localPath = path.join(UPLOADS_DIR, fileId);
|
|
66
|
-
fs.writeFileSync(localPath, fileBuffer);
|
|
67
|
-
|
|
68
|
-
// Send file info message first
|
|
69
|
-
const fileInfo = {
|
|
70
|
-
fileId,
|
|
71
|
-
fileName,
|
|
72
|
-
fileSize,
|
|
73
|
-
totalChunks,
|
|
74
|
-
mimeType: this._getMimeType(fileName),
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// Save file-info message to chat history
|
|
78
|
-
this.db.saveMessage({
|
|
79
|
-
agentId,
|
|
80
|
-
targetId,
|
|
81
|
-
fromId: identity.agentId,
|
|
82
|
-
toId: targetId,
|
|
83
|
-
type: 'file',
|
|
84
|
-
content: JSON.stringify(fileInfo),
|
|
85
|
-
status: 'sent',
|
|
86
|
-
isGroup,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// Send file info via WS
|
|
90
|
-
if (!isGroup) {
|
|
91
|
-
this.serverClient.sendWs({
|
|
92
|
-
type: 'message',
|
|
93
|
-
to: targetId,
|
|
94
|
-
data: JSON.stringify({ type: 'file-info', ...fileInfo }),
|
|
95
|
-
});
|
|
96
|
-
} else {
|
|
97
|
-
this.serverClient.sendWs({
|
|
98
|
-
type: 'group_message',
|
|
99
|
-
groupId: targetId,
|
|
100
|
-
content: JSON.stringify({ type: 'file-info', ...fileInfo }),
|
|
101
|
-
msgType: 'file',
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Send chunks
|
|
106
|
-
for (let i = 0; i < totalChunks; i++) {
|
|
107
|
-
const start = i * CHUNK_SIZE;
|
|
108
|
-
const end = Math.min(start + CHUNK_SIZE, fileSize);
|
|
109
|
-
let chunkData = fileBuffer.slice(start, end);
|
|
110
|
-
|
|
111
|
-
const chunk = {
|
|
112
|
-
fileId,
|
|
113
|
-
index: i,
|
|
114
|
-
total: totalChunks,
|
|
115
|
-
data: chunkData.toString('base64'),
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
if (sessionKey && !isGroup) {
|
|
119
|
-
// Encrypt chunk
|
|
120
|
-
const encrypted = crypto.encryptFileChunk(sessionKey, chunkData, i);
|
|
121
|
-
chunk.data = encrypted.ciphertext;
|
|
122
|
-
chunk.nonce = encrypted.nonce;
|
|
123
|
-
chunk.encrypted = true;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (!isGroup) {
|
|
127
|
-
this.serverClient.sendWs({
|
|
128
|
-
type: 'file_chunk',
|
|
129
|
-
to: targetId,
|
|
130
|
-
data: chunk,
|
|
131
|
-
});
|
|
132
|
-
} else {
|
|
133
|
-
this.serverClient.sendWs({
|
|
134
|
-
type: 'group_message',
|
|
135
|
-
groupId: targetId,
|
|
136
|
-
content: JSON.stringify(chunk),
|
|
137
|
-
msgType: 'file_chunk',
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return { fileId, fileName, fileSize, totalChunks };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Handle incoming file chunk.
|
|
147
|
-
*/
|
|
148
|
-
handleFileChunk(data) {
|
|
149
|
-
const chunkData = data.data || data;
|
|
150
|
-
const fileId = chunkData.fileId;
|
|
151
|
-
|
|
152
|
-
if (!fileId) return;
|
|
153
|
-
|
|
154
|
-
// Initialize incoming transfer if needed
|
|
155
|
-
if (!this._incoming.has(fileId)) {
|
|
156
|
-
this._incoming.set(fileId, {
|
|
157
|
-
chunks: new Map(),
|
|
158
|
-
meta: null,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const transfer = this._incoming.get(fileId);
|
|
163
|
-
|
|
164
|
-
// If this is a file-info message
|
|
165
|
-
if (chunkData.type === 'file-info') {
|
|
166
|
-
transfer.meta = chunkData;
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Store the chunk
|
|
171
|
-
transfer.chunks.set(chunkData.index, chunkData);
|
|
172
|
-
|
|
173
|
-
// Check if all chunks received
|
|
174
|
-
if (transfer.meta && transfer.chunks.size >= transfer.meta.totalChunks) {
|
|
175
|
-
this._assembleFile(fileId, transfer);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Assemble received chunks into a complete file.
|
|
181
|
-
*/
|
|
182
|
-
_assembleFile(fileId, transfer) {
|
|
183
|
-
const { meta, chunks } = transfer;
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
const sortedChunks = Array.from(chunks.entries())
|
|
187
|
-
.sort((a, b) => a[0] - b[0]);
|
|
188
|
-
|
|
189
|
-
const buffers = [];
|
|
190
|
-
for (const [index, chunk] of sortedChunks) {
|
|
191
|
-
if (chunk.encrypted) {
|
|
192
|
-
// Decrypt chunk
|
|
193
|
-
const agentId = this.identity.currentAgentId;
|
|
194
|
-
const session = this.db.loadSession(agentId, transfer.fromId);
|
|
195
|
-
if (session) {
|
|
196
|
-
const decrypted = crypto.decryptFileChunk(
|
|
197
|
-
session.session_key, chunk.nonce, chunk.data, index
|
|
198
|
-
);
|
|
199
|
-
buffers.push(decrypted);
|
|
200
|
-
}
|
|
201
|
-
} else {
|
|
202
|
-
buffers.push(Buffer.from(chunk.data, 'base64'));
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const fileBuffer = Buffer.concat(buffers);
|
|
207
|
-
|
|
208
|
-
// Save to uploads directory
|
|
209
|
-
const localPath = path.join(UPLOADS_DIR, fileId);
|
|
210
|
-
fs.writeFileSync(localPath, fileBuffer);
|
|
211
|
-
|
|
212
|
-
// Save message to chat history
|
|
213
|
-
const agentId = this.identity.currentAgentId;
|
|
214
|
-
if (agentId) {
|
|
215
|
-
this.db.saveMessage({
|
|
216
|
-
agentId,
|
|
217
|
-
targetId: meta.fromId || '',
|
|
218
|
-
fromId: meta.fromId || '',
|
|
219
|
-
toId: agentId,
|
|
220
|
-
type: 'file',
|
|
221
|
-
content: JSON.stringify({
|
|
222
|
-
fileId,
|
|
223
|
-
fileName: meta.fileName,
|
|
224
|
-
fileSize: meta.fileSize,
|
|
225
|
-
localPath,
|
|
226
|
-
}),
|
|
227
|
-
status: 'delivered',
|
|
228
|
-
isGroup: false,
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
console.log(`[FileTransfer] Assembled file ${meta.fileName} (${meta.fileSize} bytes)`);
|
|
233
|
-
} catch (e) {
|
|
234
|
-
console.error(`[FileTransfer] Assembly failed for ${fileId}:`, e.message);
|
|
235
|
-
} finally {
|
|
236
|
-
this._incoming.delete(fileId);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Serve an uploaded file.
|
|
242
|
-
*/
|
|
243
|
-
serveFile(fileId) {
|
|
244
|
-
const filePath = path.join(UPLOADS_DIR, fileId);
|
|
245
|
-
if (fs.existsSync(filePath)) {
|
|
246
|
-
return fs.readFileSync(filePath);
|
|
247
|
-
}
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Get MIME type from filename.
|
|
253
|
-
*/
|
|
254
|
-
_getMimeType(fileName) {
|
|
255
|
-
const ext = path.extname(fileName).toLowerCase();
|
|
256
|
-
const mimeTypes = {
|
|
257
|
-
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
258
|
-
'.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
|
|
259
|
-
'.pdf': 'application/pdf', '.txt': 'text/plain', '.json': 'application/json',
|
|
260
|
-
'.zip': 'application/zip', '.mp3': 'audio/mpeg', '.mp4': 'video/mp4',
|
|
261
|
-
};
|
|
262
|
-
return mimeTypes[ext] || 'application/octet-stream';
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
module.exports = FileTransferManager;
|
|
1
|
+
/**
|
|
2
|
+
* AICQ File Transfer
|
|
3
|
+
* ====================
|
|
4
|
+
* Handles chunked file upload/download via WebSocket.
|
|
5
|
+
* Files are encrypted with session keys before transmission.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const crypto = require('./crypto');
|
|
11
|
+
const { v4: uuidv4 } = require('uuid');
|
|
12
|
+
const { isoNow } = require('./database');
|
|
13
|
+
|
|
14
|
+
const CHUNK_SIZE = 65536; // 64KB chunks
|
|
15
|
+
const UPLOADS_DIR = path.join(__dirname, '..', 'uploads');
|
|
16
|
+
|
|
17
|
+
class FileTransferManager {
|
|
18
|
+
constructor(db, identity, serverClient) {
|
|
19
|
+
this.db = db;
|
|
20
|
+
this.identity = identity;
|
|
21
|
+
this.serverClient = serverClient;
|
|
22
|
+
|
|
23
|
+
// In-progress transfers: fileId -> { chunks, meta }
|
|
24
|
+
this._incoming = new Map();
|
|
25
|
+
|
|
26
|
+
// Ensure uploads directory exists
|
|
27
|
+
if (!fs.existsSync(UPLOADS_DIR)) {
|
|
28
|
+
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Upload a file to a friend.
|
|
34
|
+
* Reads the file, encrypts chunks, and sends via WebSocket.
|
|
35
|
+
*/
|
|
36
|
+
uploadFile(targetId, filePath, { isGroup = false } = {}) {
|
|
37
|
+
const agentId = this.identity.currentAgentId;
|
|
38
|
+
if (!agentId) throw new Error('No agent selected');
|
|
39
|
+
|
|
40
|
+
const identity = this.identity.getCurrent();
|
|
41
|
+
if (!identity) throw new Error('No identity');
|
|
42
|
+
|
|
43
|
+
const fileId = uuidv4();
|
|
44
|
+
const fileName = path.basename(filePath);
|
|
45
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
46
|
+
const fileSize = fileBuffer.length;
|
|
47
|
+
const totalChunks = Math.ceil(fileSize / CHUNK_SIZE);
|
|
48
|
+
|
|
49
|
+
// Get session key for encryption
|
|
50
|
+
let sessionKey = null;
|
|
51
|
+
if (!isGroup) {
|
|
52
|
+
const session = this.db.loadSession(agentId, targetId);
|
|
53
|
+
if (session) {
|
|
54
|
+
sessionKey = session.session_key;
|
|
55
|
+
} else {
|
|
56
|
+
const friend = this.db.getFriend(agentId, targetId);
|
|
57
|
+
if (friend && friend.public_key) {
|
|
58
|
+
sessionKey = crypto.sha256Hex(crypto.computeSharedSecret(identity.exchangeSecretKey, friend.public_key));
|
|
59
|
+
this.db.saveSession(agentId, targetId, sessionKey);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Save file locally
|
|
65
|
+
const localPath = path.join(UPLOADS_DIR, fileId);
|
|
66
|
+
fs.writeFileSync(localPath, fileBuffer);
|
|
67
|
+
|
|
68
|
+
// Send file info message first
|
|
69
|
+
const fileInfo = {
|
|
70
|
+
fileId,
|
|
71
|
+
fileName,
|
|
72
|
+
fileSize,
|
|
73
|
+
totalChunks,
|
|
74
|
+
mimeType: this._getMimeType(fileName),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Save file-info message to chat history
|
|
78
|
+
this.db.saveMessage({
|
|
79
|
+
agentId,
|
|
80
|
+
targetId,
|
|
81
|
+
fromId: identity.agentId,
|
|
82
|
+
toId: targetId,
|
|
83
|
+
type: 'file',
|
|
84
|
+
content: JSON.stringify(fileInfo),
|
|
85
|
+
status: 'sent',
|
|
86
|
+
isGroup,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Send file info via WS
|
|
90
|
+
if (!isGroup) {
|
|
91
|
+
this.serverClient.sendWs({
|
|
92
|
+
type: 'message',
|
|
93
|
+
to: targetId,
|
|
94
|
+
data: JSON.stringify({ type: 'file-info', ...fileInfo }),
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
this.serverClient.sendWs({
|
|
98
|
+
type: 'group_message',
|
|
99
|
+
groupId: targetId,
|
|
100
|
+
content: JSON.stringify({ type: 'file-info', ...fileInfo }),
|
|
101
|
+
msgType: 'file',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Send chunks
|
|
106
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
107
|
+
const start = i * CHUNK_SIZE;
|
|
108
|
+
const end = Math.min(start + CHUNK_SIZE, fileSize);
|
|
109
|
+
let chunkData = fileBuffer.slice(start, end);
|
|
110
|
+
|
|
111
|
+
const chunk = {
|
|
112
|
+
fileId,
|
|
113
|
+
index: i,
|
|
114
|
+
total: totalChunks,
|
|
115
|
+
data: chunkData.toString('base64'),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (sessionKey && !isGroup) {
|
|
119
|
+
// Encrypt chunk
|
|
120
|
+
const encrypted = crypto.encryptFileChunk(sessionKey, chunkData, i);
|
|
121
|
+
chunk.data = encrypted.ciphertext;
|
|
122
|
+
chunk.nonce = encrypted.nonce;
|
|
123
|
+
chunk.encrypted = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!isGroup) {
|
|
127
|
+
this.serverClient.sendWs({
|
|
128
|
+
type: 'file_chunk',
|
|
129
|
+
to: targetId,
|
|
130
|
+
data: chunk,
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
133
|
+
this.serverClient.sendWs({
|
|
134
|
+
type: 'group_message',
|
|
135
|
+
groupId: targetId,
|
|
136
|
+
content: JSON.stringify(chunk),
|
|
137
|
+
msgType: 'file_chunk',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { fileId, fileName, fileSize, totalChunks };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Handle incoming file chunk.
|
|
147
|
+
*/
|
|
148
|
+
handleFileChunk(data) {
|
|
149
|
+
const chunkData = data.data || data;
|
|
150
|
+
const fileId = chunkData.fileId;
|
|
151
|
+
|
|
152
|
+
if (!fileId) return;
|
|
153
|
+
|
|
154
|
+
// Initialize incoming transfer if needed
|
|
155
|
+
if (!this._incoming.has(fileId)) {
|
|
156
|
+
this._incoming.set(fileId, {
|
|
157
|
+
chunks: new Map(),
|
|
158
|
+
meta: null,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const transfer = this._incoming.get(fileId);
|
|
163
|
+
|
|
164
|
+
// If this is a file-info message
|
|
165
|
+
if (chunkData.type === 'file-info') {
|
|
166
|
+
transfer.meta = chunkData;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Store the chunk
|
|
171
|
+
transfer.chunks.set(chunkData.index, chunkData);
|
|
172
|
+
|
|
173
|
+
// Check if all chunks received
|
|
174
|
+
if (transfer.meta && transfer.chunks.size >= transfer.meta.totalChunks) {
|
|
175
|
+
this._assembleFile(fileId, transfer);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Assemble received chunks into a complete file.
|
|
181
|
+
*/
|
|
182
|
+
_assembleFile(fileId, transfer) {
|
|
183
|
+
const { meta, chunks } = transfer;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const sortedChunks = Array.from(chunks.entries())
|
|
187
|
+
.sort((a, b) => a[0] - b[0]);
|
|
188
|
+
|
|
189
|
+
const buffers = [];
|
|
190
|
+
for (const [index, chunk] of sortedChunks) {
|
|
191
|
+
if (chunk.encrypted) {
|
|
192
|
+
// Decrypt chunk
|
|
193
|
+
const agentId = this.identity.currentAgentId;
|
|
194
|
+
const session = this.db.loadSession(agentId, transfer.fromId);
|
|
195
|
+
if (session) {
|
|
196
|
+
const decrypted = crypto.decryptFileChunk(
|
|
197
|
+
session.session_key, chunk.nonce, chunk.data, index
|
|
198
|
+
);
|
|
199
|
+
buffers.push(decrypted);
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
buffers.push(Buffer.from(chunk.data, 'base64'));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const fileBuffer = Buffer.concat(buffers);
|
|
207
|
+
|
|
208
|
+
// Save to uploads directory
|
|
209
|
+
const localPath = path.join(UPLOADS_DIR, fileId);
|
|
210
|
+
fs.writeFileSync(localPath, fileBuffer);
|
|
211
|
+
|
|
212
|
+
// Save message to chat history
|
|
213
|
+
const agentId = this.identity.currentAgentId;
|
|
214
|
+
if (agentId) {
|
|
215
|
+
this.db.saveMessage({
|
|
216
|
+
agentId,
|
|
217
|
+
targetId: meta.fromId || '',
|
|
218
|
+
fromId: meta.fromId || '',
|
|
219
|
+
toId: agentId,
|
|
220
|
+
type: 'file',
|
|
221
|
+
content: JSON.stringify({
|
|
222
|
+
fileId,
|
|
223
|
+
fileName: meta.fileName,
|
|
224
|
+
fileSize: meta.fileSize,
|
|
225
|
+
localPath,
|
|
226
|
+
}),
|
|
227
|
+
status: 'delivered',
|
|
228
|
+
isGroup: false,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(`[FileTransfer] Assembled file ${meta.fileName} (${meta.fileSize} bytes)`);
|
|
233
|
+
} catch (e) {
|
|
234
|
+
console.error(`[FileTransfer] Assembly failed for ${fileId}:`, e.message);
|
|
235
|
+
} finally {
|
|
236
|
+
this._incoming.delete(fileId);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Serve an uploaded file.
|
|
242
|
+
*/
|
|
243
|
+
serveFile(fileId) {
|
|
244
|
+
const filePath = path.join(UPLOADS_DIR, fileId);
|
|
245
|
+
if (fs.existsSync(filePath)) {
|
|
246
|
+
return fs.readFileSync(filePath);
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get MIME type from filename.
|
|
253
|
+
*/
|
|
254
|
+
_getMimeType(fileName) {
|
|
255
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
256
|
+
const mimeTypes = {
|
|
257
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
258
|
+
'.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
|
|
259
|
+
'.pdf': 'application/pdf', '.txt': 'text/plain', '.json': 'application/json',
|
|
260
|
+
'.zip': 'application/zip', '.mp3': 'audio/mpeg', '.mp4': 'video/mp4',
|
|
261
|
+
};
|
|
262
|
+
return mimeTypes[ext] || 'application/octet-stream';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
module.exports = FileTransferManager;
|