@zaofan/dsh-qqbot 0.9.6 → 1.0.1

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.
Files changed (110) hide show
  1. package/README.md +400 -344
  2. package/README_EN.md +313 -281
  3. package/client/qqbot-settings.js +431 -10
  4. package/dist/channel-tools.d.ts +1 -1
  5. package/dist/channel-tools.d.ts.map +1 -1
  6. package/dist/channel-tools.js +166 -15
  7. package/dist/channel-tools.js.map +1 -1
  8. package/dist/commands/approve.d.ts +17 -0
  9. package/dist/commands/approve.d.ts.map +1 -0
  10. package/dist/commands/approve.js +24 -0
  11. package/dist/commands/approve.js.map +1 -0
  12. package/dist/commands/botplay.d.ts +15 -0
  13. package/dist/commands/botplay.d.ts.map +1 -0
  14. package/dist/commands/botplay.js +34 -0
  15. package/dist/commands/botplay.js.map +1 -0
  16. package/dist/commands/exit.d.ts +7 -0
  17. package/dist/commands/exit.d.ts.map +1 -0
  18. package/dist/commands/exit.js +85 -0
  19. package/dist/commands/exit.js.map +1 -0
  20. package/dist/commands/index.d.ts.map +1 -1
  21. package/dist/commands/index.js +14 -1
  22. package/dist/commands/index.js.map +1 -1
  23. package/dist/commands/outmode.d.ts +7 -3
  24. package/dist/commands/outmode.d.ts.map +1 -1
  25. package/dist/commands/outmode.js +19 -9
  26. package/dist/commands/outmode.js.map +1 -1
  27. package/dist/commands/permission.d.ts +18 -0
  28. package/dist/commands/permission.d.ts.map +1 -0
  29. package/dist/commands/permission.js +43 -0
  30. package/dist/commands/permission.js.map +1 -0
  31. package/dist/commands/session.d.ts +5 -1
  32. package/dist/commands/session.d.ts.map +1 -1
  33. package/dist/commands/session.js +51 -5
  34. package/dist/commands/session.js.map +1 -1
  35. package/dist/config.d.ts +73 -0
  36. package/dist/config.d.ts.map +1 -1
  37. package/dist/config.js +89 -0
  38. package/dist/config.js.map +1 -1
  39. package/dist/features/approval-switch.d.ts +22 -0
  40. package/dist/features/approval-switch.d.ts.map +1 -0
  41. package/dist/features/approval-switch.js +10 -0
  42. package/dist/features/approval-switch.js.map +1 -0
  43. package/dist/features/botplay.d.ts +141 -0
  44. package/dist/features/botplay.d.ts.map +1 -0
  45. package/dist/features/botplay.js +558 -0
  46. package/dist/features/botplay.js.map +1 -0
  47. package/dist/features/extension-store.d.ts +27 -0
  48. package/dist/features/extension-store.d.ts.map +1 -0
  49. package/dist/features/extension-store.js +152 -0
  50. package/dist/features/extension-store.js.map +1 -0
  51. package/dist/features/group-hub.d.ts +40 -0
  52. package/dist/features/group-hub.d.ts.map +1 -0
  53. package/dist/features/group-hub.js +174 -0
  54. package/dist/features/group-hub.js.map +1 -0
  55. package/dist/features/group-join-request.d.ts.map +1 -1
  56. package/dist/features/group-join-request.js +35 -33
  57. package/dist/features/group-join-request.js.map +1 -1
  58. package/dist/features/qq-approval.d.ts.map +1 -1
  59. package/dist/features/qq-approval.js +3 -1
  60. package/dist/features/qq-approval.js.map +1 -1
  61. package/dist/features/scheduler.d.ts.map +1 -1
  62. package/dist/features/scheduler.js +2 -1
  63. package/dist/features/scheduler.js.map +1 -1
  64. package/dist/gateway/bootstrap.d.ts.map +1 -1
  65. package/dist/gateway/bootstrap.js +91 -11
  66. package/dist/gateway/bootstrap.js.map +1 -1
  67. package/dist/gateway/data-root.d.ts +15 -0
  68. package/dist/gateway/data-root.d.ts.map +1 -0
  69. package/dist/gateway/data-root.js +59 -0
  70. package/dist/gateway/data-root.js.map +1 -0
  71. package/dist/gateway/debounce.d.ts.map +1 -1
  72. package/dist/gateway/debounce.js +3 -2
  73. package/dist/gateway/debounce.js.map +1 -1
  74. package/dist/gateway/middleware-setup.d.ts +1 -1
  75. package/dist/gateway/middleware-setup.d.ts.map +1 -1
  76. package/dist/gateway/middleware-setup.js +42 -14
  77. package/dist/gateway/middleware-setup.js.map +1 -1
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +22 -2
  80. package/dist/index.js.map +1 -1
  81. package/dist/middleware/attachment.d.ts.map +1 -1
  82. package/dist/middleware/attachment.js +2 -1
  83. package/dist/middleware/attachment.js.map +1 -1
  84. package/dist/middleware/media-history.d.ts +2 -0
  85. package/dist/middleware/media-history.d.ts.map +1 -1
  86. package/dist/middleware/media-history.js +6 -1
  87. package/dist/middleware/media-history.js.map +1 -1
  88. package/dist/middleware/sticker-capture.d.ts.map +1 -1
  89. package/dist/middleware/sticker-capture.js +2 -2
  90. package/dist/middleware/sticker-capture.js.map +1 -1
  91. package/dist/model/model-resolver.d.ts +12 -0
  92. package/dist/model/model-resolver.d.ts.map +1 -1
  93. package/dist/model/model-resolver.js +24 -0
  94. package/dist/model/model-resolver.js.map +1 -1
  95. package/dist/model/prefs-store.d.ts +16 -0
  96. package/dist/model/prefs-store.d.ts.map +1 -1
  97. package/dist/model/prefs-store.js +48 -0
  98. package/dist/model/prefs-store.js.map +1 -1
  99. package/dist/session/session-manager.d.ts +41 -2
  100. package/dist/session/session-manager.d.ts.map +1 -1
  101. package/dist/session/session-manager.js +185 -21
  102. package/dist/session/session-manager.js.map +1 -1
  103. package/dist/transport/attachment.d.ts.map +1 -1
  104. package/dist/transport/attachment.js +2 -1
  105. package/dist/transport/attachment.js.map +1 -1
  106. package/dist/transport/inbound.d.ts.map +1 -1
  107. package/dist/transport/inbound.js +9 -1
  108. package/dist/transport/inbound.js.map +1 -1
  109. package/package.json +1 -1
  110. package/settings-host.js +177 -52
@@ -851,10 +851,8 @@ window.__ModuleLoader__.load({
851
851
  var nm = c.name || (nameByMid[c.id] && nameByMid[c.id].name) || ''
852
852
  c2c.push({ scope: 'c2c', id: c.id, name: nm, lastSeen: c.lastSeen || 0, count: c.count || 0 })
853
853
  })
854
- // 台账里没有但成员表有名字的 openid 也补上(私聊候选)
855
- Object.keys(nameByMid).forEach(function (mid) {
856
- if (!seen[mid]) { seen[mid] = 1; c2c.push({ scope: 'c2c', id: mid, name: nameByMid[mid].name, lastSeen: nameByMid[mid].ts, count: 0 }) }
857
- })
854
+ // 只列真·私聊过的人(known-chats scope=c2c): 群成员没有可私聊会话(dsh 一会话一对象),
855
+ // 且群 member_openid 与 c2c openid 不同域, 补进来定时发私聊必失败 —— 不混入。
858
856
  c2c.sort(function (a, b) { return (b.lastSeen || 0) - (a.lastSeen || 0) })
859
857
  return { group: groups, c2c: c2c }
860
858
  })
@@ -1888,7 +1886,9 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
1888
1886
  setInterval(refreshBadge, 20000)
1889
1887
 
1890
1888
  // ── 面板状态(每个实例独立保存, 切回不丢) ──
1891
- 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, outMode: '', outRev: undefined }
1889
+ 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, outMode: '', outRev: undefined, bpEvents: [], bpSel: null, bpDraft: null, rosterSel: {}, rosterScope: 'all', rosterQ: '', hubSid: '', hubRev: undefined, hubBusy: '', hubMsg: '' }
1890
+ // 📇 群组管理 M1: 勾选集合本地持久化(刷新/重开不丢, 供后续群发/批量操作使用)
1891
+ try { var _rs = localStorage.getItem('qqs-roster-sel'); if (_rs) { var _rso = JSON.parse(_rs); if (_rso && typeof _rso === 'object') state.rosterSel = _rso } } catch (e) {}
1892
1892
  var chatFlash = '' // 发送结果/错误提示(短时展示, 不被列表计数覆盖)
1893
1893
  // @ mention(输入框敲 @ 弹成员候选): 群聊目标才启用
1894
1894
  var atM = { members: [], open: false, kw: '', idx: 0, range: null, key: '' }
@@ -1979,7 +1979,7 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
1979
1979
  state.groups = []
1980
1980
  var q = state.ns ? 'ns=' + encodeURIComponent(state.ns) : ''
1981
1981
  api('group/accounts', q).then(function (d) {
1982
- state.groups = (d && Array.isArray(d.groups) ? d.groups : []).map(function (g) { return { gid: g.gid, name: g.name || '', from: g.from || '' } })
1982
+ state.groups = (d && Array.isArray(d.groups) ? d.groups : []).map(function (g) { return { gid: g.gid, name: g.name || '', from: g.from || '', lastAt: (g && g.lastAt) || 0 } })
1983
1983
  // 期望目标(wantPeer)命中则选中它, 否则回落第一个
1984
1984
  var wantGid = (state.wantPeer && state.wantPeer.scope === 'group') ? state.wantPeer.peerId : ''
1985
1985
  if (state.groups.some(function (g) { return g.gid === state.gid })) { /* 保持现选 */ }
@@ -1993,7 +1993,9 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
1993
1993
  var dd = curDataDir()
1994
1994
  state.c2cs = []
1995
1995
  if (!dd) { paintBody(); return }
1996
- // 与定时任务同款三源合并: 台账 + 本地成员表补名(不露裸 id)
1996
+ // 目标候选 = 真·私聊台账(known-chats scope=c2c, 即"与该机器人私聊过/开过会话"的人)。
1997
+ // ⚠️ 不能混入 group-members(群成员≠私聊对象: 群 member_openid 与 c2c openid 不同域,
1998
+ // 且 dsh 里一个 QQ 会话只绑定一个对象, 仅群里见过的人没有可私聊会话 → 发不出去)。
1997
1999
  var qNs = state.ns ? ('?ns=' + encodeURIComponent(state.ns)) : ''
1998
2000
  var qDD = dd ? ('?dataDir=' + encodeURIComponent(dd)) : ''
1999
2001
  Promise.all([
@@ -2001,6 +2003,7 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2001
2003
  api('group/members_local', qNs.replace('?', '')),
2002
2004
  ]).then(function (rs) {
2003
2005
  var known = ((rs[0] && rs[0].chats) || [])
2006
+ // 群成员表仅用于给同 id 的私聊对象补昵称(不同域通常不命中, 无副作用)
2004
2007
  var nameByMid = {}
2005
2008
  ;((rs[1] && rs[1].members) || []).forEach(function (m) {
2006
2009
  if (m.mid && m.name) {
@@ -2016,9 +2019,6 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2016
2019
  var nm = c.name || (nameByMid[c.id] && nameByMid[c.id].name) || ''
2017
2020
  c2c.push({ id: c.id, name: nm, lastSeen: c.lastSeen || 0, count: c.count || 0 })
2018
2021
  })
2019
- Object.keys(nameByMid).forEach(function (mid) {
2020
- if (!seen[mid]) { seen[mid] = 1; c2c.push({ id: mid, name: nameByMid[mid].name, lastSeen: nameByMid[mid].ts, count: 0 }) }
2021
- })
2022
2022
  c2c.sort(function (a, b) { return (b.lastSeen || 0) - (a.lastSeen || 0) })
2023
2023
  state.c2cs = c2c
2024
2024
  // 期望目标(wantPeer)是私聊 → 自动选中该对象
@@ -2046,6 +2046,160 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2046
2046
  var c = null; state.c2cs.forEach(function (x) { if (x.id === state.sendTo) c = x })
2047
2047
  return c ? (c.name || c.id.slice(0, 10)) : '(未选)'
2048
2048
  }
2049
+ // ── 📇 群组管理 M1: 会话台账勾选(群 = 群会话; 个人 = 真私聊过的人(c2c 会话), 复用 state.groups+state.c2cs) ──
2050
+ function rosterRows() {
2051
+ var rows = []
2052
+ state.groups.forEach(function (g) { rows.push({ key: 'group:' + g.gid, scope: 'group', id: g.gid, name: g.name || '', lastSeen: (g && g.lastAt) || 0, count: 0 }) })
2053
+ state.c2cs.forEach(function (c) { rows.push({ key: 'c2c:' + c.id, scope: 'c2c', id: c.id, name: c.name || '', lastSeen: (c && c.lastSeen) || 0, count: (c && c.count) || 0 }) })
2054
+ var sel = state.rosterScope
2055
+ var q = String(state.rosterQ || '').trim().toLowerCase()
2056
+ return rows.filter(function (r) {
2057
+ if (sel !== 'all' && r.scope !== sel) return false
2058
+ if (!q) return true
2059
+ return (r.name || '').toLowerCase().indexOf(q) >= 0 || r.id.toLowerCase().indexOf(q) >= 0
2060
+ }).sort(function (a, b) { return (b.lastSeen || 0) - (a.lastSeen || 0) })
2061
+ }
2062
+ function fmtRosterTime(ts) {
2063
+ if (!ts) return ''
2064
+ try { var d = new Date(ts); var mm = d.getMonth() + 1; var hh = d.getHours() < 10 ? '0' + d.getHours() : d.getHours(); var mi = d.getMinutes() < 10 ? '0' + d.getMinutes() : d.getMinutes(); return mm + '/' + d.getDate() + ' ' + hh + ':' + mi } catch (e) { return '' }
2065
+ }
2066
+ function rosterSelN() { var n = 0; for (var k in state.rosterSel) { if (state.rosterSel[k]) n++ } return n }
2067
+ function renderRosterList() {
2068
+ if (!panel || !open) return
2069
+ var box = panel.querySelector('#dk-roster-list')
2070
+ if (!box) return
2071
+ var rows = rosterRows()
2072
+ var stat = panel.querySelector('#dk-roster-stat')
2073
+ if (stat) stat.textContent = '共 ' + rows.length + ' 条 · 已选 ' + rosterSelN()
2074
+ if (!rows.length) {
2075
+ box.innerHTML = '<div class="dk-empty">' + ((state.rosterScope === 'all' && !String(state.rosterQ || '').trim()) ? '没有会话记录: 机器人被拉进群/有人私聊过后会自动累积(仅这些对象可被直接投递)。' : '没有匹配项。') + '</div>'
2076
+ return
2077
+ }
2078
+ var html = ''
2079
+ rows.forEach(function (r) {
2080
+ var on = !!state.rosterSel[r.key]
2081
+ var tail = r.id.length > 10 ? '…' + r.id.slice(-6) : r.id
2082
+ var nm = r.name || '(未知名)'
2083
+ html += '<label class="dk-item" style="cursor:pointer;background:' + (on ? '#f1ecff' : 'transparent') + '">'
2084
+ + '<input type="checkbox" data-rkey="' + r.key + '"' + (on ? ' checked' : '') + ' style="accent-color:#7c6cf0;flex:none">'
2085
+ + '<span style="flex:1;min-width:0;display:flex;gap:6px;align-items:baseline;flex-wrap:wrap"><b>' + esc(nm) + '</b>'
2086
+ + '<span style="color:#aaa;font-size:11px">' + (r.scope === 'group' ? '👥 群' : '👤 私聊') + ' · ' + esc(tail) + '</span></span>'
2087
+ + '<span style="color:#9aa0a8;font-size:11px;flex:none">' + fmtRosterTime(r.lastSeen) + '</span></label>'
2088
+ })
2089
+ box.innerHTML = html
2090
+ }
2091
+ function rosterToggle(key) {
2092
+ if (!key) return
2093
+ if (state.rosterSel[key]) delete state.rosterSel[key]
2094
+ else state.rosterSel[key] = true
2095
+ try { localStorage.setItem('qqs-roster-sel', JSON.stringify(state.rosterSel)) } catch (e) {}
2096
+ renderRosterList()
2097
+ }
2098
+ function bindRosterEvents() {
2099
+ if (!panel) return
2100
+ var qEl = panel.querySelector('#dk-roster-q')
2101
+ if (qEl) qEl.oninput = function (e) { state.rosterQ = e.target.value; renderRosterList() }
2102
+ var scopes = panel.querySelectorAll('input[name="dk-rscope"]')
2103
+ scopes.forEach(function (r) { r.onchange = function () { state.rosterScope = r.value; renderRosterList() } })
2104
+ var ref = panel.querySelector('#dk-roster-refresh')
2105
+ if (ref) ref.onclick = function () {
2106
+ var q2 = panel.querySelector('#dk-roster-q'); if (q2) q2.value = ''
2107
+ state.rosterQ = ''; refreshAll(); loadHubState()
2108
+ }
2109
+ var allB = panel.querySelector('#dk-roster-all')
2110
+ if (allB) allB.onclick = function () {
2111
+ rosterRows().forEach(function (r) { state.rosterSel[r.key] = true })
2112
+ try { localStorage.setItem('qqs-roster-sel', JSON.stringify(state.rosterSel)) } catch (e) {}
2113
+ renderRosterList()
2114
+ }
2115
+ var clr = panel.querySelector('#dk-roster-clear')
2116
+ if (clr) clr.onclick = function () {
2117
+ state.rosterSel = {}
2118
+ try { localStorage.setItem('qqs-roster-sel', JSON.stringify(state.rosterSel)) } catch (e) {}
2119
+ renderRosterList()
2120
+ }
2121
+ var box = panel.querySelector('#dk-roster-list')
2122
+ if (box) box.onclick = function (e) {
2123
+ var cb = e.target
2124
+ while (cb && cb !== box && !(cb.tagName === 'INPUT' && cb.type === 'checkbox')) cb = cb.parentNode
2125
+ if (!cb || cb === box) return
2126
+ rosterToggle(cb.getAttribute('data-rkey'))
2127
+ }
2128
+ // 🎯 群组管理器: 设本会话/取消(走 settings groupAdmin.hubSessionId, live 热更)
2129
+ var hubSet = panel.querySelector('#dk-hub-set')
2130
+ if (hubSet) hubSet.onclick = function () {
2131
+ var sid = readCurrentSessionId()
2132
+ if (!sid) { hubHint('未识别到当前 web 会话(稍后再试)'); return }
2133
+ saveHub(sid)
2134
+ }
2135
+ var hubClr = panel.querySelector('#dk-hub-clear')
2136
+ if (hubClr) hubClr.onclick = function () { saveHub('') }
2137
+ }
2138
+ function hubStatusText() {
2139
+ if (state.hubSid) return '已设: 会话 ' + state.hubSid.slice(0, 8) + '… ← 各群群事件(入群申请/新成员/被拉群)汇总注入这里'
2140
+ return '未设置: 群事件只在原群内提醒'
2141
+ }
2142
+ function hubHint(t) {
2143
+ state.hubMsg = t || ''
2144
+ var el = panel && panel.querySelector('#dk-hub-hint')
2145
+ if (el) el.textContent = state.hubMsg
2146
+ }
2147
+ function loadHubState() {
2148
+ fetch(READ + (state.ns ? '?' + outNsQ() : '')).then(function (r) { return r.json() }).then(function (d) {
2149
+ if (!d || !d.value) return
2150
+ var ga = d.value.groupAdmin || {}
2151
+ state.hubSid = ga.hubSessionId || ''
2152
+ state.hubRev = d.revision
2153
+ var st = panel && panel.querySelector('#dk-hub-status')
2154
+ if (st) st.textContent = hubStatusText()
2155
+ var clr = panel && panel.querySelector('#dk-hub-clear')
2156
+ if (clr) clr.disabled = !state.hubSid
2157
+ }).catch(function () {})
2158
+ }
2159
+ function saveHub(sid) {
2160
+ state.hubBusy = 'hub'; hubHint('保存中…')
2161
+ var doSave = function () {
2162
+ var trySave = function (cur, rev) {
2163
+ var patch = Object.assign({}, cur || {})
2164
+ var ga = Object.assign({}, (cur && cur.groupAdmin) || {}, { hubSessionId: sid || '' })
2165
+ patch.groupAdmin = ga
2166
+ return fetch(UPDATE, {
2167
+ method: 'POST', headers: { 'content-type': 'application/json' },
2168
+ body: JSON.stringify({ ns: state.ns || undefined, patch: patch, expectedRevision: rev }),
2169
+ }).then(function (r) { return r.json().catch(function () { return null }) }).then(function (d2) {
2170
+ state.hubBusy = ''
2171
+ if (d2 && d2.value) {
2172
+ var ga2 = d2.value.groupAdmin || {}
2173
+ state.hubSid = ga2.hubSessionId || ''
2174
+ state.hubRev = d2.revision
2175
+ hubHint(sid ? '✓ 已设为本会话为群组管理器(live 生效): 各群群事件将注入此会话' : '✓ 已取消群组管理器')
2176
+ var st = panel && panel.querySelector('#dk-hub-status')
2177
+ if (st) st.textContent = hubStatusText()
2178
+ var clr = panel && panel.querySelector('#dk-hub-clear')
2179
+ if (clr) clr.disabled = !state.hubSid
2180
+ } else {
2181
+ hubHint('保存失败: ' + ((d2 && d2.error) || '未知错误') + (d2 && String(d2.error || '').indexOf('changed since') >= 0 ? '(冲突, 请重试)' : ''))
2182
+ }
2183
+ }).catch(function () { state.hubBusy = ''; hubHint('保存异常') })
2184
+ }
2185
+ // 先读最新 revision 再存(避免陈旧冲突)
2186
+ fetch(READ + (state.ns ? '?' + outNsQ() : '')).then(function (r) { return r.json() }).then(function (d) {
2187
+ if (d && d.value) trySave(d.value, d.revision)
2188
+ else { state.hubBusy = ''; hubHint('保存失败: 无法读取当前设置') }
2189
+ }).catch(function () { state.hubBusy = ''; hubHint('保存异常: 读取失败') })
2190
+ }
2191
+ // 预校验(防呆): 群事件按 hubSessionId 反查本 bot 的 QQ 会话——纯 web(未绑群/私聊)的会话注不进去
2192
+ if (!sid) { doSave(); return }
2193
+ api('session-lookup', 'sessionId=' + encodeURIComponent(sid)).then(function (d) {
2194
+ var hits = (d && d.ok && Array.isArray(d.hits)) ? d.hits : []
2195
+ if (!hits.length) {
2196
+ state.hubBusy = ''
2197
+ hubHint('⚠️ 当前会话未绑定任何 QQ 群/私聊: 群事件注入不进去。请先切到与 bot 的某个群/私聊会话(顶栏「当前会话」会显示命中)再点设置')
2198
+ return
2199
+ }
2200
+ doSave()
2201
+ }).catch(function () { doSave() })
2202
+ }
2049
2203
  function sendNow() {
2050
2204
  var t = (state.sendText || '').trim()
2051
2205
  if (!t) { state.msg = '先输入内容'; paintBody(); return }
@@ -2170,10 +2324,12 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2170
2324
  return '<option value="' + esc(c.id) + '"' + (c.id === state.sendTo ? ' selected' : '') + '>' + esc(label) + '</option>'
2171
2325
  }).join('')
2172
2326
  var tabs = '<div class="dk-tab">'
2327
+ + '<button data-t="roster" class="' + (state.tab === 'roster' ? 'on' : '') + '">📇 群组管理</button>'
2173
2328
  + '<button data-t="chat" class="' + (state.tab === 'chat' ? 'on' : '') + '">💬 聊天</button>'
2174
2329
  + '<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>'
2175
2330
  + '<button data-t="mute" class="' + (state.tab === 'mute' ? 'on' : '') + '">🔇 禁言</button>'
2176
2331
  + '<button data-t="out" class="' + (state.tab === 'out' ? 'on' : '') + '">⚙️ 出站</button>'
2332
+ + '<button data-t="bp" class="' + (state.tab === 'bp' ? 'on' : '') + '">🎮 互动事件</button>'
2177
2333
  + '</div>'
2178
2334
  var body = tabs
2179
2335
  var status = state.msg ? '<div class="dk-msg" style="color:#2f9e44;margin:4px 0">' + esc(state.msg) + '</div>' : ''
@@ -2242,6 +2398,29 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2242
2398
  body += '<div class="dk-list" id="dk-member-list"></div>'
2243
2399
  body += '<div style="font-weight:700;font-size:13px;margin:8px 0 4px">② 正在禁言中</div>'
2244
2400
  body += '<div class="dk-list" id="dk-mute-list"></div>'
2401
+ } else if (state.tab === 'roster') {
2402
+ body += '<div class="dk-row" style="font-weight:700;font-size:13px;margin:2px 0">📇 会话台账:机器人聊过的对象</div>'
2403
+ body += '<div class="dk-row"><span class="dk-msg" style="flex:1;line-height:1.5">👤 = 私聊过的人(有 c2c 会话,可直接发消息) · 👥 = 群(群成员≠私聊对象: 群里见过≠能私聊,只能群内@)</span></div>'
2404
+ body += '<div style="border:1px solid #e2d9ff;border-radius:8px;padding:6px 8px;margin:2px 0 6px;background:#faf8ff">'
2405
+ + '<div class="dk-row" style="margin:0"><b style="font-size:13px">🎯 群组管理器(M2)</b>'
2406
+ + '<span class="dk-msg" style="flex:1" id="dk-hub-status">' + hubStatusText() + '</span>'
2407
+ + '<button class="dk-btn ok" id="dk-hub-set"' + (state.hubBusy ? ' disabled' : '') + ' title="把当前 web 会话设为群组管理器: 各群群事件(入群申请/新成员加入/被拉群)将汇总注入此会话">📌 设为本会话</button>'
2408
+ + '<button class="dk-btn" id="dk-hub-clear"' + (state.hubSid ? '' : ' disabled') + '>取消</button></div>'
2409
+ + '<div class="dk-msg" id="dk-hub-hint" style="color:#2f9e44"></div></div>'
2410
+ body += '<div class="dk-row">范围: '
2411
+ + '<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:12px"><input type="radio" name="dk-rscope" value="all"' + (state.rosterScope === 'all' ? ' checked' : '') + '> 全部</label>'
2412
+ + '<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:12px"><input type="radio" name="dk-rscope" value="c2c"' + (state.rosterScope === 'c2c' ? ' checked' : '') + '> 👤 私聊过的人</label>'
2413
+ + '<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:12px"><input type="radio" name="dk-rscope" value="group"' + (state.rosterScope === 'group' ? ' checked' : '') + '> 👥 群</label>'
2414
+ + '<span style="flex:1"></span>'
2415
+ + '<button class="dk-btn" id="dk-roster-refresh" title="重新拉取群注册表+聊天台账">🔄 刷新</button></div>'
2416
+ body += '<div class="dk-row" style="margin:2px 0 4px">'
2417
+ + '<input class="qqs-txt" id="dk-roster-q" placeholder="🔍 搜昵称/群名/ID…" value="' + esc(state.rosterQ || '') + '" style="flex:1;box-sizing:border-box;font-size:12px;padding:4px 8px">'
2418
+ + '<span class="dk-msg" id="dk-roster-stat" style="white-space:nowrap"></span></div>'
2419
+ body += '<div class="dk-row" style="margin:0 0 4px">'
2420
+ + '<button class="dk-btn" id="dk-roster-all">全选(当前范围)</button>'
2421
+ + '<button class="dk-btn" id="dk-roster-clear">清空</button>'
2422
+ + '<span class="dk-msg" style="flex:1;text-align:right">勾选集合用途待开发(后续: 群发/批量操作目标)</span></div>'
2423
+ body += '<div class="dk-list" id="dk-roster-list"></div>'
2245
2424
  } else if (state.tab === 'out') {
2246
2425
  var om = state.outMode || 'adaptive'
2247
2426
  if (om === 'nothink') body += '<div class="dk-msg" style="color:#c23131;margin:2px 0">⚠️ 当前为「完全不思考」(设置页开启): QQ 入站不唤醒 AI。发 /outmode adaptive 可唤醒。</div>'
@@ -2252,6 +2431,62 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2252
2431
  body += '<div class="dk-msg" style="line-height:1.6">适配主动=和QQ有关的会话回复都发到QQ: 刚收到真人消息时前5条带引用回你(能看到回的是哪句), 第6条起自动转独立新消息, 连发不被QQ吞; 定时/后台等没有新真人消息的主动推送也走独立消息。'
2253
2432
  body += '被动=始终以「回复你那条」发出, 连发约4~5条后会被QQ吞掉。完全不出站=本机静默, 不向QQ发任何回复(鲸鱼娘可用工具随时切回)。本开关对纯web(没绑QQ)的会话不生效。</div>'
2254
2433
  body += '<span class="dk-msg" id="dk-out-hint" style="color:#2f9e44;margin:4px 0"></span>'
2434
+ } else if (state.tab === 'bp') {
2435
+ body += '<div class="dk-row" style="font-weight:700;font-size:13px;margin:4px 0 2px">🎮 互动事件装配器(/botplay 触发)</div>'
2436
+ body += '<div class="dk-row"><span class="dk-msg" style="flex:1">定义事件与按钮 → 保存即热更 → QQ 里发 <b>/botplay 事件名</b> 发卡。点击按钮走 bot 行为(回文本等)并可选影响 AI。</span></div>'
2437
+ // 事件列表
2438
+ body += '<div class="dk-list" id="dk-bp-list" style="max-height:16vh">'
2439
+ if (!state.bpEvents.length) body += '<div class="dk-empty">还没有事件。点「➕ 新建事件」开始装配。</div>'
2440
+ state.bpEvents.forEach(function (ev, i) {
2441
+ var sel = state.bpSel === i
2442
+ var btnN = (ev.buttons || []).length
2443
+ var rowc = bpRowCount(ev)
2444
+ var overL = rowc.rows > 5
2445
+ body += '<div class="dk-item" style="cursor:pointer;background:' + (sel ? '#f1ecff' : 'transparent') + '" data-bpidx="' + i + '">'
2446
+ + '<span style="flex:1"><b>' + esc(ev.name || '(未命名)') + '</b> <span style="color:#888;font-size:11px">/ ' + esc(ev.id || '?') + ' · ' + btnN + ' 按钮·每行' + rowc.per + ' → ' + rowc.rows + '行' + (overL ? ' <b style="color:#e03131">⚠超限</b>' : '') + ' · ' + (ev.perm && ev.perm.type ? esc(ev.perm.type) : 'all') + ' · ' + (ev.expireSec || 600) + 's</span></span>'
2447
+ + '<button class="dk-btn no" data-bpdel="' + i + '">🗑</button></div>'
2448
+ })
2449
+ body += '</div>'
2450
+ body += '<div class="dk-row"><button class="dk-btn ok" id="dk-bp-new">➕ 新建事件</button>'
2451
+ body += '<button class="dk-btn ok" id="dk-bp-save">💾 保存全部(live 热更)</button>'
2452
+ body += '<span class="dk-msg" id="dk-bp-hint" style="color:#2f9e44"></span></div>'
2453
+ // 编辑器
2454
+ if (state.bpDraft) {
2455
+ var D = state.bpDraft
2456
+ body += '<div style="border-top:1px dashed #e2d9ff;margin-top:8px;padding-top:8px">'
2457
+ body += '<div class="dk-row"><label style="font-size:12px;color:#555;width:56px">事件名</label><input class="qqs-txt" id="dk-bp-name" value="' + esc(D.name) + '" style="flex:1" placeholder="如: 签到"></div>'
2458
+ body += '<div class="dk-row"><label style="font-size:12px;color:#555;width:56px">id</label><input class="qqs-txt" id="dk-bp-id" value="' + esc(D.id) + '" style="flex:1" placeholder="英文/数字/_- (触发用)"></div>'
2459
+ body += '<div class="dk-row"><label style="font-size:12px;color:#555;width:56px">权限</label>'
2460
+ + '<select class="qqs-sel" id="dk-bp-perm">' + ['all','triggerer','owner','users'].map(function (p) { return '<option value="' + p + '"' + (D.perm && D.perm.type === p ? ' selected' : '') + '>' + ({ all: '所有人', triggerer: '仅触发者本人(推荐)', owner: '主人白名单', users: '指定openid' }[p]) + '</option>' }).join('') + '</select>'
2461
+ + (D.perm && D.perm.type === 'users'
2462
+ ? '<input class="qqs-txt" id="dk-bp-users" value="' + esc(((D.perm.userIds) || []).join(', ')) + '" placeholder="指定 openid(逗号分隔)" style="flex:1;min-width:160px">'
2463
+ : '')
2464
+ + '<label style="font-size:12px;color:#555">有效期(s)</label><input class="qqs-txt" id="dk-bp-expire" type="number" min="30" value="' + (D.expireSec || 600) + '" style="width:70px">'
2465
+ + '<label style="font-size:12px;color:#555">总次数(0不限)</label><input class="qqs-txt" id="dk-bp-max" type="number" min="0" value="' + (D.maxClicks || 0) + '" style="width:60px">'
2466
+ + '<label style="font-size:12px;color:#555">每行按钮</label><input class="qqs-txt" id="dk-bp-perrow" type="number" min="1" max="5" value="' + (D.buttonsPerRow || 1) + '" style="width:50px" title="每行几个按钮(1~5); QQ 键盘≤5行×每行≤5, 行数=按钮数÷每行"></div>'
2467
+ var rowc2 = bpRowCount(D)
2468
+ body += '<div style="font-weight:700;font-size:12px;margin:6px 0 2px;color:#4a3a9f">按钮 <span id="dk-bp-layout" style="font-weight:400;color:' + (rowc2.rows > 5 ? '#e03131' : '#2f9e44') + '">共 ' + rowc2.n + ' 个 · 每行 ' + rowc2.per + ' 个 → ' + rowc2.rows + ' 行' + (rowc2.rows > 5 ? '(超 QQ 上限 5 行, 发卡会报错!)' : '(≤5 行 ✅)') + '</span></div>'
2469
+ ;(D.buttons || []).forEach(function (b, bi) {
2470
+ var bt = (b.botAction && b.botAction.type) || 'reply_text'
2471
+ var md = (b.llmEffect && b.llmEffect.mode) || 'no_append'
2472
+ var ctx = (b.llmEffect && b.llmEffect.contextText) || ''
2473
+ // 视觉隔离: 每加一个按钮, 与上一个之间加一条虚线 + 按钮序号, 一眼分清几组配置
2474
+ body += '<div style="font-size:11px;color:#8b7cc7;margin:' + (bi > 0 ? '6px 0 0' : '2px 0 0') + '">── 按钮 ' + (bi + 1) + '</div>'
2475
+ body += '<div class="dk-item" style="border:1px solid #f0ecff;border-radius:8px;margin:1px 0;padding:4px 6px">'
2476
+ + '<input class="qqs-txt" data-bplabel="' + bi + '" value="' + esc(b.label) + '" placeholder="按钮文字" style="flex:1;min-width:90px">'
2477
+ + '<select class="qqs-sel" data-bpact="' + bi + '">' + ['reply_text','jump_url','callback','command'].map(function (t) { return '<option value="' + t + '"' + (bt === t ? ' selected' : '') + '>' + ({ reply_text: '回文本', jump_url: '跳链接', callback: '仅结算', command: '执行命令' }[t]) + '</option>' }).join('') + '</select>'
2478
+ + '<select class="qqs-sel" data-bpmode="' + bi + '">' + ['no_append','append_silent','append_wake'].map(function (m) { return '<option value="' + m + '"' + (md === m ? ' selected' : '') + '>' + ({ no_append: '不影响AI', append_silent: '记录不唤醒', append_wake: '记录并唤醒AI' }[m]) + '</option>' }).join('') + '</select>'
2479
+ + '<button class="dk-btn no" data-bpdelbtn="' + bi + '">✕</button></div>'
2480
+ // 动作参数行: 按类型给出对应输入(jump_url → 链接 URL; command → 命令名; 其余 → 回复文本)
2481
+ + (bt === 'jump_url'
2482
+ ? '<input class="qqs-txt" data-bpurl="' + bi + '" value="' + esc((b.botAction && b.botAction.url) || '') + '" placeholder="🔗 链接地址, 如 https://www.baidu.com(QQ 点按钮直接跳转)" style="width:calc(100% - 8px);box-sizing:border-box;margin:0 0 4px 4px">'
2483
+ : '<input class="qqs-txt" data-bpacttext="' + bi + '" value="' + esc((b.botAction && b.botAction.text) || '') + '" placeholder="' + ({ reply_text: '点按钮后 bot 回复的内容', command: '执行的命令名(如 bot-status, 不带/)', callback: '(仅结算, 无需文本)' }[bt] || '') + '" style="width:calc(100% - 8px);box-sizing:border-box;margin:0 0 4px 4px">')
2484
+ + '<input class="qqs-txt" data-bpctx="' + bi + '" value="' + esc(ctx) + '" placeholder="(记录/唤醒AI时)对AI说的话, 支持 {name} 事件名 {label} 按钮名; 空=默认" style="width:calc(100% - 8px);box-sizing:border-box;margin:0 0 2px 4px">'
2485
+ })
2486
+ body += '<div class="dk-row"><button class="dk-btn" id="dk-bp-addbtn">➕ 加按钮</button>'
2487
+ + '<span class="dk-msg" id="dk-bp-addmsg">每行 ' + rowc2.per + ' 个 × 最多 5 行 = 上限 ' + (rowc2.per * 5) + ' 个(QQ 键盘: ≤5行 × 每行≤5)</span></div>'
2488
+ body += '</div>'
2489
+ }
2255
2490
  }
2256
2491
  body += status
2257
2492
  body += '<div class="dk-row" style="border-top:1px solid #f0ecff;padding-top:8px;margin-top:8px"><span class="dk-msg">登记群(官方无群列表,手动粘贴):</span>'
@@ -2265,6 +2500,7 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2265
2500
  if (state.tab === 'chat') renderChatList()
2266
2501
  if (state.tab === 'join') renderJoinList()
2267
2502
  if (state.tab === 'mute') { renderMemberList(); renderMuteList() }
2503
+ if (state.tab === 'roster') { renderRosterList(); bindRosterEvents(); loadHubState() }
2268
2504
  // 首次进入聊天 tab 自动拉最新一页
2269
2505
  if (state.tab === 'chat' && !state.chatItems.length && !state.chatBusy) loadChat(true)
2270
2506
  setTimeout(layoutPanel, 0)
@@ -2276,6 +2512,7 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2276
2512
  state.tab = btn.getAttribute('data-t'); state.msg = ''
2277
2513
  if (state.tab === 'join') loadJoins(); else if (state.tab === 'mute') loadMutes()
2278
2514
  else if (state.tab === 'out') loadOutMode()
2515
+ else if (state.tab === 'bp') loadBotplay()
2279
2516
  else if (state.tab === 'chat') { state.chatItems = []; state.chatErr = ''; }
2280
2517
  paintBody()
2281
2518
  }
@@ -2393,6 +2630,190 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2393
2630
  }
2394
2631
  var bd = panel.querySelector('#dk-bind')
2395
2632
  if (bd) bd.onclick = function () { state.bindGid = (panel.querySelector('#dk-bindgid') || {}).value || ''; state.bindName = (panel.querySelector('#dk-bindname') || {}).value || ''; doBind() }
2633
+ // ── 🎮 互动事件装配器: 列表选择/删除/新建/保存/编辑器字段 ──
2634
+ var bpList = panel.querySelector('#dk-bp-list')
2635
+ if (bpList) bpList.querySelectorAll('[data-bpidx]').forEach(function (it) {
2636
+ it.onclick = function () {
2637
+ var idx = Number(it.getAttribute('data-bpidx'))
2638
+ state.bpSel = idx
2639
+ state.bpDraft = JSON.parse(JSON.stringify(state.bpEvents[idx] || {}))
2640
+ paintBody()
2641
+ }
2642
+ })
2643
+ bpList && bpList.querySelectorAll('[data-bpdel]').forEach(function (b) {
2644
+ b.onclick = function (e) {
2645
+ e.stopPropagation()
2646
+ var idx = Number(b.getAttribute('data-bpdel'))
2647
+ state.bpEvents.splice(idx, 1)
2648
+ if (state.bpSel === idx) { state.bpSel = null; state.bpDraft = null }
2649
+ else if (state.bpSel !== null && state.bpSel > idx) state.bpSel -= 1
2650
+ paintBody()
2651
+ }
2652
+ })
2653
+ var bpNew = panel.querySelector('#dk-bp-new')
2654
+ if (bpNew) bpNew.onclick = function () {
2655
+ state.bpSel = null
2656
+ state.bpDraft = { id: '', name: '', maxClicks: 0, expireSec: 600, buttonsPerRow: 1, perm: { type: 'all', userIds: [] }, buttons: [{ id: 'b1', label: '按钮', botAction: { type: 'reply_text', text: '' }, llmEffect: { mode: 'no_append', contextText: '' } }] }
2657
+ paintBody()
2658
+ }
2659
+ // 编辑器字段(名称/id/有效期/总次数/权限)
2660
+ var bpName = panel.querySelector('#dk-bp-name')
2661
+ if (bpName) bpName.oninput = function (e) { state.bpDraft.name = e.target.value }
2662
+ var bpId = panel.querySelector('#dk-bp-id')
2663
+ if (bpId) bpId.oninput = function (e) { state.bpDraft.id = e.target.value.trim() }
2664
+ var bpPerm = panel.querySelector('#dk-bp-perm')
2665
+ if (bpPerm) bpPerm.onchange = function (e) {
2666
+ state.bpDraft.perm = state.bpDraft.perm || {}
2667
+ state.bpDraft.perm.type = e.target.value
2668
+ paintBody() // users 档要显示 openid 输入框
2669
+ }
2670
+ var bpUsers = panel.querySelector('#dk-bp-users')
2671
+ if (bpUsers) bpUsers.oninput = function (e) {
2672
+ state.bpDraft.perm = state.bpDraft.perm || {}
2673
+ state.bpDraft.perm.userIds = String(e.target.value || '').split(/[,,\s]+/).map(function (s) { return s.trim() }).filter(Boolean)
2674
+ }
2675
+ var bpExpire = panel.querySelector('#dk-bp-expire')
2676
+ if (bpExpire) bpExpire.oninput = function (e) { state.bpDraft.expireSec = Math.max(30, Number(e.target.value) || 600) }
2677
+ var bpMax = panel.querySelector('#dk-bp-max')
2678
+ if (bpMax) bpMax.oninput = function (e) { state.bpDraft.maxClicks = Math.max(0, Number(e.target.value) || 0) }
2679
+ var bpPerRow = panel.querySelector('#dk-bp-perrow')
2680
+ if (bpPerRow) bpPerRow.oninput = function (e) { state.bpDraft.buttonsPerRow = Math.max(1, Math.min(5, Math.round(Number(e.target.value)) || 1)); bpLayoutHint() }
2681
+ // 按钮行编辑(label/botAction.type/llmEffect.mode/删除/回复文本/对AI说的话)
2682
+ var bpBtns = panel.querySelectorAll('[data-bplabel]')
2683
+ bpBtns.forEach(function (inp) {
2684
+ var bi = Number(inp.getAttribute('data-bplabel'))
2685
+ inp.oninput = function (e) {
2686
+ state.bpDraft.buttons[bi].label = e.target.value
2687
+ if (!state.bpDraft.buttons[bi].id) state.bpDraft.buttons[bi].id = 'b' + (bi + 1)
2688
+ }
2689
+ })
2690
+ var bpActs = panel.querySelectorAll('[data-bpact]')
2691
+ bpActs.forEach(function (sel) {
2692
+ var bi = Number(sel.getAttribute('data-bpact'))
2693
+ sel.onchange = function (e) {
2694
+ state.bpDraft.buttons[bi].botAction = state.bpDraft.buttons[bi].botAction || {}
2695
+ state.bpDraft.buttons[bi].botAction.type = e.target.value
2696
+ }
2697
+ })
2698
+ var bpModes = panel.querySelectorAll('[data-bpmode]')
2699
+ bpModes.forEach(function (sel) {
2700
+ var bi = Number(sel.getAttribute('data-bpmode'))
2701
+ sel.onchange = function (e) {
2702
+ state.bpDraft.buttons[bi].llmEffect = state.bpDraft.buttons[bi].llmEffect || {}
2703
+ state.bpDraft.buttons[bi].llmEffect.mode = e.target.value
2704
+ }
2705
+ })
2706
+ var bpTexts = panel.querySelectorAll('[data-bpacttext]')
2707
+ bpTexts.forEach(function (inp) {
2708
+ var bi = Number(inp.getAttribute('data-bpacttext'))
2709
+ inp.oninput = function (e) {
2710
+ state.bpDraft.buttons[bi].botAction = state.bpDraft.buttons[bi].botAction || {}
2711
+ state.bpDraft.buttons[bi].botAction.text = e.target.value
2712
+ }
2713
+ })
2714
+ // 跳链接按钮的 URL 输入
2715
+ var bpUrls = panel.querySelectorAll('[data-bpurl]')
2716
+ bpUrls.forEach(function (inp) {
2717
+ var bi = Number(inp.getAttribute('data-bpurl'))
2718
+ inp.oninput = function (e) {
2719
+ state.bpDraft.buttons[bi].botAction = state.bpDraft.buttons[bi].botAction || {}
2720
+ state.bpDraft.buttons[bi].botAction.url = e.target.value
2721
+ }
2722
+ })
2723
+ var bpCtxs = panel.querySelectorAll('[data-bpctx]')
2724
+ bpCtxs.forEach(function (inp) {
2725
+ var bi = Number(inp.getAttribute('data-bpctx'))
2726
+ inp.oninput = function (e) {
2727
+ state.bpDraft.buttons[bi].llmEffect = state.bpDraft.buttons[bi].llmEffect || {}
2728
+ state.bpDraft.buttons[bi].llmEffect.contextText = e.target.value
2729
+ }
2730
+ })
2731
+ var bpDelBtn = panel.querySelectorAll('[data-bpdelbtn]')
2732
+ bpDelBtn.forEach(function (b) {
2733
+ b.onclick = function () {
2734
+ state.bpDraft.buttons.splice(Number(b.getAttribute('data-bpdelbtn')), 1)
2735
+ paintBody()
2736
+ }
2737
+ })
2738
+ var bpAddBtn = panel.querySelector('#dk-bp-addbtn')
2739
+ if (bpAddBtn) bpAddBtn.onclick = function () {
2740
+ var cap = Math.max(1, Math.min(5, Math.round(Number(state.bpDraft.buttonsPerRow)) || 1)) * 5
2741
+ if (state.bpDraft.buttons.length >= cap) return
2742
+ state.bpDraft.buttons.push({ id: 'b' + (state.bpDraft.buttons.length + 1), label: '按钮', botAction: { type: 'reply_text', text: '' }, llmEffect: { mode: 'no_append', contextText: '' } })
2743
+ paintBody()
2744
+ }
2745
+ var bpSave = panel.querySelector('#dk-bp-save')
2746
+ if (bpSave) bpSave.onclick = saveBotplay
2747
+ }
2748
+ // QQ 键盘布局限制(官方 API): 键盘最多 5 行 × 每行最多 5 个按钮 → 行数=ceil(按钮数/每行数)≤5 才合法
2749
+ function bpRowCount(ev) {
2750
+ var per = Math.max(1, Math.min(5, Math.round(Number(ev.buttonsPerRow)) || 1))
2751
+ var n = (ev.buttons || []).length
2752
+ return { n: n, per: per, rows: Math.ceil(n / per) }
2753
+ }
2754
+ // 编辑器内实时刷新布局提示(改「每行按钮」时调用, 不重建 DOM 保输入焦点)
2755
+ function bpLayoutHint() {
2756
+ if (!state.bpDraft || !panel) return
2757
+ var el = panel.querySelector('#dk-bp-layout')
2758
+ if (!el) return
2759
+ var c = bpRowCount(state.bpDraft)
2760
+ var over = c.rows > 5
2761
+ el.textContent = '共 ' + c.n + ' 个 · 每行 ' + c.per + ' 个 → ' + c.rows + ' 行' + (over ? '(超 QQ 上限 5 行, 发卡会报错!)' : '(≤5 行 ✅)')
2762
+ el.style.color = over ? '#e03131' : '#2f9e44'
2763
+ }
2764
+ // 🎮 互动事件装配器: 读/存(全量 patch 只带 botplayEvents; settings.update 部分合并 + revision 乐观锁)
2765
+ function loadBotplay() {
2766
+ if (state.tab !== 'bp') return
2767
+ fetch(READ + (state.ns ? '?' + outNsQ() : '')).then(function (r) { return r.json() }).then(function (d) {
2768
+ if (d && d.value) {
2769
+ state.bpEvents = Array.isArray(d.value.botplayEvents) ? JSON.parse(JSON.stringify(d.value.botplayEvents)) : []
2770
+ if (!state.bpDraft && state.bpEvents.length) { state.bpSel = 0; state.bpDraft = JSON.parse(JSON.stringify(state.bpEvents[0])) }
2771
+ else if (!state.bpEvents.length) { state.bpSel = null; state.bpDraft = null }
2772
+ paintBody()
2773
+ }
2774
+ }).catch(function () {})
2775
+ }
2776
+ function saveBotplay() {
2777
+ var hint = panel.querySelector('#dk-bp-hint')
2778
+ var ok = function (v) { if (hint) hint.textContent = v }
2779
+ // 草稿回写进列表(存在编辑中草稿时)
2780
+ if (state.bpDraft) {
2781
+ var draft = state.bpDraft
2782
+ if (!draft.id || !/^[A-Za-z0-9_-]+$/.test(draft.id)) { ok('⚠️ id 必填且只能 字母/数字/_/-'); return }
2783
+ if (!draft.name || !draft.name.trim()) { ok('⚠️ 事件名必填'); return }
2784
+ if (!Array.isArray(draft.buttons) || !draft.buttons.length) { ok('⚠️ 至少 1 个按钮'); return }
2785
+ var dupId = false
2786
+ state.bpEvents.forEach(function (ev, i) { if (ev.id === draft.id && i !== state.bpSel) dupId = true })
2787
+ if (dupId) { ok('⚠️ 事件 id 重复: ' + draft.id); return }
2788
+ var draftRow = bpRowCount(draft)
2789
+ if (draftRow.rows > 5) { ok('⚠️ 「' + draft.name + '」按钮布局超限: ' + draftRow.n + ' 个按钮 × 每行 ' + draftRow.per + ' 个 = ' + draftRow.rows + ' 行(QQ 键盘最多 5 行)'); return }
2790
+ if (state.bpSel !== null && state.bpEvents[state.bpSel]) state.bpEvents[state.bpSel] = JSON.parse(JSON.stringify(draft))
2791
+ else { state.bpEvents.push(JSON.parse(JSON.stringify(draft))); state.bpSel = state.bpEvents.length - 1 }
2792
+ }
2793
+ var badEv = null
2794
+ state.bpEvents.forEach(function (ev) { var c = bpRowCount(ev); if (c.rows > 5 && !badEv) badEv = { name: ev.name, per: c.per, rows: c.rows } })
2795
+ if (badEv) { ok('⚠️ 事件「' + badEv.name + '」每行 ' + badEv.per + ' 个 → ' + badEv.rows + ' 行, 超 QQ 5 行上限, 未保存'); return }
2796
+ if (hint) hint.textContent = '保存中…'
2797
+ var trySave = function (curVal, curRev) {
2798
+ var patch = Object.assign({}, curVal || {}, { botplayEvents: state.bpEvents })
2799
+ return fetch(UPDATE, {
2800
+ method: 'POST', headers: { 'content-type': 'application/json' },
2801
+ body: JSON.stringify({ ns: state.ns || undefined, patch: patch, expectedRevision: curRev }),
2802
+ }).then(function (r) { return r.json().catch(function () { return null }) }).then(function (d) {
2803
+ if (d && d.value) {
2804
+ state.bpEvents = Array.isArray(d.value.botplayEvents) ? d.value.botplayEvents : []
2805
+ var h2 = panel.querySelector('#dk-bp-hint'); if (h2) h2.textContent = '✅ 已保存(共 ' + state.bpEvents.length + ' 个事件, live 热更已生效)'
2806
+ } else {
2807
+ var conflicted = !!(d && d.error && String(d.error).indexOf('changed since it was read') >= 0)
2808
+ if (conflicted) { fetch(READ + (state.ns ? '?' + outNsQ() : '')).then(function (r) { return r.json() }).then(function (dd) { if (dd && dd.value) trySave(dd.value, dd.revision) }).catch(function () {}) }
2809
+ else { var h3 = panel.querySelector('#dk-bp-hint'); if (h3) h3.textContent = '保存失败: ' + ((d && d.error) || '未知错误') }
2810
+ }
2811
+ }).catch(function () { var h4 = panel.querySelector('#dk-bp-hint'); if (h4) h4.textContent = '保存异常' })
2812
+ }
2813
+ fetch(READ + (state.ns ? '?' + outNsQ() : '')).then(function (r) { return r.json() }).then(function (d0) {
2814
+ if (d0 && d0.value) trySave(d0.value, d0.revision)
2815
+ else { var h5 = panel.querySelector('#dk-bp-hint'); if (h5) h5.textContent = '保存失败: 无法读取当前设置' }
2816
+ }).catch(function () { var h6 = panel.querySelector('#dk-bp-hint'); if (h6) h6.textContent = '保存失败: 读取异常' })
2396
2817
  }
2397
2818
  // ── 💬 聊天视图: 数据拉取 / QQ 风格气泡渲染 / 顶部滚动分页 ──
2398
2819
  function chatPeerReady() {
@@ -10,5 +10,5 @@ export declare const name = "qqbot-channel-tools";
10
10
  export declare const inject: string[];
11
11
  export declare function setChannelBridge(b: QQChannel | undefined): void;
12
12
  /** 装载本插件时把 qqChannel 一并注入(由 dsh-qqbot setup 提供) */
13
- export declare function apply(ctx: Context): void;
13
+ export declare function apply(ctx: Context): Promise<void>;
14
14
  //# sourceMappingURL=channel-tools.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"channel-tools.d.ts","sourceRoot":"","sources":["../src/channel-tools.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AA+BlE,6DAA6D;AAC7D,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,eAAO,MAAM,IAAI,wBAAwB,CAAC;AAC1C,eAAO,MAAM,MAAM,UAAY,CAAC;AAShC,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI,CAI/D;AAqED,mDAAmD;AACnD,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAmxBxC"}
1
+ {"version":3,"file":"channel-tools.d.ts","sourceRoot":"","sources":["../src/channel-tools.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAqDlE,6DAA6D;AAC7D,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,eAAO,MAAM,IAAI,wBAAwB,CAAC;AAC1C,eAAO,MAAM,MAAM,UAAY,CAAC;AAShC,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI,CAI/D;AAqED,mDAAmD;AACnD,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAu4BvD"}