@zaofan/dsh-qqbot 0.6.0 → 0.8.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 +17 -0
- package/README_EN.md +17 -0
- package/client/qqbot-settings.js +1385 -28
- package/dist/api/group-admin.d.ts +13 -0
- package/dist/api/group-admin.d.ts.map +1 -1
- package/dist/api/group-admin.js +23 -0
- package/dist/api/group-admin.js.map +1 -1
- package/dist/channel-tools.d.ts.map +1 -1
- package/dist/channel-tools.js +58 -1
- package/dist/channel-tools.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/features/chat-ledger.d.ts.map +1 -1
- package/dist/features/chat-ledger.js +54 -3
- package/dist/features/chat-ledger.js.map +1 -1
- package/dist/features/qq-approval.d.ts +43 -1
- package/dist/features/qq-approval.d.ts.map +1 -1
- package/dist/features/qq-approval.js +167 -17
- package/dist/features/qq-approval.js.map +1 -1
- package/dist/features/qq-user-questions.d.ts +69 -0
- package/dist/features/qq-user-questions.d.ts.map +1 -0
- package/dist/features/qq-user-questions.js +214 -0
- package/dist/features/qq-user-questions.js.map +1 -0
- package/dist/features/session-registry.d.ts +51 -0
- package/dist/features/session-registry.d.ts.map +1 -0
- package/dist/features/session-registry.js +95 -0
- package/dist/features/session-registry.js.map +1 -0
- package/dist/gateway/bootstrap.d.ts.map +1 -1
- package/dist/gateway/bootstrap.js +88 -1
- package/dist/gateway/bootstrap.js.map +1 -1
- package/dist/session/session-manager.d.ts.map +1 -1
- package/dist/session/session-manager.js +9 -14
- package/dist/session/session-manager.js.map +1 -1
- package/dist/transport/outbound-buffer.d.ts +2 -0
- package/dist/transport/outbound-buffer.d.ts.map +1 -1
- package/dist/transport/outbound-buffer.js.map +1 -1
- package/package.json +6 -3
- package/settings-host.js +962 -5
- package/voice-convert.mjs +45 -0
package/client/qqbot-settings.js
CHANGED
|
@@ -137,6 +137,7 @@ window.__ModuleLoader__.load({
|
|
|
137
137
|
|
|
138
138
|
function nv(v) { return typeof v === 'number' && isFinite(v) ? v : 0 }
|
|
139
139
|
function sv(v) { return typeof v === 'string' ? v : '' }
|
|
140
|
+
function esc(v) { return String(v ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') }
|
|
140
141
|
|
|
141
142
|
function BoolRow(props) {
|
|
142
143
|
return h('label', { style: labelStyle },
|
|
@@ -373,6 +374,7 @@ window.__ModuleLoader__.load({
|
|
|
373
374
|
// ── ⑥ QQ 群管理(单开大卡片; P3+ 设计系统版: 横排 Tab + 状态条 + 表格 + 空状态 + 预留接口位) ──
|
|
374
375
|
// 能力就绪度: 官方未开放位保留(红条+行动), 不置灰糊弄; 业务名词做 Tab 骨架。
|
|
375
376
|
var GROUP_TABS = [
|
|
377
|
+
{ key: 'send', label: '发消息', open: true },
|
|
376
378
|
{ key: 'join', label: '入群审批', open: true },
|
|
377
379
|
{ key: 'mute', label: '禁言', open: true },
|
|
378
380
|
{ key: 'members', label: '成员信息', open: false, gateHuman: '获取群成员列表: 官方尚未开放(内邀中), 等待公测后自动可用' },
|
|
@@ -392,6 +394,7 @@ window.__ModuleLoader__.load({
|
|
|
392
394
|
var [members, setMembers] = useState(null)
|
|
393
395
|
var [muteSecs, setMuteSecs] = useState('60')
|
|
394
396
|
var [muteTarget, setMuteTarget] = useState('')
|
|
397
|
+
var [sendText, setSendText] = useState('')
|
|
395
398
|
|
|
396
399
|
function loadMembers() {
|
|
397
400
|
if (!gid) return
|
|
@@ -460,9 +463,25 @@ window.__ModuleLoader__.load({
|
|
|
460
463
|
else setMsg((d && d.error) || '绑定失败')
|
|
461
464
|
}).catch(function (e) { setMsg('绑定异常: ' + e.message) })
|
|
462
465
|
}
|
|
466
|
+
function doSend() {
|
|
467
|
+
var t = sendText.trim()
|
|
468
|
+
if (!t) { setMsg('先输入要发送的内容'); return }
|
|
469
|
+
setBusy('send')
|
|
470
|
+
fetch('/api/qqbot-settings/group/send', {
|
|
471
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
472
|
+
body: JSON.stringify(Object.assign({ gid: gid, text: t }, props.ns ? { ns: props.ns } : {})),
|
|
473
|
+
}).then(function (r) { return r.json() }).then(function (d) {
|
|
474
|
+
setBusy('')
|
|
475
|
+
setMsg((d && d.msg) || (d && d.err && d.err.human) || (d && d.error) || '发送结果未知')
|
|
476
|
+
if (d && d.ok) setSendText('')
|
|
477
|
+
}).catch(function (e) { setBusy(''); setMsg('发送异常: ' + e.message) })
|
|
478
|
+
}
|
|
463
479
|
|
|
464
480
|
var curTabMeta = null
|
|
465
481
|
GROUP_TABS.forEach(function (t) { if (t.key === tab) curTabMeta = t })
|
|
482
|
+
var curGroup = null
|
|
483
|
+
;(accts || []).forEach(function (a) { if (a.gid === gid) curGroup = a })
|
|
484
|
+
var groupSelName = (curGroup && curGroup.name) ? curGroup.name : ''
|
|
466
485
|
var groupSel = accts === null ? h('span', { style: { fontSize: T.fsSm, color: T.text3 } }, '群列表加载中…')
|
|
467
486
|
: accts.length === 0 ? h('span', { style: { fontSize: T.fsSm, color: T.warn } }, '暂无群(事件累积/下方登记后出现)')
|
|
468
487
|
: h('select', { className: 'qqs-inp', style: { padding: '3px 8px', maxWidth: 320, border: '1px solid ' + T.border, borderRadius: T.radius }, value: gid, onChange: function (e) { setGid(e.target.value); setMsg('') } },
|
|
@@ -511,6 +530,14 @@ window.__ModuleLoader__.load({
|
|
|
511
530
|
'此功能位已预留 —— 官方开放接口后此处自动点亮, 无需等待插件更新'))
|
|
512
531
|
} else if (!gid) {
|
|
513
532
|
body = h(Empty, { icon: '👥', text: '先选择或登记一个群, 再查看/操作' })
|
|
533
|
+
} else if (tab === 'send') {
|
|
534
|
+
// 发消息: 以机器人身份直接向目标群发文本(面板=主人直发)
|
|
535
|
+
body = h('div', null,
|
|
536
|
+
h(StatusBar, { tone: 'info' }, '以机器人身份向「' + (groupSelName) + '」发消息 —— 内容会以 bot 名义出现在群里。'),
|
|
537
|
+
h('textarea', { className: 'qqs-area', style: { width: '100%', minHeight: 90, marginTop: 8 }, placeholder: '输入要发的消息…(@某人 用 <@对方openid> 无斜杠 或 <qqbot-at-user id="对方openid" />)', value: sendText, onChange: function (e) { setSendText(e.target.value) } }),
|
|
538
|
+
h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginTop: 8 } },
|
|
539
|
+
h(SButton, { variant: 'primary', disabled: busy === 'send', onClick: doSend }, busy === 'send' ? '发送中…' : '🚀 发送到群'),
|
|
540
|
+
h('span', { style: { fontSize: T.fsSm, color: T.text3 } }, '已输 ' + sendText.length + '/2000 字')))
|
|
514
541
|
} else if (tab === 'join') {
|
|
515
542
|
body = joins === null ? h(Empty, { icon: '⏳', text: '加载中…' })
|
|
516
543
|
: h('div', null,
|
|
@@ -1016,7 +1043,7 @@ window.__ModuleLoader__.load({
|
|
|
1016
1043
|
var [msg, setMsg] = useState('')
|
|
1017
1044
|
var [busyId, setBusyId] = useState('')
|
|
1018
1045
|
// 名称解析(2026-09-05): 群=活群真名(accounts 已剔除注销群), 私聊=台账昵称+成员表补名
|
|
1019
|
-
var [chatTg, setChatTg] = useState(
|
|
1046
|
+
var [chatTg, setChatTg] = useState(null) // 活群/昵称台账: null=加载中(不判失效), 数组=已加载
|
|
1020
1047
|
function load() {
|
|
1021
1048
|
fetch(API_TIMERS + _tmQ).then(function (r) { return r.json() }).then(function (d) {
|
|
1022
1049
|
if (d && Array.isArray(d.jobs)) setJobs(d.jobs.slice().reverse())
|
|
@@ -1025,14 +1052,17 @@ window.__ModuleLoader__.load({
|
|
|
1025
1052
|
}
|
|
1026
1053
|
useEffect(function () {
|
|
1027
1054
|
load()
|
|
1028
|
-
fetchChatTargets(props.acct).then(function (t) { setChatTg(t) }).catch(function () {})
|
|
1055
|
+
fetchChatTargets(props.acct).then(function (t) { setChatTg(t && t.group ? t : { group: [], c2c: [] }) }).catch(function () { setChatTg({ group: [], c2c: [] }) })
|
|
1029
1056
|
}, [])
|
|
1030
1057
|
function nameOf(scope, id) {
|
|
1058
|
+
if (!chatTg) return ''
|
|
1031
1059
|
var nm = ''
|
|
1032
1060
|
;(chatTg[scope] || []).forEach(function (x) { if (x.id === id) nm = x.name || nm })
|
|
1033
1061
|
return nm
|
|
1034
1062
|
}
|
|
1035
1063
|
function isGroupAlive(gid) {
|
|
1064
|
+
// 台账未加载(null)时不判失效——避免每次进页面先闪"群已失效"; 加载完成后按真实台账判定
|
|
1065
|
+
if (!chatTg) return true
|
|
1036
1066
|
return (chatTg.group || []).some(function (g) { return g.id === gid })
|
|
1037
1067
|
}
|
|
1038
1068
|
function act(job, body, okMsg) {
|
|
@@ -1215,6 +1245,14 @@ window.__ModuleLoader__.load({
|
|
|
1215
1245
|
var [copy, setCopy] = useState(null) // 复制弹层: {sourceId}
|
|
1216
1246
|
var [copyId, setCopyId] = useState('')
|
|
1217
1247
|
var [copyName, setCopyName] = useState('')
|
|
1248
|
+
var [collapsed, setCollapsed] = useState({})
|
|
1249
|
+
var [addMenu, setAddMenu] = useState(false)
|
|
1250
|
+
var [editP, setEditP] = useState(null)
|
|
1251
|
+
var [editFiles, setEditFiles] = useState([])
|
|
1252
|
+
var [editFile, setEditFile] = useState('')
|
|
1253
|
+
var [editContent, setEditContent] = useState('')
|
|
1254
|
+
var [editMsg, setEditMsg] = useState('')
|
|
1255
|
+
var [editBusy, setEditBusy] = useState(false)
|
|
1218
1256
|
var pollRef = React.useRef(null)
|
|
1219
1257
|
function load(quiet) {
|
|
1220
1258
|
Promise.all([
|
|
@@ -1239,8 +1277,8 @@ window.__ModuleLoader__.load({
|
|
|
1239
1277
|
useEffect(function () {
|
|
1240
1278
|
if (!Array.isArray(presets) || presets.length === 0) return
|
|
1241
1279
|
if (!Array.isArray(insts) || insts.length === 0) return
|
|
1242
|
-
|
|
1243
|
-
var only =
|
|
1280
|
+
// QQ 工具由会话自动挂载(2026-09-06) → 预设不再需要带标, 全部可作候选
|
|
1281
|
+
var only = presets
|
|
1244
1282
|
if (only.length === 0) return
|
|
1245
1283
|
var changed = false
|
|
1246
1284
|
var next = insts.map(function (x) {
|
|
@@ -1252,6 +1290,7 @@ window.__ModuleLoader__.load({
|
|
|
1252
1290
|
}, [insts, presets])
|
|
1253
1291
|
useEffect(function () { return function () { if (pollRef.current) clearInterval(pollRef.current) } }, [])
|
|
1254
1292
|
function upd(i, patch) { setInsts(function (list) { var n = list.slice(); n[i] = Object.assign({}, n[i], patch); return n }) }
|
|
1293
|
+
function toggleCollapse(id) { setCollapsed(function (m) { var n = Object.assign({}, m); n[id] = !n[id]; return n }) }
|
|
1255
1294
|
function addBlank() {
|
|
1256
1295
|
var base = 'im-qqbot'
|
|
1257
1296
|
var used = new Set((insts || []).map(function (x) { return x.id }))
|
|
@@ -1274,8 +1313,7 @@ window.__ModuleLoader__.load({
|
|
|
1274
1313
|
var blanks = list.filter(function (x) { return !x.appSecret && !x.hasSecret && !x._bound })
|
|
1275
1314
|
if (blanks.length && !window.confirm('有 ' + blanks.length + ' 个账号没填 appSecret(不填的会被保存为只有骨架的实例, 可能启动就弹扫码)。继续?')) { setBusy(''); return }
|
|
1276
1315
|
// 保存兜底: preset 仍为空的账号自动填第一个候选(防 effect 未触发/用户未手动选导致保存丢 preset)
|
|
1277
|
-
var
|
|
1278
|
-
var candNow = knowNow ? presets.filter(function (p) { return p.hasChannelTools }) : presets
|
|
1316
|
+
var candNow = presets
|
|
1279
1317
|
if (candNow.length > 0) {
|
|
1280
1318
|
var autoId = candNow[0].id
|
|
1281
1319
|
list = list.map(function (x) { return x.preset ? x : Object.assign({}, x, { preset: autoId }) })
|
|
@@ -1351,31 +1389,76 @@ window.__ModuleLoader__.load({
|
|
|
1351
1389
|
.then(function (r) { return r.json() }).then(function (d) { if (d && d.ok) setMsg('已在本机打开文件夹'); else if (d && d.error) setMsg(d.error) })
|
|
1352
1390
|
.catch(function (e) { setMsg('打开失败: ' + e.message) })
|
|
1353
1391
|
}
|
|
1392
|
+
// ── 人格编辑器(2026-09-07): 展开/改写预设人格文件(仅带QQ工具标记的副本可写, host 端二次校验) ──
|
|
1393
|
+
function loadEditFile(id, name) {
|
|
1394
|
+
fetch(API_PRESETS + '/file?id=' + encodeURIComponent(id) + '&name=' + encodeURIComponent(name))
|
|
1395
|
+
.then(function (r) { return r.json() }).then(function (d) {
|
|
1396
|
+
if (d && typeof d.content === 'string') { setEditContent(d.content); setEditMsg('') }
|
|
1397
|
+
else { setEditContent(''); setEditMsg('读取失败: ' + ((d && d.error) || '未知')) }
|
|
1398
|
+
}).catch(function (e) { setEditMsg('读取异常: ' + e.message) })
|
|
1399
|
+
}
|
|
1400
|
+
function openPresetEditor(p) {
|
|
1401
|
+
setEditP(p); setEditFiles([]); setEditFile(''); setEditContent(''); setEditMsg('')
|
|
1402
|
+
fetch(API_PRESETS + '/files?id=' + encodeURIComponent(p.id))
|
|
1403
|
+
.then(function (r) { return r.json() }).then(function (d) {
|
|
1404
|
+
if (d && Array.isArray(d.files)) {
|
|
1405
|
+
setEditP(function (prev) { return prev ? Object.assign({}, prev, { writable: !!d.writable }) : prev })
|
|
1406
|
+
setEditFiles(d.files)
|
|
1407
|
+
var def = d.files[0] ? d.files[0].name : ''
|
|
1408
|
+
setEditFile(def)
|
|
1409
|
+
if (def) loadEditFile(p.id, def)
|
|
1410
|
+
else setEditMsg('该预设没有可编辑文本文件')
|
|
1411
|
+
} else setEditMsg('读取失败: ' + ((d && d.error) || '未知'))
|
|
1412
|
+
}).catch(function (e) { setEditMsg('读取异常: ' + e.message) })
|
|
1413
|
+
}
|
|
1414
|
+
function pickEditFile(name) {
|
|
1415
|
+
setEditFile(name); setEditContent(''); setEditMsg('')
|
|
1416
|
+
if (editP) loadEditFile(editP.id, name)
|
|
1417
|
+
}
|
|
1418
|
+
function saveEditFile() {
|
|
1419
|
+
if (!editP || editBusy) return
|
|
1420
|
+
setEditBusy(true); setEditMsg('')
|
|
1421
|
+
fetch(API_PRESETS + '/file', {
|
|
1422
|
+
method: 'PUT', headers: { 'content-type': 'application/json' },
|
|
1423
|
+
body: JSON.stringify({ id: editP.id, name: editFile, content: editContent }),
|
|
1424
|
+
}).then(function (r) { return r.json() }).then(function (d) {
|
|
1425
|
+
setEditBusy(false)
|
|
1426
|
+
if (d && d.ok) setEditMsg('✅ 已保存 ' + d.name + ' —— 新会话生效')
|
|
1427
|
+
else setEditMsg('保存失败: ' + ((d && d.error) || '未知'))
|
|
1428
|
+
}).catch(function (e) { setEditBusy(false); setEditMsg('保存异常: ' + e.message) })
|
|
1429
|
+
}
|
|
1354
1430
|
var st = { width: '100%', padding: '5px 8px', boxSizing: 'border-box' }
|
|
1355
1431
|
return h('div', { style: { maxWidth: 860 } },
|
|
1356
|
-
h('
|
|
1432
|
+
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 } },
|
|
1433
|
+
h('h2', { style: { margin: 0 } }, '账号与预设'),
|
|
1434
|
+
h('div', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
|
|
1435
|
+
h('button', { className: 'qqs-btn', title: '保存账号配置(保存后重启生效)', onClick: save, disabled: busy === 'save', style: { fontSize: 16, padding: '4px 12px', lineHeight: '20px' } }, '💾'),
|
|
1436
|
+
h('button', { className: 'qqs-btn', title: '重新读取(放弃未保存的修改)', onClick: function () { load() }, style: { fontSize: 16, padding: '4px 12px', lineHeight: '20px' } }, '🔄'))),
|
|
1357
1437
|
h('p', { style: { fontSize: 12, color: '#888' } }, '一个 QQ 机器人 = 一个账号实例(AppID/AppSecret)。想同时开几个机器人就加几个实例,每个可以配不同的 agent 预设(人格)与工作目录(各人各数据,不串)。⚠️ appSecret 会明文存在本机 cordis.patch.yml(改前自动备份 .bak),别把配置文件传上网。'),
|
|
1358
1438
|
h('div', { style: sectionTitle }, '① 机器人账号(改完点保存, 重启生效)'),
|
|
1359
1439
|
(insts === null ? h('p', null, '加载中…') : null),
|
|
1360
1440
|
(insts || []).map(function (it, i) {
|
|
1361
|
-
//
|
|
1362
|
-
|
|
1363
|
-
var knowChannel = presets.length > 0 && presets.every(function (p) { return p.hasChannelTools === true || p.hasChannelTools === false })
|
|
1364
|
-
var usable = knowChannel ? presets.filter(function (p) { return p.hasChannelTools }) : presets.slice()
|
|
1441
|
+
// QQ 工具由机器人会话自动挂载(2026-09-06) → 预设全列可选, 不再限定"带QQ工具"标记
|
|
1442
|
+
var usable = presets.slice()
|
|
1365
1443
|
var selInvalid = it.preset && !usable.some(function (p) { return p.id === it.preset })
|
|
1366
1444
|
var opts = usable.map(function (p) { return { id: p.id, label: p.name + ' (' + p.id + ')' } })
|
|
1367
|
-
if (selInvalid) opts.unshift({ id: it.preset, label: '⚠️ ' + it.preset + '(
|
|
1445
|
+
if (selInvalid) opts.unshift({ id: it.preset, label: '⚠️ ' + it.preset + '(预设不存在, 请重选)' })
|
|
1446
|
+
// 默认收起: 老账号折叠, 新建(_new/_bound)展开待填; 用户手动切换后以 collapsed 为准
|
|
1447
|
+
var folded = collapsed[it.id] !== undefined ? !!collapsed[it.id] : (!it._new && !it._bound)
|
|
1368
1448
|
return h('div', { key: it.id, style: { border: '1px solid ' + (it.disabled ? '#ffa94d66' : '#00000026'), borderRadius: 10, padding: '8px 12px', margin: '8px 0', background: 'rgba(255,255,255,.5)' } },
|
|
1369
1449
|
h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
|
|
1370
|
-
h('
|
|
1450
|
+
h('button', { className: 'qqs-btn', title: folded ? '展开设置' : '收起设置', onClick: function () { toggleCollapse(it.id) }, style: { padding: '1px 7px', fontSize: 13, lineHeight: '16px', background: '#0000000d', borderColor: '#00000026' } }, folded ? '▸' : '▾'),
|
|
1451
|
+
h('span', { style: { fontWeight: 800, color: '#171a21', cursor: 'pointer', userSelect: 'none' }, onClick: function () { toggleCollapse(it.id) } }, it._bound ? '📱 扫码绑定' : (it._new ? '🆕 新账号' : '🤖 账号')),
|
|
1371
1452
|
h('input', { className: 'qqs-inp', style: Object.assign({ width: 170 }, st), value: it.id, disabled: !it._new && !it._bound, onChange: function (e) { upd(i, { id: e.target.value }) }, placeholder: '实例 id(如 im-qqbot-2)' }),
|
|
1372
1453
|
h('label', { style: { fontSize: 13, whiteSpace: 'nowrap' } },
|
|
1373
1454
|
h('input', { className: 'qqs-cb', type: 'checkbox', checked: !it.disabled, onChange: function (e) { upd(i, { disabled: !e.target.checked }) } }), ' 启用'),
|
|
1455
|
+
h('span', { title: it.disabled ? '已停用' : (it.online ? '在线(绿点)' : '离线/未知(灰点)'), style: { width: 10, height: 10, borderRadius: '50%', display: 'inline-block', background: it.disabled ? '#adb5bd' : (it.online ? '#2f9e44' : '#ced4da'), boxShadow: it.online ? '0 0 0 3px rgba(47,158,68,.18)' : 'none', flex: '0 0 auto', marginLeft: 2 } }),
|
|
1374
1456
|
h('button', { className: 'qqs-btn', onClick: function () { removeInst(i) }, style: { marginLeft: 'auto' } }, '删除')),
|
|
1375
|
-
|
|
1457
|
+
(folded ? null : h('div', { style: { marginTop: 4 } },
|
|
1458
|
+
inputRow('AppID: ', h('input', { className: 'qqs-inp', style: Object.assign({ width: 220 }, st), value: it.appId || '', onChange: function (e) { upd(i, { appId: e.target.value }) }, placeholder: '如 1234567890' })),
|
|
1376
1459
|
inputRow('AppSecret: ', h('input', { className: 'qqs-inp', type: 'password', style: Object.assign({ width: 320 }, st), value: (it.appSecret === SECRET_MASK || (!it.appSecret && it.hasSecret)) ? '' : (it.appSecret || ''), onChange: function (e) { upd(i, { appSecret: e.target.value }) }, placeholder: (it.hasSecret && !it._new) ? '已保存(留空/掩码=保留原值; 填新值=更换)' : '扫码绑定会自动填; 也可手动填(两个都要填全才不弹码)' })),
|
|
1377
|
-
inputRow('Agent 预设(人格): ', (function () { if (
|
|
1378
|
-
return h('span', { className: 'qqs-inp', style: { display: 'inline-block', verticalAlign: 'middle', padding: '5px 10px', fontSize: 12, color: '#e8590c', background: '#fff5f0', borderRadius: 6 } }, '⚠️
|
|
1460
|
+
inputRow('Agent 预设(人格): ', (function () { if (presets.length === 0 && !selInvalid) {
|
|
1461
|
+
return h('span', { className: 'qqs-inp', style: { display: 'inline-block', verticalAlign: 'middle', padding: '5px 10px', fontSize: 12, color: '#e8590c', background: '#fff5f0', borderRadius: 6 } }, '⚠️ 还没有任何 Agent 预设——请在下方②新建/复制一个')
|
|
1379
1462
|
} return h('select', { className: 'qqs-inp', style: { width: 320, padding: '4px 8px' }, value: it.preset || '', onChange: function (e) { upd(i, { preset: e.target.value }) } }, opts.map(function (o) { return h('option', { key: o.id, value: o.id, style: selInvalid && o.id === it.preset ? { color: '#e03131' } : null }, o.label) })) })()),
|
|
1380
1463
|
inputRow('工作目录(各账号数据放这, 留空=默认): ', h('input', { className: 'qqs-inp', style: Object.assign({ width: '90%' }, st), value: it.cwd || '', onChange: function (e) { upd(i, { cwd: e.target.value }) }, placeholder: '如 D:\\bots\\二号机' })),
|
|
1381
1464
|
(function () {
|
|
@@ -1387,30 +1470,39 @@ window.__ModuleLoader__.load({
|
|
|
1387
1470
|
return h('button', { key: w.dir, className: 'qqs-btn', style: { fontSize: 11, padding: '1px 8px', margin: '2px 4px 2px 0' }, onClick: function () { upd(i, { cwd: w.path }) } },
|
|
1388
1471
|
(w.path.length > 46 ? w.path.slice(0, 43) + '…' : w.path) + ' (' + w.count + ')')
|
|
1389
1472
|
}))
|
|
1390
|
-
|
|
1473
|
+
})())))
|
|
1391
1474
|
}),
|
|
1392
|
-
h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center', margin: '8px 0' } },
|
|
1393
|
-
h('button', { className: 'qqs-btn', onClick:
|
|
1394
|
-
h('
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1475
|
+
h('div', { style: { position: 'relative', display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center', margin: '8px 0' } },
|
|
1476
|
+
h('button', { className: 'qqs-btn', onClick: function () { setAddMenu(!addMenu) }, style: { borderColor: '#b197fc99', color: '#171a21', fontWeight: 700 } }, addMenu ? '✕ 关闭' : '➕ 添加账号'),
|
|
1477
|
+
h('span', { style: { fontSize: 12, color: '#888' } }, '点加号选: 扫码绑定 或 手动填凭据'),
|
|
1478
|
+
(addMenu
|
|
1479
|
+
? h('div', { style: { position: 'absolute', top: '100%', left: 0, marginTop: 6, zIndex: 60, minWidth: 280, background: '#fff', border: '1px solid #d9c6ff', borderRadius: 10, boxShadow: '0 10px 28px rgba(60,40,140,.18)', padding: 6, display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
1480
|
+
h('button', { className: 'qqs-btn', style: { display: 'flex', alignItems: 'center', gap: 10, textAlign: 'left', padding: '8px 10px', justifyContent: 'flex-start', background: '#f3f0ff', borderColor: '#d0bfff' }, onClick: function () { setAddMenu(false); startBind() } },
|
|
1481
|
+
h('span', { style: { fontSize: 20 } }, '📱'),
|
|
1482
|
+
h('span', null,
|
|
1483
|
+
h('b', { style: { color: '#171a21' } }, '扫码绑定新机器人'),
|
|
1484
|
+
h('div', { style: { fontSize: 11, color: '#777' } }, '推荐 — 手机 QQ 扫码自动填凭据'))),
|
|
1485
|
+
h('button', { className: 'qqs-btn', style: { display: 'flex', alignItems: 'center', gap: 10, textAlign: 'left', padding: '8px 10px', justifyContent: 'flex-start' }, onClick: function () { setAddMenu(false); addBlank() } },
|
|
1486
|
+
h('span', { style: { fontSize: 20 } }, '✍️'),
|
|
1487
|
+
h('span', null,
|
|
1488
|
+
h('b', { style: { color: '#171a21' } }, '手动新建账号'),
|
|
1489
|
+
h('div', { style: { fontSize: 11, color: '#777' } }, '自己填写 AppID / AppSecret'))))
|
|
1490
|
+
: null)),
|
|
1398
1491
|
h('div', { style: { ...sectionTitle, marginTop: 16, display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } },
|
|
1399
1492
|
'② Agent 预设(人格库)',
|
|
1400
1493
|
h('button', { className: 'qqs-btn', style: { fontSize: 12, padding: '2px 10px', borderColor: '#2b8a3e99', color: '#171a21' }, onClick: function () { setCopy({ fromStandard: true, sourceId: 'standard' }); setCopyId(''); setCopyName('') } }, '➕ 从标准模式新建(带QQ工具)')),
|
|
1401
1494
|
h('p', { style: { fontSize: 12, color: '#888' } }, PRESET_HINT),
|
|
1402
|
-
(
|
|
1403
|
-
? h('p', { style: { fontSize: 12, color: '#e8590c', background: '#fff5f0', borderRadius: 6, padding: '6px 10px' } }, '💡 选一个预设点「复制」生成的副本会自动带上 QQ 工具(send_media/发图等), 之后就能在①账号里给机器人选用了。复制源无所谓, 只要是能跑的人格。')
|
|
1404
|
-
: null),
|
|
1495
|
+
h('p', { style: { fontSize: 12, color: '#364fc7', background: '#edf2ff', borderRadius: 6, padding: '6px 10px' } }, '💡 QQ 工具(send_media/发图等)现在由机器人会话自动挂载(2026-09-06 起), 不再依赖预设是否带标记——任意预设都能在 QQ 用; 「从标准模式新建」的副本仍会自带标记供识别。'),
|
|
1405
1496
|
h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(190px,1fr))', gap: 10, margin: '8px 0' } },
|
|
1406
1497
|
presets.map(function (p) {
|
|
1407
1498
|
return h('div', { key: p.id, style: { border: '1px solid #00000026', borderRadius: 10, padding: '10px 12px', background: 'rgba(255,255,255,.5)' } },
|
|
1408
1499
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
1409
1500
|
h('span', { style: { fontWeight: 800, color: '#171a21' } }, p.name),
|
|
1410
|
-
h('span', { style: { fontSize: 10, padding: '1px 6px', borderRadius: 8, color: '#fff', background: p.hasChannelTools ? '#2b8a3e' : '#
|
|
1501
|
+
h('span', { style: { fontSize: 10, padding: '1px 6px', borderRadius: 8, color: '#fff', background: p.hasChannelTools ? '#2b8a3e' : '#5b9dff' } }, p.hasChannelTools ? 'QQ工具' : '会话自动挂载')),
|
|
1411
1502
|
h('div', { style: { fontSize: 12, color: '#666', margin: '4px 0' } }, 'id=' + p.id),
|
|
1412
1503
|
h('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
1413
1504
|
h('button', { className: 'qqs-btn', style: { padding: '2px 10px', fontSize: 12 }, onClick: function () { setCopy({ sourceId: p.id }); setCopyId(''); setCopyName('') } }, '复制'),
|
|
1505
|
+
h('button', { className: 'qqs-btn', style: { padding: '2px 10px', fontSize: 12, borderColor: '#1971c299' }, onClick: function () { openPresetEditor(p) } }, '✏️ 人格'),
|
|
1414
1506
|
h('button', { className: 'qqs-btn', style: { padding: '2px 10px', fontSize: 12 }, onClick: function () { openPreset(p.id) } }, '📂 打开文件夹')))
|
|
1415
1507
|
})),
|
|
1416
1508
|
copy ? h('div', { style: { position: 'fixed', inset: 0, background: '#000c', zIndex: 999, display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: function () { if (!busy) setCopy(null) } },
|
|
@@ -1421,6 +1513,21 @@ window.__ModuleLoader__.load({
|
|
|
1421
1513
|
h('div', { style: { textAlign: 'right', marginTop: 10 } },
|
|
1422
1514
|
h('button', { className: 'qqs-btn', onClick: function () { if (!busy) setCopy(null) }, style: { marginRight: 8 } }, '取消'),
|
|
1423
1515
|
h('button', { className: 'qqs-btn', onClick: doCopy, disabled: busy === 'copy' }, '生成新预设')))) : null,
|
|
1516
|
+
editP ? h('div', { style: { position: 'fixed', inset: 0, background: '#000c', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: function () { if (!editBusy) setEditP(null) } },
|
|
1517
|
+
h('div', { className: 'qqs-modal', style: { background: '#1f232b', color: '#e8e8e8', borderRadius: 12, padding: 14, maxWidth: 760, width: '94%', maxHeight: '88vh', display: 'flex', flexDirection: 'column' }, onClick: function (e) { e.stopPropagation() } },
|
|
1518
|
+
h('h3', { style: { marginTop: 0 } }, '✏️ 人格编辑器 — ' + editP.name + ' (' + editP.id + ')'),
|
|
1519
|
+
h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', margin: '2px 0 6px', flexWrap: 'wrap' } },
|
|
1520
|
+
h('label', { style: { fontSize: 13 } }, '文件: '),
|
|
1521
|
+
h('select', { className: 'qqs-inp', style: { padding: '4px 8px', color: '#1f2329', maxWidth: 360 }, value: editFile, onChange: function (e) { pickEditFile(e.target.value) } },
|
|
1522
|
+
editFiles.map(function (fl) { return h('option', { key: fl.name, value: fl.name }, fl.name + ' (' + Math.max(1, Math.ceil(fl.size / 1024)) + 'KB)') })),
|
|
1523
|
+
h('span', { style: { fontSize: 12, color: editP.writable === false ? '#ffa94d' : '#7fd8a8' } }, editP.writable === false ? '🔒 只读(内置预设, 请先复制一份再改)' : (editP.writable ? '✅ 可编辑(你的副本)' : '⏳ 读取权限中…'))),
|
|
1524
|
+
h('p', { style: { fontSize: 12, color: '#9fb0c9', margin: '2px 0 6px' } }, '提示: agent.cordis.yml 里 persona 插件 config.text 就是人设正文; 直接整文件编辑, 保存后新会话生效。'),
|
|
1525
|
+
h('textarea', { className: 'qqs-inp', style: { width: '100%', boxSizing: 'border-box', flex: 1, minHeight: 300, fontFamily: 'ui-monospace,Consolas,monospace', fontSize: 12, lineHeight: 1.5, color: '#1f2329', whiteSpace: 'pre', resize: 'vertical', background: '#fff' }, value: editContent, readOnly: editP.writable === false, spellCheck: false, onChange: function (e) { setEditContent(e.target.value) } }),
|
|
1526
|
+
(editMsg ? h('p', { style: { fontSize: 12, color: '#ffa94d', margin: '4px 0' } }, editMsg) : null),
|
|
1527
|
+
h('div', { style: { textAlign: 'right', marginTop: 8 } },
|
|
1528
|
+
h('button', { className: 'qqs-btn', onClick: function () { if (!editBusy) setEditP(null) }, style: { marginRight: 8 } }, '关闭'),
|
|
1529
|
+
(editP.writable === false ? null : h('button', { className: 'qqs-btn', onClick: saveEditFile, disabled: editBusy || editP.writable === undefined, style: { borderColor: '#2f9e4499', color: '#171a21' } }, editBusy ? '保存中…' : '💾 保存')))))
|
|
1530
|
+
: null,
|
|
1424
1531
|
bind ? h('div', { style: { position: 'fixed', inset: 0, background: '#000c', zIndex: 999, display: 'flex', alignItems: 'center', justifyContent: 'center' } },
|
|
1425
1532
|
h('div', { className: 'qqs-modal', style: { background: '#22252e', color: '#e8e8e8', borderRadius: 12, padding: 14, maxWidth: 620, width: '94%' }, onClick: function (e) { e.stopPropagation() } },
|
|
1426
1533
|
h('h3', { style: { marginTop: 0 } }, bind.status === 'error' ? '绑定失败' : '扫码绑定 QQ 机器人'),
|
|
@@ -1445,6 +1552,1250 @@ window.__ModuleLoader__.load({
|
|
|
1445
1552
|
var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180deg,#ffffff,#f5f6f8);border:1px solid #d0d5dd;border-radius:9px;padding:5px 13px;cursor:pointer;transition:all .15s}.qqs-btn:hover{background:#ffffff;border-color:#7c6cf0;color:#7c6cf0}.qqs-btn:active{transform:translateY(1px)}.qqs-inp,.qqs-area{font:inherit;color:#1f2329;background:#ffffff;border:1px solid #d0d5dd;border-radius:9px;padding:5px 10px;outline:none;transition:border-color .15s}.qqs-inp::placeholder,.qqs-area::placeholder{color:#9aa1ab}.qqs-inp:focus,.qqs-area:focus{border-color:#7c6cf0;box-shadow:0 0 0 2px #7c6cf033}.qqs-cb{accent-color:#7c6cf0;width:15px;height:15px}.qqs-card{transition:box-shadow .15s,border-color .15s}.qqs-card:hover{box-shadow:0 2px 10px #0002}.qqs-panel{background:linear-gradient(180deg,#ffffff,#f7f8fa);border:1px solid #e2e5ea;border-radius:16px;padding:16px;box-shadow:0 10px 34px #0002}.qqs-toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;background:#f7f8fa;border:1px solid #e2e5ea;border-radius:12px;padding:8px 10px;margin-bottom:10px}.qqs-modal{background:#ffffff;color:#1f2329}"
|
|
1446
1553
|
function ensureCss() { try { if (!document.getElementById('qqs-css')) { var st = document.createElement('style'); st.id = 'qqs-css'; st.textContent = QQS_CSS; document.head.appendChild(st) } } catch (e) {} }
|
|
1447
1554
|
|
|
1555
|
+
// ── 审批双通道 · Web 浮层(右下角小卡片) ──
|
|
1556
|
+
// 轮询 host 待办池(与 QQ 按钮/文本码共用同一 pending), 有点击即 POST 结算。
|
|
1557
|
+
// 原生 DOM 手写(不依赖 composer slot, 不与宿主原生审批面板打架), 2s 轮询够轻。
|
|
1558
|
+
var AP_FLOAT_CSS = "#qqs-ap-float{position:fixed;right:18px;bottom:84px;z-index:9999;width:min(340px,92vw);background:#fff;border:1px solid #d9c6ff;border-radius:14px;box-shadow:0 10px 34px rgba(80,40,140,.22);padding:0;overflow:hidden;font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif}#qqs-ap-float .ap-h{display:flex;align-items:center;gap:8px;padding:10px 14px;background:linear-gradient(90deg,#7c6cf01f,#7c6cf008);font-size:13px;font-weight:700;color:#4a3a9f;border-bottom:1px solid #efe8ff}#qqs-ap-float .ap-b{padding:10px 14px;font-size:13px;color:#1f2329}#qqs-ap-float .ap-b .ap-t{font-weight:600;margin-bottom:4px;word-break:break-all}#qqs-ap-float .ap-b .ap-r{color:#666;font-size:12px;margin-bottom:6px;word-break:break-all;max-height:60px;overflow:auto}#qqs-ap-float .ap-a{display:flex;gap:8px;padding:0 14px 12px}#qqs-ap-float .ap-a button{font:inherit;font-size:13px;font-weight:700;padding:7px 0;border-radius:9px;cursor:pointer;border:1px solid transparent;transition:opacity .15s}#qqs-ap-float .ap-a button:disabled{opacity:.5;cursor:default}#qqs-ap-float .ap-ok,.qqs-ap-float .ap-ok{background:#e6f7ec;color:#187a3d;border-color:#b8e6c8!important;flex:1}#qqs-ap-float .ap-no{background:#fdeeee;color:#c23131;border-color:#f3c4c4!important;flex:1}#qqs-ap-float .qs-opt{background:#f1ecff;color:#4a3a9f;border-color:#d9c6ff!important;text-align:left;padding:7px 12px;margin:2px 0;border-radius:9px;width:100%}#qqs-ap-float .ap-c{position:absolute;top:6px;right:10px;font-size:16px;color:#999;cursor:pointer;line-height:1}#qqs-ap-float .ap-done{color:#888;font-size:12px;padding:4px 14px 10px}";
|
|
1559
|
+
function ensureFloatCss() { try { if (!document.getElementById('qqs-ap-float-css')) { var st = document.createElement('style'); st.id = 'qqs-ap-float-css'; st.textContent = AP_FLOAT_CSS; document.head.appendChild(st) } } catch (e) {} }
|
|
1560
|
+
function fmtDeadline(ts) {
|
|
1561
|
+
var left = Math.max(0, Math.ceil((ts - Date.now()) / 1000))
|
|
1562
|
+
return left > 60 ? Math.ceil(left / 60) + ' 分钟' : left + ' 秒'
|
|
1563
|
+
}
|
|
1564
|
+
function startApprovalFloat() {
|
|
1565
|
+
try { ensureFloatCss() } catch (e) { return }
|
|
1566
|
+
var lastKey = ''
|
|
1567
|
+
var busy = false
|
|
1568
|
+
var hidden = false
|
|
1569
|
+
var root = null
|
|
1570
|
+
var itemType = '' // 'ap' 审批 | 'qs' 提问
|
|
1571
|
+
|
|
1572
|
+
function show(kind, titleHtml, bodyHtml, actionHtml) {
|
|
1573
|
+
if (!root) {
|
|
1574
|
+
root = document.createElement('div')
|
|
1575
|
+
root.id = 'qqs-ap-float'
|
|
1576
|
+
document.body.appendChild(root)
|
|
1577
|
+
}
|
|
1578
|
+
root.style.display = hidden ? 'none' : 'block'
|
|
1579
|
+
itemType = kind
|
|
1580
|
+
lastKey = kind + ':' + keyOf(kind)
|
|
1581
|
+
root.innerHTML =
|
|
1582
|
+
'<div class="ap-h"><span>' + titleHtml + '</span><span style="flex:1"></span><span class="ap-c" title="关闭">✕</span></div>'
|
|
1583
|
+
+ bodyHtml + actionHtml
|
|
1584
|
+
+ '<div class="ap-done" style="display:none">已处理 ✓</div>'
|
|
1585
|
+
root.querySelector('.ap-c').onclick = function () { root.style.display = 'none'; hidden = true }
|
|
1586
|
+
}
|
|
1587
|
+
function keyOf(kind) {
|
|
1588
|
+
if (kind === 'ap') return (curAp && (curAp.ns + ':' + curAp.code)) || ''
|
|
1589
|
+
return (curQs && (curQs.ns + ':' + curQs.key)) || ''
|
|
1590
|
+
}
|
|
1591
|
+
function doneMsg(m) {
|
|
1592
|
+
if (!root) return
|
|
1593
|
+
var a = root.querySelector('.ap-a'); if (a) a.style.display = 'none'
|
|
1594
|
+
var d = root.querySelector('.ap-done')
|
|
1595
|
+
if (d) { d.style.display = 'block'; d.textContent = m || '已处理 ✓' }
|
|
1596
|
+
setTimeout(function () { if (root) root.style.display = 'none' }, 1800)
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// ── 审批卡片渲染 ──
|
|
1600
|
+
var curAp = null
|
|
1601
|
+
function renderAp(list) {
|
|
1602
|
+
if (itemType && itemType !== 'ap' && !list.length) return
|
|
1603
|
+
var ap = list[0]
|
|
1604
|
+
if (!ap) { if (itemType === 'ap') { if (root) root.style.display = 'none' }; curAp = null; return }
|
|
1605
|
+
curAp = ap
|
|
1606
|
+
var k = ap.ns + ':' + ap.code
|
|
1607
|
+
if (itemType === 'ap' && k === lastKey) return
|
|
1608
|
+
hidden = false
|
|
1609
|
+
show('ap', '⚠️ DSH 权限申请',
|
|
1610
|
+
'<div class="ap-b"><div class="ap-t">工具:' + esc(ap.toolName || '?')
|
|
1611
|
+
+ '</div>' + (ap.reason ? '<div class="ap-r">' + esc(ap.reason) + '</div>' : '')
|
|
1612
|
+
+ '<div style="font-size:11px;color:#999">' + esc(ap.ownerHint || '') + ' · ' + fmtDeadline(ap.deadlineAt) + ' 后自动拒绝 · 共 ' + list.length + ' 条</div></div>',
|
|
1613
|
+
'<div class="ap-a">'
|
|
1614
|
+
+ '<button class="ap-ok" data-act="allow">✅ 允许</button>'
|
|
1615
|
+
+ '<button class="ap-no" data-act="deny">❌ 拒绝</button>'
|
|
1616
|
+
+ '</div>')
|
|
1617
|
+
bindApButtons(ap)
|
|
1618
|
+
}
|
|
1619
|
+
function bindApButtons(ap) {
|
|
1620
|
+
if (!root) return
|
|
1621
|
+
root.querySelectorAll('.ap-a button').forEach(function (btn) {
|
|
1622
|
+
btn.onclick = function () {
|
|
1623
|
+
if (busy) return
|
|
1624
|
+
busy = true
|
|
1625
|
+
var act = btn.getAttribute('data-act')
|
|
1626
|
+
btn.disabled = true
|
|
1627
|
+
fetch('/api/qqbot-settings/approval/decide', {
|
|
1628
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1629
|
+
body: JSON.stringify({ code: ap.code, act: act }),
|
|
1630
|
+
}).then(function (r) { return r.json().catch(function () { return null }) }).then(function (d) {
|
|
1631
|
+
busy = false; lastKey = ''
|
|
1632
|
+
doneMsg((d && d.msg) || (act === 'allow' ? '已允许 ✓' : '已拒绝 ✓'))
|
|
1633
|
+
}).catch(function () { busy = false; btn.disabled = false })
|
|
1634
|
+
}
|
|
1635
|
+
})
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// ── 提问卡片渲染(选项逐个列出, 点击即答) ──
|
|
1639
|
+
var curQs = null
|
|
1640
|
+
function renderQs(list) {
|
|
1641
|
+
if (itemType && itemType !== 'qs' && !list.length) return
|
|
1642
|
+
var q = list[0]
|
|
1643
|
+
if (!q) { if (itemType === 'qs') { if (root) root.style.display = 'none' }; curQs = null; return }
|
|
1644
|
+
curQs = q
|
|
1645
|
+
var k = q.ns + ':' + q.key
|
|
1646
|
+
if (itemType === 'qs' && k === lastKey) return
|
|
1647
|
+
hidden = false
|
|
1648
|
+
var optBtns = (q.options || []).map(function (o, i) {
|
|
1649
|
+
return '<button class="qs-opt" data-i="' + i + '">' + esc(String(o.label || '').slice(0, 40)) + '</button>'
|
|
1650
|
+
}).join('')
|
|
1651
|
+
show('qs', '❓ 远程提问',
|
|
1652
|
+
'<div class="ap-b">' + (q.header ? '<div class="ap-t">' + esc(q.header) + '</div>' : '')
|
|
1653
|
+
+ '<div class="ap-t">' + esc(q.question || '') + '</div>'
|
|
1654
|
+
+ (q.detail ? '<div class="ap-r">' + esc(q.detail) + '</div>' : '')
|
|
1655
|
+
+ '<div style="font-size:11px;color:#999">' + fmtDeadline(q.deadlineAt) + ' 后失效 · 共 ' + list.length + ' 条</div></div>',
|
|
1656
|
+
'<div class="ap-a" style="flex-direction:column;align-items:stretch">' + optBtns + '</div>')
|
|
1657
|
+
bindQsButtons(q)
|
|
1658
|
+
}
|
|
1659
|
+
function bindQsButtons(q) {
|
|
1660
|
+
if (!root) return
|
|
1661
|
+
root.querySelectorAll('.qs-opt').forEach(function (btn) {
|
|
1662
|
+
btn.onclick = function () {
|
|
1663
|
+
if (busy) return
|
|
1664
|
+
busy = true
|
|
1665
|
+
var i = Number(btn.getAttribute('data-i'))
|
|
1666
|
+
btn.disabled = true
|
|
1667
|
+
fetch('/api/qqbot-settings/questions/decide', {
|
|
1668
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1669
|
+
body: JSON.stringify({ key: q.key, optIdx: i }),
|
|
1670
|
+
}).then(function (r) { return r.json().catch(function () { return null }) }).then(function (d) {
|
|
1671
|
+
busy = false; lastKey = ''
|
|
1672
|
+
doneMsg((d && d.msg) || '已选择 ✓')
|
|
1673
|
+
}).catch(function () { busy = false; btn.disabled = false })
|
|
1674
|
+
}
|
|
1675
|
+
})
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
function poll() {
|
|
1679
|
+
if (busy) return
|
|
1680
|
+
try {
|
|
1681
|
+
fetch('/api/qqbot-settings/approval/pending').then(function (r) { return r.json().catch(function () { return null }) }).then(function (d) {
|
|
1682
|
+
if (d && Array.isArray(d.pending)) renderAp(d.pending)
|
|
1683
|
+
else if (itemType === 'ap' && d && !d.ok) { if (root) root.style.display = 'none'; itemType = '' }
|
|
1684
|
+
}).catch(function () {})
|
|
1685
|
+
fetch('/api/qqbot-settings/questions/pending').then(function (r) { return r.json().catch(function () { return null }) }).then(function (d) {
|
|
1686
|
+
if (d && Array.isArray(d.pending)) renderQs(d.pending)
|
|
1687
|
+
else if (itemType === 'qs' && d && !d.ok) { if (root) root.style.display = 'none'; itemType = '' }
|
|
1688
|
+
}).catch(function () {})
|
|
1689
|
+
} catch (e) {}
|
|
1690
|
+
}
|
|
1691
|
+
poll()
|
|
1692
|
+
setInterval(poll, 2000)
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// ══ 群管理悬浮球 dock(可拖拽; 点开=群管理悬浮台; 关闭收回成球) ══
|
|
1696
|
+
// 与审批浮层同款: 原生 DOM 手写挂 body, 全页面可用, 不与宿主 slots 打架。
|
|
1697
|
+
// 三类提示的分工(主人 2026-09-06 定):
|
|
1698
|
+
// · 审批/提问 → 打断式弹出(#qqs-ap-float 保持)
|
|
1699
|
+
// · 入群申请 → 悬浮球上安静红点(不弹窗)
|
|
1700
|
+
// · 群管理(发消息/审批入群/禁言) → 悬浮球点开成操作台
|
|
1701
|
+
var DOCK_CSS = "#qqs-dock-wrap{position:fixed;right:18px;bottom:190px;z-index:9997;font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif}#qqs-dock-ball{width:52px;height:52px;border-radius:50%;background:linear-gradient(160deg,#7c6cf0,#5b4fd8);color:#fff;font-size:24px;line-height:52px;text-align:center;cursor:pointer;box-shadow:0 6px 20px rgba(90,70,220,.4);user-select:none;transition:transform .12s,box-shadow .12s;position:relative}#qqs-dock-ball:hover{transform:scale(1.06)}#qqs-dock-badge{position:absolute;top:-4px;right:-4px;min-width:18px;height:18px;border-radius:9px;background:#ff4d4f;color:#fff;font-size:11px;font-weight:700;line-height:18px;padding:0 4px;box-sizing:border-box;text-align:center;display:none}#qqs-dock-panel{position:fixed;right:18px;bottom:190px;z-index:9998;width:min(720px,94vw);max-height:68vh;display:none;flex-direction:column;background:#fff;border:1px solid #d9c6ff;border-radius:16px;box-shadow:0 12px 40px rgba(60,40,140,.25);overflow:hidden;font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif}#qqs-dock-panel .dk-h{display:flex;align-items:center;gap:8px;padding:10px 14px;background:linear-gradient(90deg,#7c6cf01f,#7c6cf008);font-size:14px;font-weight:700;color:#4a3a9f;border-bottom:1px solid #efe8ff}#qqs-dock-panel .dk-b{padding:10px 14px;overflow:auto;font-size:13px;color:#1f2329}#qqs-dock-panel .dk-tab{display:flex;gap:4px;border-bottom:1px solid #eee;margin-bottom:10px}#qqs-dock-panel .dk-tab button{font:inherit;font-size:13px;padding:6px 14px;border:none;background:none;cursor:pointer;color:#666;border-bottom:2px solid transparent}#qqs-dock-panel .dk-tab button.on{color:#4a3a9f;font-weight:700;border-bottom-color:#7c6cf0}#qqs-dock-panel select.qqs-sel,#qqs-dock-panel input.qqs-txt,#qqs-dock-panel textarea.qqs-txt{font:inherit;color:#1f2329;background:#fff;border:1px solid #d0d5dd;border-radius:8px;padding:5px 8px;outline:none}#qqs-dock-panel .dk-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:6px 0}#qqs-dock-panel .dk-btn{font:inherit;font-size:13px;padding:5px 12px;border-radius:8px;cursor:pointer;border:1px solid #d9c6ff;background:#f1ecff;color:#4a3a9f}#qqs-dock-panel .dk-btn.ok{background:#e6f7ec;color:#187a3d;border-color:#b8e6c8}#qqs-dock-panel .dk-btn.no{background:#fdeeee;color:#c23131;border-color:#f3c4c4}#qqs-dock-panel .dk-btn:disabled{opacity:.5;cursor:default}#qqs-dock-panel .dk-msg{color:#888;font-size:12px;padding:2px 0}#qqs-dock-panel .dk-list{max-height:34vh;overflow:auto;border:1px solid #f0ecff;border-radius:10px;padding:4px}#qqs-dock-panel .dk-item{display:flex;align-items:center;gap:8px;padding:6px 8px;border-bottom:1px solid #f5f2ff;flex-wrap:wrap;font-size:13px}#qqs-dock-panel .dk-item:last-child{border-bottom:none}#qqs-dock-panel .dk-empty{color:#aaa;text-align:center;padding:18px 0;font-size:12px}"
|
|
1702
|
+
// ── 💬 聊天视图样式(dock 追加段, 2026-09-07): QQ 风格气泡, 群友左(bot)右 ──
|
|
1703
|
+
var DOCK_CSS2 = "#qqs-dock-panel .dk-chat-head{display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin:0 0 6px}#qqs-dock-panel .dk-chat-box{overflow-y:auto;overscroll-behavior:contain;background:#f5f6f8;border:1px solid #e6e8ec;border-radius:10px;padding:10px 12px;box-sizing:border-box;height:min(36vh,300px);min-height:140px;scroll-behavior:auto}#qqs-dock-panel .dk-chat-box::-webkit-scrollbar{width:6px}#qqs-dock-panel .dk-chat-box::-webkit-scrollbar-thumb{background:#d3d7dd;border-radius:3px}#qqs-dock-panel .dk-crow{display:flex;gap:8px;align-items:flex-start;margin:0 0 12px}#qqs-dock-panel .dk-crow.out{flex-direction:row-reverse}#qqs-dock-panel .dk-ava{width:32px;height:32px;border-radius:50%;flex:none;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:600;color:#fff;overflow:hidden;user-select:none;background:linear-gradient(150deg,#8fb3e8,#5f8fd9)}#qqs-dock-panel .dk-crow.out .dk-ava{background:linear-gradient(150deg,#5ec7f2,#3b8fe0)}#qqs-dock-panel .dk-cmain{display:flex;flex-direction:column;max-width:calc(100% - 40px);min-width:0}#qqs-dock-panel .dk-crow.in .dk-cmain{align-items:flex-start}#qqs-dock-panel .dk-crow.out .dk-cmain{align-items:flex-end}#qqs-dock-panel .dk-cmeta{font-size:11px;color:#9aa0a8;margin:0 6px 2px;max-width:100%;display:flex;align-items:center;gap:5px;flex-wrap:wrap}#qqs-dock-panel .dk-crow.out .dk-cmeta{flex-direction:row-reverse}#qqs-dock-panel .dk-cbubble{padding:7px 11px;font-size:13px;line-height:1.55;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;box-shadow:0 1px 2px rgba(20,30,60,.06);max-width:100%}#qqs-dock-panel .dk-crow.in .dk-cbubble{background:#fff;border:1px solid #e3e6ea;color:#1f2329;border-radius:3px 10px 10px 10px}#qqs-dock-panel .dk-crow.out .dk-cbubble{background:linear-gradient(180deg,#69a6ff,#3d7df5);color:#fff;border-radius:10px 3px 10px 10px}#qqs-dock-panel .dk-img{display:block;max-width:min(230px,52vw);max-height:200px;border-radius:6px;margin:0 0 3px;object-fit:cover;cursor:zoom-in}#qqs-dock-panel .dk-audio{display:block;max-width:min(260px,60vw);width:100%;height:34px;margin:0 0 2px}#qqs-dock-panel .dk-file{display:inline-flex;align-items:center;gap:5px;max-width:100%;padding:6px 12px;border-radius:8px;background:#f0f6ff;border:1px solid #cfe0fa;color:#2b6bd8;font-size:13px;text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#qqs-dock-panel .dk-file:hover{background:#e2edff}#qqs-dock-panel .dk-video{display:block;max-width:min(260px,60vw);max-height:180px;border-radius:6px;margin:0 0 2px}#qqs-dock-panel .dk-ctag{display:inline-block;font-size:10px;color:#5b8ff0;background:#eaf2ff;border:1px solid #d4e3fd;border-radius:8px;padding:0 6px}#qqs-dock-panel .dk-chat-top{text-align:center;color:#b0b4bb;font-size:11px;padding:2px 0 6px;user-select:none}#qqs-dock-panel .dk-chat-bottom{text-align:center;color:#c3c7cd;font-size:11px;padding:6px 0 0}#qqs-dock-panel .dk-composer{margin-top:8px;border:1px solid #e3e6ea;border-radius:10px;background:#fff;overflow:hidden}#qqs-dock-panel .dk-composer textarea{width:100%;box-sizing:border-box;border:none;outline:none;resize:none;font:inherit;font-size:13px;color:#1f2329;background:transparent;padding:8px 10px 4px;line-height:1.5;max-height:120px}#qqs-dock-panel .dk-cbar{display:flex;align-items:center;gap:4px;padding:4px 8px 6px;flex-wrap:wrap}#qqs-dock-panel .dk-cbar .dk-btn{padding:3px 10px;font-size:12px;border-radius:7px}#qqs-dock-panel .dk-cbar .dk-send{background:linear-gradient(180deg,#69a6ff,#3d7df5);color:#fff;border:none;border-radius:8px;padding:5px 18px;font-size:13px;font-weight:600;cursor:pointer}#qqs-dock-panel .dk-cbar .dk-send:disabled{opacity:.5;cursor:default}#qqs-lightbox{position:fixed;inset:0;z-index:2147483000;background:rgba(8,10,18,.82);display:flex;align-items:center;justify-content:center;cursor:zoom-out}#qqs-lightbox img{max-width:92vw;max-height:92vh;border-radius:8px;box-shadow:0 10px 60px rgba(0,0,0,.6)}#qqs-lightbox .lb-x{position:fixed;right:16px;top:10px;color:#fff;font-size:30px;cursor:pointer;line-height:1;padding:6px}#qqs-dock-panel.dk-full{left:0!important;top:0!important;right:0!important;bottom:0!important;width:100vw!important;max-width:100vw!important;height:100vh!important;max-height:100vh!important;border-radius:0;z-index:2147482000;display:flex;flex-direction:column}#qqs-dock-panel.dk-full .dk-h,#qqs-dock-panel.dk-full .dk-detect{flex:none}#qqs-dock-panel.dk-full .dk-b{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;padding:8px 14px 6px}#qqs-dock-panel.dk-full .dk-chat-wrap{display:flex;flex-direction:column;flex:1;min-height:0}#qqs-dock-panel.dk-full .dk-chat-head{flex:none}#qqs-dock-panel.dk-full .dk-chat-box{flex:1;height:auto!important;min-height:0!important;max-height:none!important;overflow-y:auto;overscroll-behavior:contain}#qqs-dock-panel.dk-full .dk-composer{flex:none;margin-top:6px}"
|
|
1704
|
+
function ensureDockCss2() { try { if (!document.getElementById('qqs-dock-css2')) { var st = document.createElement('style'); st.id = 'qqs-dock-css2'; st.textContent = DOCK_CSS2; document.head.appendChild(st) } } catch (e) {} };
|
|
1705
|
+
function ensureDockCss() { try { if (!document.getElementById('qqs-dock-css')) { var st = document.createElement('style'); st.id = 'qqs-dock-css'; st.textContent = DOCK_CSS; document.head.appendChild(st) } } catch (e) {} }
|
|
1706
|
+
function startQqDock(sessionsSvc) {
|
|
1707
|
+
try { ensureDockCss() } catch (e) { return }
|
|
1708
|
+
try { ensureDockCss2() } catch (e) {}
|
|
1709
|
+
// 幂等(2026-09-07): 宿主重启/client 重连若触发重复初始化, 先移除旧 dock 再建, 保证始终只有一个悬浮球
|
|
1710
|
+
var oldWrap = document.getElementById('qqs-dock-wrap')
|
|
1711
|
+
if (oldWrap) { try { oldWrap.remove() } catch (e) { if (oldWrap.parentNode) oldWrap.parentNode.removeChild(oldWrap) } }
|
|
1712
|
+
// 位置记忆(可拖拽)
|
|
1713
|
+
var pos = null; try { var raw = localStorage.getItem('qqs-dock-pos'); if (raw) pos = JSON.parse(raw) } catch (e) {}
|
|
1714
|
+
var wrap = document.createElement('div')
|
|
1715
|
+
wrap.id = 'qqs-dock-wrap'
|
|
1716
|
+
wrap.innerHTML = '<div id="qqs-dock-ball" title="QQ 群管理(拖拽移动, 点开面板)">🛡<span id="qqs-dock-badge">0</span></div>'
|
|
1717
|
+
document.body.appendChild(wrap)
|
|
1718
|
+
var ball = wrap.querySelector('#qqs-dock-ball')
|
|
1719
|
+
var badge = wrap.querySelector('#qqs-dock-badge')
|
|
1720
|
+
var panel = null
|
|
1721
|
+
var open = false
|
|
1722
|
+
if (pos) { wrap.style.left = pos.x + 'px'; wrap.style.top = pos.y + 'px'; wrap.style.right = 'auto'; wrap.style.bottom = 'auto' }
|
|
1723
|
+
// 拖拽(球上按住移动; 单击与拖拽区分)
|
|
1724
|
+
var dragState = null
|
|
1725
|
+
ball.addEventListener('mousedown', function (e) {
|
|
1726
|
+
dragState = { sx: e.clientX, sy: e.clientY, ox: wrap.offsetLeft, oy: wrap.offsetTop, moved: false }
|
|
1727
|
+
e.preventDefault()
|
|
1728
|
+
})
|
|
1729
|
+
document.addEventListener('mousemove', function (e) {
|
|
1730
|
+
if (!dragState) return
|
|
1731
|
+
var dx = e.clientX - dragState.sx, dy = e.clientY - dragState.sy
|
|
1732
|
+
if (Math.abs(dx) + Math.abs(dy) > 3) dragState.moved = true
|
|
1733
|
+
if (!dragState.moved) return
|
|
1734
|
+
wrap.style.left = Math.max(0, Math.min(window.innerWidth - 60, dragState.ox + dx)) + 'px'
|
|
1735
|
+
wrap.style.top = Math.max(0, Math.min(window.innerHeight - 60, dragState.oy + dy)) + 'px'
|
|
1736
|
+
wrap.style.right = 'auto'; wrap.style.bottom = 'auto'
|
|
1737
|
+
syncPanelPos()
|
|
1738
|
+
})
|
|
1739
|
+
document.addEventListener('mouseup', function (e) {
|
|
1740
|
+
if (!dragState) return
|
|
1741
|
+
var moved = dragState.moved
|
|
1742
|
+
dragState = null
|
|
1743
|
+
if (moved) {
|
|
1744
|
+
try { localStorage.setItem('qqs-dock-pos', JSON.stringify({ x: wrap.offsetLeft, y: wrap.offsetTop })) } catch (err) {}
|
|
1745
|
+
}
|
|
1746
|
+
if (moved && e.target === ball) return
|
|
1747
|
+
})
|
|
1748
|
+
function syncPanelPos() {
|
|
1749
|
+
if (!panel || !open) return
|
|
1750
|
+
layoutPanel()
|
|
1751
|
+
}
|
|
1752
|
+
// ── 面板自适应定位: 按悬浮球屏幕位置翻转展开方向, 边缘 clamp 保证完整可见 ──
|
|
1753
|
+
var lastBallRect = null
|
|
1754
|
+
var manualPanelPos = null // 手动拖过面板后固定位置(不再自动贴球)
|
|
1755
|
+
function layoutPanel() {
|
|
1756
|
+
if (!panel || !open) return
|
|
1757
|
+
var vw = window.innerWidth, vh = window.innerHeight
|
|
1758
|
+
var pad = 10
|
|
1759
|
+
// 手动拖拽固定位置: 保持并 clamp
|
|
1760
|
+
if (manualPanelPos) {
|
|
1761
|
+
var pw2 = panel.offsetWidth || Math.min(720, vw * 0.94)
|
|
1762
|
+
var ph2 = panel.offsetHeight || Math.round(vh * 0.68)
|
|
1763
|
+
panel.style.left = Math.max(pad, Math.min(manualPanelPos.x, vw - pw2 - pad)) + 'px'
|
|
1764
|
+
panel.style.top = Math.max(pad, Math.min(manualPanelPos.y, vh - ph2 - pad)) + 'px'
|
|
1765
|
+
panel.style.right = 'auto'
|
|
1766
|
+
panel.style.bottom = 'auto'
|
|
1767
|
+
panel.style.maxHeight = Math.round(vh - pad * 2) + 'px'
|
|
1768
|
+
return
|
|
1769
|
+
}
|
|
1770
|
+
if (!lastBallRect) { lastBallRect = { left: vw - 70, top: vh - 80, right: vw - 18, bottom: vh - 28, width: 52, height: 52 } }
|
|
1771
|
+
var pw = panel.offsetWidth || Math.min(720, vw * 0.94)
|
|
1772
|
+
var ph = Math.min(panel.offsetHeight || Math.round(vh * 0.68), vh - pad * 2)
|
|
1773
|
+
// 水平: 右侧放不下 → 向左展开; 左侧放不下 → 向右展开; 再整体 clamp
|
|
1774
|
+
var left
|
|
1775
|
+
if (lastBallRect.right + pad + pw <= vw - pad) left = lastBallRect.right + pad
|
|
1776
|
+
else if (lastBallRect.left - pad - pw >= pad) left = lastBallRect.left - pad - pw
|
|
1777
|
+
else left = (lastBallRect.left + lastBallRect.width / 2) > vw / 2 ? vw - pw - pad : pad
|
|
1778
|
+
left = Math.max(pad, Math.min(left, vw - pw - pad))
|
|
1779
|
+
// 垂直: 下方放不下 → 向上展开
|
|
1780
|
+
var top
|
|
1781
|
+
if (lastBallRect.bottom + pad + ph <= vh - pad) top = lastBallRect.bottom + pad
|
|
1782
|
+
else if (lastBallRect.top - pad - ph >= pad) top = lastBallRect.top - pad - ph
|
|
1783
|
+
else top = vh - ph - pad
|
|
1784
|
+
top = Math.max(pad, Math.min(top, vh - ph - pad))
|
|
1785
|
+
panel.style.left = Math.round(left) + 'px'
|
|
1786
|
+
panel.style.top = Math.round(top) + 'px'
|
|
1787
|
+
panel.style.right = 'auto'
|
|
1788
|
+
panel.style.bottom = 'auto'
|
|
1789
|
+
// 面板超高时内部滚动, 头/关闭按钮始终可见
|
|
1790
|
+
panel.style.maxHeight = Math.round(vh - pad * 2) + 'px'
|
|
1791
|
+
}
|
|
1792
|
+
// 展开态拖拽: 按住面板头部(.dk-h 非控件区)移动整个 dock 面板
|
|
1793
|
+
function bindPanelDrag() {
|
|
1794
|
+
if (!panel || !open) return
|
|
1795
|
+
var h = panel.querySelector('.dk-h')
|
|
1796
|
+
if (!h) return
|
|
1797
|
+
h.onmousedown = function (e) {
|
|
1798
|
+
if (e.button !== 0) return
|
|
1799
|
+
var t = e.target
|
|
1800
|
+
if (t && t.closest && t.closest('button,select,input,textarea,a,.qqs-sel,.qqs-txt')) return
|
|
1801
|
+
e.preventDefault()
|
|
1802
|
+
var sx = e.clientX, sy = e.clientY
|
|
1803
|
+
var ox = panel.offsetLeft, oy = panel.offsetTop
|
|
1804
|
+
var moved = false
|
|
1805
|
+
var mm = function (ev) {
|
|
1806
|
+
var dx = ev.clientX - sx, dy = ev.clientY - sy
|
|
1807
|
+
if (Math.abs(dx) + Math.abs(dy) > 2) moved = true
|
|
1808
|
+
panel.style.left = (ox + dx) + 'px'
|
|
1809
|
+
panel.style.top = (oy + dy) + 'px'
|
|
1810
|
+
panel.style.right = 'auto'
|
|
1811
|
+
panel.style.bottom = 'auto'
|
|
1812
|
+
}
|
|
1813
|
+
var mu = function () {
|
|
1814
|
+
document.removeEventListener('mousemove', mm)
|
|
1815
|
+
document.removeEventListener('mouseup', mu)
|
|
1816
|
+
if (moved) manualPanelPos = { x: panel.offsetLeft, y: panel.offsetTop }
|
|
1817
|
+
}
|
|
1818
|
+
document.addEventListener('mousemove', mm)
|
|
1819
|
+
document.addEventListener('mouseup', mu)
|
|
1820
|
+
}
|
|
1821
|
+
// 双击头部 → 取消手动固定, 恢复自动贴球定位
|
|
1822
|
+
h.ondblclick = function (e) {
|
|
1823
|
+
var t = e.target
|
|
1824
|
+
if (t && t.closest && t.closest('button,select,input,textarea,a')) return
|
|
1825
|
+
manualPanelPos = null
|
|
1826
|
+
layoutPanel()
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
function closePanel() {
|
|
1830
|
+
open = false
|
|
1831
|
+
if (panel) { panel.style.display = 'none'; panel.innerHTML = '' }
|
|
1832
|
+
ball.style.display = 'block'
|
|
1833
|
+
}
|
|
1834
|
+
function togglePanel() {
|
|
1835
|
+
if (dragState && dragState.moved) return
|
|
1836
|
+
if (open) { closePanel(); return }
|
|
1837
|
+
if (!panel) {
|
|
1838
|
+
panel = document.createElement('div')
|
|
1839
|
+
panel.id = 'qqs-dock-panel'
|
|
1840
|
+
document.body.appendChild(panel)
|
|
1841
|
+
}
|
|
1842
|
+
open = true
|
|
1843
|
+
// 先量球位置(隐藏前), 再隐藏球
|
|
1844
|
+
var wr = wrap.getBoundingClientRect()
|
|
1845
|
+
lastBallRect = { left: wr.left, top: wr.top, right: wr.right, bottom: wr.bottom, width: wr.width, height: wr.height }
|
|
1846
|
+
ball.style.display = 'none'
|
|
1847
|
+
panel.style.display = 'flex'
|
|
1848
|
+
panel.style.left = 'auto'; panel.style.top = 'auto'; panel.style.right = 'auto'; panel.style.bottom = 'auto'
|
|
1849
|
+
renderPanel()
|
|
1850
|
+
// 等一帧内容渲染完再定位(offsetWidth/offsetHeight 才准确)
|
|
1851
|
+
setTimeout(layoutPanel, 0)
|
|
1852
|
+
}
|
|
1853
|
+
ball.addEventListener('click', togglePanel)
|
|
1854
|
+
// Esc 或点击面板外 → 收回成球
|
|
1855
|
+
document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && open) closePanel() })
|
|
1856
|
+
document.addEventListener('mousedown', function (e) {
|
|
1857
|
+
if (!open) return
|
|
1858
|
+
// 灯箱(#qqs-lightbox)是浮在 dock 外的全屏层: 点它(关闭放大)不能把 dock 也收走
|
|
1859
|
+
if (e.target && e.target.closest && e.target.closest('#qqs-lightbox')) return
|
|
1860
|
+
if (panel && (panel.contains(e.target) || panel === e.target)) return
|
|
1861
|
+
if (wrap && (wrap.contains(e.target) || wrap === e.target)) return
|
|
1862
|
+
closePanel()
|
|
1863
|
+
})
|
|
1864
|
+
function esc(v) { return String(v == null ? '' : v).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') }
|
|
1865
|
+
function api(path, q) {
|
|
1866
|
+
var url = '/api/qqbot-settings/' + path + (q ? '?' + q : '')
|
|
1867
|
+
return fetch(url).then(function (r) { return r.json().catch(function () { return null }) }).catch(function () { return null })
|
|
1868
|
+
}
|
|
1869
|
+
function apiPost(path, body) {
|
|
1870
|
+
return fetch('/api/qqbot-settings/' + path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body || {}) })
|
|
1871
|
+
.then(function (r) { return r.json().catch(function () { return null }) }).catch(function () { return null })
|
|
1872
|
+
}
|
|
1873
|
+
// ── 红点: 入群申请待审汇总(各实例 pending 数之和, 不弹窗) ──
|
|
1874
|
+
function refreshBadge() {
|
|
1875
|
+
api('group/join-summary').then(function (d) {
|
|
1876
|
+
if (!d || !d.ok || !Array.isArray(d.items)) return
|
|
1877
|
+
var n = 0
|
|
1878
|
+
d.items.forEach(function (it) { n += (it.pending || 0) })
|
|
1879
|
+
if (n > 0) { badge.style.display = 'block'; badge.textContent = n > 99 ? '99+' : String(n) }
|
|
1880
|
+
else badge.style.display = 'none'
|
|
1881
|
+
if (panel && open) { var b = panel.querySelector('.dk-join-badge'); if (b) b.style.display = n > 0 ? 'inline-block' : 'none'; if (b) b.textContent = n > 99 ? '99+' : String(n) }
|
|
1882
|
+
})
|
|
1883
|
+
}
|
|
1884
|
+
refreshBadge()
|
|
1885
|
+
setInterval(refreshBadge, 20000)
|
|
1886
|
+
|
|
1887
|
+
// ── 面板状态(每个实例独立保存, 切回不丢) ──
|
|
1888
|
+
var state = { ns: '', accts: [], gid: '', groups: [], tab: 'chat', sendScope: 'group', sendTo: '', sendName: '', sendText: '', insertCtx: true, targetQ: '', c2cs: [], joins: null, mutes: null, members: null, muteSecs: '60', bindGid: '', bindName: '', msg: '', busy: '', wantPeer: null, lookedUp: false, detected: null, detectedHit: null, chatItems: [], chatMore: false, chatBusy: '', chatErr: '', chatOldest: 0, chatText: '', chatIns: true }
|
|
1889
|
+
var chatFlash = '' // 发送结果/错误提示(短时展示, 不被列表计数覆盖)
|
|
1890
|
+
function loadAccts() {
|
|
1891
|
+
api('accounts').then(function (d) {
|
|
1892
|
+
var list = (d && Array.isArray(d.instances) ? d.instances : []).filter(function (a) { return !a.disabled })
|
|
1893
|
+
state.accts = list
|
|
1894
|
+
if (list.length && !state.ns) {
|
|
1895
|
+
// 优先跟随 web 当前会话绑定的 QQ 目标(session-lookup); 未命中才回落到第一个实例
|
|
1896
|
+
reDetectAndSelect()
|
|
1897
|
+
} else if (list.length && state.ns) { /* 保持 */ paintHead(); paintDetect() } else { state.ns = ''; paintHead(); paintDetect() }
|
|
1898
|
+
})
|
|
1899
|
+
}
|
|
1900
|
+
// 读 web 当前会话(sessionId)并反查它是否挂在某个 qqbot 的群/私聊上
|
|
1901
|
+
// 权威源优先: ctx.sessions.list.getSnapshot().current(宿主官方, agent id === session id);
|
|
1902
|
+
// 兜底: localStorage 'dsh.sessions.current'(宿主持久化镜像, 每次切换实时写)。
|
|
1903
|
+
function readCurrentSessionId() {
|
|
1904
|
+
try {
|
|
1905
|
+
if (sessionsSvc && sessionsSvc.list && typeof sessionsSvc.list.getSnapshot === 'function') {
|
|
1906
|
+
var snap = sessionsSvc.list.getSnapshot()
|
|
1907
|
+
if (snap && snap.current) { state.detected = { src: 'sessions.list', sid: snap.current, title: (snap.byId && snap.byId[snap.current] && (snap.byId[snap.current].displayTitle || snap.byId[snap.current].title)) || '' }; return snap.current }
|
|
1908
|
+
}
|
|
1909
|
+
} catch (e) {}
|
|
1910
|
+
try { var raw = localStorage.getItem('dsh.sessions.current'); if (raw) { var o = JSON.parse(raw); if (o && o.sessionId) { state.detected = { src: 'localStorage', sid: o.sessionId, title: '' }; return o.sessionId } } } catch (e) {}
|
|
1911
|
+
state.detected = { src: 'none', sid: '', title: '' }
|
|
1912
|
+
return ''
|
|
1913
|
+
}
|
|
1914
|
+
function lookupCurrentSession(cb) {
|
|
1915
|
+
var sid = readCurrentSessionId()
|
|
1916
|
+
if (!sid) { cb(null); return }
|
|
1917
|
+
api('session-lookup', 'sessionId=' + encodeURIComponent(sid)).then(function (d) {
|
|
1918
|
+
if (d && d.ok && Array.isArray(d.hits) && d.hits.length) {
|
|
1919
|
+
state.detectedHit = d.hits[0]
|
|
1920
|
+
cb(d.hits[0])
|
|
1921
|
+
return
|
|
1922
|
+
}
|
|
1923
|
+
state.detectedHit = null
|
|
1924
|
+
cb(null)
|
|
1925
|
+
}).catch(function () { state.detectedHit = null; cb(null) })
|
|
1926
|
+
}
|
|
1927
|
+
function curDataDir() {
|
|
1928
|
+
var ac = null; state.accts.forEach(function (a) { if (a.ns === state.ns) ac = a })
|
|
1929
|
+
return ac ? (ac.dataDir || '') : ''
|
|
1930
|
+
}
|
|
1931
|
+
function refreshAll() {
|
|
1932
|
+
loadGroups(); loadC2cs()
|
|
1933
|
+
}
|
|
1934
|
+
function loadGroups() {
|
|
1935
|
+
state.groups = []
|
|
1936
|
+
var q = state.ns ? 'ns=' + encodeURIComponent(state.ns) : ''
|
|
1937
|
+
api('group/accounts', q).then(function (d) {
|
|
1938
|
+
state.groups = (d && Array.isArray(d.groups) ? d.groups : []).map(function (g) { return { gid: g.gid, name: g.name || '', from: g.from || '' } })
|
|
1939
|
+
// 期望目标(wantPeer)命中则选中它, 否则回落第一个
|
|
1940
|
+
var wantGid = (state.wantPeer && state.wantPeer.scope === 'group') ? state.wantPeer.peerId : ''
|
|
1941
|
+
if (state.groups.some(function (g) { return g.gid === state.gid })) { /* 保持现选 */ }
|
|
1942
|
+
else if (wantGid && state.groups.some(function (g) { return g.gid === wantGid })) state.gid = wantGid
|
|
1943
|
+
else if (state.groups.length) state.gid = state.groups[0].gid
|
|
1944
|
+
else state.gid = ''
|
|
1945
|
+
paintBody()
|
|
1946
|
+
})
|
|
1947
|
+
}
|
|
1948
|
+
function loadC2cs() {
|
|
1949
|
+
var dd = curDataDir()
|
|
1950
|
+
state.c2cs = []
|
|
1951
|
+
if (!dd) { paintBody(); return }
|
|
1952
|
+
// 与定时任务同款三源合并: 台账 + 本地成员表补名(不露裸 id)
|
|
1953
|
+
var qNs = state.ns ? ('?ns=' + encodeURIComponent(state.ns)) : ''
|
|
1954
|
+
var qDD = dd ? ('?dataDir=' + encodeURIComponent(dd)) : ''
|
|
1955
|
+
Promise.all([
|
|
1956
|
+
api('known-chats', qDD.replace('?', '')),
|
|
1957
|
+
api('group/members_local', qNs.replace('?', '')),
|
|
1958
|
+
]).then(function (rs) {
|
|
1959
|
+
var known = ((rs[0] && rs[0].chats) || [])
|
|
1960
|
+
var nameByMid = {}
|
|
1961
|
+
;((rs[1] && rs[1].members) || []).forEach(function (m) {
|
|
1962
|
+
if (m.mid && m.name) {
|
|
1963
|
+
var old = nameByMid[m.mid]
|
|
1964
|
+
if (!old || m.lastSeen > old.ts) nameByMid[m.mid] = { name: m.name, ts: m.lastSeen || 0 }
|
|
1965
|
+
}
|
|
1966
|
+
})
|
|
1967
|
+
var c2c = []
|
|
1968
|
+
var seen = {}
|
|
1969
|
+
known.forEach(function (c) {
|
|
1970
|
+
if (c.scope !== 'c2c' || !c.id || seen[c.id]) return
|
|
1971
|
+
seen[c.id] = 1
|
|
1972
|
+
var nm = c.name || (nameByMid[c.id] && nameByMid[c.id].name) || ''
|
|
1973
|
+
c2c.push({ id: c.id, name: nm, lastSeen: c.lastSeen || 0, count: c.count || 0 })
|
|
1974
|
+
})
|
|
1975
|
+
Object.keys(nameByMid).forEach(function (mid) {
|
|
1976
|
+
if (!seen[mid]) { seen[mid] = 1; c2c.push({ id: mid, name: nameByMid[mid].name, lastSeen: nameByMid[mid].ts, count: 0 }) }
|
|
1977
|
+
})
|
|
1978
|
+
c2c.sort(function (a, b) { return (b.lastSeen || 0) - (a.lastSeen || 0) })
|
|
1979
|
+
state.c2cs = c2c
|
|
1980
|
+
// 期望目标(wantPeer)是私聊 → 自动选中该对象
|
|
1981
|
+
if (state.wantPeer && state.wantPeer.scope === 'c2c') {
|
|
1982
|
+
var hit = null
|
|
1983
|
+
state.c2cs.forEach(function (c) { if (c.id === state.wantPeer.peerId) hit = c })
|
|
1984
|
+
if (hit) { state.sendTo = hit.id; state.sendName = hit.name || '' }
|
|
1985
|
+
else if (state.wantPeer.peerId) { state.sendTo = state.wantPeer.peerId; state.sendName = state.wantPeer.peerId }
|
|
1986
|
+
} else if (!state.sendTo && state.c2cs.length && state.sendScope === 'c2c') {
|
|
1987
|
+
state.sendTo = state.c2cs[0].id; state.sendName = state.c2cs[0].name || ''
|
|
1988
|
+
}
|
|
1989
|
+
paintBody()
|
|
1990
|
+
})
|
|
1991
|
+
}
|
|
1992
|
+
function fmtDeadline(ts) {
|
|
1993
|
+
var left = Math.max(0, Math.ceil((ts - Date.now()) / 1000))
|
|
1994
|
+
return left > 60 ? Math.ceil(left / 60) + ' 分钟' : left + ' 秒'
|
|
1995
|
+
}
|
|
1996
|
+
function groupSelName() {
|
|
1997
|
+
var g = null; state.groups.forEach(function (x) { if (x.gid === state.gid) g = x })
|
|
1998
|
+
return g ? g.name || state.gid.slice(0, 10) : '(未选群)'
|
|
1999
|
+
}
|
|
2000
|
+
function c2cSelName() {
|
|
2001
|
+
if (state.sendName) return state.sendName
|
|
2002
|
+
var c = null; state.c2cs.forEach(function (x) { if (x.id === state.sendTo) c = x })
|
|
2003
|
+
return c ? (c.name || c.id.slice(0, 10)) : '(未选)'
|
|
2004
|
+
}
|
|
2005
|
+
function sendNow() {
|
|
2006
|
+
var t = (state.sendText || '').trim()
|
|
2007
|
+
if (!t) { state.msg = '先输入内容'; paintBody(); return }
|
|
2008
|
+
state.busy = 'send'; paintBody()
|
|
2009
|
+
var body = { text: t, ns: state.ns || undefined, insertContext: state.insertCtx === true }
|
|
2010
|
+
if (state.sendScope === 'c2c') {
|
|
2011
|
+
if (!state.sendTo) { state.msg = '先选私聊对象'; state.busy = ''; paintBody(); return }
|
|
2012
|
+
body.openid = state.sendTo
|
|
2013
|
+
} else {
|
|
2014
|
+
if (!state.gid) { state.msg = '先选群'; state.busy = ''; paintBody(); return }
|
|
2015
|
+
body.gid = state.gid
|
|
2016
|
+
}
|
|
2017
|
+
apiPost(state.sendScope === 'c2c' ? 'chat/send' : 'group/send', body).then(function (d) {
|
|
2018
|
+
state.busy = ''
|
|
2019
|
+
state.msg = (d && (d.msg || (d.err && d.err.human))) || (d && d.error) || '发送结果未知'
|
|
2020
|
+
if (d && d.ok) state.sendText = ''
|
|
2021
|
+
paintBody()
|
|
2022
|
+
})
|
|
2023
|
+
}
|
|
2024
|
+
function loadJoins() {
|
|
2025
|
+
state.joins = null; paintBody()
|
|
2026
|
+
if (!state.gid) { state.joins = []; paintBody(); return }
|
|
2027
|
+
var q = 'gid=' + encodeURIComponent(state.gid) + (state.ns ? '&ns=' + encodeURIComponent(state.ns) : '')
|
|
2028
|
+
api('group/join_requests', q).then(function (d) {
|
|
2029
|
+
state.joins = (d && d.ok && Array.isArray(d.list) ? d.list : []).map(function (j) { return { member_openid: j.member_openid, username: j.username || '(未知昵称)', verify: (j.verify_info && (j.verify_info.verify_message || j.verify_info.method)) || '', risk: j.risk_tips || '', source: j.apply_source === 'invited' ? '被邀请' : '主动申请' } })
|
|
2030
|
+
paintBody()
|
|
2031
|
+
})
|
|
2032
|
+
}
|
|
2033
|
+
function doApprove(mid, op) {
|
|
2034
|
+
var reason = op === 'decline' ? window.prompt('拒绝理由(可留空)') : '-'
|
|
2035
|
+
if (op === 'decline' && reason === null) return
|
|
2036
|
+
state.busy = 'ap-' + mid; paintBody()
|
|
2037
|
+
apiPost('group/approve', { ns: state.ns || undefined, gid: state.gid, member_openid: mid, op: op, reason: reason || '' }).then(function (d) {
|
|
2038
|
+
state.busy = ''
|
|
2039
|
+
state.msg = (d && (d.msg || (d.err && d.err.human))) || '审批结果未知'
|
|
2040
|
+
refreshBadge(); loadJoins()
|
|
2041
|
+
})
|
|
2042
|
+
}
|
|
2043
|
+
function loadMutes() {
|
|
2044
|
+
state.mutes = null; state.members = null; paintBody()
|
|
2045
|
+
if (!state.gid) { state.mutes = []; state.members = []; paintBody(); return }
|
|
2046
|
+
var q = 'gid=' + encodeURIComponent(state.gid) + (state.ns ? '&ns=' + encodeURIComponent(state.ns) : '')
|
|
2047
|
+
api('group/mute_state', q).then(function (d) {
|
|
2048
|
+
var ms = (d && d.ok && d.data && Array.isArray(d.data.members) ? d.data.members : [])
|
|
2049
|
+
state.mutes = ms.map(function (m) { return { member_openid: m.member_openid, username: m.username || '', expire: m.mute_expire_at || '' } })
|
|
2050
|
+
paintBody()
|
|
2051
|
+
})
|
|
2052
|
+
api('group/members_local', q).then(function (d) {
|
|
2053
|
+
state.members = (d && d.ok && Array.isArray(d.members) ? d.members : []).map(function (m) { return { mid: m.mid, name: m.name || '' } })
|
|
2054
|
+
paintBody()
|
|
2055
|
+
})
|
|
2056
|
+
}
|
|
2057
|
+
function doMute(mid, action, seconds) {
|
|
2058
|
+
var secs = action === 'mute' ? Math.max(1, Math.round(Number(seconds || 600))) : 0
|
|
2059
|
+
state.busy = 'm-' + mid; paintBody()
|
|
2060
|
+
apiPost('group/mute', { ns: state.ns || undefined, gid: state.gid, member_openid: mid, action: action, seconds: secs }).then(function (d) {
|
|
2061
|
+
state.busy = ''
|
|
2062
|
+
state.msg = (d && (d.msg || (d.err && d.err.human))) || '操作结果未知'
|
|
2063
|
+
loadMutes(); refreshBadge()
|
|
2064
|
+
})
|
|
2065
|
+
}
|
|
2066
|
+
function doBind() {
|
|
2067
|
+
var g = (state.bindGid || '').trim()
|
|
2068
|
+
if (!g) { state.msg = '先粘贴 group_openid'; paintBody(); return }
|
|
2069
|
+
apiPost('group/bind', { ns: state.ns || undefined, gid: g, name: (state.bindName || '').trim() }).then(function (d) {
|
|
2070
|
+
if (d && d.ok) { state.msg = '已登记群 ✓'; state.bindGid = ''; state.bindName = ''; loadGroups() }
|
|
2071
|
+
else state.msg = (d && d.error) || '绑定失败'
|
|
2072
|
+
paintBody()
|
|
2073
|
+
})
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
function paintHead() {
|
|
2077
|
+
if (!panel || !open) return
|
|
2078
|
+
var h = panel.querySelector('.dk-h')
|
|
2079
|
+
if (!h) return
|
|
2080
|
+
var opts = state.accts.map(function (a) { return '<option value="' + esc(a.ns) + '"' + (a.ns === state.ns ? ' selected' : '') + '>' + esc(a.ns) + (a.appId ? ' (' + a.appId + ')' : '') + (a.disabled ? ' [停用]' : '') + '</option>' }).join('')
|
|
2081
|
+
h.innerHTML = '<span>🛡 QQ 群管理台</span>'
|
|
2082
|
+
+ '<select class="qqs-sel" id="dk-ns" style="max-width:220px">' + (opts || '<option value="">无账号(去账号页添加)</option>') + '</select>'
|
|
2083
|
+
+ '<span style="flex:1"></span>'
|
|
2084
|
+
+ '<span class="dk-msg" id="dk-headmsg" style="color:#888;font-size:12px">直连 QQ 官方 · 面板操作 = 主人直发</span>'
|
|
2085
|
+
+ '<button class="dk-btn" id="dk-full" title="全屏/还原">⛶</button>'
|
|
2086
|
+
+ '<button class="dk-btn" id="dk-close" title="收回成球">➖</button>'
|
|
2087
|
+
var fb = h.querySelector('#dk-full')
|
|
2088
|
+
if (fb) fb.onclick = function (e) { e.stopPropagation(); var fs = panel.classList.toggle('dk-full'); fb.textContent = fs ? '🗗' : '⛶'; setTimeout(layoutPanel, 0) }
|
|
2089
|
+
h.querySelector('#dk-close').onclick = function () { closePanel(); ball.style.display = 'block' }
|
|
2090
|
+
h.querySelector('#dk-ns').onchange = function (e) {
|
|
2091
|
+
state.ns = e.target.value; state.gid = ''; state.sendTo = ''; state.sendName = ''; state.wantPeer = null; state.msg = ''
|
|
2092
|
+
refreshAll(); paintHead()
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
function applyTargetFilter(panel, state) {
|
|
2096
|
+
var q = String(state.targetQ || '').trim().toLowerCase()
|
|
2097
|
+
var scope = state.sendScope
|
|
2098
|
+
var sel = scope === 'c2c' ? panel.querySelector('#dk-c2c') : panel.querySelector('#dk-gid')
|
|
2099
|
+
if (!sel) return
|
|
2100
|
+
var selVal = scope === 'c2c' ? state.sendTo : state.gid
|
|
2101
|
+
var opts = sel.querySelectorAll('option')
|
|
2102
|
+
var shown = 0, total = 0
|
|
2103
|
+
for (var i = 0; i < opts.length; i++) {
|
|
2104
|
+
var o = opts[i]
|
|
2105
|
+
total++
|
|
2106
|
+
var keep = !q || o.textContent.toLowerCase().indexOf(q) >= 0 || o.value === selVal
|
|
2107
|
+
o.hidden = !keep
|
|
2108
|
+
if (keep) shown++
|
|
2109
|
+
}
|
|
2110
|
+
var cnt = panel.querySelector('#dk-target-count')
|
|
2111
|
+
if (cnt) cnt.textContent = q ? ('匹配 ' + shown + '/' + total) : ''
|
|
2112
|
+
}
|
|
2113
|
+
function paintBody() {
|
|
2114
|
+
if (!panel || !open) return
|
|
2115
|
+
var b = panel.querySelector('.dk-b')
|
|
2116
|
+
if (!b) return
|
|
2117
|
+
var groupOpts = state.groups.map(function (g) {
|
|
2118
|
+
var tail = g.gid.length > 8 ? '…' + g.gid.slice(-6) : g.gid
|
|
2119
|
+
var label = g.name ? (g.name + ' (' + tail + ')') : ('群 ' + tail)
|
|
2120
|
+
return '<option value="' + esc(g.gid) + '"' + (g.gid === state.gid ? ' selected' : '') + '>' + esc(label) + (g.from ? ' [' + g.from + ']' : '') + '</option>'
|
|
2121
|
+
}).join('')
|
|
2122
|
+
var groupSelHtml = '<select class="qqs-sel" id="dk-gid" style="min-width:220px">' + (groupOpts || '<option value="">暂无群(可于下方登记)</option>') + '</select>'
|
|
2123
|
+
var c2cOpts = state.c2cs.map(function (c) {
|
|
2124
|
+
var tail = c.id.length > 8 ? '…' + c.id.slice(-6) : c.id
|
|
2125
|
+
var label = c.name ? (c.name + ' (' + tail + ')') : ('私聊 ' + tail)
|
|
2126
|
+
return '<option value="' + esc(c.id) + '"' + (c.id === state.sendTo ? ' selected' : '') + '>' + esc(label) + '</option>'
|
|
2127
|
+
}).join('')
|
|
2128
|
+
var tabs = '<div class="dk-tab">'
|
|
2129
|
+
+ '<button data-t="chat" class="' + (state.tab === 'chat' ? 'on' : '') + '">💬 聊天</button>'
|
|
2130
|
+
+ '<button data-t="join" class="' + (state.tab === 'join' ? 'on' : '') + '">📥 入群审批<span class="dk-join-badge" style="display:none;background:#ff4d4f;color:#fff;border-radius:8px;font-size:11px;padding:0 5px;margin-left:4px">0</span></button>'
|
|
2131
|
+
+ '<button data-t="mute" class="' + (state.tab === 'mute' ? 'on' : '') + '">🔇 禁言</button>'
|
|
2132
|
+
+ '</div>'
|
|
2133
|
+
var body = tabs
|
|
2134
|
+
var status = state.msg ? '<div class="dk-msg" style="color:#2f9e44;margin:4px 0">' + esc(state.msg) + '</div>' : ''
|
|
2135
|
+
if (!state.ns || state.accts.length === 0) {
|
|
2136
|
+
body += '<div class="dk-empty">还没有配置机器人账号 → 到「账号与预设」页添加后再回来</div>'
|
|
2137
|
+
} else if (state.tab === 'send') {
|
|
2138
|
+
body += '<div class="dk-row">目标类型: '
|
|
2139
|
+
+ '<label style="display:inline-flex;align-items:center;gap:4px"><input type="radio" name="dk-scope" value="group"' + (state.sendScope !== 'c2c' ? ' checked' : '') + '> 群聊</label> '
|
|
2140
|
+
+ '<label style="display:inline-flex;align-items:center;gap:4px"><input type="radio" name="dk-scope" value="c2c"' + (state.sendScope === 'c2c' ? ' checked' : '') + '> 私聊</label></div>'
|
|
2141
|
+
body += '<div class="dk-row" style="margin:2px 0 4px">'
|
|
2142
|
+
+ '<input class="qqs-txt" id="dk-target-search" placeholder="' + (state.sendScope === 'c2c' ? '🔍 搜私聊对象(昵称/ID)…' : '🔍 搜群(备注/昵称/ID)…') + '" value="' + esc(state.targetQ || '') + '" style="width:100%;box-sizing:border-box;font-size:12px;padding:4px 8px">'
|
|
2143
|
+
+ '<span class="dk-msg" id="dk-target-count"></span></div>'
|
|
2144
|
+
var targetSel = state.sendScope === 'c2c'
|
|
2145
|
+
? '<select class="qqs-sel" id="dk-c2c" style="min-width:220px">' + (c2cOpts || '<option value="">暂无私聊对象</option>') + '</select>'
|
|
2146
|
+
: groupSelHtml
|
|
2147
|
+
body += '<div class="dk-row" id="dk-target-row">' + targetSel + '</div>'
|
|
2148
|
+
body += '<textarea class="qqs-txt" id="dk-text" rows="3" style="width:100%;box-sizing:border-box;margin:6px 0" placeholder="输入内容…(@某人 用 <@对方openid> 无斜杠)">' + esc(state.sendText) + '</textarea>'
|
|
2149
|
+
body += '<div class="dk-row">'
|
|
2150
|
+
+ '<label style="display:inline-flex;align-items:center;gap:5px;font-size:12px;color:#666;cursor:pointer" title="发完后往该会话写入一条「用户代你发送: …」的模拟用户消息(web 流可见、不唤醒、不开回合;标记不会发到 QQ)">'
|
|
2151
|
+
+ '<input type="checkbox" id="dk-insctx"' + (state.insertCtx ? ' checked' : '') + '> 🧠 记入 bot 上下文(模拟用户消息·不唤醒)</label>'
|
|
2152
|
+
+ '<span style="flex:1"></span>'
|
|
2153
|
+
+ '<button class="dk-btn ok" id="dk-send">🚀 发送到 ' + (state.sendScope === 'c2c' ? ('私聊「' + esc(c2cSelName()) + '」') : '群「' + esc(groupSelName()) + '」') + '</button>'
|
|
2154
|
+
+ '<span class="dk-msg">已输 ' + state.sendText.length + '/2000</span></div>'
|
|
2155
|
+
} else if (state.tab === 'chat') {
|
|
2156
|
+
body += '<div class="dk-row">目标类型: '
|
|
2157
|
+
+ '<label style="display:inline-flex;align-items:center;gap:4px"><input type="radio" name="dk-scope" value="group"' + (state.sendScope !== 'c2c' ? ' checked' : '') + '> 群聊</label> '
|
|
2158
|
+
+ '<label style="display:inline-flex;align-items:center;gap:4px"><input type="radio" name="dk-scope" value="c2c"' + (state.sendScope === 'c2c' ? ' checked' : '') + '> 私聊</label></div>'
|
|
2159
|
+
body += '<div class="dk-row" style="margin:2px 0 4px">'
|
|
2160
|
+
+ '<input class="qqs-txt" id="dk-target-search" placeholder="' + (state.sendScope === 'c2c' ? '🔍 搜私聊对象(昵称/ID)…' : '🔍 搜群(备注/昵称/ID)…') + '" value="' + esc(state.targetQ || '') + '" style="width:100%;box-sizing:border-box;font-size:12px;padding:4px 8px">'
|
|
2161
|
+
+ '<span class="dk-msg" id="dk-target-count"></span></div>'
|
|
2162
|
+
var chatTargetSel = state.sendScope === 'c2c'
|
|
2163
|
+
? '<select class="qqs-sel" id="dk-c2c" style="min-width:220px">' + (c2cOpts || '<option value="">暂无私聊对象</option>') + '</select>'
|
|
2164
|
+
: groupSelHtml
|
|
2165
|
+
body += '<div class="dk-row" id="dk-target-row">' + chatTargetSel + '</div>'
|
|
2166
|
+
body += '<div class="dk-chat-wrap">'
|
|
2167
|
+
+ '<div class="dk-chat-head">'
|
|
2168
|
+
+ '<button class="dk-btn ok" id="dk-chat-refresh">🔄 刷新</button>'
|
|
2169
|
+
+ '<span class="dk-msg" id="dk-chat-status" style="flex:1">QQ 会话记录(注入/系统文本已滤)</span>'
|
|
2170
|
+
+ '</div>'
|
|
2171
|
+
+ '<div class="dk-chat-box" id="dk-chat-box"></div>'
|
|
2172
|
+
+ '</div>'
|
|
2173
|
+
// QQ 风格输入栏: 文本发送; bbcode [MEDIA:图片|路径/链接] 支持本地图与网络图
|
|
2174
|
+
body += '<div class="dk-composer">'
|
|
2175
|
+
+ '<textarea id="dk-chat-input" rows="2" placeholder="输入文字发送; 点 📷插图/📎文件 把本地路径或网络链接变成 [MEDIA:图片|来源] 放进文本框(可拖拽/粘贴图片)">' + esc(state.chatText) + '</textarea>'
|
|
2176
|
+
+ '<div class="dk-cbar">'
|
|
2177
|
+
+ '<button class="dk-btn" id="dk-chat-ins" title="发送后写一条「用户代你发送」模拟消息进 bot 上下文(web 流可见)">🧠 ' + (state.chatIns ? '记入上下文' : '不记上下文') + '</button>'
|
|
2178
|
+
+ '<span class="dk-msg" id="dk-chat-cnt" style="color:#b3b7bd">0/2000</span>'
|
|
2179
|
+
+ '<span style="flex:1"></span>'
|
|
2180
|
+
+ '<input type="file" id="dk-chat-file" multiple hidden>'
|
|
2181
|
+
+ '<button class="dk-btn" id="dk-chat-add-file" title="选择本机文件(小文件自动上传, 大文件请直接输入本机路径)">📎 文件</button>'
|
|
2182
|
+
+ '<button class="dk-btn" id="dk-chat-add-img" title="插入图片: 弹窗输入本机路径或网络图片地址 → 自动变成 [MEDIA:图片|来源]">📷 插图</button>'
|
|
2183
|
+
+ '<button class="dk-send" id="dk-chat-send">发送</button>'
|
|
2184
|
+
+ '</div>'
|
|
2185
|
+
+ '</div>'
|
|
2186
|
+
} else if (state.tab === 'join') {
|
|
2187
|
+
body += '<div class="dk-row">目标群: ' + groupSelHtml
|
|
2188
|
+
+ '<button class="dk-btn" id="dk-refresh-join">🔄 刷新</button>'
|
|
2189
|
+
+ '<span class="dk-msg">机器人需为群管理员; 通过/拒绝直接生效</span></div>'
|
|
2190
|
+
body += '<div class="dk-list" id="dk-join-list"></div>'
|
|
2191
|
+
} else if (state.tab === 'mute') {
|
|
2192
|
+
body += '<div class="dk-row">目标群: ' + groupSelHtml
|
|
2193
|
+
+ '<button class="dk-btn" id="dk-refresh-mute">🔄 刷新</button></div>'
|
|
2194
|
+
body += '<div class="dk-msg">全员禁言: 官方接口未开放 · 只能操作普通成员(群主/管理员不可禁)</div>'
|
|
2195
|
+
body += '<div style="font-weight:700;font-size:13px;margin:8px 0 4px">① 禁言成员(机器人见过的)</div>'
|
|
2196
|
+
body += '<div class="dk-list" id="dk-member-list"></div>'
|
|
2197
|
+
body += '<div style="font-weight:700;font-size:13px;margin:8px 0 4px">② 正在禁言中</div>'
|
|
2198
|
+
body += '<div class="dk-list" id="dk-mute-list"></div>'
|
|
2199
|
+
}
|
|
2200
|
+
body += status
|
|
2201
|
+
body += '<div class="dk-row" style="border-top:1px solid #f0ecff;padding-top:8px;margin-top:8px"><span class="dk-msg">登记群(官方无群列表,手动粘贴):</span>'
|
|
2202
|
+
+ '<input class="qqs-txt" id="dk-bindgid" placeholder="group_openid" value="' + esc(state.bindGid) + '" style="width:200px">'
|
|
2203
|
+
+ '<input class="qqs-txt" id="dk-bindname" placeholder="备注(可选)" value="' + esc(state.bindName) + '" style="width:110px">'
|
|
2204
|
+
+ '<button class="dk-btn" id="dk-bind">绑定</button></div>'
|
|
2205
|
+
b.innerHTML = body
|
|
2206
|
+
bindBodyEvents()
|
|
2207
|
+
bindPanelDrag()
|
|
2208
|
+
if (state.tab === 'send') applyTargetFilter(panel, state)
|
|
2209
|
+
if (state.tab === 'chat') renderChatList()
|
|
2210
|
+
if (state.tab === 'join') renderJoinList()
|
|
2211
|
+
if (state.tab === 'mute') { renderMemberList(); renderMuteList() }
|
|
2212
|
+
// 首次进入聊天 tab 自动拉最新一页
|
|
2213
|
+
if (state.tab === 'chat' && !state.chatItems.length && !state.chatBusy) loadChat(true)
|
|
2214
|
+
setTimeout(layoutPanel, 0)
|
|
2215
|
+
}
|
|
2216
|
+
function bindBodyEvents() {
|
|
2217
|
+
var tbs = panel.querySelectorAll('.dk-tab button')
|
|
2218
|
+
tbs.forEach(function (btn) {
|
|
2219
|
+
btn.onclick = function () {
|
|
2220
|
+
state.tab = btn.getAttribute('data-t'); state.msg = ''
|
|
2221
|
+
if (state.tab === 'join') loadJoins(); else if (state.tab === 'mute') loadMutes()
|
|
2222
|
+
else if (state.tab === 'chat') { state.chatItems = []; state.chatErr = ''; }
|
|
2223
|
+
paintBody()
|
|
2224
|
+
}
|
|
2225
|
+
})
|
|
2226
|
+
var gsel = panel.querySelector('#dk-gid')
|
|
2227
|
+
if (gsel) gsel.onchange = function (e) {
|
|
2228
|
+
state.gid = e.target.value; state.msg = ''
|
|
2229
|
+
if (state.tab === 'join') loadJoins(); else if (state.tab === 'mute') loadMutes()
|
|
2230
|
+
else if (state.tab === 'chat') { state.chatItems = []; state.chatErr = ''; state.chatBusy = ''; paintBody(); loadChat(true) }
|
|
2231
|
+
else paintBody()
|
|
2232
|
+
}
|
|
2233
|
+
var rads = panel.querySelectorAll('input[name="dk-scope"]')
|
|
2234
|
+
rads.forEach(function (r) {
|
|
2235
|
+
r.onchange = function () {
|
|
2236
|
+
state.sendScope = r.value; state.msg = ''
|
|
2237
|
+
if (r.value !== 'c2c') state.sendTo = ''
|
|
2238
|
+
if (state.tab === 'chat') { state.chatItems = []; state.chatErr = ''; state.chatBusy = ''; paintBody(); loadChat(true) }
|
|
2239
|
+
else paintBody()
|
|
2240
|
+
}
|
|
2241
|
+
})
|
|
2242
|
+
var c2c = panel.querySelector('#dk-c2c')
|
|
2243
|
+
if (c2c) c2c.onchange = function (e) {
|
|
2244
|
+
state.sendTo = e.target.value; state.msg = ''
|
|
2245
|
+
if (state.tab === 'chat') { state.chatItems = []; state.chatErr = ''; state.chatBusy = ''; paintBody(); loadChat(true) }
|
|
2246
|
+
else paintBody()
|
|
2247
|
+
}
|
|
2248
|
+
var srch = panel.querySelector('#dk-target-search')
|
|
2249
|
+
if (srch) srch.oninput = function (e) { state.targetQ = e.target.value; applyTargetFilter(panel, state) }
|
|
2250
|
+
var txt = panel.querySelector('#dk-text')
|
|
2251
|
+
if (txt) { txt.oninput = function (e) { state.sendText = e.target.value; var n = panel.querySelector('.dk-msg'); if (n) n.textContent = '已输 ' + state.sendText.length + '/2000' } }
|
|
2252
|
+
var ins = panel.querySelector('#dk-insctx')
|
|
2253
|
+
if (ins) { ins.onchange = function (e) { state.insertCtx = !!e.target.checked } }
|
|
2254
|
+
var snd = panel.querySelector('#dk-send')
|
|
2255
|
+
if (snd) snd.onclick = sendNow
|
|
2256
|
+
var rj = panel.querySelector('#dk-refresh-join')
|
|
2257
|
+
if (rj) rj.onclick = loadJoins
|
|
2258
|
+
var rm = panel.querySelector('#dk-refresh-mute')
|
|
2259
|
+
if (rm) rm.onclick = loadMutes
|
|
2260
|
+
var crf = panel.querySelector('#dk-chat-refresh')
|
|
2261
|
+
if (crf) crf.onclick = function () { state.chatItems = []; state.chatErr = ''; state.chatBusy = ''; loadChat(true) }
|
|
2262
|
+
var cbox = panel.querySelector('#dk-chat-box')
|
|
2263
|
+
if (cbox) cbox.onscroll = function () {
|
|
2264
|
+
if (state.tab !== 'chat' || !state.chatMore || state.chatBusy) return
|
|
2265
|
+
if (cbox.scrollTop <= 4) loadChat(false) // 顶部 → 加载更早
|
|
2266
|
+
}
|
|
2267
|
+
var cinput = panel.querySelector('#dk-chat-input')
|
|
2268
|
+
if (cinput) {
|
|
2269
|
+
cinput.oninput = function (e) { state.chatText = e.target.value; var n = panel.querySelector('#dk-chat-cnt'); if (n) n.textContent = state.chatText.length + '/2000' }
|
|
2270
|
+
cinput.onkeydown = function (e) {
|
|
2271
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); chatSend('auto') }
|
|
2272
|
+
}
|
|
2273
|
+
// 粘贴/拖拽图片 → 自动转 [MEDIA:kind|来源] 追加进文本框(有本机路径直用, 否则小文件上传到 dock-uploads)
|
|
2274
|
+
cinput.onpaste = function (e) {
|
|
2275
|
+
var files = (e.clipboardData && e.clipboardData.files) ? Array.from(e.clipboardData.files) : []
|
|
2276
|
+
// 截图/复制位图时 clipboardData.files 常为空 → 从 items 里捞图片
|
|
2277
|
+
if (!files.length && e.clipboardData && e.clipboardData.items) {
|
|
2278
|
+
for (var i = 0; i < e.clipboardData.items.length; i++) {
|
|
2279
|
+
var it = e.clipboardData.items[i]
|
|
2280
|
+
if (it.kind === 'file' || String(it.type).indexOf('image/') === 0) {
|
|
2281
|
+
var f = (it.getAsFile && it.getAsFile()) || null
|
|
2282
|
+
if (f) files.push(f)
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
if (files.length) { e.preventDefault(); chatIngestFiles(files) }
|
|
2287
|
+
}
|
|
2288
|
+
cinput.addEventListener('dragover', function (e) { e.preventDefault() })
|
|
2289
|
+
cinput.ondrop = function (e) {
|
|
2290
|
+
e.preventDefault()
|
|
2291
|
+
var files = (e.dataTransfer && e.dataTransfer.files) ? Array.from(e.dataTransfer.files) : []
|
|
2292
|
+
if (files.length) chatIngestFiles(files)
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
var cins = panel.querySelector('#dk-chat-ins')
|
|
2296
|
+
if (cins) cins.onclick = function () { state.chatIns = !state.chatIns; cins.textContent = '🧠 ' + (state.chatIns ? '记入上下文' : '不记上下文') }
|
|
2297
|
+
var csend = panel.querySelector('#dk-chat-send')
|
|
2298
|
+
if (csend) csend.onclick = function () { chatSend('auto') }
|
|
2299
|
+
var fadd = panel.querySelector('#dk-chat-add-file')
|
|
2300
|
+
var fpick = panel.querySelector('#dk-chat-file')
|
|
2301
|
+
if (fadd && fpick) fadd.onclick = function () { fpick.value = ''; fpick.click() }
|
|
2302
|
+
if (fpick) fpick.onchange = function () { var files = Array.from(fpick.files || []); if (files.length) chatIngestFiles(files, true) }
|
|
2303
|
+
var iadd = panel.querySelector('#dk-chat-add-img')
|
|
2304
|
+
if (iadd) iadd.onclick = function () {
|
|
2305
|
+
// 弹小窗输入本机路径或网络图片 → 插入 [MEDIA:image|src](追加, 不清空已有文本)
|
|
2306
|
+
var src = window.prompt('输入图片来源: 本机绝对路径(D:\\xxx\\a.jpg) 或 http(s) 图片地址', '')
|
|
2307
|
+
if (src === null) return
|
|
2308
|
+
src = String(src || '').trim()
|
|
2309
|
+
if (!src) return
|
|
2310
|
+
chatInsertMedia('image', src)
|
|
2311
|
+
}
|
|
2312
|
+
var bd = panel.querySelector('#dk-bind')
|
|
2313
|
+
if (bd) bd.onclick = function () { state.bindGid = (panel.querySelector('#dk-bindgid') || {}).value || ''; state.bindName = (panel.querySelector('#dk-bindname') || {}).value || ''; doBind() }
|
|
2314
|
+
}
|
|
2315
|
+
// ── 💬 聊天视图: 数据拉取 / QQ 风格气泡渲染 / 顶部滚动分页 ──
|
|
2316
|
+
function chatPeerReady() {
|
|
2317
|
+
return state.sendScope === 'c2c' ? !!state.sendTo : !!state.gid
|
|
2318
|
+
}
|
|
2319
|
+
function chatPeerName() {
|
|
2320
|
+
return state.sendScope === 'c2c'
|
|
2321
|
+
? ('私聊「' + (c2cSelName()) + '」')
|
|
2322
|
+
: ('群「' + (groupSelName()) + '」')
|
|
2323
|
+
}
|
|
2324
|
+
// 剥 Markdown 语法(移植自 gal-view transcript.mjs, 保留正文)让气泡像 QQ 纯文本
|
|
2325
|
+
function mdPlain(t) {
|
|
2326
|
+
if (typeof t !== 'string') return ''
|
|
2327
|
+
return t
|
|
2328
|
+
.replace(/^```[^\n]*$/gm, '')
|
|
2329
|
+
.replace(/!\[[^\]\n]*\]\([^)\n]*\)/g, '')
|
|
2330
|
+
.replace(/\[([^\]\n]+)\]\([^)\n]*\)/g, '$1')
|
|
2331
|
+
.replace(/\*\*([^*\n]+)\*\*/g, '$1')
|
|
2332
|
+
.replace(/~~([^~\n]+)~~/g, '$1')
|
|
2333
|
+
.replace(/(^|[^*\w])\*([^*\n]+?)\*(?!\*)(?![*\w])/g, '$1$2')
|
|
2334
|
+
.replace(/`([^`\n]+)`/g, '$1')
|
|
2335
|
+
.replace(/^#{1,6}[ \t]+/gm, '')
|
|
2336
|
+
.replace(/^>[ \t]?/gm, '')
|
|
2337
|
+
.replace(/^[ \t]*(?:-{3,}|\*{3,}|_{3,})[ \t]*$/gm, '')
|
|
2338
|
+
.replace(/^[-*+][ \t]+/gm, '')
|
|
2339
|
+
.replace(/^\d+\.[ \t]+/gm, '')
|
|
2340
|
+
.replace(/\r\n?/g, '\n')
|
|
2341
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
2342
|
+
.trim()
|
|
2343
|
+
}
|
|
2344
|
+
function chatTime(ts) {
|
|
2345
|
+
if (!ts) return ''
|
|
2346
|
+
var d = new Date(ts)
|
|
2347
|
+
if (isNaN(d.getTime())) return ''
|
|
2348
|
+
var p2 = function (n) { return String(n).padStart(2, '0') }
|
|
2349
|
+
var hm = p2(d.getHours()) + ':' + p2(d.getMinutes())
|
|
2350
|
+
var now = new Date()
|
|
2351
|
+
if (d.toDateString() === now.toDateString()) return hm
|
|
2352
|
+
return (d.getMonth() + 1) + '/' + d.getDate() + ' ' + hm
|
|
2353
|
+
}
|
|
2354
|
+
// reset=true 拉最新一页(清空重载); reset=false 用 chatOldest 加载更早并保持视口
|
|
2355
|
+
function loadChat(reset) {
|
|
2356
|
+
if (!state.ns) { renderChatList(); return }
|
|
2357
|
+
if (!chatPeerReady()) { state.chatItems = []; state.chatMore = false; state.chatErr = '先选一个群/私聊目标'; renderChatList(); return }
|
|
2358
|
+
if (state.chatBusy) return
|
|
2359
|
+
var mode = reset ? 'chat' : 'more'
|
|
2360
|
+
state.chatBusy = mode
|
|
2361
|
+
if (reset) { state.chatItems = []; state.chatOldest = 0 }
|
|
2362
|
+
var box = document.getElementById('dk-chat-box')
|
|
2363
|
+
var keep = 0
|
|
2364
|
+
if (box && mode === 'more') keep = box.scrollHeight - box.scrollTop
|
|
2365
|
+
renderChatList() // 立即显示加载态
|
|
2366
|
+
var peerId = state.sendScope === 'c2c' ? state.sendTo : state.gid
|
|
2367
|
+
var q = 'ns=' + encodeURIComponent(state.ns) + '&scope=' + state.sendScope + '&peerId=' + encodeURIComponent(peerId) + '&limit=50'
|
|
2368
|
+
if (mode === 'more' && state.chatOldest > 0) q += '&beforeSeq=' + state.chatOldest
|
|
2369
|
+
api('chat/history', q).then(function (d) {
|
|
2370
|
+
state.chatBusy = ''
|
|
2371
|
+
if (!d || !d.ok) { state.chatErr = (d && (d.error || d.msg)) || '加载失败'; renderChatList(); return }
|
|
2372
|
+
var list = (Array.isArray(d.items) ? d.items : []).filter(function (it) {
|
|
2373
|
+
return it && ((typeof it.text === 'string' && it.text) || (Array.isArray(it.images) && it.images.length))
|
|
2374
|
+
})
|
|
2375
|
+
if (mode === 'more') {
|
|
2376
|
+
var have = {}
|
|
2377
|
+
state.chatItems.forEach(function (x) { have[x.seq] = 1 })
|
|
2378
|
+
state.chatItems = list.filter(function (x) { return !have[x.seq] }).concat(state.chatItems)
|
|
2379
|
+
} else {
|
|
2380
|
+
state.chatItems = list
|
|
2381
|
+
}
|
|
2382
|
+
state.chatMore = d.hasMore === true
|
|
2383
|
+
state.chatErr = ''
|
|
2384
|
+
state.chatOldest = state.chatItems.length ? state.chatItems[0].seq : 0
|
|
2385
|
+
renderChatList(mode === 'more' ? keep : null)
|
|
2386
|
+
}).catch(function () {
|
|
2387
|
+
state.chatBusy = ''
|
|
2388
|
+
state.chatErr = '网络错误'
|
|
2389
|
+
renderChatList()
|
|
2390
|
+
})
|
|
2391
|
+
}
|
|
2392
|
+
function renderChatList(keepScrollOffset) {
|
|
2393
|
+
var box = document.getElementById('dk-chat-box')
|
|
2394
|
+
if (!box) return
|
|
2395
|
+
var st = document.getElementById('dk-chat-status')
|
|
2396
|
+
if (st) {
|
|
2397
|
+
if (state.chatBusy === 'send') st.textContent = '发送中…'
|
|
2398
|
+
else if (chatFlash) st.textContent = chatFlash
|
|
2399
|
+
else if (state.chatBusy) st.textContent = state.chatBusy === 'chat' ? '加载中…' : '加载更早…'
|
|
2400
|
+
else if (state.chatErr) st.textContent = state.chatErr
|
|
2401
|
+
else if (!state.chatItems.length) st.textContent = '暂无记录 —— 机器人和该目标聊过后会显示在这里'
|
|
2402
|
+
else st.textContent = '共 ' + state.chatItems.length + ' 条 · ' + chatPeerName() + (state.chatMore ? ' · 上滑加载更早' : ' · 已到最早')
|
|
2403
|
+
}
|
|
2404
|
+
if (!state.chatItems.length) {
|
|
2405
|
+
var emptyTxt = state.chatErr || (!chatPeerReady() ? '先选一个群/私聊目标' : '还没有聊天记录(该目标暂无活跃会话)')
|
|
2406
|
+
box.innerHTML = '<div class="dk-empty" style="padding:26px 0">' + esc(emptyTxt) + '</div>'
|
|
2407
|
+
return
|
|
2408
|
+
}
|
|
2409
|
+
var html = ''
|
|
2410
|
+
if (state.chatMore) html += '<div class="dk-chat-top">↑ 上滑加载更早消息</div>'
|
|
2411
|
+
html += state.chatItems.map(function (it) {
|
|
2412
|
+
var isOut = it.dir === 'out'
|
|
2413
|
+
var who = isOut ? '我' : (it.sender || (state.sendScope === 'c2c' ? c2cSelName() : '群友'))
|
|
2414
|
+
var ava = isOut ? '🐳' : chatAvaOf(who)
|
|
2415
|
+
var meta = '<span>' + esc(who) + '</span>'
|
|
2416
|
+
+ (it.tag ? '<span class="dk-ctag">' + esc(it.tag) + '</span>' : '')
|
|
2417
|
+
+ '<span>' + esc(chatTime(it.time)) + '</span>'
|
|
2418
|
+
var imgs = (Array.isArray(it.images) ? it.images : []).map(function (m) {
|
|
2419
|
+
var u = typeof m === 'string' ? m : (m && m.url)
|
|
2420
|
+
var k = typeof m === 'string' ? 'image' : ((m && m.kind) || 'image')
|
|
2421
|
+
if (!u) return ''
|
|
2422
|
+
if (k === 'file') return '<a class="dk-file" href="' + esc(u) + '" target="_blank" rel="noopener" title="' + esc(u) + '">📎 ' + esc((m && m.name) || '文件') + '</a>'
|
|
2423
|
+
if (k === 'voice') return '<audio class="dk-audio" controls preload="metadata" src="' + esc(u) + '"></audio>'
|
|
2424
|
+
if (k === 'video') return '<video class="dk-video" controls preload="metadata" src="' + esc(u) + '"></video>'
|
|
2425
|
+
return '<img class="dk-img" loading="lazy" data-lb="' + esc(u) + '" src="' + esc(u) + '" referrerpolicy="no-referrer" alt="[图片]" onerror="this.outerHTML=\'<span style=color:#888>[图加载失败]</span>\'">'
|
|
2426
|
+
}).join('')
|
|
2427
|
+
var txt = it.text ? '<div>' + esc(mdPlain(it.text)) + '</div>' : ''
|
|
2428
|
+
return '<div class="dk-crow ' + (isOut ? 'out' : 'in') + '">'
|
|
2429
|
+
+ '<div class="dk-ava">' + ava + '</div>'
|
|
2430
|
+
+ '<div class="dk-cmain">'
|
|
2431
|
+
+ '<div class="dk-cmeta">' + meta + '</div>'
|
|
2432
|
+
+ '<div class="dk-cbubble">' + imgs + txt + '</div>'
|
|
2433
|
+
+ '</div>'
|
|
2434
|
+
+ '</div>'
|
|
2435
|
+
}).join('')
|
|
2436
|
+
html += '<div class="dk-chat-bottom">—— 会话尾部 · ' + esc(chatPeerName()) + ' ——</div>'
|
|
2437
|
+
box.innerHTML = html
|
|
2438
|
+
// 图片: 单击/右键 → 灯箱放大(右键不再弹浏览器菜单, 避免误触发 onerror 变加载失败)
|
|
2439
|
+
box.querySelectorAll('img.dk-img').forEach(function (img) {
|
|
2440
|
+
var lb = img.getAttribute('data-lb') || img.src
|
|
2441
|
+
img.onclick = function (e) { e.preventDefault(); e.stopPropagation(); openLightbox(lb) }
|
|
2442
|
+
img.oncontextmenu = function (e) { e.preventDefault(); e.stopPropagation(); openLightbox(lb) }
|
|
2443
|
+
})
|
|
2444
|
+
// 滚动: 刷新/切目标 → 滚到底; 上滚加载更早 → 保持视口
|
|
2445
|
+
if (keepScrollOffset == null) box.scrollTop = box.scrollHeight
|
|
2446
|
+
else box.scrollTop = box.scrollHeight - keepScrollOffset
|
|
2447
|
+
}
|
|
2448
|
+
// 头像字符: 群友取昵称首字; 私聊/无名兜底
|
|
2449
|
+
function chatAvaOf(who) {
|
|
2450
|
+
var s = String(who || '').trim()
|
|
2451
|
+
if (!s) return '?'
|
|
2452
|
+
try { return Array.from(s)[0] } catch (e) { return s[0] || '?' }
|
|
2453
|
+
}
|
|
2454
|
+
function setChatStatus(msg) {
|
|
2455
|
+
var st = document.getElementById('dk-chat-status')
|
|
2456
|
+
if (st) st.textContent = msg
|
|
2457
|
+
}
|
|
2458
|
+
// 结果提示: 短暂保留(列表刷新/计数不覆盖), 到期自动还原
|
|
2459
|
+
function flashStatus(msg, ms) {
|
|
2460
|
+
chatFlash = msg
|
|
2461
|
+
setChatStatus(msg)
|
|
2462
|
+
var hold = ms || 3500
|
|
2463
|
+
setTimeout(function () {
|
|
2464
|
+
if (chatFlash === msg) {
|
|
2465
|
+
chatFlash = ''
|
|
2466
|
+
var st = document.getElementById('dk-chat-status')
|
|
2467
|
+
if (st && st.textContent === msg) renderChatList()
|
|
2468
|
+
}
|
|
2469
|
+
}, hold)
|
|
2470
|
+
}
|
|
2471
|
+
function closeLightbox() {
|
|
2472
|
+
var ov = document.getElementById('qqs-lightbox')
|
|
2473
|
+
if (ov) ov.remove()
|
|
2474
|
+
}
|
|
2475
|
+
function openLightbox(url) {
|
|
2476
|
+
closeLightbox()
|
|
2477
|
+
var ov = document.createElement('div')
|
|
2478
|
+
ov.id = 'qqs-lightbox'
|
|
2479
|
+
ov.innerHTML = '<span class="lb-x">✕</span><img src="' + esc(url) + '" referrerpolicy="no-referrer" alt="图片">'
|
|
2480
|
+
document.body.appendChild(ov)
|
|
2481
|
+
ov.onclick = closeLightbox
|
|
2482
|
+
var x = ov.querySelector('.lb-x')
|
|
2483
|
+
if (x) x.onclick = function (e) { e.stopPropagation(); closeLightbox() }
|
|
2484
|
+
window.addEventListener('keydown', function h(e) { if (e.key === 'Escape') { closeLightbox(); window.removeEventListener('keydown', h) } })
|
|
2485
|
+
}
|
|
2486
|
+
function chatUrls(text) {
|
|
2487
|
+
var out = []
|
|
2488
|
+
var re = /https?:\/\/[^\s]+/g
|
|
2489
|
+
var m
|
|
2490
|
+
while ((m = re.exec(String(text || ''))) !== null) {
|
|
2491
|
+
var u = m[0].replace(/[,。、;:,.;:!!??)\]】》>」』]+$/, '')
|
|
2492
|
+
if (u) out.push(u)
|
|
2493
|
+
}
|
|
2494
|
+
return out
|
|
2495
|
+
}
|
|
2496
|
+
function chatKindOfMedia(fullText) {
|
|
2497
|
+
if (/\.(jpe?g|png|gif|webp|bmp)(\?|$)/i.test(fullText) || /\[(图片|img):/i.test(fullText)) return 'image'
|
|
2498
|
+
if (/\.(mp4|webm|mov|avi)(\?|$)/i.test(fullText) || /\[(视频|video):/i.test(fullText)) return 'video'
|
|
2499
|
+
if (/\.(mp3|wav|amr|silk|m4a|ogg)(\?|$)/i.test(fullText) || /\[(语音|voice|音频):/i.test(fullText)) return 'voice'
|
|
2500
|
+
return 'file'
|
|
2501
|
+
}
|
|
2502
|
+
function chatKindLbl(kind) {
|
|
2503
|
+
return kind === 'image' ? '图片' : kind === 'video' ? '视频' : kind === 'voice' ? '语音' : '文件'
|
|
2504
|
+
}
|
|
2505
|
+
// 把文本框里的 [MEDIA:kind|src] 拆出来(src 可为 http 链接或本机路径), 其余为纯文本段
|
|
2506
|
+
function chatSplitBbcode(t) {
|
|
2507
|
+
var medias = []
|
|
2508
|
+
var texts = []
|
|
2509
|
+
var re = /\[MEDIA:([^\]|]+)\|([^\]]+)\]/gi
|
|
2510
|
+
var m
|
|
2511
|
+
var last = 0
|
|
2512
|
+
while ((m = re.exec(String(t || ''))) !== null) {
|
|
2513
|
+
var pre = t.slice(last, m.index).trim()
|
|
2514
|
+
if (pre) texts.push(pre)
|
|
2515
|
+
medias.push({ kind: String(m[1]).toLowerCase(), src: String(m[2]).trim() })
|
|
2516
|
+
last = m.index + m[0].length
|
|
2517
|
+
}
|
|
2518
|
+
var tail = t.slice(last).trim()
|
|
2519
|
+
if (tail) texts.push(tail)
|
|
2520
|
+
// kind 中文兼容: [MEDIA:图片|…]
|
|
2521
|
+
medias.forEach(function (x) {
|
|
2522
|
+
if (x.kind === '图片' || x.kind === 'img') x.kind = 'image'
|
|
2523
|
+
else if (x.kind === '视频' || x.kind === 'video') x.kind = 'video'
|
|
2524
|
+
else if (x.kind === '语音' || x.kind === 'voice' || x.kind === '音频') x.kind = 'voice'
|
|
2525
|
+
else if (x.kind === '文件' || x.kind === 'file') x.kind = 'file'
|
|
2526
|
+
if (x.kind !== 'image' && x.kind !== 'video' && x.kind !== 'voice' && x.kind !== 'file') x.kind = 'file'
|
|
2527
|
+
})
|
|
2528
|
+
return { medias: medias, texts: texts }
|
|
2529
|
+
}
|
|
2530
|
+
// 发送单条媒体: src 支持 http(s) 或本机绝对路径; 勾了🧠记入上下文时 host 写「用户代发」模拟消息
|
|
2531
|
+
function chatSendMediaOne(kind, src) {
|
|
2532
|
+
var body = { ns: state.ns || undefined, scope: state.sendScope, kind: kind }
|
|
2533
|
+
var peerId = state.sendScope === 'c2c' ? state.sendTo : state.gid
|
|
2534
|
+
if (!peerId) return Promise.resolve({ ok: false, msg: '未选目标' })
|
|
2535
|
+
body.peerId = peerId
|
|
2536
|
+
if (/^https?:\/\//i.test(src)) body.url = src
|
|
2537
|
+
else body.localPath = src
|
|
2538
|
+
body.insertContext = state.chatIns === true
|
|
2539
|
+
body.relayText = src // 完整来源(路径/URL)进「用户代发」上下文, dock 可渲染/可读
|
|
2540
|
+
return apiPost('chat/media', body)
|
|
2541
|
+
}
|
|
2542
|
+
// 媒体来源的短描述(记入上下文用): 取文件名/链接尾
|
|
2543
|
+
function chatMediaDesc(src) {
|
|
2544
|
+
var s = String(src || '')
|
|
2545
|
+
var i = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\'))
|
|
2546
|
+
var base = i >= 0 ? s.slice(i + 1) : s
|
|
2547
|
+
base = base.replace(/[?&#].*$/, '')
|
|
2548
|
+
return base || '媒体'
|
|
2549
|
+
}
|
|
2550
|
+
// 发送中 UI: 禁用发送按钮/输入框(防连点重复发送), 按钮显示进度
|
|
2551
|
+
function setSending(on, label) {
|
|
2552
|
+
var btn = document.getElementById('dk-chat-send')
|
|
2553
|
+
if (btn) { btn.disabled = !!on; btn.textContent = on ? (label || '发送中…') : '发送' }
|
|
2554
|
+
var ta = document.getElementById('dk-chat-input')
|
|
2555
|
+
if (ta) ta.disabled = !!on
|
|
2556
|
+
}
|
|
2557
|
+
// 依次发送媒体列表, 全部成功后清空输入并刷新; 部分失败即停并提示
|
|
2558
|
+
function chatSendMediaSeq(list, onDone) {
|
|
2559
|
+
if (!list.length) { if (onDone) onDone(); return }
|
|
2560
|
+
if (state.chatBusy) return // 防重入
|
|
2561
|
+
state.chatBusy = 'send'
|
|
2562
|
+
setSending(true, '发送中…')
|
|
2563
|
+
setChatStatus('正在发送 ' + list.length + ' 个媒体…')
|
|
2564
|
+
renderChatList()
|
|
2565
|
+
var i = 0
|
|
2566
|
+
var step = function () {
|
|
2567
|
+
setSending(true, '发送中 ' + (i + 1) + '/' + list.length)
|
|
2568
|
+
setChatStatus('正在发送' + chatKindLbl(list[i].kind) + ' ' + (i + 1) + '/' + list.length + '…(完成前勿再点发送)')
|
|
2569
|
+
chatSendMediaOne(list[i].kind, list[i].src).then(function (d) {
|
|
2570
|
+
if (!d || !d.ok) {
|
|
2571
|
+
state.chatBusy = ''; setSending(false)
|
|
2572
|
+
flashStatus('发送失败: ' + ((d && (d.msg || d.error)) || '未知') + '(输入已保留, 可改后重发)', 6000)
|
|
2573
|
+
return
|
|
2574
|
+
}
|
|
2575
|
+
i++
|
|
2576
|
+
if (i >= list.length) {
|
|
2577
|
+
state.chatText = ''; renderChatInput(); state.chatBusy = ''; setSending(false)
|
|
2578
|
+
flashStatus('已发送 ' + chatKindLbl(list[0].kind) + (list.length > 1 ? ' ×' + list.length : '') + ' ✓')
|
|
2579
|
+
loadChat(true)
|
|
2580
|
+
if (onDone) onDone()
|
|
2581
|
+
} else step()
|
|
2582
|
+
})
|
|
2583
|
+
}
|
|
2584
|
+
step()
|
|
2585
|
+
}
|
|
2586
|
+
// 发送入口(kindArg 保留兼容): 有 [MEDIA:…] → 逐条发媒体 + 剩余文字; 否则 纯链接自动判 / 纯文本
|
|
2587
|
+
function chatSend(kindArg) {
|
|
2588
|
+
if (state.chatBusy) { setChatStatus('正在发送中, 请稍候…'); return }
|
|
2589
|
+
if (!state.ns) { setChatStatus('先选账号实例(头部下拉)'); return }
|
|
2590
|
+
if (!chatPeerReady()) { setChatStatus('先选一个群/私聊目标'); return }
|
|
2591
|
+
var t = (state.chatText || '').trim()
|
|
2592
|
+
if (!t) { setChatStatus('先输入内容, 或用 📷插图/📎文件 加入媒体'); return }
|
|
2593
|
+
var blk = chatSplitBbcode(t)
|
|
2594
|
+
if (blk.medias.length) {
|
|
2595
|
+
chatSendMediaSeq(blk.medias, function () {
|
|
2596
|
+
if (blk.texts.length && !state.chatBusy) chatSendText(blk.texts.join('\n'))
|
|
2597
|
+
})
|
|
2598
|
+
return
|
|
2599
|
+
}
|
|
2600
|
+
var urls = chatUrls(t)
|
|
2601
|
+
if (!urls.length) { chatSendText(t); return }
|
|
2602
|
+
var kind = chatKindOfMedia(t)
|
|
2603
|
+
// 纯链接: 自动当媒体发; 若有残留描述文字(去掉链接后), 媒体成功后补发
|
|
2604
|
+
var clean = t.replace(/https?:\/\/[^\s]+/g, '').replace(/\[(图片|附件|文件|语音|视频|音频):\s*\]/g, '').replace(/\[Attachment:[^\]]*\]/g, '').trim()
|
|
2605
|
+
chatSendMediaSeq([{ kind: kind, src: urls[0] }], function () { if (clean && !state.chatBusy) chatSendText(clean) })
|
|
2606
|
+
}
|
|
2607
|
+
function chatSendText(t) {
|
|
2608
|
+
if (state.chatBusy) return // 防重入(媒体后补发文本也要等空闲)
|
|
2609
|
+
state.chatBusy = 'send'
|
|
2610
|
+
setSending(true, '发送中…')
|
|
2611
|
+
setChatStatus('正在发送文本…')
|
|
2612
|
+
renderChatList()
|
|
2613
|
+
var body = { text: t, ns: state.ns || undefined, insertContext: state.chatIns === true }
|
|
2614
|
+
if (state.sendScope === 'c2c') body.openid = state.sendTo
|
|
2615
|
+
else body.gid = state.gid
|
|
2616
|
+
apiPost(state.sendScope === 'c2c' ? 'chat/send' : 'group/send', body).then(function (d) {
|
|
2617
|
+
state.chatBusy = ''
|
|
2618
|
+
setSending(false)
|
|
2619
|
+
if (d && d.ok) { state.chatText = ''; renderChatInput(); flashStatus(d.msg || '已发送 ✓'); loadChat(true) }
|
|
2620
|
+
else flashStatus((d && (d.msg || (d.err && d.err.human) || d.error)) || '发送结果未知', 6000)
|
|
2621
|
+
})
|
|
2622
|
+
}
|
|
2623
|
+
function chatKindOfFile(f) {
|
|
2624
|
+
var t = String((f && f.type) || '').toLowerCase()
|
|
2625
|
+
var n = String((f && f.name) || '').toLowerCase()
|
|
2626
|
+
if (t.indexOf('image/') === 0 || /\.(jpe?g|png|gif|webp|bmp)$/.test(n)) return 'image'
|
|
2627
|
+
if (t.indexOf('video/') === 0 || /\.(mp4|webm|mov|avi)$/.test(n)) return 'video'
|
|
2628
|
+
if (t.indexOf('audio/') === 0 || /\.(mp3|wav|amr|silk|m4a|ogg)$/.test(n)) return 'voice'
|
|
2629
|
+
return 'file'
|
|
2630
|
+
}
|
|
2631
|
+
function chatFileExt(f) {
|
|
2632
|
+
var n = String((f && f.name) || '')
|
|
2633
|
+
var i = n.lastIndexOf('.')
|
|
2634
|
+
var e = i >= 0 ? n.slice(i + 1).toLowerCase() : ''
|
|
2635
|
+
if (/^[a-z0-9]{1,6}$/.test(e)) return e
|
|
2636
|
+
return 'bin'
|
|
2637
|
+
}
|
|
2638
|
+
// 把 [MEDIA:kind|src] 追加进文本框(不清空已有内容)
|
|
2639
|
+
function chatInsertMedia(kind, src) {
|
|
2640
|
+
var cur = state.chatText || ''
|
|
2641
|
+
if (cur && !/\n$/.test(cur)) cur += '\n'
|
|
2642
|
+
state.chatText = cur + '[MEDIA:' + chatKindLbl(kind) + '|' + src + ']'
|
|
2643
|
+
renderChatInput()
|
|
2644
|
+
var ta = document.getElementById('dk-chat-input')
|
|
2645
|
+
if (ta) { ta.focus(); ta.scrollTop = ta.scrollHeight }
|
|
2646
|
+
setChatStatus('已插入 ' + chatKindLbl(kind) + ', 点发送即可发出')
|
|
2647
|
+
}
|
|
2648
|
+
// 处理选中的文件: 有本机路径(Electron 场景)直接用; 否则 base64 上传到 bot 临时目录拿路径
|
|
2649
|
+
function chatIngestFiles(files, fromPicker) {
|
|
2650
|
+
var list = Array.isArray(files) ? files : []
|
|
2651
|
+
if (!list.length) return
|
|
2652
|
+
var total = list.length
|
|
2653
|
+
var done = 0
|
|
2654
|
+
var finish = function () { done++; if (fromPicker && done >= total) { var fp = document.getElementById('dk-chat-file'); if (fp) fp.value = '' } }
|
|
2655
|
+
list.forEach(function (f) {
|
|
2656
|
+
var kind = chatKindOfFile(f)
|
|
2657
|
+
var directPath = ''
|
|
2658
|
+
try { if (f.path && /^[a-zA-Z]:[\\/]/.test(String(f.path))) directPath = String(f.path) } catch (e) { directPath = '' }
|
|
2659
|
+
if (directPath) { chatInsertMedia(kind, directPath); finish(); return }
|
|
2660
|
+
if (f.size > 300 * 1024 * 1024) { setChatStatus('「' + (f.name || '文件') + '」超过 300MB 上限, 请手动输入本机路径'); finish(); return }
|
|
2661
|
+
// 一律走流式上传(不限 ~5MB): 浏览器把 File 直接当 body 传, host 写盘后返回本机路径
|
|
2662
|
+
var ext = chatFileExt(f)
|
|
2663
|
+
setChatStatus('正在上传 ' + (f.name || '文件') + '…')
|
|
2664
|
+
var q = 'ns=' + encodeURIComponent(state.ns || '') + '&ext=' + encodeURIComponent(ext)
|
|
2665
|
+
fetch('/api/qqbot-settings/chat/upload-raw?' + q, {
|
|
2666
|
+
method: 'POST',
|
|
2667
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
2668
|
+
body: f,
|
|
2669
|
+
}).then(function (r) { return r.json().catch(function () { return null }) }).then(function (d) {
|
|
2670
|
+
if (d && d.ok && d.path) { chatInsertMedia(kind, d.path); setChatStatus('已上传 ' + (f.name || '文件') + ', 点发送即可发出') }
|
|
2671
|
+
else setChatStatus('上传失败: ' + ((d && (d.error || d.msg)) || '未知'))
|
|
2672
|
+
finish()
|
|
2673
|
+
}).catch(function () { setChatStatus('上传失败(网络错误)'); finish() })
|
|
2674
|
+
})
|
|
2675
|
+
}
|
|
2676
|
+
function renderChatInput() {
|
|
2677
|
+
var ta = document.getElementById('dk-chat-input')
|
|
2678
|
+
if (ta) ta.value = state.chatText || ''
|
|
2679
|
+
var n = document.getElementById('dk-chat-cnt')
|
|
2680
|
+
if (n) n.textContent = (state.chatText || '').length + '/2000'
|
|
2681
|
+
}
|
|
2682
|
+
function renderJoinList() {
|
|
2683
|
+
var box = panel.querySelector('#dk-join-list')
|
|
2684
|
+
if (!box) return
|
|
2685
|
+
var badgeEl = panel.querySelector('.dk-join-badge')
|
|
2686
|
+
if (state.joins === null) { box.innerHTML = '<div class="dk-empty">加载中…</div>'; return }
|
|
2687
|
+
if (state.joins.length === 0) { box.innerHTML = '<div class="dk-empty">当前没有待审批的入群申请 ✓</div>'; return }
|
|
2688
|
+
box.innerHTML = state.joins.map(function (j) {
|
|
2689
|
+
var busying = state.busy === 'ap-' + j.member_openid
|
|
2690
|
+
return '<div class="dk-item">'
|
|
2691
|
+
+ '<span style="font-weight:600">' + esc(j.username) + '</span>'
|
|
2692
|
+
+ '<span style="color:#999;font-size:12px">' + esc(j.source) + '</span>'
|
|
2693
|
+
+ (j.verify ? '<span style="color:#666;font-size:12px">验证: ' + esc(j.verify) + '</span>' : '')
|
|
2694
|
+
+ (j.risk ? '<span style="color:#c23131;font-size:12px">⚠ ' + esc(j.risk) + '</span>' : '')
|
|
2695
|
+
+ '<span style="flex:1"></span>'
|
|
2696
|
+
+ '<button class="dk-btn ok" data-ap="approve" data-mid="' + esc(j.member_openid) + '"' + (busying ? ' disabled' : '') + '>' + (busying ? '处理中…' : '通过') + '</button>'
|
|
2697
|
+
+ '<button class="dk-btn no" data-ap="decline" data-mid="' + esc(j.member_openid) + '"' + (busying ? ' disabled' : '') + '>拒绝</button>'
|
|
2698
|
+
+ '</div>'
|
|
2699
|
+
}).join('')
|
|
2700
|
+
box.querySelectorAll('button[data-ap]').forEach(function (btn) {
|
|
2701
|
+
btn.onclick = function () { doApprove(btn.getAttribute('data-mid'), btn.getAttribute('data-ap')) }
|
|
2702
|
+
})
|
|
2703
|
+
if (badgeEl) { badgeEl.style.display = 'inline-block'; badgeEl.textContent = state.joins.length }
|
|
2704
|
+
}
|
|
2705
|
+
function renderMemberList() {
|
|
2706
|
+
var box = panel.querySelector('#dk-member-list')
|
|
2707
|
+
if (!box) return
|
|
2708
|
+
if (state.members === null) { box.innerHTML = '<div class="dk-empty">加载中…</div>'; return }
|
|
2709
|
+
if (state.members.length === 0) { box.innerHTML = '<div class="dk-empty">暂无成员记录 —— 群友发言后自动出现(官方成员列表未开放)</div>'; return }
|
|
2710
|
+
box.innerHTML = state.members.map(function (m) {
|
|
2711
|
+
var busying = state.busy === 'm-' + m.mid
|
|
2712
|
+
return '<div class="dk-item">'
|
|
2713
|
+
+ '<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(m.name || '(未知名)') + ' · ' + esc(String(m.mid).slice(0, 10)) + '…</span>'
|
|
2714
|
+
+ '<input class="qqs-txt" data-min="' + esc(m.mid) + '" type="number" min="1" value="' + esc(state.muteSecs) + '" style="width:64px">'
|
|
2715
|
+
+ '<span style="color:#999;font-size:12px">分钟</span>'
|
|
2716
|
+
+ '<button class="dk-btn no" data-mute="' + esc(m.mid) + '"' + (busying ? ' disabled' : '') + '>' + (busying ? '处理中…' : '禁言') + '</button>'
|
|
2717
|
+
+ '</div>'
|
|
2718
|
+
}).join('')
|
|
2719
|
+
box.querySelectorAll('input[data-min]').forEach(function (inp) {
|
|
2720
|
+
inp.oninput = function () { state.muteSecs = inp.value }
|
|
2721
|
+
})
|
|
2722
|
+
box.querySelectorAll('button[data-mute]').forEach(function (btn) {
|
|
2723
|
+
btn.onclick = function () { doMute(btn.getAttribute('data-mute'), 'mute', Number(state.muteSecs || 60) * 60) }
|
|
2724
|
+
})
|
|
2725
|
+
}
|
|
2726
|
+
function renderMuteList() {
|
|
2727
|
+
var box = panel.querySelector('#dk-mute-list')
|
|
2728
|
+
if (!box) return
|
|
2729
|
+
if (state.mutes === null) { box.innerHTML = '<div class="dk-empty">加载中…</div>'; return }
|
|
2730
|
+
if (state.mutes.length === 0) { box.innerHTML = '<div class="dk-empty">没有正在禁言的成员</div>'; return }
|
|
2731
|
+
box.innerHTML = state.mutes.map(function (m) {
|
|
2732
|
+
var busying = state.busy === 'm-' + m.member_openid
|
|
2733
|
+
return '<div class="dk-item">'
|
|
2734
|
+
+ '<span style="flex:1">' + esc(m.username || String(m.member_openid).slice(0, 12)) + '</span>'
|
|
2735
|
+
+ '<span style="color:#999;font-size:12px">至 ' + esc(String(m.expire || '').replace('T', ' ').slice(0, 16)) + '</span>'
|
|
2736
|
+
+ '<button class="dk-btn" data-um="' + esc(m.member_openid) + '"' + (busying ? ' disabled' : '') + '>' + (busying ? '处理中…' : '解除') + '</button>'
|
|
2737
|
+
+ '</div>'
|
|
2738
|
+
}).join('')
|
|
2739
|
+
box.querySelectorAll('button[data-um]').forEach(function (btn) {
|
|
2740
|
+
btn.onclick = function () { doMute(btn.getAttribute('data-um'), 'unmute') }
|
|
2741
|
+
})
|
|
2742
|
+
}
|
|
2743
|
+
function renderPanel() {
|
|
2744
|
+
panel.innerHTML = '<div class="dk-h"></div><div class="dk-detect"></div><div class="dk-b"></div>'
|
|
2745
|
+
paintHead(); paintDetect(); paintBody()
|
|
2746
|
+
if (state.tab === 'join') loadJoins()
|
|
2747
|
+
// 每次展开都重查当前 web 会话(跟随主人切换会话; 不打断已有手动选择)
|
|
2748
|
+
if (sessionsSvc || true) { reDetectAndSelect() }
|
|
2749
|
+
}
|
|
2750
|
+
// 检测状态条: 显示 web 当前会话的 sessionId 与反查结果(悬浮球调试/自动选中依据)
|
|
2751
|
+
function paintDetect() {
|
|
2752
|
+
if (!panel || !open) return
|
|
2753
|
+
var d = panel.querySelector('.dk-detect')
|
|
2754
|
+
if (!d) return
|
|
2755
|
+
var det = state.detected
|
|
2756
|
+
var sids = (det && det.sid) || ''
|
|
2757
|
+
var srcLbl = det ? (det.src === 'sessions.list' ? 'sessions.list' : det.src === 'localStorage' ? 'localStorage' : '未检测到') : '检测中…'
|
|
2758
|
+
var hit = state.detectedHit
|
|
2759
|
+
var hitTxt = '未命中 QQ 会话'
|
|
2760
|
+
if (hit) {
|
|
2761
|
+
hitTxt = (hit.scope === 'group' ? '群「' : '私聊「') + esc((hit.name || '') + '」' + (hit.ns ? ' [' + hit.ns + ']' : '')) + (hit.peerId ? ' · ' + hit.peerId.slice(0, 10) + '…' : '')
|
|
2762
|
+
}
|
|
2763
|
+
d.innerHTML = '<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:6px 14px;background:#f4f0ff;border-bottom:1px solid #efe8ff;font-size:12px;color:#4a3a9f">'
|
|
2764
|
+
+ '<span style="font-weight:700">📡 当前会话</span>'
|
|
2765
|
+
+ '<span style="color:#666">来源:' + esc(srcLbl) + '</span>'
|
|
2766
|
+
+ (sids ? '<code style="font-size:11px;background:#fff;border:1px solid #e3d9ff;border-radius:5px;padding:1px 6px">' + esc(sids.slice(0, 36)) + (sids.length > 36 ? '…' : '') + '</code>' : '<span style="color:#999">(无)</span>')
|
|
2767
|
+
+ '<span style="color:#2f9e44">→ ' + hitTxt + '</span>'
|
|
2768
|
+
+ '<button class="dk-btn" id="dk-redetect" style="padding:1px 8px;font-size:11px;margin-left:auto">🔄 重查</button>'
|
|
2769
|
+
+ '</div>'
|
|
2770
|
+
var btn = d.querySelector('#dk-redetect')
|
|
2771
|
+
if (btn) btn.onclick = function () { reDetectAndSelect() }
|
|
2772
|
+
}
|
|
2773
|
+
// 重新检测当前 web 会话并自动选中对应 QQ 目标
|
|
2774
|
+
function reDetectAndSelect() {
|
|
2775
|
+
state.detected = null; state.detectedHit = null
|
|
2776
|
+
state.wantPeer = null
|
|
2777
|
+
paintDetect()
|
|
2778
|
+
lookupCurrentSession(function (hit) {
|
|
2779
|
+
if (!state.accts.length) { api('accounts').then(function (dd) { var list = (dd && Array.isArray(dd.instances) ? dd.instances : []).filter(function (a) { return !a.disabled }); state.accts = list; applyHit(hit, list) }) }
|
|
2780
|
+
else applyHit(hit, state.accts)
|
|
2781
|
+
})
|
|
2782
|
+
}
|
|
2783
|
+
function applyHit(hit, list) {
|
|
2784
|
+
if (hit && list.length) {
|
|
2785
|
+
state.ns = hit.ns || list[0].ns || ''
|
|
2786
|
+
state.wantPeer = { scope: hit.scope, peerId: hit.peerId }
|
|
2787
|
+
if (hit.scope === 'c2c') { state.sendScope = 'c2c'; state.sendTo = hit.peerId; state.sendName = hit.name || '' }
|
|
2788
|
+
else { state.sendScope = 'group'; state.gid = hit.peerId }
|
|
2789
|
+
} else if (list.length && !state.ns) {
|
|
2790
|
+
state.ns = list[0].ns || ''
|
|
2791
|
+
}
|
|
2792
|
+
refreshAll(); paintHead(); paintDetect()
|
|
2793
|
+
}
|
|
2794
|
+
loadAccts()
|
|
2795
|
+
// 首次自动展开?不——保持球形态(审批/提问仍走打断弹出)。主人点开才展开。
|
|
2796
|
+
closePanel()
|
|
2797
|
+
}
|
|
2798
|
+
|
|
1448
2799
|
function apply(ctx) {
|
|
1449
2800
|
ensureCss()
|
|
1450
2801
|
var slots = ctx.slots
|
|
@@ -1454,9 +2805,15 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
1454
2805
|
{ name: 'settings.section', id: 'qqbot', order: 32, label: 'QQ 机器人' },
|
|
1455
2806
|
function (props) { return h(QqbotHome, { close: props && props.close }) })
|
|
1456
2807
|
})
|
|
2808
|
+
startApprovalFloat()
|
|
2809
|
+
// sessions 服务(宿主根服务): 权威读 web 当前会话(current)与 cwd(agent id === session id)
|
|
2810
|
+
var sessionsSvc = null
|
|
2811
|
+
try { sessionsSvc = (ctx && (ctx.sessions || (ctx.get && ctx.get('sessions')))) || null } catch (e) { sessionsSvc = null }
|
|
2812
|
+
if (!sessionsSvc) { try { sessionsSvc = ctx.get && ctx.get('sessions') } catch (e) {} }
|
|
2813
|
+
startQqDock(sessionsSvc)
|
|
1457
2814
|
}
|
|
1458
2815
|
|
|
1459
|
-
exports.inject = ['slots']
|
|
2816
|
+
exports.inject = ['slots', 'sessions']
|
|
1460
2817
|
exports.apply = apply
|
|
1461
2818
|
return module.exports
|
|
1462
2819
|
}
|