aicq-chat-plugin 2.2.0 → 2.3.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/index.js +82 -0
- package/lib/database.js +16 -0
- package/lib/identity.js +11 -0
- package/lib/server-client.js +7 -0
- package/package.json +1 -1
- package/public/index.html +77 -4
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/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/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
|
@@ -317,6 +317,13 @@ class ServerClient {
|
|
|
317
317
|
this._running = false;
|
|
318
318
|
this.disconnect();
|
|
319
319
|
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get the current JWT access token for a given agent
|
|
323
|
+
*/
|
|
324
|
+
getAccessToken(agentId) {
|
|
325
|
+
return this.jwtToken || '';
|
|
326
|
+
}
|
|
320
327
|
}
|
|
321
328
|
|
|
322
329
|
module.exports = ServerClient;
|
package/package.json
CHANGED
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}
|
|
@@ -225,6 +226,14 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
|
|
|
225
226
|
<div class="modal">
|
|
226
227
|
<h3>设置</h3>
|
|
227
228
|
<div style="display:flex;flex-direction:column;gap:12px">
|
|
229
|
+
<div style="text-align:center;padding:8px 0">
|
|
230
|
+
<div style="position:relative;display:inline-block">
|
|
231
|
+
<div class="avatar" id="settingsAvatar" style="width:64px;height:64px;font-size:24px;cursor:pointer" onclick="document.getElementById('avatarUploadInput').click()">A</div>
|
|
232
|
+
<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>
|
|
233
|
+
</div>
|
|
234
|
+
<input type="file" id="avatarUploadInput" accept="image/*" style="display:none" onchange="handlePluginAvatarUpload(this)">
|
|
235
|
+
<div style="font-size:11px;color:var(--text2);margin-top:4px">点击更换头像</div>
|
|
236
|
+
</div>
|
|
228
237
|
<button class="btn btn-secondary" onclick="showQRCode()" style="width:100%;text-align:left">📱 二维码 — 客户端扫描添加好友</button>
|
|
229
238
|
<button class="btn btn-secondary" onclick="showNicknameModal()" style="width:100%;text-align:left">✏️ 修改昵称</button>
|
|
230
239
|
<button class="btn btn-secondary" onclick="generateFriendCode()" style="width:100%;text-align:left">🔢 生成好友码 (24小时有效)</button>
|
|
@@ -426,7 +435,7 @@ async function loadFriends() {
|
|
|
426
435
|
div.className = 'friend-item' + (currentTarget?.id === f.id && currentTarget?.type === 'friend' ? ' active' : '');
|
|
427
436
|
div.onclick = () => selectTarget(f.id, f.ai_name || f.fingerprint?.slice(0,8) || f.id.slice(0,8), 'friend', !!f.is_online);
|
|
428
437
|
div.innerHTML = `
|
|
429
|
-
<div class="avatar ${f.is_online ? 'online' : ''}">${(f.ai_name||f.id).charAt(0).toUpperCase()}</div>
|
|
438
|
+
<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
439
|
<div class="info">
|
|
431
440
|
<div class="name">${f.ai_name || f.fingerprint?.slice(0,16) || f.id.slice(0,12)}</div>
|
|
432
441
|
<div class="status"><span class="${f.is_online ? 'badge-online' : 'badge-offline'}">●</span> ${f.is_online ? '在线' : '离线'}</div>
|
|
@@ -462,7 +471,29 @@ async function selectTarget(id, name, type, isOnline = false, silent = false) {
|
|
|
462
471
|
showChatView();
|
|
463
472
|
|
|
464
473
|
// Update header
|
|
465
|
-
document.getElementById('chatAvatar')
|
|
474
|
+
const chatAvatar = document.getElementById('chatAvatar');
|
|
475
|
+
// Try to get avatar from friend data
|
|
476
|
+
let friendAvatar = null;
|
|
477
|
+
if (type === 'friend') {
|
|
478
|
+
const friends = document.querySelectorAll('.friend-item');
|
|
479
|
+
// We'll use the currentTarget data
|
|
480
|
+
}
|
|
481
|
+
if (type === 'group') {
|
|
482
|
+
chatAvatar.innerHTML = '👥';
|
|
483
|
+
} else {
|
|
484
|
+
const name = currentTarget?.name || id;
|
|
485
|
+
chatAvatar.textContent = name.charAt(0).toUpperCase();
|
|
486
|
+
chatAvatar.style.background = '';
|
|
487
|
+
}
|
|
488
|
+
// If friend has avatar, try to find it in loaded friends
|
|
489
|
+
try {
|
|
490
|
+
const friendData = await api('GET', `/api/friends?agent_id=${currentAgentId}`);
|
|
491
|
+
const friend = (friendData.friends || []).find(f => f.id === id);
|
|
492
|
+
if (friend && friend.ai_avatar) {
|
|
493
|
+
chatAvatar.innerHTML = `<img src="${friend.ai_avatar}" alt="">`;
|
|
494
|
+
chatAvatar.style.background = 'none';
|
|
495
|
+
}
|
|
496
|
+
} catch(e) {}
|
|
466
497
|
document.getElementById('chatName').textContent = name;
|
|
467
498
|
document.getElementById('chatStatus').textContent = type === 'group' ? '群聊' : (isOnline ? '在线' : '离线');
|
|
468
499
|
document.getElementById('silentBtn').textContent = silent ? '🔕' : '🔔';
|
|
@@ -769,7 +800,21 @@ async function toggleSilent() {
|
|
|
769
800
|
}
|
|
770
801
|
|
|
771
802
|
// ─── Modal Helpers ──────────────────────────────────────────────────
|
|
772
|
-
function showModal(name) {
|
|
803
|
+
function showModal(name) {
|
|
804
|
+
document.getElementById('modal-' + name).classList.add('show');
|
|
805
|
+
// Update settings avatar when opening settings modal
|
|
806
|
+
if (name === 'settings' && currentAgentId) {
|
|
807
|
+
api('GET', `/api/identity?agent_id=${currentAgentId}`).then(info => {
|
|
808
|
+
const avatarEl = document.getElementById('settingsAvatar');
|
|
809
|
+
if (info.avatar) {
|
|
810
|
+
avatarEl.innerHTML = `<img src="${info.avatar}" alt="头像">`;
|
|
811
|
+
avatarEl.style.background = 'none';
|
|
812
|
+
} else {
|
|
813
|
+
avatarEl.textContent = (info.nickname || 'A').charAt(0).toUpperCase();
|
|
814
|
+
}
|
|
815
|
+
}).catch(() => {});
|
|
816
|
+
}
|
|
817
|
+
}
|
|
773
818
|
function hideModal(name) { document.getElementById('modal-' + name).classList.remove('show'); }
|
|
774
819
|
|
|
775
820
|
// ─── Add Friend ─────────────────────────────────────────────────────
|
|
@@ -906,6 +951,34 @@ function showChatInfo() {
|
|
|
906
951
|
: `好友: ${currentTarget.name}\nID: ${currentTarget.id}\n状态: ${currentTarget.isOnline ? '在线' : '离线'}`);
|
|
907
952
|
}
|
|
908
953
|
|
|
954
|
+
// ─── Avatar Upload ──────────────────────────────────────────────────
|
|
955
|
+
async function handlePluginAvatarUpload(input) {
|
|
956
|
+
const file = input.files && input.files[0];
|
|
957
|
+
if (!file || !currentAgentId) return;
|
|
958
|
+
if (!file.type.startsWith('image/')) { alert('请选择图片文件'); return; }
|
|
959
|
+
if (file.size > 2 * 1024 * 1024) { alert('图片不能超过2MB'); return; }
|
|
960
|
+
try {
|
|
961
|
+
const formData = new FormData();
|
|
962
|
+
formData.append('avatar', file);
|
|
963
|
+
formData.append('agent_id', currentAgentId);
|
|
964
|
+
const resp = await fetch(API + '/api/identity/avatar', { method: 'POST', body: formData });
|
|
965
|
+
const data = await resp.json();
|
|
966
|
+
if (data.success || data.avatar) {
|
|
967
|
+
// Update the settings avatar display
|
|
968
|
+
const avatarUrl = data.avatar || (data.account && data.account.avatar);
|
|
969
|
+
if (avatarUrl) {
|
|
970
|
+
document.getElementById('settingsAvatar').innerHTML = `<img src="${avatarUrl}" alt="头像">`;
|
|
971
|
+
}
|
|
972
|
+
alert('头像已更新');
|
|
973
|
+
} else {
|
|
974
|
+
alert('上传失败: ' + (data.error || '未知错误'));
|
|
975
|
+
}
|
|
976
|
+
} catch (e) {
|
|
977
|
+
alert('头像上传失败: ' + e.message);
|
|
978
|
+
}
|
|
979
|
+
input.value = '';
|
|
980
|
+
}
|
|
981
|
+
|
|
909
982
|
// ─── Init ───────────────────────────────────────────────────────────
|
|
910
983
|
init();
|
|
911
984
|
|