aicq-chat-plugin 2.1.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 +74 -0
- package/bin/aicq-plugin.js +118 -0
- package/bin/postinstall.js +27 -0
- package/index.js +499 -0
- package/lib/chat.js +244 -0
- package/lib/crypto.js +156 -0
- package/lib/database.js +319 -0
- package/lib/file-transfer.js +266 -0
- package/lib/handshake.js +147 -0
- package/lib/identity.js +154 -0
- package/lib/server-client.js +322 -0
- package/openclaw.plugin.json +45 -0
- package/package.json +58 -0
- package/public/index.html +921 -0
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>AICQ 加密聊天</title>
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
|
9
|
+
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
|
10
|
+
<script src="https://cdn.jsdelivr.net/npm/marked@12.0.0/marked.min.js"></script>
|
|
11
|
+
<style>
|
|
12
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
13
|
+
:root{--primary:#4f46e5;--primary-light:#6366f1;--bg:#0f172a;--bg2:#1e293b;--bg3:#334155;--text:#f1f5f9;--text2:#94a3b8;--border:#475569;--success:#22c55e;--danger:#ef4444;--warning:#f59e0b;--info:#3b82f6;--bubble-me:#4f46e5;--bubble-them:#334155}
|
|
14
|
+
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans SC",sans-serif;background:var(--bg);color:var(--text);display:flex;height:100vh;overflow:hidden}
|
|
15
|
+
/* Layout */
|
|
16
|
+
.app{display:flex;width:100%;height:100%}
|
|
17
|
+
.right-panel{width:280px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}
|
|
18
|
+
.main-panel{flex:1;display:flex;flex-direction:column;min-width:0}
|
|
19
|
+
.chat-area{flex:1;overflow:hidden;display:flex;flex-direction:column}
|
|
20
|
+
/* Right Panel */
|
|
21
|
+
.agent-select{padding:12px;border-bottom:1px solid var(--border)}
|
|
22
|
+
.agent-select select{width:100%;padding:8px 12px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:6px;font-size:14px;cursor:pointer}
|
|
23
|
+
.action-buttons{display:flex;gap:6px;padding:10px 12px;border-bottom:1px solid var(--border)}
|
|
24
|
+
.action-btn{flex:1;padding:7px 4px;background:var(--bg3);color:var(--text2);border:none;border-radius:6px;cursor:pointer;font-size:12px;display:flex;flex-direction:column;align-items:center;gap:3px;transition:all .2s}
|
|
25
|
+
.action-btn:hover{background:var(--primary);color:#fff}
|
|
26
|
+
.action-btn .icon{font-size:18px}
|
|
27
|
+
.list-section{flex:1;overflow-y:auto}
|
|
28
|
+
.list-section h4{padding:10px 12px 6px;color:var(--text2);font-size:12px;text-transform:uppercase;letter-spacing:1px}
|
|
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
|
+
.friend-item:hover,.group-item:hover{background:var(--bg3)}
|
|
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}
|
|
33
|
+
.avatar.online{box-shadow:0 0 0 2px var(--success)}
|
|
34
|
+
.info{flex:1;min-width:0}
|
|
35
|
+
.info .name{font-size:14px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
36
|
+
.info .status{font-size:11px;color:var(--text2)}
|
|
37
|
+
.badge-online{color:var(--success);font-size:10px}
|
|
38
|
+
.badge-offline{color:var(--text2);font-size:10px}
|
|
39
|
+
.silent-badge{font-size:9px;background:var(--warning);color:#000;padding:1px 4px;border-radius:3px;margin-left:4px}
|
|
40
|
+
/* Chat Header */
|
|
41
|
+
.chat-header{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;background:var(--bg2)}
|
|
42
|
+
.chat-header .avatar{width:40px;height:40px;font-size:16px}
|
|
43
|
+
.chat-header .info .name{font-size:16px}
|
|
44
|
+
.chat-header .actions{margin-left:auto;display:flex;gap:8px}
|
|
45
|
+
.chat-header .actions button{background:none;border:none;color:var(--text2);cursor:pointer;font-size:18px;padding:4px 8px;border-radius:4px}
|
|
46
|
+
.chat-header .actions button:hover{background:var(--bg3);color:var(--text)}
|
|
47
|
+
/* Messages */
|
|
48
|
+
.messages-container{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:4px}
|
|
49
|
+
.load-more{text-align:center;padding:8px;color:var(--text2);font-size:12px;cursor:pointer}
|
|
50
|
+
.load-more:hover{color:var(--primary)}
|
|
51
|
+
.msg-row{display:flex;max-width:75%;gap:8px}
|
|
52
|
+
.msg-row.me{margin-left:auto;flex-direction:row-reverse}
|
|
53
|
+
.msg-row.them{margin-right:auto}
|
|
54
|
+
.msg-bubble{padding:10px 14px;border-radius:12px;font-size:14px;line-height:1.6;word-break:break-word;position:relative;max-width:100%}
|
|
55
|
+
.msg-row.me .msg-bubble{background:var(--bubble-me);color:#fff;border-bottom-right-radius:4px}
|
|
56
|
+
.msg-row.them .msg-bubble{background:var(--bubble-them);color:var(--text);border-bottom-left-radius:4px}
|
|
57
|
+
.msg-bubble img{max-width:300px;max-height:300px;border-radius:8px;margin-top:6px;cursor:pointer}
|
|
58
|
+
.msg-bubble .file-link{display:flex;align-items:center;gap:6px;padding:6px 10px;background:rgba(255,255,255,.1);border-radius:6px;margin-top:6px;cursor:pointer;color:inherit;text-decoration:none}
|
|
59
|
+
.msg-bubble .file-link:hover{background:rgba(255,255,255,.2)}
|
|
60
|
+
.msg-time{font-size:10px;color:var(--text2);margin-top:4px;text-align:right}
|
|
61
|
+
.msg-row.them .msg-time{text-align:left}
|
|
62
|
+
.msg-actions{position:absolute;top:-8px;right:-4px;display:none;gap:2px}
|
|
63
|
+
.msg-row.me .msg-actions{right:auto;left:-4px}
|
|
64
|
+
.msg-row:hover .msg-actions{display:flex}
|
|
65
|
+
.msg-action-btn{width:24px;height:24px;border-radius:50%;background:var(--bg3);border:none;color:var(--text2);cursor:pointer;font-size:11px;display:flex;align-items:center;justify-content:center}
|
|
66
|
+
.msg-action-btn:hover{background:var(--primary);color:#fff}
|
|
67
|
+
.msg-action-btn.danger:hover{background:var(--danger)}
|
|
68
|
+
/* Mention */
|
|
69
|
+
.mention{color:var(--info);font-weight:600;cursor:pointer}
|
|
70
|
+
.mention:hover{text-decoration:underline}
|
|
71
|
+
/* Input Area */
|
|
72
|
+
.input-area{padding:12px 16px;border-top:1px solid var(--border);background:var(--bg2);display:flex;flex-direction:column;gap:8px}
|
|
73
|
+
.input-top{display:flex;gap:8px;align-items:center}
|
|
74
|
+
.input-top input{flex:1;padding:10px 14px;background:var(--bg);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:14px;outline:none}
|
|
75
|
+
.input-top input:focus{border-color:var(--primary)}
|
|
76
|
+
.send-btn{padding:10px 20px;background:var(--primary);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:14px;font-weight:500;transition:background .2s}
|
|
77
|
+
.send-btn:hover{background:var(--primary-light)}
|
|
78
|
+
.send-btn:disabled{opacity:.5;cursor:not-allowed}
|
|
79
|
+
.input-toolbar{display:flex;gap:4px}
|
|
80
|
+
.tool-btn{background:none;border:none;color:var(--text2);cursor:pointer;font-size:16px;padding:4px 8px;border-radius:4px}
|
|
81
|
+
.tool-btn:hover{background:var(--bg3);color:var(--text)}
|
|
82
|
+
/* Modals */
|
|
83
|
+
.modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.6);display:none;align-items:center;justify-content:center;z-index:1000}
|
|
84
|
+
.modal-overlay.show{display:flex}
|
|
85
|
+
.modal{background:var(--bg2);border:1px solid var(--border);border-radius:12px;padding:24px;max-width:480px;width:90%;max-height:80vh;overflow-y:auto}
|
|
86
|
+
.modal h3{margin-bottom:16px;font-size:18px}
|
|
87
|
+
.modal .form-group{margin-bottom:14px}
|
|
88
|
+
.modal label{display:block;margin-bottom:6px;font-size:13px;color:var(--text2)}
|
|
89
|
+
.modal input,.modal select,.modal textarea{width:100%;padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px}
|
|
90
|
+
.modal textarea{min-height:80px;resize:vertical}
|
|
91
|
+
.modal .btn-row{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}
|
|
92
|
+
.modal .btn{padding:8px 16px;border:none;border-radius:6px;cursor:pointer;font-size:14px;transition:all .2s}
|
|
93
|
+
.modal .btn-primary{background:var(--primary);color:#fff}
|
|
94
|
+
.modal .btn-primary:hover{background:var(--primary-light)}
|
|
95
|
+
.modal .btn-secondary{background:var(--bg3);color:var(--text)}
|
|
96
|
+
.modal .btn-danger{background:var(--danger);color:#fff}
|
|
97
|
+
.qr-container{text-align:center;margin:16px 0}
|
|
98
|
+
.qr-container img{max-width:256px;border-radius:8px}
|
|
99
|
+
.key-display{background:var(--bg);padding:10px;border-radius:6px;font-family:monospace;font-size:12px;word-break:break-all;margin:8px 0;color:var(--text2)}
|
|
100
|
+
.warning-box{background:rgba(239,68,68,.15);border:1px solid var(--danger);border-radius:6px;padding:12px;margin:12px 0;color:#fca5a5;font-size:13px}
|
|
101
|
+
/* Empty State */
|
|
102
|
+
.empty-state{flex:1;display:flex;align-items:center;justify-content:center;color:var(--text2);font-size:16px;flex-direction:column;gap:12px}
|
|
103
|
+
.empty-state .icon{font-size:48px;opacity:.3}
|
|
104
|
+
/* Mention dropdown */
|
|
105
|
+
.mention-dropdown{position:absolute;bottom:100%;left:0;background:var(--bg2);border:1px solid var(--border);border-radius:6px;max-height:200px;overflow-y:auto;z-index:100;display:none;min-width:200px}
|
|
106
|
+
.mention-dropdown.show{display:block}
|
|
107
|
+
.mention-option{padding:8px 12px;cursor:pointer;font-size:13px;display:flex;align-items:center;gap:8px}
|
|
108
|
+
.mention-option:hover{background:var(--bg3)}
|
|
109
|
+
/* Scrollbar */
|
|
110
|
+
::-webkit-scrollbar{width:6px}
|
|
111
|
+
::-webkit-scrollbar-track{background:var(--bg)}
|
|
112
|
+
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
|
113
|
+
::-webkit-scrollbar-thumb:hover{background:var(--text2)}
|
|
114
|
+
/* Markdown in bubbles */
|
|
115
|
+
.msg-bubble p{margin:4px 0}
|
|
116
|
+
.msg-bubble code{background:rgba(255,255,255,.1);padding:1px 4px;border-radius:3px;font-size:13px}
|
|
117
|
+
.msg-bubble pre{background:rgba(0,0,0,.2);padding:8px;border-radius:6px;overflow-x:auto;margin:6px 0}
|
|
118
|
+
.msg-bubble pre code{background:none;padding:0}
|
|
119
|
+
.msg-bubble blockquote{border-left:3px solid var(--primary);padding-left:10px;margin:6px 0;color:var(--text2)}
|
|
120
|
+
.msg-bubble ul,.msg-bubble ol{padding-left:20px;margin:4px 0}
|
|
121
|
+
.msg-bubble a{color:var(--info)}
|
|
122
|
+
.msg-bubble table{border-collapse:collapse;margin:6px 0}
|
|
123
|
+
.msg-bubble th,.msg-bubble td{border:1px solid var(--border);padding:4px 8px;font-size:13px}
|
|
124
|
+
</style>
|
|
125
|
+
</head>
|
|
126
|
+
<body>
|
|
127
|
+
<div class="app">
|
|
128
|
+
<!-- Right Panel -->
|
|
129
|
+
<div class="right-panel">
|
|
130
|
+
<div class="agent-select">
|
|
131
|
+
<select id="agentSelect" onchange="switchAgent(this.value)">
|
|
132
|
+
<option value="">选择 Agent...</option>
|
|
133
|
+
</select>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="action-buttons">
|
|
136
|
+
<button class="action-btn" onclick="showModal('addFriend')">
|
|
137
|
+
<span class="icon">👤</span>添加好友
|
|
138
|
+
</button>
|
|
139
|
+
<button class="action-btn" onclick="showModal('joinGroup')">
|
|
140
|
+
<span class="icon">👥</span>加入群聊
|
|
141
|
+
</button>
|
|
142
|
+
<button class="action-btn" onclick="showModal('settings')">
|
|
143
|
+
<span class="icon">⚙️</span>设置
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="list-section">
|
|
147
|
+
<h4>好友</h4>
|
|
148
|
+
<div id="friendsList"></div>
|
|
149
|
+
<h4>群聊</h4>
|
|
150
|
+
<div id="groupsList"></div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<!-- Main Panel -->
|
|
155
|
+
<div class="main-panel">
|
|
156
|
+
<div id="emptyState" class="empty-state">
|
|
157
|
+
<div class="icon">💬</div>
|
|
158
|
+
<div>选择好友或群聊开始对话</div>
|
|
159
|
+
</div>
|
|
160
|
+
<div id="chatView" class="chat-area" style="display:none">
|
|
161
|
+
<div class="chat-header">
|
|
162
|
+
<div class="avatar" id="chatAvatar">U</div>
|
|
163
|
+
<div class="info">
|
|
164
|
+
<div class="name" id="chatName">-</div>
|
|
165
|
+
<div class="status" id="chatStatus">-</div>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="actions">
|
|
168
|
+
<button onclick="toggleSilent()" id="silentBtn" title="静默模式">🔔</button>
|
|
169
|
+
<button onclick="showChatInfo()" title="信息">ℹ️</button>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="messages-container" id="messagesContainer">
|
|
173
|
+
<div class="load-more" id="loadMore" onclick="loadMoreMessages()" style="display:none">加载更多消息</div>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="input-area" style="position:relative">
|
|
176
|
+
<div class="mention-dropdown" id="mentionDropdown"></div>
|
|
177
|
+
<div class="input-top">
|
|
178
|
+
<input type="text" id="messageInput" placeholder="输入消息... (@提及)" onkeydown="handleInputKey(event)" oninput="handleInputChange()">
|
|
179
|
+
<button class="send-btn" onclick="sendMessage()">发送</button>
|
|
180
|
+
</div>
|
|
181
|
+
<div class="input-toolbar">
|
|
182
|
+
<button class="tool-btn" onclick="document.getElementById('fileInput').click()" title="上传文件">📎</button>
|
|
183
|
+
<button class="tool-btn" onclick="document.getElementById('imageInput').click()" title="上传图片">🖼️</button>
|
|
184
|
+
<button class="tool-btn" onclick="insertMarkdown('**','**')" title="粗体">𝐁</button>
|
|
185
|
+
<button class="tool-btn" onclick="insertMarkdown('`','`')" title="代码">⟨⟩</button>
|
|
186
|
+
<button class="tool-btn" onclick="insertMarkdown('$','$')" title="LaTeX">∑</button>
|
|
187
|
+
</div>
|
|
188
|
+
<input type="file" id="fileInput" style="display:none" onchange="uploadFile(this,'file')">
|
|
189
|
+
<input type="file" id="imageInput" style="display:none" accept="image/*" onchange="uploadFile(this,'image')">
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<!-- Modals -->
|
|
196
|
+
<div class="modal-overlay" id="modal-addFriend">
|
|
197
|
+
<div class="modal">
|
|
198
|
+
<h3>添加好友</h3>
|
|
199
|
+
<div class="form-group">
|
|
200
|
+
<label>好友码 / 临时号码</label>
|
|
201
|
+
<input type="text" id="friendCodeInput" placeholder="输入6位好友码或临时号码">
|
|
202
|
+
</div>
|
|
203
|
+
<div class="btn-row">
|
|
204
|
+
<button class="btn btn-secondary" onclick="hideModal('addFriend')">取消</button>
|
|
205
|
+
<button class="btn btn-primary" onclick="addFriendByCode()">添加</button>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div class="modal-overlay" id="modal-joinGroup">
|
|
211
|
+
<div class="modal">
|
|
212
|
+
<h3>加入群聊</h3>
|
|
213
|
+
<div class="form-group">
|
|
214
|
+
<label>创建新群聊</label>
|
|
215
|
+
<input type="text" id="newGroupName" placeholder="群聊名称">
|
|
216
|
+
</div>
|
|
217
|
+
<div class="btn-row">
|
|
218
|
+
<button class="btn btn-secondary" onclick="hideModal('joinGroup')">取消</button>
|
|
219
|
+
<button class="btn btn-primary" onclick="createGroup()">创建</button>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div class="modal-overlay" id="modal-settings">
|
|
225
|
+
<div class="modal">
|
|
226
|
+
<h3>设置</h3>
|
|
227
|
+
<div style="display:flex;flex-direction:column;gap:12px">
|
|
228
|
+
<button class="btn btn-secondary" onclick="showQRCode()" style="width:100%;text-align:left">📱 二维码 — 客户端扫描添加好友</button>
|
|
229
|
+
<button class="btn btn-secondary" onclick="showNicknameModal()" style="width:100%;text-align:left">✏️ 修改昵称</button>
|
|
230
|
+
<button class="btn btn-secondary" onclick="generateFriendCode()" style="width:100%;text-align:left">🔢 生成好友码 (24小时有效)</button>
|
|
231
|
+
<button class="btn btn-secondary" onclick="showKeysModal()" style="width:100%;text-align:left">🔑 显示/重新生成密钥</button>
|
|
232
|
+
<button class="btn btn-secondary" onclick="createNewAgent()" style="width:100%;text-align:left">➕ 创建新 Agent</button>
|
|
233
|
+
<button class="btn btn-secondary" onclick="syncData()" style="width:100%;text-align:left">🔄 同步服务器数据</button>
|
|
234
|
+
</div>
|
|
235
|
+
<div class="btn-row" style="margin-top:20px">
|
|
236
|
+
<button class="btn btn-primary" onclick="hideModal('settings')">关闭</button>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<div class="modal-overlay" id="modal-qr">
|
|
242
|
+
<div class="modal">
|
|
243
|
+
<h3>二维码 — 扫码添加好友</h3>
|
|
244
|
+
<div class="qr-container" id="qrContainer">加载中...</div>
|
|
245
|
+
<p style="font-size:12px;color:var(--text2);text-align:center">客户端扫描此二维码,通过验证后即可添加该 Agent 为好友</p>
|
|
246
|
+
<div class="btn-row">
|
|
247
|
+
<button class="btn btn-primary" onclick="hideModal('qr')">关闭</button>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<div class="modal-overlay" id="modal-nickname">
|
|
253
|
+
<div class="modal">
|
|
254
|
+
<h3>修改昵称</h3>
|
|
255
|
+
<div class="form-group">
|
|
256
|
+
<label>新昵称</label>
|
|
257
|
+
<input type="text" id="nicknameInput" placeholder="输入新昵称">
|
|
258
|
+
</div>
|
|
259
|
+
<div class="btn-row">
|
|
260
|
+
<button class="btn btn-secondary" onclick="hideModal('nickname')">取消</button>
|
|
261
|
+
<button class="btn btn-primary" onclick="saveNickname()">保存</button>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div class="modal-overlay" id="modal-friendCode">
|
|
267
|
+
<div class="modal">
|
|
268
|
+
<h3>好友码</h3>
|
|
269
|
+
<div id="friendCodeDisplay" style="text-align:center;padding:20px">生成中...</div>
|
|
270
|
+
<p style="font-size:12px;color:var(--text2);text-align:center">24小时有效的6位数字好友码,对方添加此号码即可自动连接服务器并添加好友</p>
|
|
271
|
+
<div class="btn-row">
|
|
272
|
+
<button class="btn btn-primary" onclick="hideModal('friendCode')">关闭</button>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<div class="modal-overlay" id="modal-keys">
|
|
278
|
+
<div class="modal">
|
|
279
|
+
<h3>密钥管理</h3>
|
|
280
|
+
<div class="warning-box">⚠️ 警告:私钥用于执行模式聊天。拥有私钥的人可以通过聊天安排任务执行、调用工具。请勿泄露私钥!</div>
|
|
281
|
+
<div id="keysDisplay">加载中...</div>
|
|
282
|
+
<div class="btn-row">
|
|
283
|
+
<button class="btn btn-danger" onclick="confirmRotateKeys()">重新生成密钥</button>
|
|
284
|
+
<button class="btn btn-primary" onclick="hideModal('keys')">关闭</button>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<div class="modal-overlay" id="modal-confirmRotate">
|
|
290
|
+
<div class="modal">
|
|
291
|
+
<h3>⚠️ 确认重新生成密钥</h3>
|
|
292
|
+
<div class="warning-box">重新生成密钥后,旧密钥将立即失效。所有好友需要重新建立连接。此操作不可撤销!</div>
|
|
293
|
+
<div class="btn-row">
|
|
294
|
+
<button class="btn btn-secondary" onclick="hideModal('confirmRotate')">取消</button>
|
|
295
|
+
<button class="btn btn-danger" onclick="rotateKeys()">确认重新生成</button>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<div class="modal-overlay" id="modal-createAgent">
|
|
301
|
+
<div class="modal">
|
|
302
|
+
<h3>创建新 Agent</h3>
|
|
303
|
+
<div class="form-group">
|
|
304
|
+
<label>Agent ID</label>
|
|
305
|
+
<input type="text" id="newAgentId" placeholder="输入唯一标识">
|
|
306
|
+
</div>
|
|
307
|
+
<div class="form-group">
|
|
308
|
+
<label>昵称</label>
|
|
309
|
+
<input type="text" id="newAgentName" placeholder="输入昵称">
|
|
310
|
+
</div>
|
|
311
|
+
<div class="btn-row">
|
|
312
|
+
<button class="btn btn-secondary" onclick="hideModal('createAgent')">取消</button>
|
|
313
|
+
<button class="btn btn-primary" onclick="doCreateAgent()">创建</button>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<div class="modal-overlay" id="modal-deleteMsg">
|
|
319
|
+
<div class="modal">
|
|
320
|
+
<h3>确认删除</h3>
|
|
321
|
+
<p>确定要删除这条消息吗?此操作不可撤销。</p>
|
|
322
|
+
<div class="btn-row">
|
|
323
|
+
<button class="btn btn-secondary" onclick="hideModal('deleteMsg')">取消</button>
|
|
324
|
+
<button class="btn btn-danger" onclick="doDeleteMessage()">删除</button>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
<!-- Image Preview Modal -->
|
|
330
|
+
<div class="modal-overlay" id="modal-imagePreview">
|
|
331
|
+
<div class="modal" style="max-width:90vw;max-height:90vh;padding:8px">
|
|
332
|
+
<img id="previewImage" style="max-width:100%;max-height:85vh;border-radius:8px">
|
|
333
|
+
<div class="btn-row"><button class="btn btn-primary" onclick="hideModal('imagePreview')">关闭</button></div>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<script>
|
|
338
|
+
// ─── State ──────────────────────────────────────────────────────────
|
|
339
|
+
let currentAgentId = '';
|
|
340
|
+
let currentTarget = null; // { id, name, type: 'friend'|'group', isOnline, silent }
|
|
341
|
+
let chatMessages = [];
|
|
342
|
+
let oldestTimestamp = null;
|
|
343
|
+
let wsReconnectTimer = null;
|
|
344
|
+
let pendingDeleteMsgId = null;
|
|
345
|
+
|
|
346
|
+
const API = '';
|
|
347
|
+
|
|
348
|
+
// ─── API Helper ─────────────────────────────────────────────────────
|
|
349
|
+
async function api(method, path, body = null) {
|
|
350
|
+
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
|
351
|
+
if (body) opts.body = JSON.stringify(body);
|
|
352
|
+
const resp = await fetch(API + path, opts);
|
|
353
|
+
return resp.json();
|
|
354
|
+
}
|
|
355
|
+
|
|
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
|
+
// ─── Agents ─────────────────────────────────────────────────────────
|
|
370
|
+
async function loadAgents() {
|
|
371
|
+
const data = await api('GET', '/api/agents');
|
|
372
|
+
const sel = document.getElementById('agentSelect');
|
|
373
|
+
sel.innerHTML = '<option value="">选择 Agent...</option>';
|
|
374
|
+
for (const a of data.agents || []) {
|
|
375
|
+
const opt = document.createElement('option');
|
|
376
|
+
opt.value = a.agent_id;
|
|
377
|
+
opt.textContent = a.nickname || a.agent_id;
|
|
378
|
+
if (a.agent_id === currentAgentId) opt.selected = true;
|
|
379
|
+
sel.appendChild(opt);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function switchAgent(agentId) {
|
|
384
|
+
if (!agentId) return;
|
|
385
|
+
currentAgentId = agentId;
|
|
386
|
+
currentTarget = null;
|
|
387
|
+
showEmptyState();
|
|
388
|
+
await loadFriends();
|
|
389
|
+
await loadGroups();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function showEmptyState() {
|
|
393
|
+
document.getElementById('emptyState').style.display = 'flex';
|
|
394
|
+
document.getElementById('chatView').style.display = 'none';
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function showChatView() {
|
|
398
|
+
document.getElementById('emptyState').style.display = 'none';
|
|
399
|
+
document.getElementById('chatView').style.display = 'flex';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function createNewAgent() {
|
|
403
|
+
hideModal('settings');
|
|
404
|
+
showModal('createAgent');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function doCreateAgent() {
|
|
408
|
+
const id = document.getElementById('newAgentId').value.trim();
|
|
409
|
+
const name = document.getElementById('newAgentName').value.trim();
|
|
410
|
+
if (!id) return alert('请输入 Agent ID');
|
|
411
|
+
await api('POST', '/api/agents', { agent_id: id, nickname: name });
|
|
412
|
+
hideModal('createAgent');
|
|
413
|
+
currentAgentId = id;
|
|
414
|
+
await loadAgents();
|
|
415
|
+
await loadFriends();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ─── Friends ────────────────────────────────────────────────────────
|
|
419
|
+
async function loadFriends() {
|
|
420
|
+
if (!currentAgentId) return;
|
|
421
|
+
const data = await api('GET', `/api/friends?agent_id=${currentAgentId}`);
|
|
422
|
+
const container = document.getElementById('friendsList');
|
|
423
|
+
container.innerHTML = '';
|
|
424
|
+
for (const f of data.friends || []) {
|
|
425
|
+
const div = document.createElement('div');
|
|
426
|
+
div.className = 'friend-item' + (currentTarget?.id === f.id && currentTarget?.type === 'friend' ? ' active' : '');
|
|
427
|
+
div.onclick = () => selectTarget(f.id, f.ai_name || f.fingerprint?.slice(0,8) || f.id.slice(0,8), 'friend', !!f.is_online);
|
|
428
|
+
div.innerHTML = `
|
|
429
|
+
<div class="avatar ${f.is_online ? 'online' : ''}">${(f.ai_name||f.id).charAt(0).toUpperCase()}</div>
|
|
430
|
+
<div class="info">
|
|
431
|
+
<div class="name">${f.ai_name || f.fingerprint?.slice(0,16) || f.id.slice(0,12)}</div>
|
|
432
|
+
<div class="status"><span class="${f.is_online ? 'badge-online' : 'badge-offline'}">●</span> ${f.is_online ? '在线' : '离线'}</div>
|
|
433
|
+
</div>`;
|
|
434
|
+
container.appendChild(div);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ─── Groups ─────────────────────────────────────────────────────────
|
|
439
|
+
async function loadGroups() {
|
|
440
|
+
if (!currentAgentId) return;
|
|
441
|
+
const data = await api('GET', `/api/groups?agent_id=${currentAgentId}`);
|
|
442
|
+
const container = document.getElementById('groupsList');
|
|
443
|
+
container.innerHTML = '';
|
|
444
|
+
for (const g of data.groups || []) {
|
|
445
|
+
const silent = false; // will check
|
|
446
|
+
const div = document.createElement('div');
|
|
447
|
+
div.className = 'group-item' + (currentTarget?.id === g.id && currentTarget?.type === 'group' ? ' active' : '');
|
|
448
|
+
div.onclick = () => selectTarget(g.id, g.name, 'group', true, silent);
|
|
449
|
+
div.innerHTML = `
|
|
450
|
+
<div class="avatar">👥</div>
|
|
451
|
+
<div class="info">
|
|
452
|
+
<div class="name">${g.name}${silent ? '<span class="silent-badge">静默</span>' : ''}</div>
|
|
453
|
+
<div class="status">${(JSON.parse(g.members_json||'[]')).length} 成员</div>
|
|
454
|
+
</div>`;
|
|
455
|
+
container.appendChild(div);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ─── Select Chat Target ────────────────────────────────────────────
|
|
460
|
+
async function selectTarget(id, name, type, isOnline = false, silent = false) {
|
|
461
|
+
currentTarget = { id, name, type, isOnline, silent };
|
|
462
|
+
showChatView();
|
|
463
|
+
|
|
464
|
+
// Update header
|
|
465
|
+
document.getElementById('chatAvatar').textContent = type === 'group' ? '👥' : name.charAt(0).toUpperCase();
|
|
466
|
+
document.getElementById('chatName').textContent = name;
|
|
467
|
+
document.getElementById('chatStatus').textContent = type === 'group' ? '群聊' : (isOnline ? '在线' : '离线');
|
|
468
|
+
document.getElementById('silentBtn').textContent = silent ? '🔕' : '🔔';
|
|
469
|
+
document.getElementById('silentBtn').style.display = type === 'group' ? '' : 'none';
|
|
470
|
+
|
|
471
|
+
// Reload lists to show active state
|
|
472
|
+
await loadFriends();
|
|
473
|
+
await loadGroups();
|
|
474
|
+
|
|
475
|
+
// Load chat history
|
|
476
|
+
await loadMessages();
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ─── Messages ───────────────────────────────────────────────────────
|
|
480
|
+
async function loadMessages() {
|
|
481
|
+
if (!currentTarget || !currentAgentId) return;
|
|
482
|
+
chatMessages = [];
|
|
483
|
+
oldestTimestamp = null;
|
|
484
|
+
const container = document.getElementById('messagesContainer');
|
|
485
|
+
// Keep loadMore button, clear rest
|
|
486
|
+
container.innerHTML = '<div class="load-more" id="loadMore" onclick="loadMoreMessages()" style="display:none">加载更多消息</div>';
|
|
487
|
+
|
|
488
|
+
const data = await api('GET', `/api/chat/${currentTarget.id}?agent_id=${currentAgentId}&limit=50`);
|
|
489
|
+
const msgs = (data.messages || []).reverse();
|
|
490
|
+
if (msgs.length > 0) {
|
|
491
|
+
oldestTimestamp = msgs[0].timestamp;
|
|
492
|
+
document.getElementById('loadMore').style.display = msgs.length >= 50 ? 'block' : 'none';
|
|
493
|
+
}
|
|
494
|
+
chatMessages = msgs;
|
|
495
|
+
renderMessages();
|
|
496
|
+
scrollToBottom();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function loadMoreMessages() {
|
|
500
|
+
if (!currentTarget || !oldestTimestamp) return;
|
|
501
|
+
const data = await api('GET', `/api/chat/${currentTarget.id}?agent_id=${currentAgentId}&limit=50&before=${oldestTimestamp}`);
|
|
502
|
+
const msgs = (data.messages || []).reverse();
|
|
503
|
+
if (msgs.length > 0) {
|
|
504
|
+
oldestTimestamp = msgs[0].timestamp;
|
|
505
|
+
chatMessages = [...msgs, ...chatMessages];
|
|
506
|
+
document.getElementById('loadMore').style.display = msgs.length >= 50 ? 'block' : 'none';
|
|
507
|
+
renderMessages();
|
|
508
|
+
} else {
|
|
509
|
+
document.getElementById('loadMore').style.display = 'none';
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function renderMessages() {
|
|
514
|
+
const container = document.getElementById('messagesContainer');
|
|
515
|
+
const loadMore = document.getElementById('loadMore');
|
|
516
|
+
// Clear except loadMore
|
|
517
|
+
while (container.children.length > 1) container.removeChild(container.lastChild);
|
|
518
|
+
|
|
519
|
+
for (const msg of chatMessages) {
|
|
520
|
+
const isMe = msg.from_id === currentAgentId;
|
|
521
|
+
const row = document.createElement('div');
|
|
522
|
+
row.className = 'msg-row ' + (isMe ? 'me' : 'them');
|
|
523
|
+
row.dataset.msgId = msg.id;
|
|
524
|
+
|
|
525
|
+
const bubble = document.createElement('div');
|
|
526
|
+
bubble.className = 'msg-bubble';
|
|
527
|
+
|
|
528
|
+
// Render content based on type
|
|
529
|
+
let content = msg.content || '';
|
|
530
|
+
if (msg.type === 'image' && msg.file_url) {
|
|
531
|
+
content = renderMarkdown(content) + `<img src="${msg.file_url}" onclick="previewImage('${msg.file_url}')" alt="图片">`;
|
|
532
|
+
} else if (msg.type === 'file' && msg.file_url) {
|
|
533
|
+
content = renderMarkdown(content) + `<a class="file-link" href="${msg.file_url}" download>📄 ${msg.file_name || '下载文件'}</a>`;
|
|
534
|
+
} else {
|
|
535
|
+
// Process mentions
|
|
536
|
+
content = processMentions(content);
|
|
537
|
+
content = renderMarkdown(content);
|
|
538
|
+
content = renderLatex(content);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
bubble.innerHTML = content;
|
|
542
|
+
|
|
543
|
+
// Time
|
|
544
|
+
const timeDiv = document.createElement('div');
|
|
545
|
+
timeDiv.className = 'msg-time';
|
|
546
|
+
timeDiv.textContent = formatTime(msg.timestamp);
|
|
547
|
+
|
|
548
|
+
// Action buttons
|
|
549
|
+
const actions = document.createElement('div');
|
|
550
|
+
actions.className = 'msg-actions';
|
|
551
|
+
actions.innerHTML = `
|
|
552
|
+
<button class="msg-action-btn" onclick="copyMessage('${msg.id}')" title="复制">📋</button>
|
|
553
|
+
<button class="msg-action-btn danger" onclick="confirmDeleteMessage('${msg.id}')" title="删除">🗑️</button>`;
|
|
554
|
+
|
|
555
|
+
bubble.appendChild(actions);
|
|
556
|
+
bubble.appendChild(timeDiv);
|
|
557
|
+
row.appendChild(bubble);
|
|
558
|
+
container.appendChild(row);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Re-render KaTeX
|
|
562
|
+
renderAllLatex();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function renderMarkdown(text) {
|
|
566
|
+
try {
|
|
567
|
+
return marked.parse(text, { breaks: true, gfm: true });
|
|
568
|
+
} catch (e) {
|
|
569
|
+
return text.replace(/</g, '<').replace(/>/g, '>');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function renderLatex(html) {
|
|
574
|
+
// Inline and block LaTeX with $$...$$ and $...$
|
|
575
|
+
return html.replace(/\$\$([\s\S]+?)\$\$/g, (m, tex) => {
|
|
576
|
+
try { return katex.renderToString(tex.trim(), { displayMode: true, throwOnError: false }); }
|
|
577
|
+
catch (e) { return m; }
|
|
578
|
+
}).replace(/\$([^\$]+?)\$/g, (m, tex) => {
|
|
579
|
+
try { return katex.renderToString(tex.trim(), { displayMode: false, throwOnError: false }); }
|
|
580
|
+
catch (e) { return m; }
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function renderAllLatex() {
|
|
585
|
+
// Re-render any remaining LaTeX
|
|
586
|
+
try {
|
|
587
|
+
document.querySelectorAll('.msg-bubble').forEach(el => {
|
|
588
|
+
renderMathInElement(el, {
|
|
589
|
+
delimiters: [
|
|
590
|
+
{left: '$$', right: '$$', display: true},
|
|
591
|
+
{left: '$', right: '$', display: false},
|
|
592
|
+
],
|
|
593
|
+
throwOnError: false
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
} catch(e) {}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function processMentions(text) {
|
|
600
|
+
// Highlight @mentions
|
|
601
|
+
return text.replace(/@(\S+)/g, '<span class="mention">@$1</span>');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function formatTime(ts) {
|
|
605
|
+
try {
|
|
606
|
+
const d = new Date(ts);
|
|
607
|
+
return d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
|
608
|
+
} catch { return ts; }
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function scrollToBottom() {
|
|
612
|
+
const container = document.getElementById('messagesContainer');
|
|
613
|
+
container.scrollTop = container.scrollHeight;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ─── Send Message ───────────────────────────────────────────────────
|
|
617
|
+
async function sendMessage() {
|
|
618
|
+
const input = document.getElementById('messageInput');
|
|
619
|
+
const content = input.value.trim();
|
|
620
|
+
if (!content || !currentTarget || !currentAgentId) return;
|
|
621
|
+
|
|
622
|
+
input.value = '';
|
|
623
|
+
try {
|
|
624
|
+
await api('POST', '/api/chat/send', {
|
|
625
|
+
agent_id: currentAgentId,
|
|
626
|
+
targetId: currentTarget.id,
|
|
627
|
+
content,
|
|
628
|
+
type: 'text',
|
|
629
|
+
isGroup: currentTarget.type === 'group',
|
|
630
|
+
mentions: extractMentions(content),
|
|
631
|
+
});
|
|
632
|
+
await loadMessages();
|
|
633
|
+
} catch (e) {
|
|
634
|
+
alert('发送失败: ' + e.message);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function extractMentions(text) {
|
|
639
|
+
const mentions = [];
|
|
640
|
+
const re = /@(\S+)/g;
|
|
641
|
+
let m;
|
|
642
|
+
while ((m = re.exec(text)) !== null) {
|
|
643
|
+
mentions.push(m[1]);
|
|
644
|
+
}
|
|
645
|
+
return mentions;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function handleInputKey(e) {
|
|
649
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
650
|
+
e.preventDefault();
|
|
651
|
+
sendMessage();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function handleInputChange() {
|
|
656
|
+
const input = document.getElementById('messageInput');
|
|
657
|
+
const val = input.value;
|
|
658
|
+
const cursorPos = input.selectionStart;
|
|
659
|
+
const textBefore = val.substring(0, cursorPos);
|
|
660
|
+
const atMatch = textBefore.match(/@(\S*)$/);
|
|
661
|
+
const dropdown = document.getElementById('mentionDropdown');
|
|
662
|
+
|
|
663
|
+
if (atMatch && currentTarget?.type === 'group') {
|
|
664
|
+
// Show mention dropdown
|
|
665
|
+
const query = atMatch[1].toLowerCase();
|
|
666
|
+
// Get group members from the group info
|
|
667
|
+
dropdown.innerHTML = '';
|
|
668
|
+
dropdown.innerHTML += `<div class="mention-option" onclick="insertMention('all')">📢 @所有人</div>`;
|
|
669
|
+
// Add friend suggestions
|
|
670
|
+
const friends = document.querySelectorAll('.friend-item .name');
|
|
671
|
+
friends.forEach(f => {
|
|
672
|
+
const name = f.textContent;
|
|
673
|
+
if (!query || name.toLowerCase().includes(query)) {
|
|
674
|
+
dropdown.innerHTML += `<div class="mention-option" onclick="insertMention('${name}')">👤 ${name}</div>`;
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
dropdown.classList.add('show');
|
|
678
|
+
} else {
|
|
679
|
+
dropdown.classList.remove('show');
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function insertMention(name) {
|
|
684
|
+
const input = document.getElementById('messageInput');
|
|
685
|
+
const val = input.value;
|
|
686
|
+
const cursorPos = input.selectionStart;
|
|
687
|
+
const textBefore = val.substring(0, cursorPos);
|
|
688
|
+
const textAfter = val.substring(cursorPos);
|
|
689
|
+
const newText = textBefore.replace(/@\S*$/, '@' + name + ' ') + textAfter;
|
|
690
|
+
input.value = newText;
|
|
691
|
+
input.focus();
|
|
692
|
+
document.getElementById('mentionDropdown').classList.remove('show');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ─── File Upload ────────────────────────────────────────────────────
|
|
696
|
+
async function uploadFile(input, type) {
|
|
697
|
+
if (!input.files[0] || !currentTarget) return;
|
|
698
|
+
const formData = new FormData();
|
|
699
|
+
formData.append('file', input.files[0]);
|
|
700
|
+
formData.append('agent_id', currentAgentId);
|
|
701
|
+
formData.append('targetId', currentTarget.id);
|
|
702
|
+
formData.append('isGroup', currentTarget.type === 'group' ? 'true' : 'false');
|
|
703
|
+
|
|
704
|
+
try {
|
|
705
|
+
const resp = await fetch(API + '/api/upload', { method: 'POST', body: formData });
|
|
706
|
+
const data = await resp.json();
|
|
707
|
+
if (data.success) {
|
|
708
|
+
await loadMessages();
|
|
709
|
+
} else {
|
|
710
|
+
alert('上传失败: ' + (data.error || '未知错误'));
|
|
711
|
+
}
|
|
712
|
+
} catch (e) {
|
|
713
|
+
alert('上传失败: ' + e.message);
|
|
714
|
+
}
|
|
715
|
+
input.value = '';
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ─── Message Actions ────────────────────────────────────────────────
|
|
719
|
+
function copyMessage(msgId) {
|
|
720
|
+
const row = document.querySelector(`.msg-row[data-msg-id="${msgId}"]`);
|
|
721
|
+
if (!row) return;
|
|
722
|
+
const bubble = row.querySelector('.msg-bubble');
|
|
723
|
+
// Get text content, excluding action buttons and time
|
|
724
|
+
const clone = bubble.cloneNode(true);
|
|
725
|
+
clone.querySelectorAll('.msg-actions, .msg-time').forEach(el => el.remove());
|
|
726
|
+
navigator.clipboard.writeText(clone.textContent.trim()).then(() => {
|
|
727
|
+
// Brief visual feedback
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function confirmDeleteMessage(msgId) {
|
|
732
|
+
pendingDeleteMsgId = msgId;
|
|
733
|
+
showModal('deleteMsg');
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function doDeleteMessage() {
|
|
737
|
+
if (!pendingDeleteMsgId) return;
|
|
738
|
+
await api('DELETE', `/api/chat/${pendingDeleteMsgId}?agent_id=${currentAgentId}`);
|
|
739
|
+
hideModal('deleteMsg');
|
|
740
|
+
pendingDeleteMsgId = null;
|
|
741
|
+
await loadMessages();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// ─── Markdown Toolbar ───────────────────────────────────────────────
|
|
745
|
+
function insertMarkdown(before, after) {
|
|
746
|
+
const input = document.getElementById('messageInput');
|
|
747
|
+
const start = input.selectionStart;
|
|
748
|
+
const end = input.selectionEnd;
|
|
749
|
+
const selected = input.value.substring(start, end);
|
|
750
|
+
const replacement = before + (selected || 'text') + after;
|
|
751
|
+
input.value = input.value.substring(0, start) + replacement + input.value.substring(end);
|
|
752
|
+
input.focus();
|
|
753
|
+
input.setSelectionRange(start + before.length, start + before.length + (selected || 'text').length);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// ─── Image Preview ──────────────────────────────────────────────────
|
|
757
|
+
function previewImage(url) {
|
|
758
|
+
document.getElementById('previewImage').src = url;
|
|
759
|
+
showModal('imagePreview');
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ─── Silent Mode ────────────────────────────────────────────────────
|
|
763
|
+
async function toggleSilent() {
|
|
764
|
+
if (!currentTarget || currentTarget.type !== 'group') return;
|
|
765
|
+
const newSilent = !currentTarget.silent;
|
|
766
|
+
await api('PUT', `/api/groups/${currentTarget.id}/silent?agent_id=${currentAgentId}`, { silent: newSilent });
|
|
767
|
+
currentTarget.silent = newSilent;
|
|
768
|
+
document.getElementById('silentBtn').textContent = newSilent ? '🔕' : '🔔';
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// ─── Modal Helpers ──────────────────────────────────────────────────
|
|
772
|
+
function showModal(name) { document.getElementById('modal-' + name).classList.add('show'); }
|
|
773
|
+
function hideModal(name) { document.getElementById('modal-' + name).classList.remove('show'); }
|
|
774
|
+
|
|
775
|
+
// ─── Add Friend ─────────────────────────────────────────────────────
|
|
776
|
+
async function addFriendByCode() {
|
|
777
|
+
const code = document.getElementById('friendCodeInput').value.trim();
|
|
778
|
+
if (!code) return alert('请输入好友码');
|
|
779
|
+
try {
|
|
780
|
+
const result = await api('POST', '/api/friends/add', { temp_number: code, agent_id: currentAgentId });
|
|
781
|
+
if (result.success) {
|
|
782
|
+
alert('好友添加成功!');
|
|
783
|
+
hideModal('addFriend');
|
|
784
|
+
document.getElementById('friendCodeInput').value = '';
|
|
785
|
+
await loadFriends();
|
|
786
|
+
} else {
|
|
787
|
+
alert('添加失败: ' + (result.error || '未知错误'));
|
|
788
|
+
}
|
|
789
|
+
} catch (e) {
|
|
790
|
+
alert('添加失败: ' + e.message);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ─── Create Group ───────────────────────────────────────────────────
|
|
795
|
+
async function createGroup() {
|
|
796
|
+
const name = document.getElementById('newGroupName').value.trim();
|
|
797
|
+
if (!name) return alert('请输入群聊名称');
|
|
798
|
+
try {
|
|
799
|
+
await api('POST', '/api/groups', { name, agent_id: currentAgentId });
|
|
800
|
+
hideModal('joinGroup');
|
|
801
|
+
document.getElementById('newGroupName').value = '';
|
|
802
|
+
await loadGroups();
|
|
803
|
+
} catch (e) {
|
|
804
|
+
alert('创建失败: ' + e.message);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ─── Settings Actions ───────────────────────────────────────────────
|
|
809
|
+
async function showQRCode() {
|
|
810
|
+
hideModal('settings');
|
|
811
|
+
try {
|
|
812
|
+
const data = await api('GET', `/api/identity/qr?agent_id=${currentAgentId}`);
|
|
813
|
+
document.getElementById('qrContainer').innerHTML = data.qr
|
|
814
|
+
? `<img src="${data.qr}" alt="QR Code">`
|
|
815
|
+
: '<p>生成失败</p>';
|
|
816
|
+
} catch (e) {
|
|
817
|
+
document.getElementById('qrContainer').innerHTML = '<p>生成失败: ' + e.message + '</p>';
|
|
818
|
+
}
|
|
819
|
+
showModal('qr');
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function showNicknameModal() {
|
|
823
|
+
hideModal('settings');
|
|
824
|
+
showModal('nickname');
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
async function saveNickname() {
|
|
828
|
+
const nickname = document.getElementById('nicknameInput').value.trim();
|
|
829
|
+
if (!nickname) return alert('请输入昵称');
|
|
830
|
+
await api('POST', '/api/identity/nickname', { agent_id: currentAgentId, nickname });
|
|
831
|
+
hideModal('nickname');
|
|
832
|
+
await loadAgents();
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
async function generateFriendCode() {
|
|
836
|
+
hideModal('settings');
|
|
837
|
+
try {
|
|
838
|
+
const data = await api('POST', '/api/identity/friend-code', { agent_id: currentAgentId });
|
|
839
|
+
document.getElementById('friendCodeDisplay').innerHTML = data.code
|
|
840
|
+
? `<div style="font-size:48px;font-weight:700;letter-spacing:8px;color:var(--primary)">${data.code}</div><div style="font-size:12px;color:var(--text2);margin-top:8px">有效期至: ${data.expires_at || '24小时'}</div>`
|
|
841
|
+
: '<p>生成失败</p>';
|
|
842
|
+
} catch (e) {
|
|
843
|
+
document.getElementById('friendCodeDisplay').innerHTML = '<p>生成失败: ' + e.message + '</p>';
|
|
844
|
+
}
|
|
845
|
+
showModal('friendCode');
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function showKeysModal() {
|
|
849
|
+
hideModal('settings');
|
|
850
|
+
try {
|
|
851
|
+
const data = await api('GET', `/api/identity/keys?agent_id=${currentAgentId}`);
|
|
852
|
+
document.getElementById('keysDisplay').innerHTML = `
|
|
853
|
+
<div><strong>Agent ID:</strong></div>
|
|
854
|
+
<div class="key-display">${data.agent_id || '-'}</div>
|
|
855
|
+
<div><strong>昵称:</strong> ${data.nickname || '-'}</div>
|
|
856
|
+
<div><strong>指纹:</strong></div>
|
|
857
|
+
<div class="key-display">${data.fingerprint || '-'}</div>
|
|
858
|
+
<div><strong>公钥 (签名):</strong> <small style="color:var(--text2)">用于连接服务端,获取好友列表</small></div>
|
|
859
|
+
<div class="key-display">${data.signing_public_key || '-'}</div>
|
|
860
|
+
<div><strong>公钥 (交换):</strong></div>
|
|
861
|
+
<div class="key-display">${data.exchange_public_key || '-'}</div>
|
|
862
|
+
<details style="margin-top:12px"><summary style="cursor:pointer;color:var(--warning)">🔑 显示私钥 (危险)</summary>
|
|
863
|
+
<div class="warning-box" style="margin-top:8px">⚠️ 私钥用于执行模式的聊天。拥有此私钥的人可以通过聊天安排任务执行、调用工具。请勿泄露!</div>
|
|
864
|
+
<div><strong>私钥 (签名):</strong></div>
|
|
865
|
+
<div class="key-display">${data.signing_secret_key || '-'}</div>
|
|
866
|
+
<div><strong>私钥 (交换):</strong></div>
|
|
867
|
+
<div class="key-display">${data.exchange_secret_key || '-'}</div>
|
|
868
|
+
</details>`;
|
|
869
|
+
} catch (e) {
|
|
870
|
+
document.getElementById('keysDisplay').innerHTML = '<p>加载失败: ' + e.message + '</p>';
|
|
871
|
+
}
|
|
872
|
+
showModal('keys');
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function confirmRotateKeys() {
|
|
876
|
+
hideModal('keys');
|
|
877
|
+
showModal('confirmRotate');
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
async function rotateKeys() {
|
|
881
|
+
try {
|
|
882
|
+
await api('POST', '/api/identity/rotate-keys', { agent_id: currentAgentId });
|
|
883
|
+
hideModal('confirmRotate');
|
|
884
|
+
await showKeysModal();
|
|
885
|
+
} catch (e) {
|
|
886
|
+
alert('密钥轮换失败: ' + e.message);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async function syncData() {
|
|
891
|
+
hideModal('settings');
|
|
892
|
+
try {
|
|
893
|
+
await api('POST', '/api/sync', { agent_id: currentAgentId });
|
|
894
|
+
await loadFriends();
|
|
895
|
+
await loadGroups();
|
|
896
|
+
alert('同步完成');
|
|
897
|
+
} catch (e) {
|
|
898
|
+
alert('同步失败: ' + e.message);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function showChatInfo() {
|
|
903
|
+
if (!currentTarget) return;
|
|
904
|
+
alert(currentTarget.type === 'group'
|
|
905
|
+
? `群聊: ${currentTarget.name}\nID: ${currentTarget.id}`
|
|
906
|
+
: `好友: ${currentTarget.name}\nID: ${currentTarget.id}\n状态: ${currentTarget.isOnline ? '在线' : '离线'}`);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// ─── Init ───────────────────────────────────────────────────────────
|
|
910
|
+
init();
|
|
911
|
+
|
|
912
|
+
// Periodic refresh
|
|
913
|
+
setInterval(async () => {
|
|
914
|
+
if (currentAgentId) {
|
|
915
|
+
await loadFriends();
|
|
916
|
+
await loadGroups();
|
|
917
|
+
}
|
|
918
|
+
}, 30000);
|
|
919
|
+
</script>
|
|
920
|
+
</body>
|
|
921
|
+
</html>
|