aicq-chat-plugin 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -0
- package/index.js +82 -0
- package/lib/chat.js +0 -0
- package/lib/crypto.js +14 -2
- package/lib/database.js +16 -0
- package/lib/file-transfer.js +0 -0
- package/lib/handshake.js +0 -0
- package/lib/identity.js +11 -0
- package/lib/server-client.js +20 -5
- package/openclaw.plugin.json +0 -0
- package/package.json +1 -1
- package/postinstall.js +0 -0
- package/public/index.html +520 -18
package/README.md
CHANGED
|
File without changes
|
package/index.js
CHANGED
|
@@ -426,6 +426,88 @@ app.post('/api/identity/rotate-keys', (req, res) => {
|
|
|
426
426
|
}
|
|
427
427
|
});
|
|
428
428
|
|
|
429
|
+
// Avatar upload
|
|
430
|
+
const avatarUpload = multer({
|
|
431
|
+
storage: multer.memoryStorage(),
|
|
432
|
+
limits: { fileSize: 2 * 1024 * 1024 }, // 2MB max
|
|
433
|
+
fileFilter: (req, file, cb) => {
|
|
434
|
+
if (file.mimetype && file.mimetype.startsWith('image/')) {
|
|
435
|
+
cb(null, true);
|
|
436
|
+
} else {
|
|
437
|
+
cb(new Error('Only image files are allowed'));
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
app.post('/api/identity/avatar', avatarUpload.single('avatar'), async (req, res) => {
|
|
443
|
+
try {
|
|
444
|
+
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
|
|
445
|
+
const agentId = req.body.agent_id || currentAgentId;
|
|
446
|
+
|
|
447
|
+
// Save avatar locally
|
|
448
|
+
const avatarsDir = path.join(DATA_DIR, 'avatars');
|
|
449
|
+
fs.mkdirSync(avatarsDir, { recursive: true });
|
|
450
|
+
const ext = req.file.mimetype.split('/')[1] || 'png';
|
|
451
|
+
const avatarId = Date.now() + '-' + Math.random().toString(36).slice(2, 8);
|
|
452
|
+
const filename = `${avatarId}.${ext}`;
|
|
453
|
+
const filePath = path.join(avatarsDir, filename);
|
|
454
|
+
fs.writeFileSync(filePath, req.file.buffer);
|
|
455
|
+
|
|
456
|
+
// Update local identity
|
|
457
|
+
const avatarUrl = `/api/identity/avatars/${filename}`;
|
|
458
|
+
identity.updateAvatar(agentId, avatarUrl);
|
|
459
|
+
|
|
460
|
+
// Try to upload to server too
|
|
461
|
+
try {
|
|
462
|
+
await serverClient.ensureAuth(agentId);
|
|
463
|
+
const FormData = (await import('form-data')).default;
|
|
464
|
+
const form = new FormData();
|
|
465
|
+
form.append('avatar', req.file.buffer, {
|
|
466
|
+
filename: req.file.originalname || 'avatar.' + ext,
|
|
467
|
+
contentType: req.file.mimetype,
|
|
468
|
+
});
|
|
469
|
+
// Use node-fetch to upload to server
|
|
470
|
+
const fetch = (await import('node-fetch')).default;
|
|
471
|
+
const serverUrl = SERVER_URL + '/api/v1/accounts/avatar';
|
|
472
|
+
const serverResp = await fetch(serverUrl, {
|
|
473
|
+
method: 'POST',
|
|
474
|
+
body: form,
|
|
475
|
+
headers: {
|
|
476
|
+
...form.getHeaders(),
|
|
477
|
+
'Authorization': 'Bearer ' + serverClient.getAccessToken(agentId),
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
if (serverResp.ok) {
|
|
481
|
+
const serverData = await serverResp.json();
|
|
482
|
+
if (serverData.avatar) {
|
|
483
|
+
identity.updateAvatar(agentId, serverData.avatar);
|
|
484
|
+
return res.json({ success: true, avatar: serverData.avatar });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} catch (e) {
|
|
488
|
+
console.error('[AICQ] Server avatar upload failed:', e.message);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
res.json({ success: true, avatar: avatarUrl });
|
|
492
|
+
} catch (e) {
|
|
493
|
+
res.status(500).json({ error: e.message });
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
app.get('/api/identity/avatars/:filename', (req, res) => {
|
|
498
|
+
const filename = req.params.filename;
|
|
499
|
+
// Security: prevent directory traversal
|
|
500
|
+
if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
|
|
501
|
+
return res.status(400).json({ error: 'Invalid filename' });
|
|
502
|
+
}
|
|
503
|
+
const filePath = path.join(DATA_DIR, 'avatars', filename);
|
|
504
|
+
if (fs.existsSync(filePath)) {
|
|
505
|
+
res.sendFile(filePath);
|
|
506
|
+
} else {
|
|
507
|
+
res.status(404).json({ error: 'Avatar not found' });
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
429
511
|
app.get('/api/identity/keys', (req, res) => {
|
|
430
512
|
const agentId = getAgentId(req);
|
|
431
513
|
const info = identity.loadAgent(agentId);
|
package/lib/chat.js
CHANGED
|
File without changes
|
package/lib/crypto.js
CHANGED
|
@@ -31,7 +31,13 @@ function generateExchangeKeypair() {
|
|
|
31
31
|
|
|
32
32
|
function signMessage(message, secretKeyHex) {
|
|
33
33
|
const secretKey = Buffer.from(secretKeyHex, 'hex');
|
|
34
|
-
|
|
34
|
+
// If message looks like hex (64 chars), treat as raw bytes to match server's bytes.fromhex()
|
|
35
|
+
let messageBytes;
|
|
36
|
+
if (/^[0-9a-fA-F]{64}$/.test(message)) {
|
|
37
|
+
messageBytes = Buffer.from(message, 'hex');
|
|
38
|
+
} else {
|
|
39
|
+
messageBytes = naclUtil.decodeUTF8(message);
|
|
40
|
+
}
|
|
35
41
|
const signature = nacl.sign.detached(messageBytes, secretKey);
|
|
36
42
|
return Buffer.from(signature).toString('hex');
|
|
37
43
|
}
|
|
@@ -39,7 +45,13 @@ function signMessage(message, secretKeyHex) {
|
|
|
39
45
|
function verifySignature(message, signatureHex, publicKeyHex) {
|
|
40
46
|
try {
|
|
41
47
|
const publicKey = Buffer.from(publicKeyHex, 'hex');
|
|
42
|
-
|
|
48
|
+
// If message looks like hex (64 chars), treat as raw bytes to match server
|
|
49
|
+
let messageBytes;
|
|
50
|
+
if (/^[0-9a-fA-F]{64}$/.test(message)) {
|
|
51
|
+
messageBytes = Buffer.from(message, 'hex');
|
|
52
|
+
} else {
|
|
53
|
+
messageBytes = naclUtil.decodeUTF8(message);
|
|
54
|
+
}
|
|
43
55
|
const signature = Buffer.from(signatureHex, 'hex');
|
|
44
56
|
return nacl.sign.detached.verify(messageBytes, signature, publicKey);
|
|
45
57
|
} catch (e) {
|
package/lib/database.js
CHANGED
|
@@ -24,6 +24,7 @@ class PluginDatabase {
|
|
|
24
24
|
CREATE TABLE IF NOT EXISTS identity (
|
|
25
25
|
agent_id TEXT PRIMARY KEY,
|
|
26
26
|
nickname TEXT NOT NULL DEFAULT '',
|
|
27
|
+
avatar TEXT NOT NULL DEFAULT '',
|
|
27
28
|
signing_public_key TEXT NOT NULL,
|
|
28
29
|
signing_secret_key TEXT NOT NULL,
|
|
29
30
|
exchange_public_key TEXT NOT NULL,
|
|
@@ -150,6 +151,21 @@ class PluginDatabase {
|
|
|
150
151
|
this.db.prepare('UPDATE identity SET nickname = ?, updated_at = ? WHERE agent_id = ?').run(nickname, now, agentId);
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
updateAvatar(agentId, avatarUrl) {
|
|
155
|
+
const now = new Date().toISOString();
|
|
156
|
+
// Ensure column exists (migration for existing databases)
|
|
157
|
+
try {
|
|
158
|
+
this.db.prepare('UPDATE identity SET avatar = ?, updated_at = ? WHERE agent_id = ?').run(avatarUrl, now, agentId);
|
|
159
|
+
} catch (e) {
|
|
160
|
+
if (e.message.includes('no column named avatar')) {
|
|
161
|
+
this.db.exec('ALTER TABLE identity ADD COLUMN avatar TEXT NOT NULL DEFAULT ""');
|
|
162
|
+
this.db.prepare('UPDATE identity SET avatar = ?, updated_at = ? WHERE agent_id = ?').run(avatarUrl, now, agentId);
|
|
163
|
+
} else {
|
|
164
|
+
throw e;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
153
169
|
// ─── Friends ───────────────────────────────────────────────────────
|
|
154
170
|
|
|
155
171
|
addFriend({ agent_id, id, public_key, fingerprint, friend_type = 'ai', ai_name = '', permissions = ['chat'] }) {
|
package/lib/file-transfer.js
CHANGED
|
File without changes
|
package/lib/handshake.js
CHANGED
|
File without changes
|
package/lib/identity.js
CHANGED
|
@@ -135,6 +135,16 @@ class IdentityManager {
|
|
|
135
135
|
return this._cache[agentId];
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Update agent avatar
|
|
140
|
+
*/
|
|
141
|
+
updateAvatar(agentId, avatarUrl) {
|
|
142
|
+
this.db.updateAvatar(agentId, avatarUrl);
|
|
143
|
+
if (this._cache[agentId]) {
|
|
144
|
+
this._cache[agentId].avatar = avatarUrl;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
138
148
|
/**
|
|
139
149
|
* Get identity info (public keys only, no secrets)
|
|
140
150
|
*/
|
|
@@ -144,6 +154,7 @@ class IdentityManager {
|
|
|
144
154
|
return {
|
|
145
155
|
agent_id: identity.agent_id,
|
|
146
156
|
nickname: identity.nickname,
|
|
157
|
+
avatar: identity.avatar || null,
|
|
147
158
|
signing_public_key: identity.signing_public_key,
|
|
148
159
|
exchange_public_key: identity.exchange_public_key,
|
|
149
160
|
fingerprint: identity.fingerprint,
|
package/lib/server-client.js
CHANGED
|
@@ -53,9 +53,13 @@ class ServerClient {
|
|
|
53
53
|
public_key: identity.signing_public_key,
|
|
54
54
|
agent_name: identity.nickname || agentId,
|
|
55
55
|
});
|
|
56
|
-
if (data.accessToken) {
|
|
57
|
-
this.jwtToken = data.accessToken;
|
|
56
|
+
if (data.access_token || data.accessToken) {
|
|
57
|
+
this.jwtToken = data.access_token || data.accessToken;
|
|
58
58
|
this.currentAgentId = agentId;
|
|
59
|
+
// Store server-side account ID for WS auth (nodeId must match JWT sub)
|
|
60
|
+
if (data.account && data.account.id) {
|
|
61
|
+
this.serverAccountId = data.account.id;
|
|
62
|
+
}
|
|
59
63
|
}
|
|
60
64
|
return data;
|
|
61
65
|
}
|
|
@@ -83,9 +87,13 @@ class ServerClient {
|
|
|
83
87
|
challenge,
|
|
84
88
|
});
|
|
85
89
|
|
|
86
|
-
if (loginData.accessToken) {
|
|
87
|
-
this.jwtToken = loginData.accessToken;
|
|
90
|
+
if (loginData.access_token || loginData.accessToken) {
|
|
91
|
+
this.jwtToken = loginData.access_token || loginData.accessToken;
|
|
88
92
|
this.currentAgentId = agentId;
|
|
93
|
+
// Store server-side account ID for WS auth (nodeId must match JWT sub)
|
|
94
|
+
if (loginData.account && loginData.account.id) {
|
|
95
|
+
this.serverAccountId = loginData.account.id;
|
|
96
|
+
}
|
|
89
97
|
}
|
|
90
98
|
return loginData;
|
|
91
99
|
}
|
|
@@ -198,7 +206,7 @@ class ServerClient {
|
|
|
198
206
|
console.log('[WS] Connected, sending auth...');
|
|
199
207
|
this.ws.send(JSON.stringify({
|
|
200
208
|
type: 'online',
|
|
201
|
-
nodeId: this.currentAgentId,
|
|
209
|
+
nodeId: this.serverAccountId || this.currentAgentId,
|
|
202
210
|
token: this.jwtToken,
|
|
203
211
|
}));
|
|
204
212
|
});
|
|
@@ -317,6 +325,13 @@ class ServerClient {
|
|
|
317
325
|
this._running = false;
|
|
318
326
|
this.disconnect();
|
|
319
327
|
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get the current JWT access token for a given agent
|
|
331
|
+
*/
|
|
332
|
+
getAccessToken(agentId) {
|
|
333
|
+
return this.jwtToken || '';
|
|
334
|
+
}
|
|
320
335
|
}
|
|
321
336
|
|
|
322
337
|
module.exports = ServerClient;
|
package/openclaw.plugin.json
CHANGED
|
File without changes
|
package/package.json
CHANGED
package/postinstall.js
CHANGED
|
File without changes
|
package/public/index.html
CHANGED
|
@@ -29,7 +29,8 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
|
|
|
29
29
|
.friend-item,.group-item{display:flex;align-items:center;gap:10px;padding:10px 12px;cursor:pointer;transition:background .15s;border-left:3px solid transparent}
|
|
30
30
|
.friend-item:hover,.group-item:hover{background:var(--bg3)}
|
|
31
31
|
.friend-item.active,.group-item.active{background:rgba(79,70,229,.2);border-left-color:var(--primary)}
|
|
32
|
-
.avatar{width:36px;height:36px;border-radius:50%;background:var(--primary);display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;flex-shrink:0;color:#fff}
|
|
32
|
+
.avatar{width:36px;height:36px;border-radius:50%;background:var(--primary);display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;flex-shrink:0;color:#fff;overflow:hidden}
|
|
33
|
+
.avatar img{width:100%;height:100%;object-fit:cover;border-radius:50%}
|
|
33
34
|
.avatar.online{box-shadow:0 0 0 2px var(--success)}
|
|
34
35
|
.info{flex:1;min-width:0}
|
|
35
36
|
.info .name{font-size:14px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -121,9 +122,28 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
|
|
|
121
122
|
.msg-bubble a{color:var(--info)}
|
|
122
123
|
.msg-bubble table{border-collapse:collapse;margin:6px 0}
|
|
123
124
|
.msg-bubble th,.msg-bubble td{border:1px solid var(--border);padding:4px 8px;font-size:13px}
|
|
125
|
+
/* Toast */
|
|
126
|
+
.toast{position:fixed;top:20px;left:50%;transform:translateX(-50%);background:var(--bg2);border:1px solid var(--border);color:var(--text);padding:12px 24px;border-radius:8px;font-size:14px;z-index:9999;box-shadow:0 4px 20px rgba(0,0,0,.5);max-width:90%;text-align:center;opacity:0;transition:opacity .3s;pointer-events:none}
|
|
127
|
+
.toast.show{opacity:1}
|
|
128
|
+
.toast.warning{border-color:var(--warning);background:rgba(245,158,11,.15);color:#fcd34d}
|
|
129
|
+
/* Backup section */
|
|
130
|
+
.backup-section{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;margin-top:8px}
|
|
131
|
+
.backup-section h4{font-size:14px;margin-bottom:8px;color:var(--text)}
|
|
132
|
+
.backup-section p{font-size:12px;color:var(--text2);line-height:1.6;margin:4px 0}
|
|
133
|
+
.backup-section .warning-box{font-size:12px;padding:8px;margin:8px 0}
|
|
134
|
+
.backup-btns{display:flex;gap:8px;margin-top:10px}
|
|
135
|
+
.backup-btns .btn{flex:1;text-align:center}
|
|
136
|
+
/* Key match row */
|
|
137
|
+
.key-match-row{display:flex;align-items:center;gap:10px;padding:10px 0;border-bottom:1px solid var(--border)}
|
|
138
|
+
.key-match-row:last-child{border-bottom:none}
|
|
139
|
+
.key-match-row label{font-size:13px;color:var(--text2);min-width:120px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
140
|
+
.key-match-row select{flex:1;padding:6px 10px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px}
|
|
124
141
|
</style>
|
|
125
142
|
</head>
|
|
126
143
|
<body>
|
|
144
|
+
<!-- Toast -->
|
|
145
|
+
<div class="toast" id="toast"></div>
|
|
146
|
+
|
|
127
147
|
<div class="app">
|
|
128
148
|
<!-- Right Panel -->
|
|
129
149
|
<div class="right-panel">
|
|
@@ -142,6 +162,9 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
|
|
|
142
162
|
<button class="action-btn" onclick="showModal('settings')">
|
|
143
163
|
<span class="icon">⚙️</span>设置
|
|
144
164
|
</button>
|
|
165
|
+
<button class="action-btn" onclick="confirmLogout()">
|
|
166
|
+
<span class="icon">🚪</span>登出
|
|
167
|
+
</button>
|
|
145
168
|
</div>
|
|
146
169
|
<div class="list-section">
|
|
147
170
|
<h4>好友</h4>
|
|
@@ -225,14 +248,39 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
|
|
|
225
248
|
<div class="modal">
|
|
226
249
|
<h3>设置</h3>
|
|
227
250
|
<div style="display:flex;flex-direction:column;gap:12px">
|
|
251
|
+
<div style="text-align:center;padding:8px 0">
|
|
252
|
+
<div style="position:relative;display:inline-block">
|
|
253
|
+
<div class="avatar" id="settingsAvatar" style="width:64px;height:64px;font-size:24px;cursor:pointer" onclick="document.getElementById('avatarUploadInput').click()">A</div>
|
|
254
|
+
<div style="position:absolute;bottom:-2px;right:-2px;width:20px;height:20px;background:var(--primary);border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-size:10px;cursor:pointer" onclick="document.getElementById('avatarUploadInput').click()">📷</div>
|
|
255
|
+
</div>
|
|
256
|
+
<input type="file" id="avatarUploadInput" accept="image/*" style="display:none" onchange="handlePluginAvatarUpload(this)">
|
|
257
|
+
<div style="font-size:11px;color:var(--text2);margin-top:4px">点击更换头像</div>
|
|
258
|
+
</div>
|
|
228
259
|
<button class="btn btn-secondary" onclick="showQRCode()" style="width:100%;text-align:left">📱 二维码 — 客户端扫描添加好友</button>
|
|
229
260
|
<button class="btn btn-secondary" onclick="showNicknameModal()" style="width:100%;text-align:left">✏️ 修改昵称</button>
|
|
230
261
|
<button class="btn btn-secondary" onclick="generateFriendCode()" style="width:100%;text-align:left">🔢 生成好友码 (24小时有效)</button>
|
|
231
262
|
<button class="btn btn-secondary" onclick="showKeysModal()" style="width:100%;text-align:left">🔑 显示/重新生成密钥</button>
|
|
232
263
|
<button class="btn btn-secondary" onclick="createNewAgent()" style="width:100%;text-align:left">➕ 创建新 Agent</button>
|
|
233
264
|
<button class="btn btn-secondary" onclick="syncData()" style="width:100%;text-align:left">🔄 同步服务器数据</button>
|
|
265
|
+
<div class="backup-section">
|
|
266
|
+
<h4>💾 数据备份</h4>
|
|
267
|
+
<p><strong>导出内容:</strong>聊天消息、好友关系、群聊信息、Agent 身份密钥(含私钥)</p>
|
|
268
|
+
<div class="warning-box">⚠️ 私钥包含在导出文件中!请妥善保管备份文件,切勿分享给他人。拥有私钥的人可以冒充你的身份发送消息。</div>
|
|
269
|
+
<p><strong>数据丢失场景:</strong></p>
|
|
270
|
+
<p>• 清除浏览器缓存数据</p>
|
|
271
|
+
<p>• 删除浏览器数据/历史记录</p>
|
|
272
|
+
<p>• 使用无痕/隐身模式(关闭窗口后数据清除)</p>
|
|
273
|
+
<p>• 更换浏览器或设备</p>
|
|
274
|
+
<p><strong>恢复方法:</strong>点击「导入备份」选择之前导出的 JSON 文件。若备份中的 Agent 私钥与当前 Agent 不匹配,系统会提示你手动对应。</p>
|
|
275
|
+
<div class="backup-btns">
|
|
276
|
+
<button class="btn btn-primary" onclick="exportBackup()">📤 导出备份</button>
|
|
277
|
+
<button class="btn btn-secondary" onclick="triggerImportBackup()">📥 导入备份</button>
|
|
278
|
+
</div>
|
|
279
|
+
<input type="file" id="backupFileInput" accept=".json" style="display:none" onchange="handleImportBackup(this)">
|
|
280
|
+
</div>
|
|
234
281
|
</div>
|
|
235
282
|
<div class="btn-row" style="margin-top:20px">
|
|
283
|
+
<button class="btn btn-danger" onclick="confirmLogout()">🚪 登出</button>
|
|
236
284
|
<button class="btn btn-primary" onclick="hideModal('settings')">关闭</button>
|
|
237
285
|
</div>
|
|
238
286
|
</div>
|
|
@@ -334,6 +382,20 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
|
|
|
334
382
|
</div>
|
|
335
383
|
</div>
|
|
336
384
|
|
|
385
|
+
<!-- Backup Key Match Modal -->
|
|
386
|
+
<div class="modal-overlay" id="modal-backupMatch">
|
|
387
|
+
<div class="modal" style="max-width:560px">
|
|
388
|
+
<h3>🔑 Agent 密钥匹配</h3>
|
|
389
|
+
<div class="warning-box">备份中的 Agent 私钥与当前 Agent 不匹配,请手动选择对应关系。未匹配的聊天记录将不会导入。</div>
|
|
390
|
+
<p style="font-size:13px;color:var(--text2);margin-bottom:12px">左侧为备份中的 Agent,右侧选择当前对应的 Agent:</p>
|
|
391
|
+
<div id="backupMatchList"></div>
|
|
392
|
+
<div class="btn-row">
|
|
393
|
+
<button class="btn btn-secondary" onclick="skipBackupMatch()">跳过不匹配项</button>
|
|
394
|
+
<button class="btn btn-primary" onclick="applyBackupMatch()">确认匹配并导入</button>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
|
|
337
399
|
<script>
|
|
338
400
|
// ─── State ──────────────────────────────────────────────────────────
|
|
339
401
|
let currentAgentId = '';
|
|
@@ -353,19 +415,6 @@ async function api(method, path, body = null) {
|
|
|
353
415
|
return resp.json();
|
|
354
416
|
}
|
|
355
417
|
|
|
356
|
-
// ─── Initialize ─────────────────────────────────────────────────────
|
|
357
|
-
async function init() {
|
|
358
|
-
await loadAgents();
|
|
359
|
-
await loadFriends();
|
|
360
|
-
await loadGroups();
|
|
361
|
-
// Auto-select first agent
|
|
362
|
-
const sel = document.getElementById('agentSelect');
|
|
363
|
-
if (sel.options.length > 1) {
|
|
364
|
-
currentAgentId = sel.options[1].value;
|
|
365
|
-
sel.value = currentAgentId;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
418
|
// ─── Agents ─────────────────────────────────────────────────────────
|
|
370
419
|
async function loadAgents() {
|
|
371
420
|
const data = await api('GET', '/api/agents');
|
|
@@ -382,6 +431,7 @@ async function loadAgents() {
|
|
|
382
431
|
|
|
383
432
|
async function switchAgent(agentId) {
|
|
384
433
|
if (!agentId) return;
|
|
434
|
+
saveChatToLocalStorage();
|
|
385
435
|
currentAgentId = agentId;
|
|
386
436
|
currentTarget = null;
|
|
387
437
|
showEmptyState();
|
|
@@ -426,7 +476,7 @@ async function loadFriends() {
|
|
|
426
476
|
div.className = 'friend-item' + (currentTarget?.id === f.id && currentTarget?.type === 'friend' ? ' active' : '');
|
|
427
477
|
div.onclick = () => selectTarget(f.id, f.ai_name || f.fingerprint?.slice(0,8) || f.id.slice(0,8), 'friend', !!f.is_online);
|
|
428
478
|
div.innerHTML = `
|
|
429
|
-
<div class="avatar ${f.is_online ? 'online' : ''}">${(f.ai_name||f.id).charAt(0).toUpperCase()}</div>
|
|
479
|
+
<div class="avatar ${f.is_online ? 'online' : ''}" ${f.ai_avatar ? 'style="background:none"' : ''}>${f.ai_avatar ? `<img src="${f.ai_avatar}" alt="">` : (f.ai_name||f.id).charAt(0).toUpperCase()}</div>
|
|
430
480
|
<div class="info">
|
|
431
481
|
<div class="name">${f.ai_name || f.fingerprint?.slice(0,16) || f.id.slice(0,12)}</div>
|
|
432
482
|
<div class="status"><span class="${f.is_online ? 'badge-online' : 'badge-offline'}">●</span> ${f.is_online ? '在线' : '离线'}</div>
|
|
@@ -462,7 +512,29 @@ async function selectTarget(id, name, type, isOnline = false, silent = false) {
|
|
|
462
512
|
showChatView();
|
|
463
513
|
|
|
464
514
|
// Update header
|
|
465
|
-
document.getElementById('chatAvatar')
|
|
515
|
+
const chatAvatar = document.getElementById('chatAvatar');
|
|
516
|
+
// Try to get avatar from friend data
|
|
517
|
+
let friendAvatar = null;
|
|
518
|
+
if (type === 'friend') {
|
|
519
|
+
const friends = document.querySelectorAll('.friend-item');
|
|
520
|
+
// We'll use the currentTarget data
|
|
521
|
+
}
|
|
522
|
+
if (type === 'group') {
|
|
523
|
+
chatAvatar.innerHTML = '👥';
|
|
524
|
+
} else {
|
|
525
|
+
const name = currentTarget?.name || id;
|
|
526
|
+
chatAvatar.textContent = name.charAt(0).toUpperCase();
|
|
527
|
+
chatAvatar.style.background = '';
|
|
528
|
+
}
|
|
529
|
+
// If friend has avatar, try to find it in loaded friends
|
|
530
|
+
try {
|
|
531
|
+
const friendData = await api('GET', `/api/friends?agent_id=${currentAgentId}`);
|
|
532
|
+
const friend = (friendData.friends || []).find(f => f.id === id);
|
|
533
|
+
if (friend && friend.ai_avatar) {
|
|
534
|
+
chatAvatar.innerHTML = `<img src="${friend.ai_avatar}" alt="">`;
|
|
535
|
+
chatAvatar.style.background = 'none';
|
|
536
|
+
}
|
|
537
|
+
} catch(e) {}
|
|
466
538
|
document.getElementById('chatName').textContent = name;
|
|
467
539
|
document.getElementById('chatStatus').textContent = type === 'group' ? '群聊' : (isOnline ? '在线' : '离线');
|
|
468
540
|
document.getElementById('silentBtn').textContent = silent ? '🔕' : '🔔';
|
|
@@ -630,6 +702,7 @@ async function sendMessage() {
|
|
|
630
702
|
mentions: extractMentions(content),
|
|
631
703
|
});
|
|
632
704
|
await loadMessages();
|
|
705
|
+
saveChatToLocalStorage();
|
|
633
706
|
} catch (e) {
|
|
634
707
|
alert('发送失败: ' + e.message);
|
|
635
708
|
}
|
|
@@ -769,7 +842,21 @@ async function toggleSilent() {
|
|
|
769
842
|
}
|
|
770
843
|
|
|
771
844
|
// ─── Modal Helpers ──────────────────────────────────────────────────
|
|
772
|
-
function showModal(name) {
|
|
845
|
+
function showModal(name) {
|
|
846
|
+
document.getElementById('modal-' + name).classList.add('show');
|
|
847
|
+
// Update settings avatar when opening settings modal
|
|
848
|
+
if (name === 'settings' && currentAgentId) {
|
|
849
|
+
api('GET', `/api/identity?agent_id=${currentAgentId}`).then(info => {
|
|
850
|
+
const avatarEl = document.getElementById('settingsAvatar');
|
|
851
|
+
if (info.avatar) {
|
|
852
|
+
avatarEl.innerHTML = `<img src="${info.avatar}" alt="头像">`;
|
|
853
|
+
avatarEl.style.background = 'none';
|
|
854
|
+
} else {
|
|
855
|
+
avatarEl.textContent = (info.nickname || 'A').charAt(0).toUpperCase();
|
|
856
|
+
}
|
|
857
|
+
}).catch(() => {});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
773
860
|
function hideModal(name) { document.getElementById('modal-' + name).classList.remove('show'); }
|
|
774
861
|
|
|
775
862
|
// ─── Add Friend ─────────────────────────────────────────────────────
|
|
@@ -906,14 +993,429 @@ function showChatInfo() {
|
|
|
906
993
|
: `好友: ${currentTarget.name}\nID: ${currentTarget.id}\n状态: ${currentTarget.isOnline ? '在线' : '离线'}`);
|
|
907
994
|
}
|
|
908
995
|
|
|
996
|
+
// ─── Avatar Upload ──────────────────────────────────────────────────
|
|
997
|
+
async function handlePluginAvatarUpload(input) {
|
|
998
|
+
const file = input.files && input.files[0];
|
|
999
|
+
if (!file || !currentAgentId) return;
|
|
1000
|
+
if (!file.type.startsWith('image/')) { alert('请选择图片文件'); return; }
|
|
1001
|
+
if (file.size > 2 * 1024 * 1024) { alert('图片不能超过2MB'); return; }
|
|
1002
|
+
try {
|
|
1003
|
+
const formData = new FormData();
|
|
1004
|
+
formData.append('avatar', file);
|
|
1005
|
+
formData.append('agent_id', currentAgentId);
|
|
1006
|
+
const resp = await fetch(API + '/api/identity/avatar', { method: 'POST', body: formData });
|
|
1007
|
+
const data = await resp.json();
|
|
1008
|
+
if (data.success || data.avatar) {
|
|
1009
|
+
// Update the settings avatar display
|
|
1010
|
+
const avatarUrl = data.avatar || (data.account && data.account.avatar);
|
|
1011
|
+
if (avatarUrl) {
|
|
1012
|
+
document.getElementById('settingsAvatar').innerHTML = `<img src="${avatarUrl}" alt="头像">`;
|
|
1013
|
+
}
|
|
1014
|
+
alert('头像已更新');
|
|
1015
|
+
} else {
|
|
1016
|
+
alert('上传失败: ' + (data.error || '未知错误'));
|
|
1017
|
+
}
|
|
1018
|
+
} catch (e) {
|
|
1019
|
+
alert('头像上传失败: ' + e.message);
|
|
1020
|
+
}
|
|
1021
|
+
input.value = '';
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// ─── Toast Notification ─────────────────────────────────────────────
|
|
1025
|
+
let toastTimer = null;
|
|
1026
|
+
function showToast(message, type = '', duration = 5000) {
|
|
1027
|
+
const el = document.getElementById('toast');
|
|
1028
|
+
el.textContent = message;
|
|
1029
|
+
el.className = 'toast show' + (type ? ' ' + type : '');
|
|
1030
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
1031
|
+
toastTimer = setTimeout(() => { el.className = 'toast'; }, duration);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// ─── localStorage Chat Cache ───────────────────────────────────────
|
|
1035
|
+
const CACHE_KEY = 'aicq_plugin_chat_cache';
|
|
1036
|
+
|
|
1037
|
+
function saveChatToLocalStorage() {
|
|
1038
|
+
try {
|
|
1039
|
+
const cache = {
|
|
1040
|
+
currentAgentId,
|
|
1041
|
+
currentTarget,
|
|
1042
|
+
chatMessages,
|
|
1043
|
+
oldestTimestamp,
|
|
1044
|
+
savedAt: Date.now()
|
|
1045
|
+
};
|
|
1046
|
+
localStorage.setItem(CACHE_KEY, JSON.stringify(cache));
|
|
1047
|
+
} catch (e) {
|
|
1048
|
+
// localStorage might be full or unavailable
|
|
1049
|
+
console.warn('Failed to save chat cache:', e);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function loadChatFromLocalStorage() {
|
|
1054
|
+
try {
|
|
1055
|
+
const raw = localStorage.getItem(CACHE_KEY);
|
|
1056
|
+
if (!raw) return null;
|
|
1057
|
+
return JSON.parse(raw);
|
|
1058
|
+
} catch (e) {
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function clearChatCache() {
|
|
1064
|
+
try { localStorage.removeItem(CACHE_KEY); } catch(e) {}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// ─── Logout ─────────────────────────────────────────────────────────
|
|
1068
|
+
function confirmLogout() {
|
|
1069
|
+
if (confirm('确定要登出吗?登出前会自动保存聊天缓存到本地。')) {
|
|
1070
|
+
saveChatToLocalStorage();
|
|
1071
|
+
// Clear state
|
|
1072
|
+
currentAgentId = '';
|
|
1073
|
+
currentTarget = null;
|
|
1074
|
+
chatMessages = [];
|
|
1075
|
+
oldestTimestamp = null;
|
|
1076
|
+
document.getElementById('agentSelect').value = '';
|
|
1077
|
+
showEmptyState();
|
|
1078
|
+
document.getElementById('friendsList').innerHTML = '';
|
|
1079
|
+
document.getElementById('groupsList').innerHTML = '';
|
|
1080
|
+
hideModal('settings');
|
|
1081
|
+
showToast('已登出,聊天缓存已保存到本地');
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// ─── Backup Export ──────────────────────────────────────────────────
|
|
1086
|
+
async function exportBackup() {
|
|
1087
|
+
try {
|
|
1088
|
+
showToast('正在生成备份...');
|
|
1089
|
+
// Get all agents
|
|
1090
|
+
const agentsData = await api('GET', '/api/agents');
|
|
1091
|
+
const agents = agentsData.agents || [];
|
|
1092
|
+
|
|
1093
|
+
// Collect data for each agent
|
|
1094
|
+
const backupAgents = [];
|
|
1095
|
+
for (const agent of agents) {
|
|
1096
|
+
// Get identity/keys for this agent (includes private keys)
|
|
1097
|
+
let keys = {};
|
|
1098
|
+
try {
|
|
1099
|
+
keys = await api('GET', `/api/identity/keys?agent_id=${agent.agent_id}`);
|
|
1100
|
+
} catch(e) {}
|
|
1101
|
+
|
|
1102
|
+
// Get friends
|
|
1103
|
+
let friends = [];
|
|
1104
|
+
try {
|
|
1105
|
+
const fd = await api('GET', `/api/friends?agent_id=${agent.agent_id}`);
|
|
1106
|
+
friends = fd.friends || [];
|
|
1107
|
+
} catch(e) {}
|
|
1108
|
+
|
|
1109
|
+
// Get groups
|
|
1110
|
+
let groups = [];
|
|
1111
|
+
try {
|
|
1112
|
+
const gd = await api('GET', `/api/groups?agent_id=${agent.agent_id}`);
|
|
1113
|
+
groups = gd.groups || [];
|
|
1114
|
+
} catch(e) {}
|
|
1115
|
+
|
|
1116
|
+
// Get chat messages for each friend and group
|
|
1117
|
+
const chatData = {};
|
|
1118
|
+
for (const f of friends) {
|
|
1119
|
+
try {
|
|
1120
|
+
const md = await api('GET', `/api/chat/${f.id}?agent_id=${agent.agent_id}&limit=9999`);
|
|
1121
|
+
chatData['friend_' + f.id] = md.messages || [];
|
|
1122
|
+
} catch(e) {}
|
|
1123
|
+
}
|
|
1124
|
+
for (const g of groups) {
|
|
1125
|
+
try {
|
|
1126
|
+
const md = await api('GET', `/api/chat/${g.id}?agent_id=${agent.agent_id}&limit=9999`);
|
|
1127
|
+
chatData['group_' + g.id] = md.messages || [];
|
|
1128
|
+
} catch(e) {}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
backupAgents.push({
|
|
1132
|
+
agent_id: agent.agent_id,
|
|
1133
|
+
nickname: agent.nickname || agent.agent_id,
|
|
1134
|
+
avatar: agent.avatar || null,
|
|
1135
|
+
signing_public_key: keys.signing_public_key || null,
|
|
1136
|
+
exchange_public_key: keys.exchange_public_key || null,
|
|
1137
|
+
signing_secret_key: keys.signing_secret_key || null,
|
|
1138
|
+
exchange_secret_key: keys.exchange_secret_key || null,
|
|
1139
|
+
fingerprint: keys.fingerprint || null,
|
|
1140
|
+
friends,
|
|
1141
|
+
groups,
|
|
1142
|
+
chatData
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const backup = {
|
|
1147
|
+
version: 'aicq-plugin-backup-v1',
|
|
1148
|
+
exportedAt: new Date().toISOString(),
|
|
1149
|
+
agents: backupAgents
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
// Download as JSON file
|
|
1153
|
+
const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' });
|
|
1154
|
+
const url = URL.createObjectURL(blob);
|
|
1155
|
+
const a = document.createElement('a');
|
|
1156
|
+
a.href = url;
|
|
1157
|
+
a.download = `aicq-backup-${new Date().toISOString().slice(0,10)}.json`;
|
|
1158
|
+
document.body.appendChild(a);
|
|
1159
|
+
a.click();
|
|
1160
|
+
document.body.removeChild(a);
|
|
1161
|
+
URL.revokeObjectURL(url);
|
|
1162
|
+
|
|
1163
|
+
showToast('备份已导出!请妥善保管此文件,内含私钥。', 'warning', 6000);
|
|
1164
|
+
} catch (e) {
|
|
1165
|
+
alert('导出失败: ' + e.message);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// ─── Backup Import ──────────────────────────────────────────────────
|
|
1170
|
+
function triggerImportBackup() {
|
|
1171
|
+
document.getElementById('backupFileInput').click();
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Store pending backup for matching
|
|
1175
|
+
let pendingBackup = null;
|
|
1176
|
+
let pendingMatchMap = {};
|
|
1177
|
+
|
|
1178
|
+
async function handleImportBackup(input) {
|
|
1179
|
+
if (!input.files || !input.files[0]) return;
|
|
1180
|
+
const file = input.files[0];
|
|
1181
|
+
input.value = '';
|
|
1182
|
+
|
|
1183
|
+
try {
|
|
1184
|
+
const text = await file.text();
|
|
1185
|
+
const backup = JSON.parse(text);
|
|
1186
|
+
|
|
1187
|
+
if (!backup.version || !backup.agents || !Array.isArray(backup.agents)) {
|
|
1188
|
+
alert('无效的备份文件格式');
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
await processImport(backup);
|
|
1193
|
+
} catch (e) {
|
|
1194
|
+
alert('读取备份文件失败: ' + e.message);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
async function processImport(backup) {
|
|
1199
|
+
// Get current agents
|
|
1200
|
+
const agentsData = await api('GET', '/api/agents');
|
|
1201
|
+
const currentAgents = agentsData.agents || [];
|
|
1202
|
+
|
|
1203
|
+
// Get current agents' keys for comparison
|
|
1204
|
+
const currentAgentKeys = {};
|
|
1205
|
+
for (const agent of currentAgents) {
|
|
1206
|
+
try {
|
|
1207
|
+
const keys = await api('GET', `/api/identity/keys?agent_id=${agent.agent_id}`);
|
|
1208
|
+
currentAgentKeys[agent.agent_id] = {
|
|
1209
|
+
agent_id: agent.agent_id,
|
|
1210
|
+
nickname: agent.nickname || agent.agent_id,
|
|
1211
|
+
signing_public_key: keys.signing_public_key,
|
|
1212
|
+
fingerprint: keys.fingerprint
|
|
1213
|
+
};
|
|
1214
|
+
} catch(e) {
|
|
1215
|
+
currentAgentKeys[agent.agent_id] = {
|
|
1216
|
+
agent_id: agent.agent_id,
|
|
1217
|
+
nickname: agent.nickname || agent.agent_id
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Check which backup agents don't match current agents
|
|
1223
|
+
const matchedAgents = []; // { backupAgent, currentAgent }
|
|
1224
|
+
const unmatchedBackupAgents = []; // backup agents with no key match
|
|
1225
|
+
|
|
1226
|
+
for (const ba of backup.agents) {
|
|
1227
|
+
// Try to find matching current agent by signing_public_key or fingerprint
|
|
1228
|
+
let matched = null;
|
|
1229
|
+
for (const [cid, ck] of Object.entries(currentAgentKeys)) {
|
|
1230
|
+
if (ba.signing_public_key && ck.signing_public_key && ba.signing_public_key === ck.signing_public_key) {
|
|
1231
|
+
matched = ck;
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
if (ba.fingerprint && ck.fingerprint && ba.fingerprint === ck.fingerprint) {
|
|
1235
|
+
matched = ck;
|
|
1236
|
+
break;
|
|
1237
|
+
}
|
|
1238
|
+
// Also match by agent_id
|
|
1239
|
+
if (ba.agent_id === cid) {
|
|
1240
|
+
matched = ck;
|
|
1241
|
+
break;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (matched) {
|
|
1246
|
+
matchedAgents.push({ backupAgent: ba, currentAgent: matched });
|
|
1247
|
+
} else {
|
|
1248
|
+
unmatchedBackupAgents.push(ba);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// If there are unmatched agents, show matching modal
|
|
1253
|
+
if (unmatchedBackupAgents.length > 0 && Object.keys(currentAgentKeys).length > 0) {
|
|
1254
|
+
pendingBackup = backup;
|
|
1255
|
+
pendingMatchMap = {};
|
|
1256
|
+
|
|
1257
|
+
// Auto-add matched ones
|
|
1258
|
+
for (const m of matchedAgents) {
|
|
1259
|
+
pendingMatchMap[m.backupAgent.agent_id] = m.currentAgent.agent_id;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
showBackupMatchModal(unmatchedBackupAgents, Object.values(currentAgentKeys));
|
|
1263
|
+
} else {
|
|
1264
|
+
// All matched or no current agents, proceed directly
|
|
1265
|
+
await doImportBackup(backup, matchedAgents.reduce((map, m) => { map[m.backupAgent.agent_id] = m.currentAgent.agent_id; return map; }, {}));
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function showBackupMatchModal(unmatchedAgents, currentAgentList) {
|
|
1270
|
+
const container = document.getElementById('backupMatchList');
|
|
1271
|
+
container.innerHTML = '';
|
|
1272
|
+
|
|
1273
|
+
for (const ba of unmatchedAgents) {
|
|
1274
|
+
const row = document.createElement('div');
|
|
1275
|
+
row.className = 'key-match-row';
|
|
1276
|
+
row.innerHTML = `
|
|
1277
|
+
<label title="${ba.agent_id}">${ba.nickname || ba.agent_id}</label>
|
|
1278
|
+
<select id="match-${ba.agent_id}" data-backup-agent="${ba.agent_id}">
|
|
1279
|
+
<option value="">-- 不导入 --</option>
|
|
1280
|
+
${currentAgentList.map(ca => `<option value="${ca.agent_id}">${ca.nickname || ca.agent_id}</option>`).join('')}
|
|
1281
|
+
</select>
|
|
1282
|
+
`;
|
|
1283
|
+
container.appendChild(row);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
showModal('backupMatch');
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
async function applyBackupMatch() {
|
|
1290
|
+
// Collect user selections
|
|
1291
|
+
const selects = document.querySelectorAll('#backupMatchList select');
|
|
1292
|
+
for (const sel of selects) {
|
|
1293
|
+
const backupAgentId = sel.dataset.backupAgent;
|
|
1294
|
+
const chosenAgentId = sel.value;
|
|
1295
|
+
if (chosenAgentId) {
|
|
1296
|
+
pendingMatchMap[backupAgentId] = chosenAgentId;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
hideModal('backupMatch');
|
|
1301
|
+
await doImportBackup(pendingBackup, pendingMatchMap);
|
|
1302
|
+
pendingBackup = null;
|
|
1303
|
+
pendingMatchMap = {};
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
async function skipBackupMatch() {
|
|
1307
|
+
hideModal('backupMatch');
|
|
1308
|
+
// Import only the already-matched agents (without the unmatched ones)
|
|
1309
|
+
await doImportBackup(pendingBackup, pendingMatchMap);
|
|
1310
|
+
pendingBackup = null;
|
|
1311
|
+
pendingMatchMap = {};
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
async function doImportBackup(backup, matchMap) {
|
|
1315
|
+
try {
|
|
1316
|
+
showToast('正在导入备份...', '', 8000);
|
|
1317
|
+
let importedCount = 0;
|
|
1318
|
+
let importedMessages = 0;
|
|
1319
|
+
|
|
1320
|
+
for (const ba of backup.agents) {
|
|
1321
|
+
const targetAgentId = matchMap[ba.agent_id];
|
|
1322
|
+
if (!targetAgentId) continue;
|
|
1323
|
+
|
|
1324
|
+
// Import friends (via API if possible, or just save to cache)
|
|
1325
|
+
// The plugin uses local SQLite, so we store to localStorage cache
|
|
1326
|
+
for (const friend of (ba.friends || [])) {
|
|
1327
|
+
try {
|
|
1328
|
+
// Try to add friend via API
|
|
1329
|
+
await api('POST', '/api/friends/add-by-fingerprint', {
|
|
1330
|
+
agent_id: targetAgentId,
|
|
1331
|
+
fingerprint: friend.fingerprint,
|
|
1332
|
+
ai_name: friend.ai_name,
|
|
1333
|
+
ai_avatar: friend.ai_avatar
|
|
1334
|
+
});
|
|
1335
|
+
} catch(e) {
|
|
1336
|
+
// Friend might already exist, that's fine
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// Import chat messages to localStorage cache
|
|
1341
|
+
if (ba.chatData) {
|
|
1342
|
+
const chatCacheKey = `aicq_chat_${targetAgentId}`;
|
|
1343
|
+
try {
|
|
1344
|
+
const existing = JSON.parse(localStorage.getItem(chatCacheKey) || '{}');
|
|
1345
|
+
for (const [targetKey, messages] of Object.entries(ba.chatData)) {
|
|
1346
|
+
existing[targetKey] = messages;
|
|
1347
|
+
}
|
|
1348
|
+
localStorage.setItem(chatCacheKey, JSON.stringify(existing));
|
|
1349
|
+
importedMessages += Object.keys(ba.chatData).length;
|
|
1350
|
+
} catch(e) {}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
importedCount++;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Refresh UI
|
|
1357
|
+
await loadAgents();
|
|
1358
|
+
await loadFriends();
|
|
1359
|
+
await loadGroups();
|
|
1360
|
+
|
|
1361
|
+
hideModal('settings');
|
|
1362
|
+
showToast(`导入完成!已导入 ${importedCount} 个 Agent 的数据,${importedMessages} 个会话记录。`, '', 6000);
|
|
1363
|
+
} catch (e) {
|
|
1364
|
+
alert('导入失败: ' + e.message);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
909
1368
|
// ─── Init ───────────────────────────────────────────────────────────
|
|
1369
|
+
async function init() {
|
|
1370
|
+
await loadAgents();
|
|
1371
|
+
await loadFriends();
|
|
1372
|
+
await loadGroups();
|
|
1373
|
+
// Auto-select first agent
|
|
1374
|
+
const sel = document.getElementById('agentSelect');
|
|
1375
|
+
if (sel.options.length > 1) {
|
|
1376
|
+
currentAgentId = sel.options[1].value;
|
|
1377
|
+
sel.value = currentAgentId;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// Load chat cache from localStorage
|
|
1381
|
+
const cached = loadChatFromLocalStorage();
|
|
1382
|
+
if (cached && cached.currentAgentId) {
|
|
1383
|
+
// Restore cache if agent still exists
|
|
1384
|
+
const sel2 = document.getElementById('agentSelect');
|
|
1385
|
+
let agentExists = false;
|
|
1386
|
+
for (let i = 0; i < sel2.options.length; i++) {
|
|
1387
|
+
if (sel2.options[i].value === cached.currentAgentId) {
|
|
1388
|
+
agentExists = true;
|
|
1389
|
+
break;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
if (agentExists) {
|
|
1393
|
+
currentAgentId = cached.currentAgentId;
|
|
1394
|
+
sel2.value = currentAgentId;
|
|
1395
|
+
if (cached.currentTarget) {
|
|
1396
|
+
currentTarget = cached.currentTarget;
|
|
1397
|
+
await loadFriends();
|
|
1398
|
+
await loadGroups();
|
|
1399
|
+
// Will load messages from server, which is more reliable
|
|
1400
|
+
if (currentTarget) {
|
|
1401
|
+
selectTarget(currentTarget.id, currentTarget.name, currentTarget.type, currentTarget.isOnline, currentTarget.silent);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Show login reminder toast
|
|
1408
|
+
showToast('提醒:聊天记录仅保存在本地浏览器中,清除缓存数据或删除浏览器内容将丢失', 'warning', 8000);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
910
1411
|
init();
|
|
911
1412
|
|
|
912
|
-
// Periodic refresh
|
|
1413
|
+
// Periodic refresh and auto-save
|
|
913
1414
|
setInterval(async () => {
|
|
914
1415
|
if (currentAgentId) {
|
|
915
1416
|
await loadFriends();
|
|
916
1417
|
await loadGroups();
|
|
1418
|
+
saveChatToLocalStorage();
|
|
917
1419
|
}
|
|
918
1420
|
}, 30000);
|
|
919
1421
|
</script>
|