qq-bot-roost 1.0.2 → 1.0.4

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/web/index.html CHANGED
@@ -1,1116 +1,1203 @@
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">
6
- <title>栖 Roost · 控制台</title>
7
- <!-- 主题脚本必须外置:CSP 的 script-src 'self' 会拦掉内联脚本 -->
8
- <script src="/theme-boot.js"></script>
9
- <link rel="stylesheet" href="/style.css">
10
- </head>
11
- <body>
12
-
13
- <header class="topbar">
14
- <div class="brand" title="栖 Roost · 倦飞之鸟,终有一栖">
15
- <span class="logo" aria-hidden="true">栖</span>
16
- <div>
17
- <div class="title">栖<span class="brand-en">Roost</span></div>
18
- <div class="sub" id="botName">未连接</div>
19
- </div>
20
- </div>
21
- <div class="bot-switch" id="botSwitch">
22
- <button class="bot-trigger" id="botTrigger" aria-haspopup="listbox" aria-expanded="false">
23
- <span class="bot-dot" id="botDot"></span>
24
- <span class="bot-name" id="botNameLabel">加载中…</span>
25
- <svg class="bot-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor"
26
- stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
27
- <path d="M6 9l6 6 6-6"/>
28
- </svg>
29
- </button>
30
- <div class="bot-menu" id="botMenu" hidden role="listbox">
31
- <input class="bot-search" id="botSearch" placeholder="搜索机器人" hidden>
32
- <div class="bot-list" id="botList"></div>
33
- <div class="bot-empty" id="botEmpty" hidden>没有匹配的机器人</div>
34
- <div class="bot-menu-foot">
35
- <button type="button" id="botAddBtn">
36
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
37
- <path d="M12 5v14M5 12h14"/>
38
- </svg>
39
- 新建机器人
40
- </button>
41
- </div>
42
- </div>
43
- </div>
44
-
45
- <nav class="tabs">
46
- <button class="tab active" data-tab="overview">概览</button>
47
-
48
- <div class="tab-group" data-group="data">
49
- <button class="tab tab-parent" aria-haspopup="true" aria-expanded="false">
50
- 数据
51
- <svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor"
52
- stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
53
- <path d="M6 9l6 6 6-6"/>
54
- </svg>
55
- </button>
56
- <div class="tab-menu" hidden>
57
- <button class="tab-item" data-tab="stats">统计</button>
58
- <button class="tab-item" data-tab="events">事件</button>
59
- <button class="tab-item" data-tab="reach">可达性</button>
60
- <button class="tab-item" data-tab="delivery">投递</button>
61
- <button class="tab-item" data-tab="logs">日志</button>
62
- </div>
63
- </div>
64
-
65
- <div class="tab-group" data-group="settings">
66
- <button class="tab tab-parent" aria-haspopup="true" aria-expanded="false">
67
- 设置
68
- <svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor"
69
- stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
70
- <path d="M6 9l6 6 6-6"/>
71
- </svg>
72
- </button>
73
- <div class="tab-menu" hidden>
74
- <button class="tab-item" data-tab="config">配置</button>
75
- <button class="tab-item" data-tab="groups">群组</button>
76
- <button class="tab-item" data-tab="roles">权限</button>
77
- </div>
78
- </div>
79
-
80
- <div class="tab-group" data-group="plugins">
81
- <button class="tab tab-parent" aria-haspopup="true" aria-expanded="false">
82
- 插件
83
- <svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor"
84
- stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
85
- <path d="M6 9l6 6 6-6"/>
86
- </svg>
87
- </button>
88
- <div class="tab-menu" hidden>
89
- <button class="tab-item" data-tab="plugins">插件管理</button>
90
- <!-- 插件注册的页面挂到这里,见 loadPluginPages() -->
91
- <div class="tab-menu-sep" id="pluginPageSep" hidden></div>
92
- <div class="tab-menu-label" id="pluginPageLabel" hidden>插件页面</div>
93
- <div class="tab-menu-sub" id="pluginPageItems"></div>
94
- </div>
95
- </div>
96
-
97
- <!-- 身份:QQ号 / 群号 ↔ openid。常用页而不是设置项 -->
98
- <button class="tab" data-tab="identity">身份</button>
99
-
100
- <!-- 通知:SMTP 邮箱配置 + 普通成员自助绑定。字段参考 BetterGI「设置 → 通知」 -->
101
- <button class="tab" data-tab="notify">通知</button>
102
-
103
- <button class="tab" data-tab="about">关于</button>
104
- </nav>
105
- <button class="icon-btn" id="btnTheme" title="切换日间 / 夜间" aria-label="切换主题">
106
- <svg class="ic-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
107
- stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
108
- <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/>
109
- </svg>
110
- <svg class="ic-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor"
111
- stroke-width="1.8" stroke-linecap="round">
112
- <circle cx="12" cy="12" r="4.2"/>
113
- <path d="M12 2.5v2M12 19.5v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M2.5 12h2M19.5 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/>
114
- </svg>
115
- </button>
116
-
117
- <button class="icon-btn icon-btn-danger" id="btnShutdown" title="关闭程序" aria-label="关闭程序">
118
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
119
- stroke-width="1.8" stroke-linecap="round">
120
- <path d="M12 3.2v8.4"/>
121
- <path d="M6.6 6.6a8 8 0 1 0 10.8 0"/>
122
- </svg>
123
- </button>
124
-
125
- <button class="status-pill" id="connPill" title="点击上线 / 下线">
126
- <span class="dot" id="connDot"></span>
127
- <span id="connText">连接中</span>
128
- <span class="pill-sep"></span>
129
- <span id="connAction">上线</span>
130
- </button>
131
- </header>
132
-
133
- <main>
134
- <!-- 概览 -->
135
- <section id="tab-overview" class="panel active">
136
- <div class="cards">
137
- <div class="card"><div class="k">连接状态</div><div class="v" id="c-conn">-</div></div>
138
- <div class="card"><div class="k">运行时长</div><div class="v" id="c-uptime">-</div></div>
139
- <div class="card"><div class="k">凭证有效期</div><div class="v" id="c-cred">-</div></div>
140
- <div class="card"><div class="k">内存占用</div><div class="v" id="c-mem">-</div></div>
141
- <div class="card"><div class="k">收到消息</div><div class="v" id="c-recv">0</div></div>
142
- <div class="card"><div class="k">发出消息</div><div class="v" id="c-sent">0</div></div>
143
- <div class="card"><div class="k">命令命中</div><div class="v" id="c-cmd">0</div></div>
144
- <div class="card"><div class="k">错误 / 拦截</div><div class="v" id="c-err">0</div></div>
145
- </div>
146
-
147
- <div class="row">
148
- <div class="box grow">
149
- <div class="box-hd">
150
- <h3>活跃会话</h3>
151
- <span class="hint">按消息数排序,最多 20 条</span>
152
- </div>
153
- <div class="table-wrap">
154
- <table>
155
- <thead><tr><th>openid</th><th>类型</th><th class="num">消息数</th><th>最后活跃</th></tr></thead>
156
- <tbody id="chatBody"><tr><td colspan="4" class="empty">暂无数据</td></tr></tbody>
157
- </table>
158
- </div>
159
- </div>
160
-
161
- <div class="box">
162
- <div class="box-hd"><h3>运行信息</h3></div>
163
- <dl class="kv">
164
- <dt>机器人 ID</dt><dd id="i-botid">-</dd>
165
- <dt>网关</dt><dd id="i-gw" class="mono">-</dd>
166
- <dt>会话 ID</dt><dd id="i-session" class="mono">-</dd>
167
- <dt>intents</dt><dd id="i-intents">-</dd>
168
- <dt>命令前缀</dt><dd id="i-prefix">-</dd>
169
- <dt>重连次数</dt><dd id="i-reconn">0</dd>
170
- <dt>事件统计</dt><dd id="i-events">-</dd>
171
- <dt>Node</dt><dd id="i-node">-</dd>
172
- </dl>
173
- <div class="btn-row">
174
- <button class="btn" id="btnReconnect">重连网关</button>
175
- <button class="btn" id="btnRefreshCred">刷新凭证</button>
176
- </div>
177
- </div>
178
- </div>
179
-
180
- <!--
181
- 暂停闸:唯一的刹车,所以放在「发送消息(调试)」之前。
182
-
183
- 放在概览而不是单开一页:暂停是个紧急操作,要能在打开面板的第一屏
184
- 就找到。藏在二级菜单里的刹车,出事时想不起来。
185
- -->
186
- <div class="box" id="gateBox">
187
- <div class="box-hd">
188
- <h3>暂停闸</h3>
189
- <span class="hint">关着就不对任何人说话。消息照样进账本,恢复后能补看</span>
190
- </div>
191
- <div class="form-inline">
192
- <button class="btn" id="btnGateGlobal">全局暂停</button>
193
- <button class="btn" id="btnGateBot">暂停这只鸟</button>
194
- <span class="hint" id="gateState">-</span>
195
- </div>
196
- <div class="hint" id="gateReasons" hidden></div>
197
- </div>
198
-
199
- <div class="box">
200
- <div class="box-hd"><h3>发送消息(调试)</h3></div>
201
- <div class="form-inline">
202
- <select id="sendScene"><option value="group">群聊</option><option value="c2c">私聊</option></select>
203
- <input id="sendTarget" placeholder="群 openid 或用户 openid">
204
- <input id="sendContent" placeholder="要发送的内容">
205
- <button class="btn primary" id="btnSend">发送</button>
206
- </div>
207
- <div class="hint" id="sendHint">主动消息有频次限制,仅建议调试用。</div>
208
- </div>
209
-
210
- <div class="box">
211
- <div class="box-hd">
212
- <h3>后台服务</h3>
213
- <span class="hint">健康巡检、文件清理、配置热加载、定时重启、检查更新</span>
214
- </div>
215
- <div id="serviceList" class="service-list">
216
- <div class="empty">加载中…</div>
217
- </div>
218
- </div>
219
-
220
- <!-- 体检:放最后。它是排查工具,不是每次都要看的指标 -->
221
- <div class="box" id="doctorBox">
222
- <div class="box-hd">
223
- <h3>体检</h3>
224
- <span class="hint" id="doctorSummary">点一下,看看哪里还没通</span>
225
- <button class="btn small" id="btnDoctor">检查</button>
226
- </div>
227
- <div id="doctorList" class="doctor-list" hidden></div>
228
- </div>
229
- </section>
230
-
231
- <!-- 身份:QQ号 / 群号 ↔ openid -->
232
- <section id="tab-identity" class="panel">
233
- <div class="box">
234
- <div class="box-hd">
235
- <h3>为什么要在这一页绑</h3>
236
- <span class="hint">openid 换机器人就变,号码不会</span>
237
- </div>
238
- <ul class="tips">
239
- <li>官方只给 <b>openid</b>,而且<b>每个机器人一套</b>——换 AppID 之后,同一个人在系统里就变成了另一个人,
240
- 记忆、权限、群档案全都对不上</li>
241
- <li><b>QQ号 / 群号</b>不会变,但官方<b>没有任何「号码 → openid」的查询接口</b>,
242
- 所以只能在这里人工指认一次</li>
243
- <li>绑好之后插件和 AI 都能用号码代替 openid;读旧记忆时,框架会把这个号码名下的
244
- <b>所有</b> openid 一起读进来</li>
245
- <li>一个号码可以挂多个 openid(换机器人用)。<b>解绑只影响当前这只鸟</b>,
246
- 旧记录留痕不删——它是读旧记忆唯一的线索</li>
247
- </ul>
248
- </div>
249
-
250
- <div class="box">
251
- <div class="box-hd">
252
- <h3>还没认出来的人</h3>
253
- <span class="hint" id="identUserHint">说过话、但还没绑 QQ号 的</span>
254
- <div class="btn-row" style="margin:0">
255
- <button class="btn small" id="btnIdentReload">刷新</button>
256
- </div>
257
- </div>
258
- <div class="table-wrap">
259
- <table>
260
- <thead>
261
- <tr>
262
- <th>用户</th>
263
- <th>活跃</th>
264
- <th>消息</th>
265
- <th>最近说过</th>
266
- <th style="width:200px">绑 QQ号</th>
267
- </tr>
268
- </thead>
269
- <tbody id="identUserBody">
270
- <tr><td colspan="5" class="empty">加载中…</td></tr>
271
- </tbody>
272
- </table>
273
- </div>
274
- <div class="hint" style="margin-top:8px">
275
- 列表按最近发言排序。前几条会显示「在哪个群说过什么」,方便认出是谁。
276
- </div>
277
- </div>
278
-
279
- <div class="box">
280
- <div class="box-hd">
281
- <h3>还没认出来的群</h3>
282
- <span class="hint" id="identGroupHint">机器人加入过、但还没绑群号的</span>
283
- </div>
284
- <div class="table-wrap">
285
- <table>
286
- <thead>
287
- <tr>
288
- <th>群</th>
289
- <th>最近活跃</th>
290
- <th style="width:200px">绑群号</th>
291
- </tr>
292
- </thead>
293
- <tbody id="identGroupBody">
294
- <tr><td colspan="3" class="empty">加载中…</td></tr>
295
- </tbody>
296
- </table>
297
- </div>
298
- </div>
299
-
300
- <div class="box">
301
- <div class="box-hd">
302
- <h3>已绑定</h3>
303
- <span class="hint" id="identBoundHint">这张表跨机器人共享,换机器人也认得出</span>
304
- </div>
305
- <div class="table-wrap">
306
- <table>
307
- <thead>
308
- <tr>
309
- <th>类型</th>
310
- <th>号码</th>
311
- <th>这只鸟下的 openid</th>
312
- <th>全部 openid</th>
313
- <th>最近</th>
314
- <th style="width:100px">操作</th>
315
- </tr>
316
- </thead>
317
- <tbody id="identBoundBody">
318
- <tr><td colspan="6" class="empty">还没有绑定</td></tr>
319
- </tbody>
320
- </table>
321
- </div>
322
- </div>
323
- </section>
324
-
325
- <!-- 通知 -->
326
- <section id="tab-notify" class="panel">
327
- <form id="notifyForm">
328
- <div class="box">
329
- <div class="box-hd">
330
- <h3>邮箱通知</h3>
331
- <label class="switch"><input type="checkbox" name="enabled"> 启用邮箱通知</label>
332
- </div>
333
- <div class="hint">
334
- 账号密码只保存在本机 <span class="mono">config/bot.json</span>,不会上传。
335
- QQ 邮箱 / 163 邮箱等需要用<b>授权码</b>而不是登录密码,并先在邮箱网页端开启 SMTP 服务。
336
- SMTP 用户名留空表示不做认证(内网中继)。
337
- </div>
338
- <div class="grid2">
339
- <label>SMTP 服务器<input name="host" placeholder="smtp.qq.com / smtp.163.com"></label>
340
- <label>SMTP 服务器端口<input name="port" type="number" min="1" max="65535" placeholder="一般为 587"></label>
341
- <label>SMTP 用户名<input name="user" placeholder="一般是完整邮箱地址"></label>
342
- <label>SMTP 密码 / 授权码<input name="pass" type="password" placeholder="留空则不修改"></label>
343
- <label>发件人邮箱<input name="from" placeholder="通常与登录账号一致"></label>
344
- <label>发件人姓名<input name="fromName" placeholder="栖"></label>
345
- <label>主人邮箱<input name="ownerEmail" placeholder="测试邮件发到这里"></label>
346
- </div>
347
- <div class="hint" id="notifySecurityHint">
348
- 加密方式默认「自动」:465 走隐式 TLS、587 走 STARTTLS、25 走明文,其它端口按服务器能力协商;
349
- 网易系(163 / 126 / yeah)的 587 会自动改走隐式 TLS。
350
- <span id="notifySecurityNow"></span>
351
- </div>
352
- <div class="cred-actions">
353
- <span class="hint" id="notifyMsg"></span>
354
- <button type="submit" class="btn primary">保存配置</button>
355
- </div>
356
- </div>
357
- </form>
358
-
359
- <div class="box">
360
- <div class="box-hd">
361
- <h3>测试邮件通知</h3>
362
- <span class="hint">发送一封测试邮件到「主人邮箱」</span>
363
- </div>
364
- <div class="cred-actions">
365
- <span class="hint" id="notifyTestMsg"></span>
366
- <button type="button" class="btn" id="btnNotifyTest">发送</button>
367
- </div>
368
- </div>
369
-
370
- <div class="box">
371
- <div class="box-hd"><h3>普通成员自助绑定</h3></div>
372
- <div class="hint">
373
- 成员发 <b>绑定QQ 自己的QQ号</b> → 验证码发到 <b>QQ号@qq.com</b> → 再发
374
- <b>QQ验证码 6位数字</b> 即完成绑定。收件邮箱由 QQ 号自动拼接,不需要在这里配置。
375
- QQ 号已被别人绑定时会直接拒绝;未开通数字邮箱的 QQ 号请找主人用「绑定成员」代绑。
376
- </div>
377
- </div>
378
- </section>
379
-
380
- <!-- 关于 -->
381
- <section id="tab-about" class="panel">
382
- <div class="box">
383
- <div class="about-hero">
384
- <span class="logo about-logo" aria-hidden="true">栖</span>
385
- <div>
386
- <div class="about-name">栖 Roost</div>
387
- <div class="hint">倦飞之鸟,终有一栖 · 零依赖的 QQ 官方机器人框架</div>
388
- <div class="hint">当前版本 v<span id="updCurrent">-</span></div>
389
- </div>
390
- </div>
391
- </div>
392
-
393
- <div class="box">
394
- <div class="box-hd">
395
- <h3>版本更新</h3>
396
- <div class="box-hd-right">
397
- <button class="btn small" id="btnCheckUpdate">检查更新</button>
398
- <button class="btn small primary" id="btnApplyUpdate" hidden>立即更新</button>
399
- </div>
400
- </div>
401
- <div class="hint" id="updMsg">
402
- 从 GitHub 检查新版本。更新只替换代码目录,<strong>配置、存档、日志、插件都不会动</strong>,更新后需重启程序。
403
- </div>
404
- </div>
405
-
406
- <div class="box">
407
- <div class="box-hd">
408
- <h3>联系方式</h3>
409
- <span class="hint">反馈问题、提建议都欢迎</span>
410
- </div>
411
- <dl class="kv">
412
- <dt>QQ</dt><dd class="mono">2014433751</dd>
413
- <dt>邮箱</dt><dd class="mono">jfzn9594@outlook.com</dd>
414
- <dt>项目主页</dt>
415
- <dd>
416
- <a href="https://github.com/tdcq9594/qq-bot-roost" target="_blank" rel="noopener">GitHub</a>
417
- ·
418
- <a href="https://gitee.com/tdcq9594/qq-bot-roost" target="_blank" rel="noopener">Gitee</a>
419
- </dd>
420
- </dl>
421
- </div>
422
-
423
- <div class="box">
424
- <div class="box-hd"><h3>许可</h3></div>
425
- <div class="hint">永久免费、禁止任何收费、禁止商业使用(source-available)。详见项目根目录 LICENSE。</div>
426
- </div>
427
- </section>
428
-
429
- <!-- 配置 -->
430
- <section id="tab-config" class="panel">
431
- <form id="cfgForm">
432
- <div class="box">
433
- <div class="box-hd"><h3>连接凭证</h3>
434
- <span class="hint">AppID 或 intents 变化会自动重连</span>
435
- </div>
436
- <div class="grid2">
437
- <label>AppID<input name="appId" placeholder="机器人 AppID"></label>
438
- <label>AppSecret<input name="appSecret" type="password" placeholder="留空则不修改"></label>
439
- <label>API 地址<input name="baseUrl"></label>
440
- <label>命令前缀<input name="commandPrefix" maxlength="3"></label>
441
- <label>收事件方式
442
- <select name="connectionMode">
443
- <option value="websocket">WebSocket 长连接(本地推荐)</option>
444
- <option value="webhook">Webhook 回调(公网 / Serverless)</option>
445
- </select>
446
- </label>
447
- </div>
448
- <div class="cred-actions">
449
- <span class="hint" id="cfgMsgTop"></span>
450
- <button type="submit" class="btn primary">保存配置</button>
451
- </div>
452
- <div class="full intents-block">
453
- <div class="intents-head">
454
- <span class="intents-title">事件订阅(intents)</span>
455
- <span class="hint">勾选/取消后<strong>立即生效</strong>,不用点保存。会短暂重连一次。</span>
456
- </div>
457
-
458
- <div class="intent-group">
459
- <div class="intent-group-title">群聊 / 私聊</div>
460
- <div id="intentGroupChat" class="intent-list"></div>
461
- </div>
462
-
463
- <div class="intent-group">
464
- <div class="intent-group-title">频道(guild)</div>
465
- <div id="intentGroupGuild" class="intent-list"></div>
466
- </div>
467
-
468
- <div class="intents-summary">
469
- <span>当前值 <b id="intentsValue">33554432</b></span>
470
- <span class="hint" id="intentsDesc">群聊与私聊</span>
471
- </div>
472
-
473
- <div class="btn-row">
474
- <button type="button" class="btn small" data-preset="chat">群聊 + 私聊(推荐)</button>
475
- <button type="button" class="btn small" data-preset="guild">频道基础</button>
476
- <button type="button" class="btn small" data-preset="all">全部基础</button>
477
- <button type="button" class="btn small" data-preset="none">清空</button>
478
- </div>
479
-
480
- <label class="switch small" style="margin-top:12px">
481
- <input type="checkbox" id="intentsAdvanced"> 高级:手动填写数值
482
- </label>
483
- <input name="intents" id="intentsManual" type="number" style="display:none;margin-top:8px">
484
-
485
- <div class="intent-warn" id="intentsWarn"></div>
486
- </div>
487
- </div>
488
-
489
- <div class="box">
490
- <div class="box-hd"><h3>行为控制</h3></div>
491
- <div class="grid2">
492
- <label class="switch"><input type="checkbox" name="whitelistEnabled"> 启用白名单</label>
493
- <label class="switch"><input type="checkbox" name="rateLimitEnabled"> 启用限流</label>
494
- <label>每分钟每会话上限<input name="perChatPerMinute" type="number" min="1"></label>
495
- <label>去重窗口(秒)<input name="dedupWindowSec" type="number" min="0"></label>
496
- </div>
497
- <div class="divider"></div>
498
- <div class="intent-group-title">消息日志</div>
499
- <label class="switch">
500
- <input type="checkbox" name="messageLogEnabled">
501
- 把收到的每一条消息写进日志(便于排查"为什么没反应")
502
- </label>
503
- <div class="mute-row">
504
- <span class="hint">屏蔽以下来源,不记录也不显示:</span>
505
- <label class="switch small"><input type="checkbox" name="muteGroup"> 群聊</label>
506
- <label class="switch small"><input type="checkbox" name="muteC2c"> 私聊</label>
507
- <label class="switch small"><input type="checkbox" name="muteGuild"> 频道</label>
508
- <label class="switch small"><input type="checkbox" name="muteDm"> 频道私信</label>
509
- </div>
510
-
511
- <label class="full">白名单群 openid(每行一个)
512
- <textarea name="whitelistGroups" rows="2"></textarea>
513
- </label>
514
- <label class="full">白名单用户 openid(每行一个)
515
- <textarea name="whitelistUsers" rows="2"></textarea>
516
- </label>
517
- </div>
518
-
519
- <div class="box">
520
- <div class="box-hd"><h3>群管能力</h3><span class="hint">禁言 / 撤回已开放;群成员与踢人需官方白名单</span></div>
521
- <label class="switch">
522
- <input type="checkbox" name="groupInvited">
523
- 我已申请到群管内邀资格(群成员列表 / 踢人 / 群黑名单)
524
- </label>
525
- <div class="hint" style="margin-top:8px">
526
- 官方群管理接口分两档:<b>禁言、撤回、群基本信息</b>对所有机器人开放;
527
- <b>群成员列表、批量移除(踢人)、群黑名单</b>只对白名单机器人开放,
528
- 没申请就调会返回 <code>11253</code>。<br>
529
- 所以这里默认关闭。去开放平台申请通过后再勾上,勾选后立即生效、不用重启。
530
- 没申请而勾上,只会让机器人每次调用都白跑一趟报错。
531
- </div>
532
- </div>
533
-
534
- <div class="box">
535
- <div class="box-hd"><h3>面板与日志</h3></div>
536
- <div class="grid2">
537
- <label>监听地址<input name="webuiHost" placeholder="127.0.0.1"></label>
538
- <label>端口<input name="webuiPort" type="number"></label>
539
- <label>面板访问密钥(留空则不校验;已设置时不会回显,想改就重填)<input name="webuiAccessKey" type="password" autocomplete="new-password"></label>
540
- <label>日志级别
541
- <select name="logLevel">
542
- <option value="debug">debug</option>
543
- <option value="info">info</option>
544
- <option value="warn">warn</option>
545
- <option value="error">error</option>
546
- </select>
547
- </label>
548
- </div>
549
- <div class="hint">修改监听地址或端口需要重启进程才生效。</div>
550
- </div>
551
-
552
- <div class="box">
553
- <div class="box-hd"><h3>插件来源校验</h3></div>
554
- <label class="switch">
555
- <input type="checkbox" name="trustEnforce">
556
- 强制校验插件市场签名
557
- <span class="beta-tag" title="签名文件刚铺开,还没经过一轮完整发版验证">Beta</span>
558
- <span class="hint">默认关闭</span>
559
- </label>
560
- <div class="hint" id="trustHint">
561
- 开启后,官方市场里未签名或签名不符的插件会被拒绝安装和更新;手写插件、以及你自己配置的市场源不受影响。
562
- 需要框架内置信任公钥——没内置时这个开关不起作用(所有插件照常放行)。
563
- </div>
564
- <div class="hint">
565
- <b>Beta:</b>签名文件还没跟着一次正式发版走完流程,现在打开可能让某些插件装不上。
566
- <b>建议先保持关闭</b>,等发版验证过一轮再考虑。
567
- </div>
568
- </div>
569
-
570
- <div class="sticky-actions">
571
- <span id="cfgMsg" class="hint"></span>
572
- <button type="submit" class="btn primary">保存配置</button>
573
- </div>
574
- </form>
575
- </section>
576
-
577
- <!-- 统计 -->
578
- <section id="tab-stats" class="panel">
579
- <div class="box">
580
- <div class="box-hd">
581
- <h3>消息统计</h3>
582
- <div class="plugin-tools">
583
- <select id="statsDays" class="mini-select" aria-label="统计范围">
584
- <option value="7">最近 7 天</option>
585
- <option value="14">最近 14 天</option>
586
- <option value="30">最近 30 天</option>
587
- </select>
588
- <button class="btn small" id="btnRefreshStats">刷新</button>
589
- <button class="btn small" id="btnExportStats" title="导出汇总 CSV(不含消息正文)">导出</button>
590
- </div>
591
- </div>
592
- <div id="statsUnavailable" class="empty" hidden></div>
593
- <div class="cards" id="statsCards"></div>
594
- <div class="stat-chart" id="statsChart"></div>
595
- <div class="hint" id="statsFoot"></div>
596
- </div>
597
-
598
- <div class="row">
599
- <div class="box grow">
600
- <div class="box-hd">
601
- <h3>命令排行</h3>
602
- <span class="hint">按调用次数</span>
603
- </div>
604
- <div id="statsTopCommands" class="stat-list"></div>
605
- </div>
606
- <div class="box grow">
607
- <div class="box-hd">
608
- <h3>会话排行</h3>
609
- <span class="hint">按消息数</span>
610
- </div>
611
- <div id="statsTopChats" class="stat-list"></div>
612
- </div>
613
- </div>
614
- </section>
615
-
616
- <!-- 插件 -->
617
- <section id="tab-plugins" class="panel">
618
- <div class="box">
619
- <div class="box-hd">
620
- <h3>已安装插件</h3>
621
- <div class="plugin-tools">
622
- <span class="hint">往 plugins/ 里放了新插件,点「刷新」即可加载,不用重启</span>
623
- <button class="btn small" id="btnRescanPlugins">刷新</button>
624
- <button class="btn small" id="btnLoadMarket">插件市场</button>
625
- </div>
626
- </div>
627
- <div class="upd-tip" id="pluginUpdateTip"></div>
628
- <div id="pluginList" class="plugin-list"></div>
629
- </div>
630
-
631
- <div class="box" id="marketBox" hidden>
632
- <div class="box-hd">
633
- <h3>插件市场</h3>
634
- <div class="plugin-tools">
635
- <span class="hint" id="marketMeta">加载中…</span>
636
- <button class="btn small" id="btnRefreshMarket">重新获取</button>
637
- </div>
638
- </div>
639
-
640
- <!-- 下载源:国内直连 GitHub 经常超时,这里可以换镜像 -->
641
- <div class="mirror-bar">
642
- <span class="hint">下载源</span>
643
- <select id="mirrorSelect" class="mini-select" aria-label="下载源"></select>
644
- <input
645
- id="mirrorCustom"
646
- class="mini-input"
647
- placeholder="自定义前缀,如 https://gh.example/"
648
- hidden
649
- />
650
- <input
651
- id="mirrorGiteeToken"
652
- class="mini-input"
653
- type="password"
654
- placeholder="Gitee 私人令牌(选填,留空走匿名)"
655
- hidden
656
- />
657
- <button class="btn small" id="btnMirrorTest" title="并发测一遍,按你这边实际网络选">测速</button>
658
- <span class="hint" id="mirrorTip"></span>
659
- </div>
660
-
661
- <!-- 插件一多就得能找 -->
662
- <div class="market-filter">
663
- <input id="marketSearch" class="mini-input" placeholder="搜索插件名 / 描述 / 标签" />
664
- <select id="marketCat" class="mini-select" aria-label="分类"></select>
665
- </div>
666
-
667
- <div id="marketList" class="plugin-list"></div>
668
- </div>
669
- </section>
670
-
671
- <!-- 事件 -->
672
- <section id="tab-events" class="panel">
673
- <div class="box">
674
- <div class="box-hd">
675
- <h3>事件订阅与接收</h3>
676
- <span class="hint">
677
- 官方事件全部可接收并转交插件。灰掉的是当前 intents 没订阅,
678
- 去「配置 → 事件订阅」勾上才会推送
679
- </span>
680
- </div>
681
- <div id="eventSummary" class="event-summary"></div>
682
- <div class="event-legend">
683
- <span><b class="ev-dot on"></b>已订阅</span>
684
- <span><b class="ev-dot off"></b>未订阅</span>
685
- <span><b class="ev-dot hit"></b>收到过</span>
686
- </div>
687
- <div id="eventGroups" class="event-groups"></div>
688
- </div>
689
- </section>
690
-
691
- <!-- 投递账本 -->
692
- <section id="tab-delivery" class="panel">
693
- <div class="box">
694
- <div class="box-hd">
695
- <h3>投递账本</h3>
696
- <span class="hint">带幂等键的消息发没发出去,这里查得到</span>
697
- </div>
698
- <ul class="tips">
699
- <li><b>已发送</b>确认送达;<b>审核中</b>是主动消息进了审核——既不是成功也不是失败,
700
- 等审核事件回来才会落定</li>
701
- <li><b>结果未知</b>:超时或进程崩了,我们<b>不知道服务端收没收到</b>。
702
- 这种<b>不会自动重发</b>——宁可让人核对一次,也不重复打扰</li>
703
- <li><b>失败</b>:确定没发出去(离线 / 超窗 / 额度 / 限流),可以重试</li>
704
- <li>只有<b>带幂等键</b>的发送才记账(`qq_send` 那类「一次调用 = 一段话」的)。
705
- 插件直接回一句话不记账,靠 `msg_seq` 天然去重</li>
706
- </ul>
707
- <div class="cards" id="deliveryStats"></div>
708
- <div class="table-wrap">
709
- <table>
710
- <thead>
711
- <tr><th>时间</th><th>会话</th><th>状态</th><th>摘要</th><th class="num">重试</th><th>原因码</th></tr>
712
- </thead>
713
- <tbody id="deliveryBody"><tr><td colspan="6" class="empty">暂无数据</td></tr></tbody>
714
- </table>
715
- </div>
716
- </div>
717
- </section>
718
-
719
- <!-- 可达性 -->
720
- <section id="tab-reach" class="panel">
721
- <div class="box">
722
- <div class="box-hd">
723
- <h3>能不能主动找到人</h3>
724
- <span class="hint">两道门:先得是好友,还得开着主动推送</span>
725
- </div>
726
- <ul class="tips">
727
- <li><b>好友</b>是用户把机器人加为好友;<b>主动推送</b>是用户在机器人资料卡里单独开的开关——
728
- <b>有好友也可能被关掉</b>,关了就发不出去</li>
729
- <li>官方<b>没有查询好友的接口</b>,这里的值全部来自事件推送。机器人启动之前就存在的老关系拿不到事件,
730
- 会一直是「未知」</li>
731
- <li><b>未知不等于不可达</b>——未知会照常尝试发送,失败了才记下来。把未知当成「不能」,
732
- 所有老用户就都被静音了</li>
733
- </ul>
734
- </div>
735
-
736
- <div class="box">
737
- <div class="box-hd">
738
- <h3>单聊</h3>
739
- <div class="btn-row" style="margin:0">
740
- <button class="btn small" id="btnReloadReach">刷新</button>
741
- </div>
742
- </div>
743
- <div class="table-wrap">
744
- <table>
745
- <thead>
746
- <tr>
747
- <th>用户</th>
748
- <th>好友</th>
749
- <th>主动推送</th>
750
- <th>结论</th>
751
- <th>依据</th>
752
- <th>更新</th>
753
- </tr>
754
- </thead>
755
- <tbody id="reachUserBody">
756
- <tr><td colspan="6" class="empty">还没有记录。收到私聊消息、或被加好友之后才会出现。</td></tr>
757
- </tbody>
758
- </table>
759
- </div>
760
- <div class="hint" style="margin-top:8px">
761
- 每行的两个下拉框就是<b>人工标注</b>,改完立刻生效并覆盖事件推断的值;选「未知」等于撤销标注。
762
- </div>
763
- <div class="form-inline" style="margin-top:12px">
764
- <input id="reachOpenidInput" placeholder="openid(列表里没有的人可以直接登记)">
765
- <select id="reachFriendSel" class="mini-select">
766
- <option value="">好友:未知</option>
767
- <option value="1">好友:是</option>
768
- <option value="0">好友:否</option>
769
- </select>
770
- <select id="reachPushSel" class="mini-select">
771
- <option value="">推送:未知</option>
772
- <option value="1">推送:开</option>
773
- <option value="0">推送:关</option>
774
- </select>
775
- <button class="btn primary" id="btnReachSave">登记</button>
776
- </div>
777
- </div>
778
-
779
- <div class="box">
780
- <div class="box-hd">
781
- <h3>群主动推送</h3>
782
- <span class="hint">群管理员可以整个群关掉机器人的主动消息</span>
783
- </div>
784
- <div class="table-wrap">
785
- <table>
786
- <thead>
787
- <tr>
788
- <th>群</th>
789
- <th>主动推送</th>
790
- <th>操作人</th>
791
- <th>依据</th>
792
- <th>更新</th>
793
- </tr>
794
- </thead>
795
- <tbody id="reachGroupBody">
796
- <tr><td colspan="5" class="empty">还没有记录。群管理员改过推送开关之后才会出现。</td></tr>
797
- </tbody>
798
- </table>
799
- </div>
800
- <div class="hint" style="margin-top:8px">
801
- 没记录过的群按「未知」处理,照常尝试推送。
802
- </div>
803
- </div>
804
- </section>
805
-
806
- <!-- 群组 -->
807
- <section id="tab-groups" class="panel">
808
- <div class="box">
809
- <div class="box-hd">
810
- <h3>机器人主人</h3>
811
- <span class="hint">主人可以使用全部管理命令</span>
812
- </div>
813
- <div id="ownerChips" class="chips"></div>
814
- <div class="form-inline" style="margin-top:12px">
815
- <input id="ownerInput" placeholder="填 openid,回车添加">
816
- <button class="btn primary" id="btnAddOwner">添加</button>
817
- </div>
818
- <div class="hint" style="margin-top:8px">
819
- 不知道 openid?在群里 @机器人 发 /whoami,或用上面的「活跃会话」看。
820
- </div>
821
- </div>
822
-
823
- <div class="box">
824
- <div class="box-hd"><h3>入群后自动授予的权限</h3></div>
825
- <ul class="tips">
826
- <li><b>邀请人</b> → 该群管理员(1 级),只影响这一个群</li>
827
- <li><b>群主</b> → 该群管理员(1 级)。群主发过言才能识别(报文里 member_role=owner)</li>
828
- <li>两者都<b>不会</b>获得全局主人或全局管理员</li>
829
- </ul>
830
- <div class="hint" style="margin-top:8px">
831
- 要调整或撤销,去「权限」页改对应的 <code>group:xxx</code> 作用域。
832
- 全局主人只能在 config.json 的 owners 里配,或首次启动时用认领码认领。
833
- </div>
834
- </div>
835
-
836
- <div class="box">
837
- <div class="box-hd">
838
- <h3>手动登记群</h3>
839
- <span class="hint">官方没有「列出机器人所在的群」的接口,群只能靠事件发现</span>
840
- </div>
841
- <ul class="tips">
842
- <li>机器人被拉进群、或在群里被 @ 过一次,下面列表里就会自动出现</li>
843
- <li><b>一直不出现</b>:多半是没开「群聊与私聊」intent——去概览页点「检查」能看到</li>
844
- <li>已经知道群 openid 的话,可以手动登记,方便提前配 <code>group:xxx</code> 权限</li>
845
- </ul>
846
- <div class="form-inline" style="margin-top:12px">
847
- <input id="groupOpenidInput" placeholder="群 openid" />
848
- <input id="groupNoteInput" placeholder="备注(可选)" style="width:200px" />
849
- <button class="btn primary" id="btnAddGroup">登记</button>
850
- </div>
851
- </div>
852
-
853
- <div class="box">
854
- <div class="box-hd">
855
- <h3>群列表</h3>
856
- <div class="btn-row" style="margin:0">
857
- <button class="btn small" id="btnReloadGroups">刷新</button>
858
- </div>
859
- </div>
860
- <div class="table-wrap">
861
- <table>
862
- <thead>
863
- <tr>
864
- <th>群 openid</th>
865
- <th>邀请者</th>
866
- <th>群主(识别)</th>
867
- <th>状态</th>
868
- <th>最后活跃</th>
869
- <th class="num">操作</th>
870
- </tr>
871
- </thead>
872
- <tbody id="groupBody">
873
- <tr><td colspan="6" class="empty">还没有群。把机器人拉进群后这里会自动出现。</td></tr>
874
- </tbody>
875
- </table>
876
- </div>
877
- <div class="hint" style="margin-top:8px">
878
- 这里只显示群档案。谁能在这个群做什么,统一在「权限」页按
879
- <code>group:&lt;群openid&gt;</code> 作用域配置。
880
- </div>
881
-
882
- <div class="divider"></div>
883
-
884
- <div class="hint" style="margin-bottom:8px">
885
- <b>看不到你的群?</b>官方没有「查询机器人所在群列表」的接口,
886
- 群只能靠事件被动发现。请按顺序排查:
887
- </div>
888
- <ol class="tips">
889
- <li>在群里 <b>@机器人</b> 发一句话(不 @ 不会推送消息)</li>
890
- <li>确认这个群在开放平台「沙箱配置」里(开发中的机器人只收沙箱群消息)</li>
891
- <li>确认机器人是在它<b>运行期间</b>被拉进群的——离线时入群的事件会丢</li>
892
- <li>看上面「事件统计」:全是 0 说明平台一条都没推过来</li>
893
- </ol>
894
- <div class="form-inline" style="margin-top:12px">
895
- <input id="groupInput" placeholder="已知群 openid 时可直接登记">
896
- <button class="btn" id="btnAddGroupManual">手动登记</button>
897
- </div>
898
- </div>
899
- </section>
900
-
901
- <!-- 权限 -->
902
- <section id="tab-roles" class="panel">
903
- <div class="box">
904
- <div class="box-hd">
905
- <h3>权限等级</h3>
906
- <span class="hint">作用域决定「在哪管」,等级决定「能管谁」;数字越小权限越高,只能授予数字更大的等级</span>
907
- </div>
908
- <div class="perm-legend">
909
- <span class="hint" style="margin-right:6px">数字越小权限越高:</span>
910
- <span><b>0</b> 主人(config.json)</span>
911
- <span><b>1</b> 管理员</span>
912
- <span><b>2</b> 群管</span>
913
- <span><b>3</b> 助手</span>
914
- <span><b>4-9</b> 框架预留</span>
915
- <span><b>10+</b> 插件区</span>
916
- <span><b>9999</b> 无权限</span>
917
- </div>
918
- <div class="perm-legend" style="margin-top:6px">
919
- <span><b>global</b> 全局</span>
920
- <span><b>group:xxx</b> 某个群</span>
921
- <span><b>channel:xxx</b> 频道</span>
922
- <span><b>user:xxx</b> 私聊</span>
923
- <span><b>feature:插件</b> 功能</span>
924
- </div>
925
- </div>
926
-
927
- <div class="box">
928
- <div class="box-hd"><h3>添加 / 修改</h3></div>
929
- <div class="form-inline">
930
- <input id="roleScope" list="scopeList" placeholder="作用域,如 group:xxx / global">
931
- <datalist id="scopeList"></datalist>
932
- <input id="roleOpenid" placeholder="openid">
933
- <input id="roleLevel" type="number" min="1" max="998" value="2"
934
- style="width:120px" title="数字越小权限越高">
935
- <span class="hint">等级(1 管理员 / 2 群管 / 3 助手 / 10+ 插件区,最大 998)</span>
936
- <button class="btn primary" id="btnAddRole">保存</button>
937
- </div>
938
- <div class="hint" style="margin-top:8px">
939
- 面板操作视为本机管理员行为,不受等级限制;聊天里请用 /admin set。
940
- </div>
941
- </div>
942
-
943
- <div class="box">
944
- <div class="box-hd">
945
- <h3>最近说过话的人</h3>
946
- <span class="hint">点一下自动填好 openid 和作用域,不用去日志里翻</span>
947
- <button class="btn small" id="btnReloadPeople">刷新</button>
948
- </div>
949
- <div id="peopleList" class="people-list">
950
- <div class="empty">还没有收到消息。让要授权的人在群里说句话,这里就会出现。</div>
951
- </div>
952
- </div>
953
-
954
- <div class="box">
955
- <div class="box-hd">
956
- <h3>已授权列表</h3>
957
- <button class="btn small" id="btnReloadRoles">刷新</button>
958
- </div>
959
- <div class="table-wrap">
960
- <table>
961
- <thead><tr><th>作用域</th><th>openid</th><th>等级</th><th class="num">操作</th></tr></thead>
962
- <tbody id="roleBody"><tr><td colspan="4" class="empty">还没有配置权限</td></tr></tbody>
963
- </table>
964
- </div>
965
- </div>
966
- </section>
967
-
968
- <!-- 日志 -->
969
- <section id="tab-logs" class="panel">
970
- <div class="box">
971
- <div class="box-hd">
972
- <h3>实时日志</h3>
973
- <span class="hint">按来源和关键词筛选,不影响实时接收</span>
974
- </div>
975
- <div class="log-toolbar">
976
- <input id="logSearch" class="log-search" placeholder="搜索日志" autocomplete="off">
977
- <select id="logSourceFilter">
978
- <option value="all">全部来源</option>
979
- <option value="core">框架</option>
980
- <option value="net">网关 / 接口</option>
981
- <option value="plugin">插件</option>
982
- </select>
983
- <select id="logLevelFilter">
984
- <option value="debug">全部</option>
985
- <option value="info">info 及以上</option>
986
- <option value="warn">warn 及以上</option>
987
- <option value="error">仅 error</option>
988
- </select>
989
- <label class="switch small"><input type="checkbox" id="autoScroll" checked> 自动滚动</label>
990
- <button class="btn small" id="btnClearLog">清屏</button>
991
- </div>
992
- <div class="log-legend">
993
- <span><b class="lg-dot in"></b>收到消息</span>
994
- <span><b class="lg-dot ok"></b>成功 / 已就绪</span>
995
- <span><b class="lg-dot err"></b>失败 / 错误</span>
996
- <span><b class="lg-dot cmd"></b>指令</span>
997
- <span class="lg-sep"></span>
998
- <span><b class="lg-chip info">INFO</b>普通</span>
999
- <span><b class="lg-chip warn">WARN</b>警告</span>
1000
- <span><b class="lg-chip error">ERROR</b>错误</span>
1001
- <span class="lg-sep"></span>
1002
- <span class="hint">模块标签按来源上色:gateway / api / core / plugins / roles …</span>
1003
- </div>
1004
- <div id="logView" class="log-view"></div>
1005
- </div>
1006
- </section>
1007
- </main>
1008
-
1009
- <!-- 通用确认弹窗 -->
1010
- <div class="modal-mask" id="modalMask" hidden>
1011
- <div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
1012
- <h3 class="modal-title" id="modalTitle">确认</h3>
1013
- <div class="modal-body" id="modalBody"></div>
1014
- <div class="modal-actions">
1015
- <button class="btn" id="modalCancel">取消</button>
1016
- <button class="btn danger" id="modalOk">确定</button>
1017
- </div>
1018
- </div>
1019
- </div>
1020
-
1021
- <!-- 分群插件开关:这个群能用哪些插件 -->
1022
- <div class="modal-mask" id="gplugMask" hidden>
1023
- <div class="modal wide" role="dialog" aria-modal="true" aria-labelledby="gplugTitle">
1024
- <h3 class="modal-title" id="gplugTitle">分群插件开关</h3>
1025
- <div class="hint" id="gplugSub" style="margin-bottom:10px"></div>
1026
-
1027
- <div class="gplug-mode">
1028
- <label class="gplug-radio">
1029
- <input type="radio" name="gplugMode" value="global" checked>
1030
- <span>跟随全局<em id="gplugGlobalHint"></em></span>
1031
- </label>
1032
- <label class="gplug-radio">
1033
- <input type="radio" name="gplugMode" value="custom">
1034
- <span>本群单独配置</span>
1035
- </label>
1036
- </div>
1037
-
1038
- <div class="gplug-list" id="gplugList" hidden></div>
1039
-
1040
- <div class="modal-actions">
1041
- <button class="btn" id="gplugCancel">取消</button>
1042
- <button class="btn primary" id="gplugSave">保存</button>
1043
- </div>
1044
- </div>
1045
- </div>
1046
-
1047
- <!-- 新建机器人 -->
1048
- <div class="modal-mask" id="newBotMask" hidden>
1049
- <div class="modal" role="dialog" aria-modal="true" aria-labelledby="newBotTitle">
1050
- <h3 class="modal-title" id="newBotTitle">新建机器人</h3>
1051
-
1052
- <!-- 扫码一键创建:手机 QQ 扫一下,AppID / AppSecret 自动填好 -->
1053
- <div class="nb-qr-wrap">
1054
- <button class="btn primary" id="nbQrStart" type="button">扫码一键创建</button>
1055
- <i class="nb-qr-hint">推荐:不用去 q.qq.com 手动建机器人</i>
1056
- </div>
1057
- <div class="nb-qr" id="nbQr" hidden>
1058
- <div class="nb-qr-code" id="nbQrCode"></div>
1059
- <div class="nb-qr-tip" id="nbQrTip"></div>
1060
- </div>
1061
-
1062
- <div class="nb-or">或者手动填(已有机器人、或扫码用不了时)</div>
1063
-
1064
- <div class="bot-form">
1065
- <label>
1066
- <span>ID</span>
1067
- <input id="nbId" placeholder="main-bot,小写字母数字短横线" autocomplete="off">
1068
- <i>用来做文件夹名,创建后不能改</i>
1069
- </label>
1070
- <label>
1071
- <span>显示名</span>
1072
- <input id="nbName" placeholder="留空则用 ID" autocomplete="off">
1073
- </label>
1074
- <label>
1075
- <span>AppID</span>
1076
- <input id="nbAppId" placeholder="q.qq.com 开发设置里的 AppID" autocomplete="off">
1077
- </label>
1078
- <label>
1079
- <span>AppSecret</span>
1080
- <input id="nbSecret" type="password" placeholder="AppSecret" autocomplete="off">
1081
- </label>
1082
- <div class="bot-form-err" id="nbErr" hidden></div>
1083
- </div>
1084
- <div class="modal-actions">
1085
- <button class="btn" id="nbCancel">取消</button>
1086
- <button class="btn primary" id="nbOk">创建并连接</button>
1087
- </div>
1088
- </div>
1089
- </div>
1090
-
1091
- <!-- 插件设置:配置 + 玩家数据 -->
1092
- <div class="modal-mask" id="pluginPanelMask" hidden>
1093
- <div class="modal wide" role="dialog" aria-modal="true" aria-labelledby="ppTitle">
1094
- <h3 class="modal-title" id="ppTitle">插件设置</h3>
1095
- <div class="pp-tabs">
1096
- <button type="button" class="pp-tab active" data-pp="config">配置</button>
1097
- <button type="button" class="pp-tab" data-pp="data">玩家数据</button>
1098
- <button type="button" class="pp-tab" data-pp="dash">运行数据</button>
1099
- <button type="button" class="pp-tab" data-pp="groups">分群开关</button>
1100
- </div>
1101
- <div id="ppConfig" class="pp-body"></div>
1102
- <div id="ppData" class="pp-body" hidden></div>
1103
- <div id="ppDash" class="pp-body" hidden></div>
1104
- <div id="ppGroups" class="pp-body" hidden></div>
1105
- <div class="modal-actions">
1106
- <button class="btn" id="ppClose">关闭</button>
1107
- <button class="btn danger" id="ppReset" title="把所有配置项恢复成插件声明的默认值">恢复默认</button>
1108
- <button class="btn primary" id="ppSave">保存配置</button>
1109
- </div>
1110
- </div>
1111
- </div>
1112
-
1113
- <script src="/vendor/qrcode.js"></script>
1114
- <script src="/app.js"></script>
1115
- </body>
1116
- </html>
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">
6
+ <title>栖 Roost · 控制台</title>
7
+ <!-- 主题脚本必须外置:CSP 的 script-src 'self' 会拦掉内联脚本 -->
8
+ <script src="/theme-boot.js"></script>
9
+ <link rel="stylesheet" href="/style.css">
10
+ </head>
11
+ <body>
12
+
13
+ <header class="topbar">
14
+ <div class="brand" title="栖 Roost · 倦飞之鸟,终有一栖">
15
+ <span class="logo" aria-hidden="true">栖</span>
16
+ <div>
17
+ <div class="title">栖<span class="brand-en">Roost</span></div>
18
+ <div class="sub" id="botName">未连接</div>
19
+ </div>
20
+ </div>
21
+ <div class="bot-switch" id="botSwitch">
22
+ <button class="bot-trigger" id="botTrigger" aria-haspopup="listbox" aria-expanded="false">
23
+ <span class="bot-dot" id="botDot"></span>
24
+ <span class="bot-name" id="botNameLabel">加载中…</span>
25
+ <svg class="bot-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor"
26
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
27
+ <path d="M6 9l6 6 6-6"/>
28
+ </svg>
29
+ </button>
30
+ <div class="bot-menu" id="botMenu" hidden role="listbox">
31
+ <input class="bot-search" id="botSearch" placeholder="搜索机器人" hidden>
32
+ <div class="bot-list" id="botList"></div>
33
+ <div class="bot-empty" id="botEmpty" hidden>没有匹配的机器人</div>
34
+ <div class="bot-menu-foot">
35
+ <button type="button" id="botAddBtn">
36
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
37
+ <path d="M12 5v14M5 12h14"/>
38
+ </svg>
39
+ 新建机器人
40
+ </button>
41
+ </div>
42
+ </div>
43
+ </div>
44
+
45
+ <nav class="tabs">
46
+ <button class="tab active" data-tab="overview">概览</button>
47
+
48
+ <div class="tab-group" data-group="data">
49
+ <button class="tab tab-parent" aria-haspopup="true" aria-expanded="false">
50
+ 数据
51
+ <svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor"
52
+ stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
53
+ <path d="M6 9l6 6 6-6"/>
54
+ </svg>
55
+ </button>
56
+ <div class="tab-menu" hidden>
57
+ <button class="tab-item" data-tab="stats">统计</button>
58
+ <button class="tab-item" data-tab="events">事件</button>
59
+ <button class="tab-item" data-tab="reach">可达性</button>
60
+ <button class="tab-item" data-tab="delivery">投递</button>
61
+ <button class="tab-item" data-tab="ledger">账本</button>
62
+ <button class="tab-item" data-tab="logs">日志</button>
63
+ </div>
64
+ </div>
65
+
66
+ <div class="tab-group" data-group="settings">
67
+ <button class="tab tab-parent" aria-haspopup="true" aria-expanded="false">
68
+ 设置
69
+ <svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor"
70
+ stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
71
+ <path d="M6 9l6 6 6-6"/>
72
+ </svg>
73
+ </button>
74
+ <div class="tab-menu" hidden>
75
+ <button class="tab-item" data-tab="config">配置</button>
76
+ <button class="tab-item" data-tab="groups">群组</button>
77
+ <button class="tab-item" data-tab="roles">权限</button>
78
+ </div>
79
+ </div>
80
+
81
+ <div class="tab-group" data-group="plugins">
82
+ <button class="tab tab-parent" aria-haspopup="true" aria-expanded="false">
83
+ 插件
84
+ <svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor"
85
+ stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
86
+ <path d="M6 9l6 6 6-6"/>
87
+ </svg>
88
+ </button>
89
+ <div class="tab-menu" hidden>
90
+ <button class="tab-item" data-tab="plugins">插件管理</button>
91
+ <!-- 插件注册的页面挂到这里,见 loadPluginPages() -->
92
+ <div class="tab-menu-sep" id="pluginPageSep" hidden></div>
93
+ <div class="tab-menu-label" id="pluginPageLabel" hidden>插件页面</div>
94
+ <div class="tab-menu-sub" id="pluginPageItems"></div>
95
+ </div>
96
+ </div>
97
+
98
+ <!-- 身份:QQ号 / 群号 ↔ openid。常用页而不是设置项 -->
99
+ <button class="tab" data-tab="identity">身份</button>
100
+
101
+ <!-- 通知:SMTP 邮箱配置 + 普通成员自助绑定。字段参考 BetterGI「设置 → 通知」 -->
102
+ <button class="tab" data-tab="notify">通知</button>
103
+
104
+ <button class="tab" data-tab="about">关于</button>
105
+ </nav>
106
+ <button class="icon-btn" id="btnTheme" title="切换日间 / 夜间" aria-label="切换主题">
107
+ <svg class="ic-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
108
+ stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
109
+ <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/>
110
+ </svg>
111
+ <svg class="ic-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor"
112
+ stroke-width="1.8" stroke-linecap="round">
113
+ <circle cx="12" cy="12" r="4.2"/>
114
+ <path d="M12 2.5v2M12 19.5v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M2.5 12h2M19.5 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/>
115
+ </svg>
116
+ </button>
117
+
118
+ <button class="icon-btn icon-btn-danger" id="btnShutdown" title="关闭程序" aria-label="关闭程序">
119
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
120
+ stroke-width="1.8" stroke-linecap="round">
121
+ <path d="M12 3.2v8.4"/>
122
+ <path d="M6.6 6.6a8 8 0 1 0 10.8 0"/>
123
+ </svg>
124
+ </button>
125
+
126
+ <button class="status-pill" id="connPill" title="点击上线 / 下线">
127
+ <span class="dot" id="connDot"></span>
128
+ <span id="connText">连接中</span>
129
+ <span class="pill-sep"></span>
130
+ <span id="connAction">上线</span>
131
+ </button>
132
+ </header>
133
+
134
+ <main>
135
+ <!-- 概览 -->
136
+ <section id="tab-overview" class="panel active">
137
+ <div class="cards">
138
+ <div class="card"><div class="k">连接状态</div><div class="v" id="c-conn">-</div></div>
139
+ <div class="card"><div class="k">运行时长</div><div class="v" id="c-uptime">-</div></div>
140
+ <div class="card"><div class="k">凭证有效期</div><div class="v" id="c-cred">-</div></div>
141
+ <div class="card"><div class="k">内存占用</div><div class="v" id="c-mem">-</div></div>
142
+ <div class="card"><div class="k">收到消息</div><div class="v" id="c-recv">0</div></div>
143
+ <div class="card"><div class="k">发出消息</div><div class="v" id="c-sent">0</div></div>
144
+ <div class="card"><div class="k">命令命中</div><div class="v" id="c-cmd">0</div></div>
145
+ <div class="card"><div class="k">错误 / 拦截</div><div class="v" id="c-err">0</div></div>
146
+ </div>
147
+
148
+ <div class="row">
149
+ <div class="box grow">
150
+ <div class="box-hd">
151
+ <h3>活跃会话</h3>
152
+ <span class="hint">按消息数排序,最多 20 条</span>
153
+ </div>
154
+ <div class="table-wrap">
155
+ <table>
156
+ <thead><tr><th>openid</th><th>类型</th><th class="num">消息数</th><th>最后活跃</th></tr></thead>
157
+ <tbody id="chatBody"><tr><td colspan="4" class="empty">暂无数据</td></tr></tbody>
158
+ </table>
159
+ </div>
160
+ </div>
161
+
162
+ <div class="box">
163
+ <div class="box-hd"><h3>运行信息</h3></div>
164
+ <dl class="kv">
165
+ <dt>机器人 ID</dt><dd id="i-botid">-</dd>
166
+ <dt>网关</dt><dd id="i-gw" class="mono">-</dd>
167
+ <dt>会话 ID</dt><dd id="i-session" class="mono">-</dd>
168
+ <dt>intents</dt><dd id="i-intents">-</dd>
169
+ <dt>命令前缀</dt><dd id="i-prefix">-</dd>
170
+ <dt>重连次数</dt><dd id="i-reconn">0</dd>
171
+ <dt>事件统计</dt><dd id="i-events">-</dd>
172
+ <dt>Node</dt><dd id="i-node">-</dd>
173
+ </dl>
174
+ <div class="btn-row">
175
+ <button class="btn" id="btnReconnect">重连网关</button>
176
+ <button class="btn" id="btnRefreshCred">刷新凭证</button>
177
+ </div>
178
+ </div>
179
+ </div>
180
+
181
+ <!--
182
+ 暂停闸:唯一的刹车,所以放在「发送消息(调试)」之前。
183
+
184
+ 放在概览而不是单开一页:暂停是个紧急操作,要能在打开面板的第一屏
185
+ 就找到。藏在二级菜单里的刹车,出事时想不起来。
186
+ -->
187
+ <div class="box" id="gateBox">
188
+ <div class="box-hd">
189
+ <h3>暂停闸</h3>
190
+ <span class="hint">关着就不对任何人说话。消息照样进账本,恢复后能补看</span>
191
+ </div>
192
+ <div class="form-inline">
193
+ <button class="btn" id="btnGateGlobal">全局暂停</button>
194
+ <button class="btn" id="btnGateBot">暂停这只鸟</button>
195
+ <span class="hint" id="gateState">-</span>
196
+ </div>
197
+ <div class="hint" id="gateReasons" hidden></div>
198
+ </div>
199
+
200
+ <div class="box">
201
+ <div class="box-hd"><h3>发送消息(调试)</h3></div>
202
+ <div class="form-inline">
203
+ <select id="sendScene"><option value="group">群聊</option><option value="c2c">私聊</option></select>
204
+ <input id="sendTarget" placeholder="群 openid 或用户 openid">
205
+ <input id="sendContent" placeholder="要发送的内容">
206
+ <button class="btn primary" id="btnSend">发送</button>
207
+ </div>
208
+ <div class="hint" id="sendHint">主动消息有频次限制,仅建议调试用。</div>
209
+ </div>
210
+
211
+ <div class="box">
212
+ <div class="box-hd">
213
+ <h3>后台服务</h3>
214
+ <span class="hint">健康巡检、文件清理、配置热加载、定时重启、检查更新</span>
215
+ </div>
216
+ <div id="serviceList" class="service-list">
217
+ <div class="empty">加载中…</div>
218
+ </div>
219
+ </div>
220
+
221
+ <!-- 体检:放最后。它是排查工具,不是每次都要看的指标 -->
222
+ <div class="box" id="doctorBox">
223
+ <div class="box-hd">
224
+ <h3>体检</h3>
225
+ <span class="hint" id="doctorSummary">点一下,看看哪里还没通</span>
226
+ <button class="btn small" id="btnDoctor">检查</button>
227
+ </div>
228
+ <div id="doctorList" class="doctor-list" hidden></div>
229
+ </div>
230
+ </section>
231
+
232
+ <!-- 身份:QQ号 / 群号 ↔ openid -->
233
+ <section id="tab-identity" class="panel">
234
+ <div class="box">
235
+ <div class="box-hd">
236
+ <h3>为什么要在这一页绑</h3>
237
+ <span class="hint">openid 换机器人就变,号码不会</span>
238
+ </div>
239
+ <ul class="tips">
240
+ <li>官方只给 <b>openid</b>,而且<b>每个机器人一套</b>——换 AppID 之后,同一个人在系统里就变成了另一个人,
241
+ 记忆、权限、群档案全都对不上</li>
242
+ <li><b>QQ号 / 群号</b>不会变,但官方<b>没有任何「号码 → openid」的查询接口</b>,
243
+ 所以只能在这里人工指认一次</li>
244
+ <li>绑好之后插件和 AI 都能用号码代替 openid;读旧记忆时,框架会把这个号码名下的
245
+ <b>所有</b> openid 一起读进来</li>
246
+ <li>一个号码可以挂多个 openid(换机器人用)。<b>解绑只影响当前这只鸟</b>,
247
+ 旧记录留痕不删——它是读旧记忆唯一的线索</li>
248
+ </ul>
249
+ </div>
250
+
251
+ <div class="box">
252
+ <div class="box-hd">
253
+ <h3>还没认出来的人</h3>
254
+ <span class="hint" id="identUserHint">说过话、但还没绑 QQ号 的</span>
255
+ <div class="btn-row" style="margin:0">
256
+ <button class="btn small" id="btnIdentReload">刷新</button>
257
+ </div>
258
+ </div>
259
+ <div class="table-wrap">
260
+ <table>
261
+ <thead>
262
+ <tr>
263
+ <th>用户</th>
264
+ <th>活跃</th>
265
+ <th>消息</th>
266
+ <th>最近说过</th>
267
+ <th style="width:200px">绑 QQ号</th>
268
+ </tr>
269
+ </thead>
270
+ <tbody id="identUserBody">
271
+ <tr><td colspan="5" class="empty">加载中…</td></tr>
272
+ </tbody>
273
+ </table>
274
+ </div>
275
+ <div class="hint" style="margin-top:8px">
276
+ 列表按最近发言排序。前几条会显示「在哪个群说过什么」,方便认出是谁。
277
+ </div>
278
+ </div>
279
+
280
+ <div class="box">
281
+ <div class="box-hd">
282
+ <h3>还没认出来的群</h3>
283
+ <span class="hint" id="identGroupHint">机器人加入过、但还没绑群号的</span>
284
+ </div>
285
+ <div class="table-wrap">
286
+ <table>
287
+ <thead>
288
+ <tr>
289
+ <th>群</th>
290
+ <th>最近活跃</th>
291
+ <th style="width:200px">绑群号</th>
292
+ </tr>
293
+ </thead>
294
+ <tbody id="identGroupBody">
295
+ <tr><td colspan="3" class="empty">加载中…</td></tr>
296
+ </tbody>
297
+ </table>
298
+ </div>
299
+ </div>
300
+
301
+ <div class="box">
302
+ <div class="box-hd">
303
+ <h3>已绑定</h3>
304
+ <span class="hint" id="identBoundHint">这张表跨机器人共享,换机器人也认得出</span>
305
+ </div>
306
+ <div class="table-wrap">
307
+ <table>
308
+ <thead>
309
+ <tr>
310
+ <th>类型</th>
311
+ <th>号码</th>
312
+ <th>这只鸟下的 openid</th>
313
+ <th>全部 openid</th>
314
+ <th>最近</th>
315
+ <th style="width:100px">操作</th>
316
+ </tr>
317
+ </thead>
318
+ <tbody id="identBoundBody">
319
+ <tr><td colspan="6" class="empty">还没有绑定</td></tr>
320
+ </tbody>
321
+ </table>
322
+ </div>
323
+ </div>
324
+ </section>
325
+
326
+ <!-- 通知 -->
327
+ <section id="tab-notify" class="panel">
328
+ <form id="notifyForm">
329
+ <div class="box">
330
+ <div class="box-hd">
331
+ <h3>邮箱通知</h3>
332
+ <label class="switch"><input type="checkbox" name="enabled"> 启用邮箱通知</label>
333
+ </div>
334
+ <div class="hint">
335
+ 账号密码只保存在本机 <span class="mono">config/bot.json</span>,不会上传。
336
+ QQ 邮箱 / 163 邮箱等需要用<b>授权码</b>而不是登录密码,并先在邮箱网页端开启 SMTP 服务。
337
+ SMTP 用户名留空表示不做认证(内网中继)。
338
+ </div>
339
+ <div class="grid2">
340
+ <label>SMTP 服务器<input name="host" placeholder="smtp.qq.com / smtp.163.com"></label>
341
+ <label>SMTP 服务器端口<input name="port" type="number" min="1" max="65535" placeholder="一般为 587"></label>
342
+ <label>SMTP 用户名<input name="user" placeholder="一般是完整邮箱地址"></label>
343
+ <label>SMTP 密码 / 授权码<input name="pass" type="password" placeholder="留空则不修改"></label>
344
+ <label>发件人邮箱<input name="from" placeholder="通常与登录账号一致"></label>
345
+ <label>发件人姓名<input name="fromName" placeholder="栖"></label>
346
+ <label>主人邮箱<input name="ownerEmail" placeholder="测试邮件发到这里"></label>
347
+ </div>
348
+ <div class="hint" id="notifySecurityHint">
349
+ 加密方式默认「自动」:465 走隐式 TLS、587 走 STARTTLS、25 走明文,其它端口按服务器能力协商;
350
+ 网易系(163 / 126 / yeah)的 587 会自动改走隐式 TLS。
351
+ <span id="notifySecurityNow"></span>
352
+ </div>
353
+ <div class="cred-actions">
354
+ <span class="hint" id="notifyMsg"></span>
355
+ <button type="submit" class="btn primary">保存配置</button>
356
+ </div>
357
+ </div>
358
+ </form>
359
+
360
+ <div class="box">
361
+ <div class="box-hd">
362
+ <h3>测试邮件通知</h3>
363
+ <span class="hint">发送一封测试邮件到「主人邮箱」</span>
364
+ </div>
365
+ <div class="cred-actions">
366
+ <span class="hint" id="notifyTestMsg"></span>
367
+ <button type="button" class="btn" id="btnNotifyTest">发送</button>
368
+ </div>
369
+ </div>
370
+
371
+ <div class="box">
372
+ <div class="box-hd"><h3>普通成员自助绑定</h3></div>
373
+ <div class="hint">
374
+ 成员发 <b>绑定QQ 自己的QQ号</b> → 验证码发到 <b>QQ号@qq.com</b> → 再发
375
+ <b>QQ验证码 6位数字</b> 即完成绑定。收件邮箱由 QQ 号自动拼接,不需要在这里配置。
376
+ QQ 号已被别人绑定时会直接拒绝;未开通数字邮箱的 QQ 号请找主人用「绑定成员」代绑。
377
+ </div>
378
+ </div>
379
+ </section>
380
+
381
+ <!-- 关于 -->
382
+ <section id="tab-about" class="panel">
383
+ <div class="box">
384
+ <div class="about-hero">
385
+ <span class="logo about-logo" aria-hidden="true">栖</span>
386
+ <div>
387
+ <div class="about-name">栖 Roost</div>
388
+ <div class="hint">倦飞之鸟,终有一栖 · 零依赖的 QQ 官方机器人框架</div>
389
+ <div class="hint">当前版本 v<span id="updCurrent">-</span></div>
390
+ </div>
391
+ </div>
392
+ </div>
393
+
394
+ <div class="box">
395
+ <div class="box-hd">
396
+ <h3>版本更新</h3>
397
+ <div class="box-hd-right">
398
+ <button class="btn small" id="btnCheckUpdate">检查更新</button>
399
+ <button class="btn small primary" id="btnApplyUpdate" hidden>立即更新</button>
400
+ </div>
401
+ </div>
402
+ <div class="hint" id="updMsg">
403
+ 从 GitHub 检查新版本。更新只替换代码目录,<strong>配置、存档、日志、插件都不会动</strong>,更新后需重启程序。
404
+ </div>
405
+ </div>
406
+
407
+ <div class="box">
408
+ <div class="box-hd">
409
+ <h3>联系方式</h3>
410
+ <span class="hint">反馈问题、提建议都欢迎</span>
411
+ </div>
412
+ <dl class="kv">
413
+ <dt>QQ</dt><dd class="mono">2014433751</dd>
414
+ <dt>邮箱</dt><dd class="mono">jfzn9594@outlook.com</dd>
415
+ <dt>项目主页</dt>
416
+ <dd>
417
+ <a href="https://github.com/tdcq9594/qq-bot-roost" target="_blank" rel="noopener">GitHub</a>
418
+ ·
419
+ <a href="https://gitee.com/tdcq9594/qq-bot-roost" target="_blank" rel="noopener">Gitee</a>
420
+ </dd>
421
+ </dl>
422
+ </div>
423
+
424
+ <div class="box">
425
+ <div class="box-hd"><h3>许可</h3></div>
426
+ <div class="hint">永久免费、禁止任何收费、禁止商业使用(source-available)。详见项目根目录 LICENSE。</div>
427
+ </div>
428
+ </section>
429
+
430
+ <!-- 配置 -->
431
+ <section id="tab-config" class="panel">
432
+ <form id="cfgForm">
433
+ <div class="box">
434
+ <div class="box-hd"><h3>连接凭证</h3>
435
+ <span class="hint">AppID 或 intents 变化会自动重连</span>
436
+ </div>
437
+ <div class="grid2">
438
+ <label>AppID<input name="appId" placeholder="机器人 AppID"></label>
439
+ <label>AppSecret<input name="appSecret" type="password" placeholder="留空则不修改"></label>
440
+ <label>API 地址<input name="baseUrl"></label>
441
+ <label>命令前缀<input name="commandPrefix" maxlength="3"></label>
442
+ <label>收事件方式
443
+ <select name="connectionMode">
444
+ <option value="websocket">WebSocket 长连接(本地推荐)</option>
445
+ <option value="webhook">Webhook 回调(公网 / Serverless)</option>
446
+ </select>
447
+ </label>
448
+ </div>
449
+ <div class="cred-actions">
450
+ <span class="hint" id="cfgMsgTop"></span>
451
+ <button type="submit" class="btn primary">保存配置</button>
452
+ </div>
453
+ <div class="full intents-block">
454
+ <div class="intents-head">
455
+ <span class="intents-title">事件订阅(intents)</span>
456
+ <span class="hint">勾选/取消后<strong>立即生效</strong>,不用点保存。会短暂重连一次。</span>
457
+ </div>
458
+
459
+ <div class="intent-group">
460
+ <div class="intent-group-title">群聊 / 私聊</div>
461
+ <div id="intentGroupChat" class="intent-list"></div>
462
+ </div>
463
+
464
+ <div class="intent-group">
465
+ <div class="intent-group-title">频道(guild)</div>
466
+ <div id="intentGroupGuild" class="intent-list"></div>
467
+ </div>
468
+
469
+ <div class="intents-summary">
470
+ <span>当前值 <b id="intentsValue">33554432</b></span>
471
+ <span class="hint" id="intentsDesc">群聊与私聊</span>
472
+ </div>
473
+
474
+ <div class="btn-row">
475
+ <button type="button" class="btn small" data-preset="chat">群聊 + 私聊(推荐)</button>
476
+ <button type="button" class="btn small" data-preset="guild">频道基础</button>
477
+ <button type="button" class="btn small" data-preset="all">全部基础</button>
478
+ <button type="button" class="btn small" data-preset="none">清空</button>
479
+ </div>
480
+
481
+ <label class="switch small" style="margin-top:12px">
482
+ <input type="checkbox" id="intentsAdvanced"> 高级:手动填写数值
483
+ </label>
484
+ <input name="intents" id="intentsManual" type="number" style="display:none;margin-top:8px">
485
+
486
+ <div class="intent-warn" id="intentsWarn"></div>
487
+ </div>
488
+ </div>
489
+
490
+ <div class="box">
491
+ <div class="box-hd"><h3>行为控制</h3></div>
492
+ <div class="grid2">
493
+ <label class="switch"><input type="checkbox" name="whitelistEnabled"> 启用白名单</label>
494
+ <label class="switch"><input type="checkbox" name="rateLimitEnabled"> 启用限流</label>
495
+ <label>每分钟每会话上限<input name="perChatPerMinute" type="number" min="1"></label>
496
+ <label>去重窗口(秒)<input name="dedupWindowSec" type="number" min="0"></label>
497
+ </div>
498
+ <div class="divider"></div>
499
+ <div class="intent-group-title">消息日志</div>
500
+ <label class="switch">
501
+ <input type="checkbox" name="messageLogEnabled">
502
+ 把收到的每一条消息写进日志(便于排查"为什么没反应")
503
+ </label>
504
+ <div class="mute-row">
505
+ <span class="hint">屏蔽以下来源,不记录也不显示:</span>
506
+ <label class="switch small"><input type="checkbox" name="muteGroup"> 群聊</label>
507
+ <label class="switch small"><input type="checkbox" name="muteC2c"> 私聊</label>
508
+ <label class="switch small"><input type="checkbox" name="muteGuild"> 频道</label>
509
+ <label class="switch small"><input type="checkbox" name="muteDm"> 频道私信</label>
510
+ </div>
511
+
512
+ <label class="full">白名单群 openid(每行一个)
513
+ <textarea name="whitelistGroups" rows="2"></textarea>
514
+ </label>
515
+ <label class="full">白名单用户 openid(每行一个)
516
+ <textarea name="whitelistUsers" rows="2"></textarea>
517
+ </label>
518
+ </div>
519
+
520
+ <div class="box">
521
+ <div class="box-hd"><h3>群管能力</h3><span class="hint">禁言 / 撤回已开放;群成员与踢人需官方白名单</span></div>
522
+ <label class="switch">
523
+ <input type="checkbox" name="groupInvited">
524
+ 我已申请到群管内邀资格(群成员列表 / 踢人 / 群黑名单)
525
+ </label>
526
+ <div class="hint" style="margin-top:8px">
527
+ 官方群管理接口分两档:<b>禁言、撤回、群基本信息</b>对所有机器人开放;
528
+ <b>群成员列表、批量移除(踢人)、群黑名单</b>只对白名单机器人开放,
529
+ 没申请就调会返回 <code>11253</code>。<br>
530
+ 所以这里默认关闭。去开放平台申请通过后再勾上,勾选后立即生效、不用重启。
531
+ 没申请而勾上,只会让机器人每次调用都白跑一趟报错。
532
+ </div>
533
+ </div>
534
+
535
+ <div class="box">
536
+ <div class="box-hd"><h3>面板与日志</h3></div>
537
+ <div class="grid2">
538
+ <label>监听地址<input name="webuiHost" placeholder="127.0.0.1"></label>
539
+ <label>端口<input name="webuiPort" type="number"></label>
540
+ <label>面板访问密钥(留空则不校验;已设置时不会回显,想改就重填)<input name="webuiAccessKey" type="password" autocomplete="new-password"></label>
541
+ <label>日志级别
542
+ <select name="logLevel">
543
+ <option value="debug">debug</option>
544
+ <option value="info">info</option>
545
+ <option value="warn">warn</option>
546
+ <option value="error">error</option>
547
+ </select>
548
+ </label>
549
+ </div>
550
+ <div class="hint">修改监听地址或端口需要重启进程才生效。</div>
551
+ </div>
552
+
553
+ <div class="box">
554
+ <div class="box-hd"><h3>插件来源校验</h3></div>
555
+ <label class="switch">
556
+ <input type="checkbox" name="trustEnforce">
557
+ 强制校验插件市场签名
558
+ <span class="beta-tag" title="签名文件刚铺开,还没经过一轮完整发版验证">Beta</span>
559
+ <span class="hint">默认关闭</span>
560
+ </label>
561
+ <div class="hint" id="trustHint">
562
+ 开启后,官方市场里未签名或签名不符的插件会被拒绝安装和更新;手写插件、以及你自己配置的市场源不受影响。
563
+ 需要框架内置信任公钥——没内置时这个开关不起作用(所有插件照常放行)。
564
+ </div>
565
+ <div class="hint">
566
+ <b>Beta:</b>签名文件还没跟着一次正式发版走完流程,现在打开可能让某些插件装不上。
567
+ <b>建议先保持关闭</b>,等发版验证过一轮再考虑。
568
+ </div>
569
+ </div>
570
+
571
+ <div class="sticky-actions">
572
+ <span id="cfgMsg" class="hint"></span>
573
+ <button type="submit" class="btn primary">保存配置</button>
574
+ </div>
575
+ </form>
576
+ </section>
577
+
578
+ <!-- 统计 -->
579
+ <section id="tab-stats" class="panel">
580
+ <div class="box">
581
+ <div class="box-hd">
582
+ <h3>消息统计</h3>
583
+ <div class="plugin-tools">
584
+ <select id="statsDays" class="mini-select" aria-label="统计范围">
585
+ <option value="7">最近 7 天</option>
586
+ <option value="14">最近 14 天</option>
587
+ <option value="30">最近 30 天</option>
588
+ </select>
589
+ <button class="btn small" id="btnRefreshStats">刷新</button>
590
+ <button class="btn small" id="btnExportStats" title="导出汇总 CSV(不含消息正文)">导出</button>
591
+ </div>
592
+ </div>
593
+ <div id="statsUnavailable" class="empty" hidden></div>
594
+ <div class="cards" id="statsCards"></div>
595
+ <div class="stat-chart" id="statsChart"></div>
596
+ <div class="hint" id="statsFoot"></div>
597
+ </div>
598
+
599
+ <div class="row">
600
+ <div class="box grow">
601
+ <div class="box-hd">
602
+ <h3>命令排行</h3>
603
+ <span class="hint">按调用次数</span>
604
+ </div>
605
+ <div id="statsTopCommands" class="stat-list"></div>
606
+ </div>
607
+ <div class="box grow">
608
+ <div class="box-hd">
609
+ <h3>会话排行</h3>
610
+ <span class="hint">按消息数</span>
611
+ </div>
612
+ <div id="statsTopChats" class="stat-list"></div>
613
+ </div>
614
+ </div>
615
+ </section>
616
+
617
+ <!-- 插件 -->
618
+ <section id="tab-plugins" class="panel">
619
+ <div class="box">
620
+ <div class="box-hd">
621
+ <h3>已安装插件</h3>
622
+ <div class="plugin-tools">
623
+ <span class="hint">往 plugins/ 里放了新插件,点「刷新」即可加载,不用重启</span>
624
+ <button class="btn small" id="btnRescanPlugins">刷新</button>
625
+ <button class="btn small" id="btnLoadMarket">插件市场</button>
626
+ </div>
627
+ </div>
628
+ <div class="upd-tip" id="pluginUpdateTip"></div>
629
+ <div id="pluginList" class="plugin-list"></div>
630
+ </div>
631
+
632
+ <div class="box" id="marketBox" hidden>
633
+ <div class="box-hd">
634
+ <h3>插件市场</h3>
635
+ <div class="plugin-tools">
636
+ <span class="hint" id="marketMeta">加载中…</span>
637
+ <button class="btn small" id="btnRefreshMarket">重新获取</button>
638
+ </div>
639
+ </div>
640
+
641
+ <!-- 下载源:国内直连 GitHub 经常超时,这里可以换镜像 -->
642
+ <div class="mirror-bar">
643
+ <span class="hint">下载源</span>
644
+ <select id="mirrorSelect" class="mini-select" aria-label="下载源"></select>
645
+ <input
646
+ id="mirrorCustom"
647
+ class="mini-input"
648
+ placeholder="自定义前缀,如 https://gh.example/"
649
+ hidden
650
+ />
651
+ <input
652
+ id="mirrorGiteeToken"
653
+ class="mini-input"
654
+ type="password"
655
+ placeholder="Gitee 私人令牌(选填,留空走匿名)"
656
+ hidden
657
+ />
658
+ <button class="btn small" id="btnMirrorTest" title="并发测一遍,按你这边实际网络选">测速</button>
659
+ <span class="hint" id="mirrorTip"></span>
660
+ </div>
661
+
662
+ <!-- 插件一多就得能找 -->
663
+ <div class="market-filter">
664
+ <input id="marketSearch" class="mini-input" placeholder="搜索插件名 / 描述 / 标签" />
665
+ <select id="marketCat" class="mini-select" aria-label="分类"></select>
666
+ </div>
667
+
668
+ <div id="marketList" class="plugin-list"></div>
669
+ </div>
670
+ </section>
671
+
672
+ <!-- 事件 -->
673
+ <section id="tab-events" class="panel">
674
+ <div class="box">
675
+ <div class="box-hd">
676
+ <h3>事件订阅与接收</h3>
677
+ <span class="hint">
678
+ 官方事件全部可接收并转交插件。灰掉的是当前 intents 没订阅,
679
+ 去「配置 → 事件订阅」勾上才会推送
680
+ </span>
681
+ </div>
682
+ <div id="eventSummary" class="event-summary"></div>
683
+ <div class="event-legend">
684
+ <span><b class="ev-dot on"></b>已订阅</span>
685
+ <span><b class="ev-dot off"></b>未订阅</span>
686
+ <span><b class="ev-dot hit"></b>收到过</span>
687
+ </div>
688
+ <div id="eventGroups" class="event-groups"></div>
689
+ </div>
690
+ </section>
691
+
692
+ <!-- 投递账本 -->
693
+ <section id="tab-delivery" class="panel">
694
+ <div class="box">
695
+ <div class="box-hd">
696
+ <h3>投递账本</h3>
697
+ <span class="hint">带幂等键的消息发没发出去,这里查得到</span>
698
+ </div>
699
+ <ul class="tips">
700
+ <li><b>已发送</b>确认送达;<b>审核中</b>是主动消息进了审核——既不是成功也不是失败,
701
+ 等审核事件回来才会落定</li>
702
+ <li><b>结果未知</b>:超时或进程崩了,我们<b>不知道服务端收没收到</b>。
703
+ 这种<b>不会自动重发</b>——宁可让人核对一次,也不重复打扰</li>
704
+ <li><b>失败</b>:确定没发出去(离线 / 超窗 / 额度 / 限流),可以重试</li>
705
+ <li>只有<b>带幂等键</b>的发送才记账(`qq_send` 那类「一次调用 = 一段话」的)。
706
+ 插件直接回一句话不记账,靠 `msg_seq` 天然去重</li>
707
+ </ul>
708
+ <div class="cards" id="deliveryStats"></div>
709
+ <div class="table-wrap">
710
+ <table>
711
+ <thead>
712
+ <tr><th>时间</th><th>会话</th><th>状态</th><th>摘要</th><th class="num">重试</th><th>原因码</th></tr>
713
+ </thead>
714
+ <tbody id="deliveryBody"><tr><td colspan="6" class="empty">暂无数据</td></tr></tbody>
715
+ </table>
716
+ </div>
717
+ </div>
718
+ </section>
719
+
720
+ <!-- 事件账本 -->
721
+ <section id="tab-ledger" class="panel">
722
+ <div class="box">
723
+ <div class="box-hd">
724
+ <h3>事件账本</h3>
725
+ <span class="hint">每条消息的原文都在这——她翻记录、查上下文都靠它</span>
726
+ </div>
727
+ <ul class="tips">
728
+ <li>默认<b>永久保留</b>。全文落库意味着这台机器会一直留着群聊原文(含别人的私聊)——
729
+ 要控制占用就改保留期,或者手动删。</li>
730
+ <li><b>删除不可逆</b>,但会先让你核对条数。删完文件<b>不会立刻变小</b>,
731
+ 点「压缩数据库」才会(压缩在下次清理时做,约每小时一次,期间不影响收发)。</li>
732
+ <li><b>导出 JSON 是给导回用的</b>:一行一条、不带转义,导回去重复的会自动跳过。
733
+ 导出 CSV 是给人看的,Excel 能直接打开,但正文里的换行会让它没法原样导回。</li>
734
+ </ul>
735
+ <div class="cards" id="ledgerStats"></div>
736
+ <div class="btn-row" style="align-items:center">
737
+ <label for="ledgerRetention" class="hint" style="margin:0">保留</label>
738
+ <select id="ledgerRetention" style="width:150px">
739
+ <option value="0">永久</option>
740
+ <option value="7">7 天</option>
741
+ <option value="30">30 天</option>
742
+ <option value="custom">自定义…</option>
743
+ </select>
744
+ <input id="ledgerRetentionDays" type="number" min="1" max="36500" placeholder="天数"
745
+ style="width:110px;display:none">
746
+ <button id="ledgerRetentionSave" class="btn small">保存</button>
747
+ <span style="flex:1"></span>
748
+ <button id="ledgerExport" class="btn small">导出 CSV</button>
749
+ <button id="ledgerExportJson" class="btn small">导出 JSON</button>
750
+ <button id="ledgerImport" class="btn small">导入…</button>
751
+ <input id="ledgerImportFile" type="file" accept=".jsonl,.json,application/x-ndjson" hidden>
752
+ <button id="ledgerVacuum" class="btn small">压缩数据库</button>
753
+ </div>
754
+ <div class="table-wrap">
755
+ <table>
756
+ <thead>
757
+ <tr><th>会话</th><th>类型</th><th class="num">条数</th><th>时间范围</th><th>操作</th></tr>
758
+ </thead>
759
+ <tbody id="ledgerChats"><tr><td colspan="5" class="empty">加载中…</td></tr></tbody>
760
+ </table>
761
+ </div>
762
+ <div id="ledgerRowsBox" hidden style="margin-top:14px">
763
+ <div class="box-hd">
764
+ <h3 id="ledgerRowsTitle">记录</h3>
765
+ <span class="hint" id="ledgerRowsHint"></span>
766
+ </div>
767
+ <div class="table-wrap">
768
+ <table>
769
+ <thead><tr><th>时间</th><th>谁</th><th>内容</th></tr></thead>
770
+ <tbody id="ledgerRows"></tbody>
771
+ </table>
772
+ </div>
773
+ <div class="btn-row">
774
+ <button id="ledgerMore" class="btn small">下一页</button>
775
+ <button id="ledgerDelChat" class="btn small danger">删掉这个会话的记录</button>
776
+ </div>
777
+ </div>
778
+ </div>
779
+
780
+ <div class="box" style="margin-top:14px">
781
+ <div class="box-hd">
782
+ <h3>openid 变更</h3>
783
+ <span class="hint">换 AppID / 重新绑定之后,把旧 openid 下的记录改到新的</span>
784
+ </div>
785
+ <ul class="tips">
786
+ <li>一个号码可能同时挂着<b>多条</b> openid——绑定是<b>有意不撤销旧的</b>
787
+ (旧 openid 是读旧记忆唯一的线索)。所以这里要你手动指定「从哪条改到哪条」,
788
+ 代码猜不出来。</li>
789
+ <li>只改「会话」和「发送者」两列;去重键不动(老消息本来也不会重推)。</li>
790
+ <li>openid 从「身份」页复制:那里每个号码下面列着它的全部 openid 和最后出现时间。</li>
791
+ </ul>
792
+ <div class="btn-row" style="align-items:center">
793
+ <input id="ledgerRemapFrom" placeholder="旧 openid" style="width:250px">
794
+ <span class="hint" style="margin:0">→</span>
795
+ <input id="ledgerRemapTo" placeholder="新 openid" style="width:250px">
796
+ <select id="ledgerRemapScope" style="width:130px">
797
+ <option value="both">会话 + 发送者</option>
798
+ <option value="chat">只改会话</option>
799
+ <option value="user">只改发送者</option>
800
+ </select>
801
+ <button id="ledgerRemapGo" class="btn small">预览并执行</button>
802
+ </div>
803
+ </div>
804
+ </section>
805
+
806
+ <!-- 可达性 -->
807
+ <section id="tab-reach" class="panel">
808
+ <div class="box">
809
+ <div class="box-hd">
810
+ <h3>能不能主动找到人</h3>
811
+ <span class="hint">两道门:先得是好友,还得开着主动推送</span>
812
+ </div>
813
+ <ul class="tips">
814
+ <li><b>好友</b>是用户把机器人加为好友;<b>主动推送</b>是用户在机器人资料卡里单独开的开关——
815
+ <b>有好友也可能被关掉</b>,关了就发不出去</li>
816
+ <li>官方<b>没有查询好友的接口</b>,这里的值全部来自事件推送。机器人启动之前就存在的老关系拿不到事件,
817
+ 会一直是「未知」</li>
818
+ <li><b>未知不等于不可达</b>——未知会照常尝试发送,失败了才记下来。把未知当成「不能」,
819
+ 所有老用户就都被静音了</li>
820
+ </ul>
821
+ </div>
822
+
823
+ <div class="box">
824
+ <div class="box-hd">
825
+ <h3>单聊</h3>
826
+ <div class="btn-row" style="margin:0">
827
+ <button class="btn small" id="btnReloadReach">刷新</button>
828
+ </div>
829
+ </div>
830
+ <div class="table-wrap">
831
+ <table>
832
+ <thead>
833
+ <tr>
834
+ <th>用户</th>
835
+ <th>好友</th>
836
+ <th>主动推送</th>
837
+ <th>结论</th>
838
+ <th>依据</th>
839
+ <th>更新</th>
840
+ </tr>
841
+ </thead>
842
+ <tbody id="reachUserBody">
843
+ <tr><td colspan="6" class="empty">还没有记录。收到私聊消息、或被加好友之后才会出现。</td></tr>
844
+ </tbody>
845
+ </table>
846
+ </div>
847
+ <div class="hint" style="margin-top:8px">
848
+ 每行的两个下拉框就是<b>人工标注</b>,改完立刻生效并覆盖事件推断的值;选「未知」等于撤销标注。
849
+ </div>
850
+ <div class="form-inline" style="margin-top:12px">
851
+ <input id="reachOpenidInput" placeholder="openid(列表里没有的人可以直接登记)">
852
+ <select id="reachFriendSel" class="mini-select">
853
+ <option value="">好友:未知</option>
854
+ <option value="1">好友:是</option>
855
+ <option value="0">好友:否</option>
856
+ </select>
857
+ <select id="reachPushSel" class="mini-select">
858
+ <option value="">推送:未知</option>
859
+ <option value="1">推送:开</option>
860
+ <option value="0">推送:关</option>
861
+ </select>
862
+ <button class="btn primary" id="btnReachSave">登记</button>
863
+ </div>
864
+ </div>
865
+
866
+ <div class="box">
867
+ <div class="box-hd">
868
+ <h3>群主动推送</h3>
869
+ <span class="hint">群管理员可以整个群关掉机器人的主动消息</span>
870
+ </div>
871
+ <div class="table-wrap">
872
+ <table>
873
+ <thead>
874
+ <tr>
875
+ <th>群</th>
876
+ <th>主动推送</th>
877
+ <th>操作人</th>
878
+ <th>依据</th>
879
+ <th>更新</th>
880
+ </tr>
881
+ </thead>
882
+ <tbody id="reachGroupBody">
883
+ <tr><td colspan="5" class="empty">还没有记录。群管理员改过推送开关之后才会出现。</td></tr>
884
+ </tbody>
885
+ </table>
886
+ </div>
887
+ <div class="hint" style="margin-top:8px">
888
+ 没记录过的群按「未知」处理,照常尝试推送。
889
+ </div>
890
+ </div>
891
+ </section>
892
+
893
+ <!-- 群组 -->
894
+ <section id="tab-groups" class="panel">
895
+ <div class="box">
896
+ <div class="box-hd">
897
+ <h3>机器人主人</h3>
898
+ <span class="hint">主人可以使用全部管理命令</span>
899
+ </div>
900
+ <div id="ownerChips" class="chips"></div>
901
+ <div class="form-inline" style="margin-top:12px">
902
+ <input id="ownerInput" placeholder="填 openid,回车添加">
903
+ <button class="btn primary" id="btnAddOwner">添加</button>
904
+ </div>
905
+ <div class="hint" style="margin-top:8px">
906
+ 不知道 openid?在群里 @机器人 发 /whoami,或用上面的「活跃会话」看。
907
+ </div>
908
+ </div>
909
+
910
+ <div class="box">
911
+ <div class="box-hd"><h3>入群后自动授予的权限</h3></div>
912
+ <ul class="tips">
913
+ <li><b>邀请人</b> → 该群管理员(1 级),只影响这一个群</li>
914
+ <li><b>群主</b> → 该群管理员(1 级)。群主发过言才能识别(报文里 member_role=owner)</li>
915
+ <li>两者都<b>不会</b>获得全局主人或全局管理员</li>
916
+ </ul>
917
+ <div class="hint" style="margin-top:8px">
918
+ 要调整或撤销,去「权限」页改对应的 <code>group:xxx</code> 作用域。
919
+ 全局主人只能在 config.json 的 owners 里配,或首次启动时用认领码认领。
920
+ </div>
921
+ </div>
922
+
923
+ <div class="box">
924
+ <div class="box-hd">
925
+ <h3>手动登记群</h3>
926
+ <span class="hint">官方没有「列出机器人所在的群」的接口,群只能靠事件发现</span>
927
+ </div>
928
+ <ul class="tips">
929
+ <li>机器人被拉进群、或在群里被 @ 过一次,下面列表里就会自动出现</li>
930
+ <li><b>一直不出现</b>:多半是没开「群聊与私聊」intent——去概览页点「检查」能看到</li>
931
+ <li>已经知道群 openid 的话,可以手动登记,方便提前配 <code>group:xxx</code> 权限</li>
932
+ </ul>
933
+ <div class="form-inline" style="margin-top:12px">
934
+ <input id="groupOpenidInput" placeholder="群 openid" />
935
+ <input id="groupNoteInput" placeholder="备注(可选)" style="width:200px" />
936
+ <button class="btn primary" id="btnAddGroup">登记</button>
937
+ </div>
938
+ </div>
939
+
940
+ <div class="box">
941
+ <div class="box-hd">
942
+ <h3>群列表</h3>
943
+ <div class="btn-row" style="margin:0">
944
+ <button class="btn small" id="btnReloadGroups">刷新</button>
945
+ </div>
946
+ </div>
947
+ <div class="table-wrap">
948
+ <table>
949
+ <thead>
950
+ <tr>
951
+ <th>群 openid</th>
952
+ <th>邀请者</th>
953
+ <th>群主(识别)</th>
954
+ <th>状态</th>
955
+ <th>最后活跃</th>
956
+ <th class="num">操作</th>
957
+ </tr>
958
+ </thead>
959
+ <tbody id="groupBody">
960
+ <tr><td colspan="6" class="empty">还没有群。把机器人拉进群后这里会自动出现。</td></tr>
961
+ </tbody>
962
+ </table>
963
+ </div>
964
+ <div class="hint" style="margin-top:8px">
965
+ 这里只显示群档案。谁能在这个群做什么,统一在「权限」页按
966
+ <code>group:&lt;群openid&gt;</code> 作用域配置。
967
+ </div>
968
+
969
+ <div class="divider"></div>
970
+
971
+ <div class="hint" style="margin-bottom:8px">
972
+ <b>看不到你的群?</b>官方没有「查询机器人所在群列表」的接口,
973
+ 群只能靠事件被动发现。请按顺序排查:
974
+ </div>
975
+ <ol class="tips">
976
+ <li>在群里 <b>@机器人</b> 发一句话(不 @ 不会推送消息)</li>
977
+ <li>确认这个群在开放平台「沙箱配置」里(开发中的机器人只收沙箱群消息)</li>
978
+ <li>确认机器人是在它<b>运行期间</b>被拉进群的——离线时入群的事件会丢</li>
979
+ <li>看上面「事件统计」:全是 0 说明平台一条都没推过来</li>
980
+ </ol>
981
+ <div class="form-inline" style="margin-top:12px">
982
+ <input id="groupInput" placeholder="已知群 openid 时可直接登记">
983
+ <button class="btn" id="btnAddGroupManual">手动登记</button>
984
+ </div>
985
+ </div>
986
+ </section>
987
+
988
+ <!-- 权限 -->
989
+ <section id="tab-roles" class="panel">
990
+ <div class="box">
991
+ <div class="box-hd">
992
+ <h3>权限等级</h3>
993
+ <span class="hint">作用域决定「在哪管」,等级决定「能管谁」;数字越小权限越高,只能授予数字更大的等级</span>
994
+ </div>
995
+ <div class="perm-legend">
996
+ <span class="hint" style="margin-right:6px">数字越小权限越高:</span>
997
+ <span><b>0</b> 主人(config.json)</span>
998
+ <span><b>1</b> 管理员</span>
999
+ <span><b>2</b> 群管</span>
1000
+ <span><b>3</b> 助手</span>
1001
+ <span><b>4-9</b> 框架预留</span>
1002
+ <span><b>10+</b> 插件区</span>
1003
+ <span><b>9999</b> 无权限</span>
1004
+ </div>
1005
+ <div class="perm-legend" style="margin-top:6px">
1006
+ <span><b>global</b> 全局</span>
1007
+ <span><b>group:xxx</b> 某个群</span>
1008
+ <span><b>channel:xxx</b> 频道</span>
1009
+ <span><b>user:xxx</b> 私聊</span>
1010
+ <span><b>feature:插件</b> 功能</span>
1011
+ </div>
1012
+ </div>
1013
+
1014
+ <div class="box">
1015
+ <div class="box-hd"><h3>添加 / 修改</h3></div>
1016
+ <div class="form-inline">
1017
+ <input id="roleScope" list="scopeList" placeholder="作用域,如 group:xxx / global">
1018
+ <datalist id="scopeList"></datalist>
1019
+ <input id="roleOpenid" placeholder="openid">
1020
+ <input id="roleLevel" type="number" min="1" max="998" value="2"
1021
+ style="width:120px" title="数字越小权限越高">
1022
+ <span class="hint">等级(1 管理员 / 2 群管 / 3 助手 / 10+ 插件区,最大 998)</span>
1023
+ <button class="btn primary" id="btnAddRole">保存</button>
1024
+ </div>
1025
+ <div class="hint" style="margin-top:8px">
1026
+ 面板操作视为本机管理员行为,不受等级限制;聊天里请用 /admin set。
1027
+ </div>
1028
+ </div>
1029
+
1030
+ <div class="box">
1031
+ <div class="box-hd">
1032
+ <h3>最近说过话的人</h3>
1033
+ <span class="hint">点一下自动填好 openid 和作用域,不用去日志里翻</span>
1034
+ <button class="btn small" id="btnReloadPeople">刷新</button>
1035
+ </div>
1036
+ <div id="peopleList" class="people-list">
1037
+ <div class="empty">还没有收到消息。让要授权的人在群里说句话,这里就会出现。</div>
1038
+ </div>
1039
+ </div>
1040
+
1041
+ <div class="box">
1042
+ <div class="box-hd">
1043
+ <h3>已授权列表</h3>
1044
+ <button class="btn small" id="btnReloadRoles">刷新</button>
1045
+ </div>
1046
+ <div class="table-wrap">
1047
+ <table>
1048
+ <thead><tr><th>作用域</th><th>openid</th><th>等级</th><th class="num">操作</th></tr></thead>
1049
+ <tbody id="roleBody"><tr><td colspan="4" class="empty">还没有配置权限</td></tr></tbody>
1050
+ </table>
1051
+ </div>
1052
+ </div>
1053
+ </section>
1054
+
1055
+ <!-- 日志 -->
1056
+ <section id="tab-logs" class="panel">
1057
+ <div class="box">
1058
+ <div class="box-hd">
1059
+ <h3>实时日志</h3>
1060
+ <span class="hint">按来源和关键词筛选,不影响实时接收</span>
1061
+ </div>
1062
+ <div class="log-toolbar">
1063
+ <input id="logSearch" class="log-search" placeholder="搜索日志" autocomplete="off">
1064
+ <select id="logSourceFilter">
1065
+ <option value="all">全部来源</option>
1066
+ <option value="core">框架</option>
1067
+ <option value="net">网关 / 接口</option>
1068
+ <option value="plugin">插件</option>
1069
+ </select>
1070
+ <select id="logLevelFilter">
1071
+ <option value="debug">全部</option>
1072
+ <option value="info">info 及以上</option>
1073
+ <option value="warn">warn 及以上</option>
1074
+ <option value="error">仅 error</option>
1075
+ </select>
1076
+ <label class="switch small"><input type="checkbox" id="autoScroll" checked> 自动滚动</label>
1077
+ <button class="btn small" id="btnClearLog">清屏</button>
1078
+ </div>
1079
+ <div class="log-legend">
1080
+ <span><b class="lg-dot in"></b>收到消息</span>
1081
+ <span><b class="lg-dot ok"></b>成功 / 已就绪</span>
1082
+ <span><b class="lg-dot err"></b>失败 / 错误</span>
1083
+ <span><b class="lg-dot cmd"></b>指令</span>
1084
+ <span class="lg-sep"></span>
1085
+ <span><b class="lg-chip info">INFO</b>普通</span>
1086
+ <span><b class="lg-chip warn">WARN</b>警告</span>
1087
+ <span><b class="lg-chip error">ERROR</b>错误</span>
1088
+ <span class="lg-sep"></span>
1089
+ <span class="hint">模块标签按来源上色:gateway / api / core / plugins / roles …</span>
1090
+ </div>
1091
+ <div id="logView" class="log-view"></div>
1092
+ </div>
1093
+ </section>
1094
+ </main>
1095
+
1096
+ <!-- 通用确认弹窗 -->
1097
+ <div class="modal-mask" id="modalMask" hidden>
1098
+ <div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
1099
+ <h3 class="modal-title" id="modalTitle">确认</h3>
1100
+ <div class="modal-body" id="modalBody"></div>
1101
+ <div class="modal-actions">
1102
+ <button class="btn" id="modalCancel">取消</button>
1103
+ <button class="btn danger" id="modalOk">确定</button>
1104
+ </div>
1105
+ </div>
1106
+ </div>
1107
+
1108
+ <!-- 分群插件开关:这个群能用哪些插件 -->
1109
+ <div class="modal-mask" id="gplugMask" hidden>
1110
+ <div class="modal wide" role="dialog" aria-modal="true" aria-labelledby="gplugTitle">
1111
+ <h3 class="modal-title" id="gplugTitle">分群插件开关</h3>
1112
+ <div class="hint" id="gplugSub" style="margin-bottom:10px"></div>
1113
+
1114
+ <div class="gplug-mode">
1115
+ <label class="gplug-radio">
1116
+ <input type="radio" name="gplugMode" value="global" checked>
1117
+ <span>跟随全局<em id="gplugGlobalHint"></em></span>
1118
+ </label>
1119
+ <label class="gplug-radio">
1120
+ <input type="radio" name="gplugMode" value="custom">
1121
+ <span>本群单独配置</span>
1122
+ </label>
1123
+ </div>
1124
+
1125
+ <div class="gplug-list" id="gplugList" hidden></div>
1126
+
1127
+ <div class="modal-actions">
1128
+ <button class="btn" id="gplugCancel">取消</button>
1129
+ <button class="btn primary" id="gplugSave">保存</button>
1130
+ </div>
1131
+ </div>
1132
+ </div>
1133
+
1134
+ <!-- 新建机器人 -->
1135
+ <div class="modal-mask" id="newBotMask" hidden>
1136
+ <div class="modal" role="dialog" aria-modal="true" aria-labelledby="newBotTitle">
1137
+ <h3 class="modal-title" id="newBotTitle">新建机器人</h3>
1138
+
1139
+ <!-- 扫码一键创建:手机 QQ 扫一下,AppID / AppSecret 自动填好 -->
1140
+ <div class="nb-qr-wrap">
1141
+ <button class="btn primary" id="nbQrStart" type="button">扫码一键创建</button>
1142
+ <i class="nb-qr-hint">推荐:不用去 q.qq.com 手动建机器人</i>
1143
+ </div>
1144
+ <div class="nb-qr" id="nbQr" hidden>
1145
+ <div class="nb-qr-code" id="nbQrCode"></div>
1146
+ <div class="nb-qr-tip" id="nbQrTip"></div>
1147
+ </div>
1148
+
1149
+ <div class="nb-or">或者手动填(已有机器人、或扫码用不了时)</div>
1150
+
1151
+ <div class="bot-form">
1152
+ <label>
1153
+ <span>ID</span>
1154
+ <input id="nbId" placeholder="main-bot,小写字母数字短横线" autocomplete="off">
1155
+ <i>用来做文件夹名,创建后不能改</i>
1156
+ </label>
1157
+ <label>
1158
+ <span>显示名</span>
1159
+ <input id="nbName" placeholder="留空则用 ID" autocomplete="off">
1160
+ </label>
1161
+ <label>
1162
+ <span>AppID</span>
1163
+ <input id="nbAppId" placeholder="q.qq.com 开发设置里的 AppID" autocomplete="off">
1164
+ </label>
1165
+ <label>
1166
+ <span>AppSecret</span>
1167
+ <input id="nbSecret" type="password" placeholder="AppSecret" autocomplete="off">
1168
+ </label>
1169
+ <div class="bot-form-err" id="nbErr" hidden></div>
1170
+ </div>
1171
+ <div class="modal-actions">
1172
+ <button class="btn" id="nbCancel">取消</button>
1173
+ <button class="btn primary" id="nbOk">创建并连接</button>
1174
+ </div>
1175
+ </div>
1176
+ </div>
1177
+
1178
+ <!-- 插件设置:配置 + 玩家数据 -->
1179
+ <div class="modal-mask" id="pluginPanelMask" hidden>
1180
+ <div class="modal wide" role="dialog" aria-modal="true" aria-labelledby="ppTitle">
1181
+ <h3 class="modal-title" id="ppTitle">插件设置</h3>
1182
+ <div class="pp-tabs">
1183
+ <button type="button" class="pp-tab active" data-pp="config">配置</button>
1184
+ <button type="button" class="pp-tab" data-pp="data">玩家数据</button>
1185
+ <button type="button" class="pp-tab" data-pp="dash">运行数据</button>
1186
+ <button type="button" class="pp-tab" data-pp="groups">分群开关</button>
1187
+ </div>
1188
+ <div id="ppConfig" class="pp-body"></div>
1189
+ <div id="ppData" class="pp-body" hidden></div>
1190
+ <div id="ppDash" class="pp-body" hidden></div>
1191
+ <div id="ppGroups" class="pp-body" hidden></div>
1192
+ <div class="modal-actions">
1193
+ <button class="btn danger act-left" id="ppReset" title="把所有配置项恢复成插件声明的默认值">恢复默认</button>
1194
+ <button class="btn" id="ppClose">关闭</button>
1195
+ <button class="btn primary" id="ppSave">保存配置</button>
1196
+ </div>
1197
+ </div>
1198
+ </div>
1199
+
1200
+ <script src="/vendor/qrcode.js"></script>
1201
+ <script src="/app.js"></script>
1202
+ </body>
1203
+ </html>