@zaofan/dsh-qqbot 1.4.1 → 1.4.6

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 (66) hide show
  1. package/README.md +69 -0
  2. package/README_EN.md +60 -0
  3. package/client/qqbot-settings.js +429 -4
  4. package/dist/channel-tools.d.ts +15 -0
  5. package/dist/channel-tools.d.ts.map +1 -1
  6. package/dist/channel-tools.js +59 -1
  7. package/dist/channel-tools.js.map +1 -1
  8. package/dist/config.d.ts +32 -0
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +15 -1
  11. package/dist/config.js.map +1 -1
  12. package/dist/features/image-path-cache.d.ts +23 -0
  13. package/dist/features/image-path-cache.d.ts.map +1 -0
  14. package/dist/features/image-path-cache.js +38 -0
  15. package/dist/features/image-path-cache.js.map +1 -0
  16. package/dist/features/image-url-ledger.d.ts +19 -0
  17. package/dist/features/image-url-ledger.d.ts.map +1 -0
  18. package/dist/features/image-url-ledger.js +133 -0
  19. package/dist/features/image-url-ledger.js.map +1 -0
  20. package/dist/features/local-embed.d.ts +43 -0
  21. package/dist/features/local-embed.d.ts.map +1 -0
  22. package/dist/features/local-embed.js +143 -0
  23. package/dist/features/local-embed.js.map +1 -0
  24. package/dist/features/sticker-store.d.ts +48 -2
  25. package/dist/features/sticker-store.d.ts.map +1 -1
  26. package/dist/features/sticker-store.js +156 -1
  27. package/dist/features/sticker-store.js.map +1 -1
  28. package/dist/features/value-sample.d.ts +25 -0
  29. package/dist/features/value-sample.d.ts.map +1 -0
  30. package/dist/features/value-sample.js +122 -0
  31. package/dist/features/value-sample.js.map +1 -0
  32. package/dist/features/value-score.d.ts +36 -0
  33. package/dist/features/value-score.d.ts.map +1 -0
  34. package/dist/features/value-score.js +122 -0
  35. package/dist/features/value-score.js.map +1 -0
  36. package/dist/gateway/bootstrap.d.ts.map +1 -1
  37. package/dist/gateway/bootstrap.js +4 -0
  38. package/dist/gateway/bootstrap.js.map +1 -1
  39. package/dist/gateway/debounce.d.ts.map +1 -1
  40. package/dist/gateway/debounce.js +16 -5
  41. package/dist/gateway/debounce.js.map +1 -1
  42. package/dist/gateway/middleware-setup.d.ts.map +1 -1
  43. package/dist/gateway/middleware-setup.js +12 -1
  44. package/dist/gateway/middleware-setup.js.map +1 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +8 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/middleware/sticker-capture.d.ts +8 -2
  49. package/dist/middleware/sticker-capture.d.ts.map +1 -1
  50. package/dist/middleware/sticker-capture.js +63 -45
  51. package/dist/middleware/sticker-capture.js.map +1 -1
  52. package/dist/model/model-resolver.js +12 -6
  53. package/dist/model/model-resolver.js.map +1 -1
  54. package/dist/transport/attachment.d.ts +5 -1
  55. package/dist/transport/attachment.d.ts.map +1 -1
  56. package/dist/transport/attachment.js +16 -5
  57. package/dist/transport/attachment.js.map +1 -1
  58. package/dist/transport/inbound.d.ts.map +1 -1
  59. package/dist/transport/inbound.js +351 -9
  60. package/dist/transport/inbound.js.map +1 -1
  61. package/dist/transport/inject-rules.d.ts.map +1 -1
  62. package/dist/transport/inject-rules.js +3 -1
  63. package/dist/transport/inject-rules.js.map +1 -1
  64. package/package.json +3 -2
  65. package/scripts/download-model.mjs +112 -0
  66. package/settings-host.js +337 -5
@@ -1993,6 +1993,277 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
1993
1993
  return ac ? (ac.dataDir || '') : ''
1994
1994
  }
1995
1995
  function outNsQ() { return state.ns ? 'ns=' + encodeURIComponent(state.ns) : '' }
1996
+ // ⚙ 单会话设置 · 智能回复(本地小模型; 2026-09-13 主人定, 15:2x 改名)
1997
+ // 📊 评分列表 HTML(单独抽出: 供"局部刷新"只替换内容, 不整块重绘)
1998
+ function scoreListHtml(vs) {
1999
+ if (vs === undefined || vs === null) return '<div class="dk-empty" style="padding:8px 0">读取中…</div>'
2000
+ if (!vs.items || !vs.items.length) return '<div class="dk-empty" style="padding:8px 0">还没有记录 —— 群里有人说句话之后, 这里会列出每条消息的分数</div>'
2001
+ var out = '<div style="max-height:230px;overflow:auto;border:1px solid #eee;border-radius:6px">'
2002
+ vs.items.slice().reverse().forEach(function (it) {
2003
+ var tt = new Date(it.ts || 0)
2004
+ var hh = ('0' + tt.getHours()).slice(-2) + ':' + ('0' + tt.getMinutes()).slice(-2) + ':' + ('0' + tt.getSeconds()).slice(-2)
2005
+ var col = it.worth ? '#2f9e44' : '#c23131'
2006
+ out += '<div style="padding:4px 8px;border-bottom:1px solid #f5f5f5;font-size:12px">'
2007
+ + '<span style="color:#999">' + hh + '</span> '
2008
+ + '<b style="color:' + col + '">' + (typeof it.score === 'number' ? it.score.toFixed(2) : (it.img ? '📷' : '-')) + '</b>'
2009
+ + (it.img ? ' <span style="color:#999" title="图片消息: ' + (it.lib ? '已在库 → 借它的标签当文字评分' : '无文字, 不可评分(默认不拦)') + '">' + (it.lib ? '[库内]' : '[无文字]') + '</span>' : '')
2010
+ + (it.mention ? ' <span style="color:#1c7ed6" title="被@, 必回">[@]</span>' : '')
2011
+ + (it.conf !== undefined && it.conf < 0.5 ? ' <span style="color:#999" title="低置信: 最近邻居相似度只有 ' + it.conf + ', 地图上没这类样本 —— 仅作补样本提示, 不再影响拦截">[?]</span>' : '')
2012
+ + (it.agg ? ' <span style="color:#e8590c" title="聚合了 ' + it.agg + ' 条消息, 取其中最高分">[合' + it.agg + ']</span>' : '')
2013
+ + ' <span style="color:#555">' + esc(String(it.sender || '?')) + ':</span> '
2014
+ + esc(String(it.text || '').slice(0, 56))
2015
+ + '<div style="color:#bbb;font-size:11px;margin-left:14px">近邻: ' + esc((it.top || []).join(' · ')) + '</div>'
2016
+ + '</div>'
2017
+ })
2018
+ out += '</div>'
2019
+ out += '<div class="dk-msg" style="font-size:11px;color:#888">共 ' + (vs.total || 0) + ' 条 · 明细文件: {数据根}/.qqbot/value-scores.jsonl</div>'
2020
+ return out
2021
+ }
2022
+ // 局部刷新评分列表(不调 paintBody → 不闪、不打断滚动、不丢输入框焦点)
2023
+ function refreshScoreList() {
2024
+ if (state.tab !== 'session') return
2025
+ fetch('/api/qqbot-settings/value-scores' + scoreQuery())
2026
+ .then(function (r) { return r.json() })
2027
+ .then(function (d) {
2028
+ var next = d && d.ok ? d : { items: [], total: 0 }
2029
+ // ⚠️ 2026-09-13 修(主人反馈"一直被重绘"): 原来每 10 秒**无条件**替换 innerHTML →
2030
+ // 内容没变也会闪一下。现在比较签名(最后3条+总数), 一样就完全不碰 DOM。
2031
+ var sig = (next.total || 0) + '|' + JSON.stringify((next.items || []).slice(-3))
2032
+ if (sig === state.vsSig) { state.valueScores = next; return }
2033
+ state.vsSig = sig
2034
+ state.valueScores = next
2035
+ var box = panel ? panel.querySelector('#dk-vs-box') : null
2036
+ if (box) box.innerHTML = scoreListHtml(state.valueScores)
2037
+ })
2038
+ .catch(function () { /* 静默: 拉取失败保持原样 */ })
2039
+ }
2040
+ // 当前会话命中的 QQ 目标(2026-09-13 主人要求: 评分/操作要跟随"当前会话", 与群管理页一致)
2041
+ function curHitGid() {
2042
+ var hit = state.detectedHit
2043
+ return (hit && hit.scope === 'group' && hit.peerId) ? hit.peerId : ''
2044
+ }
2045
+ function curHitLabel() {
2046
+ var hit = state.detectedHit
2047
+ if (!hit || !hit.peerId) return '(当前会话不是 QQ 会话)'
2048
+ return (hit.scope === 'group' ? '群「' + (hit.name || '…') + '」' : '私聊「' + (hit.name || '…') + '」')
2049
+ }
2050
+ function scoreQuery() {
2051
+ var q = []
2052
+ if (state.ns) q.push(outNsQ())
2053
+ var g = curHitGid()
2054
+ if (g) q.push('gid=' + encodeURIComponent(g))
2055
+ q.push('limit=20')
2056
+ return '?' + q.join('&')
2057
+ }
2058
+ // ✍️ 样例库编辑: 读现有内容(展开时才拉)
2059
+ function loadValueSamplesEditor() {
2060
+ fetch('/api/qqbot-settings/value-samples' + (state.ns ? '?' + outNsQ() : ''))
2061
+ .then(function (r) { return r.json() })
2062
+ .then(function (d) {
2063
+ var stat = panel ? panel.querySelector('#dk-vs-stat') : null
2064
+ var ta = panel ? panel.querySelector('#dk-vs-text') : null
2065
+ if (d && d.ok) {
2066
+ if (stat) stat.textContent = '共 ' + d.total + ' 条(她会接 ' + d.pos + ' / 不会理 ' + d.neg + ')' + (d.isDefault ? ' · 暂用内置默认, 保存后自建' : '')
2067
+ if (ta) ta.value = d.text || ''
2068
+ } else if (stat) stat.textContent = (d && d.error) || '读取失败'
2069
+ })
2070
+ .catch(function () { /* 静默 */ })
2071
+ }
2072
+ function saveValueSamplesEditor() {
2073
+ var ta = panel ? panel.querySelector('#dk-vs-text') : null
2074
+ var hint = panel ? panel.querySelector('#dk-vs-hint') : null
2075
+ if (!ta) return
2076
+ if (hint) hint.textContent = '保存中…'
2077
+ fetch('/api/qqbot-settings/value-samples', {
2078
+ method: 'POST', headers: { 'content-type': 'application/json' },
2079
+ body: JSON.stringify({ ns: state.ns, text: ta.value }),
2080
+ }).then(function (r) { return r.json() }).then(function (d) {
2081
+ if (d && d.ok) {
2082
+ if (hint) hint.textContent = '已保存 ✓ ' + d.saved + ' 条(她会接 ' + d.pos + ' / 不会理 ' + d.neg + ') · 下一条群消息生效'
2083
+ loadValueSamplesEditor()
2084
+ } else if (hint) hint.textContent = '保存失败: ' + ((d && d.error) || '未知')
2085
+ }).catch(function (e) { if (hint) hint.textContent = '异常: ' + e.message })
2086
+ }
2087
+ function loadLocalModel() {
2088
+ if (state.tab !== 'session') return
2089
+ // ⚠️ 2026-09-13 修(主人反馈面板一直闪): 原来 paintBody 的绑定区里也调了 loadLocalModel(),
2090
+ // 而它完成后又调 paintBody() → 无限循环重绘。现在绑定区只做绑定, 这里再加一道防重入锁。
2091
+ if (state.lmBusy) return
2092
+ state.lmBusy = true
2093
+ var nsq = state.ns ? '?' + outNsQ() : ''
2094
+ state.valueScores = null
2095
+ fetch('/api/qqbot-settings/local-model/status' + nsq).then(function (r) { return r.json() }).then(function (d) {
2096
+ state.localModel = d && d.ok ? d : null
2097
+ paintBody()
2098
+ }).catch(function () { state.localModel = null; paintBody() })
2099
+ // ⚠️ 2026-09-13 加固(主人反馈"永远显示只记录"): **直读 settings 配置**拿真实 valueGate/enabled/门槛,
2100
+ // 不再依赖 status 接口有没有带这些字段(host 未重启时旧接口不返回 → 会一直显示默认"只记录")。
2101
+ fetch(READ + (state.ns ? '?' + outNsQ() : '')).then(function (r) { return r.json() }).then(function (d) {
2102
+ state.lmCfg = (d && d.value && d.value.localModel) || null
2103
+ paintBody()
2104
+ }).catch(function () { state.lmCfg = null; paintBody() })
2105
+ fetch('/api/qqbot-settings/value-scores' + scoreQuery())
2106
+ .then(function (r) { return r.json() })
2107
+ .then(function (d) { state.valueScores = d && d.ok ? d : { items: [], total: 0 }; paintBody() })
2108
+ .catch(function () { state.valueScores = { items: [], total: 0 }; paintBody() })
2109
+ .then(function () { state.lmBusy = false })
2110
+ // 每 10 秒局部刷新一次评分列表(只换 #dk-vs-box 内容, 不整块重绘 → 不闪)
2111
+ if (state.lmTimer) clearInterval(state.lmTimer)
2112
+ state.lmTimer = setInterval(refreshScoreList, 10000)
2113
+ if (state.vsOpen) loadValueSamplesEditor() // 重绘后把编辑区内容补回来(展开状态已存 state)
2114
+ }
2115
+ // 保存(全量 patch 合并: settings update 是全量语义, 不能只发单字段)
2116
+ /** 当前会话的覆盖键: "group:<gid>" / "c2c:<openid>"; 未命中(非 QQ 会话) → '' */
2117
+ function curOvKey() {
2118
+ var hit = state.detectedHit
2119
+ return (hit && hit.peerId) ? (hit.scope + ':' + hit.peerId) : ''
2120
+ }
2121
+ function saveLocalModel() {
2122
+ var hint = panel ? panel.querySelector('#dk-lm-hint') : null
2123
+ var onEl = panel ? panel.querySelector('#dk-lm-on') : null
2124
+ var dirEl = panel ? panel.querySelector('#dk-lm-dir') : null
2125
+ var gateEl = panel ? panel.querySelector('input[name="dk-lm-gate"]:checked') : null
2126
+ var minEl = panel ? panel.querySelector('#dk-lm-min') : null
2127
+ var ovKey = curOvKey()
2128
+ // ⚠️ 2026-09-13 会话级(主人要求): 评分模式/门槛/启用 存进 **当前会话** 的覆盖项;
2129
+ // 模型目录是账号级(模型只有一份), 仍写账号默认。
2130
+ if (!ovKey) {
2131
+ if (hint) hint.textContent = '✗ 当前会话不是 QQ 会话 —— 单会话设置需先切到与 bot 的群/私聊(顶栏「当前会话」要命中)'
2132
+ return
2133
+ }
2134
+ var ovNext = {
2135
+ enabled: onEl ? !!onEl.checked : true,
2136
+ valueGate: gateEl ? gateEl.value : 'log',
2137
+ valueMinScore: minEl ? (parseFloat(minEl.value) || 0.5) : 0.5,
2138
+ }
2139
+ if (hint) hint.textContent = '保存中…'
2140
+ fetch(READ + (state.ns ? '?' + outNsQ() : '')).then(function (r) { return r.json() }).then(function (d) {
2141
+ var cur = (d && d.value) || {}
2142
+ var lm = Object.assign({}, cur.localModel || {})
2143
+ if (dirEl) lm.modelDir = String(dirEl.value || '').trim() // 账号级
2144
+ lm.overrides = Object.assign({}, (lm.overrides || {}))
2145
+ lm.overrides[ovKey] = ovNext // 会话级
2146
+ var patch = Object.assign({}, cur, { localModel: lm })
2147
+ return fetch(UPDATE, {
2148
+ method: 'POST', headers: { 'content-type': 'application/json' },
2149
+ body: JSON.stringify({ ns: state.ns || undefined, patch: patch, expectedRevision: d && d.revision }),
2150
+ }).then(function (r2) { return r2.json() })
2151
+ }).then(function (res) {
2152
+ if (res && !res.error) {
2153
+ if (hint) hint.textContent = '已保存 ✓(仅对本会话生效, live)'
2154
+ // 立刻把新值写进本地缓存并渲染(不等接口回读)
2155
+ var lm2 = Object.assign({}, state.lmCfg || {})
2156
+ lm2.overrides = Object.assign({}, (lm2.overrides || {}))
2157
+ lm2.overrides[ovKey] = ovNext
2158
+ if (dirEl) lm2.modelDir = String(dirEl.value || '').trim()
2159
+ state.lmCfg = lm2
2160
+ paintBody()
2161
+ setTimeout(function () { loadLocalModel() }, 2000)
2162
+ } else if (hint) hint.textContent = '保存失败: ' + ((res && res.error) || '未知响应')
2163
+ }).catch(function (e) { if (hint) hint.textContent = '保存异常: ' + e.message })
2164
+ }
2165
+ function bindLocalModel() {
2166
+ if (state.tab !== 'session' || !panel) return
2167
+ var hint = function (t) { var el = panel.querySelector('#dk-lm-hint'); if (el) el.textContent = t }
2168
+ var onEl = panel.querySelector('#dk-lm-on')
2169
+ if (onEl) onEl.onchange = function () { saveLocalModel() }
2170
+ // 评分模式 + 门槛: 改动即自动保存
2171
+ // ⚠️ 2026-09-13 修(主人反馈"低分不唤醒怎么保存不了"): 原来只有"启用"开关绑了 onchange,
2172
+ // 点三个评分模式单选按钮不会保存 → 看着像"选不了"; 现在 radio 与门槛都自动保存。
2173
+ panel.querySelectorAll('input[name="dk-lm-gate"]').forEach(function (r) {
2174
+ r.onchange = function () { saveLocalModel() }
2175
+ })
2176
+ var minInput = panel.querySelector('#dk-lm-min')
2177
+ if (minInput) minInput.onchange = function () { saveLocalModel() }
2178
+ // ↩ 恢复继承: 删掉本会话的覆盖项(2026-09-13 会话级)
2179
+ var resetBtn = panel.querySelector('#dk-lm-reset')
2180
+ if (resetBtn) resetBtn.onclick = function () {
2181
+ var h = panel.querySelector('#dk-lm-hint')
2182
+ var ovKey = curOvKey()
2183
+ if (!ovKey) return
2184
+ if (h) h.textContent = '恢复继承中…'
2185
+ fetch(READ + (state.ns ? '?' + outNsQ() : '')).then(function (r) { return r.json() }).then(function (d) {
2186
+ var cur = (d && d.value) || {}
2187
+ var lm = Object.assign({}, cur.localModel || {})
2188
+ var ovs = Object.assign({}, (lm.overrides || {}))
2189
+ delete ovs[ovKey]
2190
+ lm.overrides = ovs
2191
+ var patch = Object.assign({}, cur, { localModel: lm })
2192
+ return fetch(UPDATE, {
2193
+ method: 'POST', headers: { 'content-type': 'application/json' },
2194
+ body: JSON.stringify({ ns: state.ns || undefined, patch: patch, expectedRevision: d && d.revision }),
2195
+ }).then(function (r2) { return r2.json() })
2196
+ }).then(function (res) {
2197
+ if (res && !res.error) {
2198
+ if (h) h.textContent = '已恢复继承账号默认 ✓'
2199
+ if (state.lmCfg && state.lmCfg.overrides) {
2200
+ var o2 = Object.assign({}, state.lmCfg.overrides)
2201
+ delete o2[ovKey]
2202
+ state.lmCfg = Object.assign({}, state.lmCfg, { overrides: o2 })
2203
+ }
2204
+ paintBody()
2205
+ setTimeout(function () { loadLocalModel() }, 2000)
2206
+ } else if (h) h.textContent = '恢复失败: ' + ((res && res.error) || '未知')
2207
+ }).catch(function (e) { if (h) h.textContent = '异常: ' + e.message })
2208
+ }
2209
+ var saveBtn = panel.querySelector('#dk-lm-save')
2210
+ if (saveBtn) saveBtn.onclick = function () { saveLocalModel() }
2211
+ var chkBtn = panel.querySelector('#dk-lm-check')
2212
+ if (chkBtn) chkBtn.onclick = function () { state.localModel = null; paintBody(); loadLocalModel() }
2213
+ var vsBtn = panel.querySelector('#dk-vs-reload')
2214
+ if (vsBtn) vsBtn.onclick = function () { refreshScoreList() }
2215
+ // ✍️ 样例库编辑区
2216
+ var vsToggle = panel.querySelector('#dk-vs-edit-toggle')
2217
+ if (vsToggle) vsToggle.onclick = function () {
2218
+ var box = panel.querySelector('#dk-vs-editor')
2219
+ if (!box) return
2220
+ state.vsOpen = !state.vsOpen // 存进 state: 重绘后不缩回
2221
+ box.style.display = state.vsOpen ? 'block' : 'none'
2222
+ vsToggle.textContent = state.vsOpen ? '收起' : '展开编辑'
2223
+ if (state.vsOpen) loadValueSamplesEditor()
2224
+ }
2225
+ var vsSaveBtn = panel.querySelector('#dk-vs-save')
2226
+ if (vsSaveBtn) vsSaveBtn.onclick = function () { saveValueSamplesEditor() }
2227
+ // 🤖 让ai写(2026-09-13 主人要的): 模拟一条用户消息唤醒她 → 她自己读聊天记录写样例
2228
+ // 目标 = 面板「当前会话」命中的群/私聊(与群管理页一致); 未命中 QQ 会话则明确提示
2229
+ var aiWriteBtn = panel.querySelector('#dk-vs-aiwrite')
2230
+ if (aiWriteBtn) aiWriteBtn.onclick = function () {
2231
+ var h = panel.querySelector('#dk-vs-hint')
2232
+ var hit = state.detectedHit
2233
+ if (!hit || !hit.peerId) {
2234
+ if (h) h.textContent = '✗ 当前会话不是 QQ 会话(顶栏「当前会话」未命中群/私聊) —— 请先切到与 bot 的群/私聊会话'
2235
+ return
2236
+ }
2237
+ if (h) h.textContent = '发给 AI 中…(' + (hit.scope === 'group' ? '群' : '私聊') + '「' + (hit.name || '') + '」)'
2238
+ fetch('/api/qqbot-settings/value-samples/let-ai-write', {
2239
+ method: 'POST', headers: { 'content-type': 'application/json' },
2240
+ body: JSON.stringify({ ns: hit.ns || state.ns, scope: hit.scope, peerId: hit.peerId }),
2241
+ }).then(function (r) { return r.json() }).then(function (d) {
2242
+ if (h) h.textContent = d && d.ok ? ('✓ ' + d.msg) : ('失败: ' + ((d && d.error) || (d && d.msg) || '未知'))
2243
+ }).catch(function (e) { if (h) h.textContent = '异常: ' + e.message })
2244
+ }
2245
+ // ⚙ 单会话设置: 次级标签切换(🧠 智能回复 / 📄 空白样板)
2246
+ panel.querySelectorAll('[data-sttab]').forEach(function (sb) {
2247
+ sb.onclick = function () {
2248
+ var v = sb.getAttribute('data-sttab')
2249
+ state.stTab = v
2250
+ paintBody()
2251
+ if (v === 'lm') loadLocalModel()
2252
+ }
2253
+ })
2254
+ var dlBtn = panel.querySelector('#dk-lm-dl')
2255
+ if (dlBtn) dlBtn.onclick = function () {
2256
+ dlBtn.disabled = true
2257
+ hint('下载中…(约 23MB, 1~2 分钟, 面板别关)')
2258
+ fetch('/api/qqbot-settings/local-model/download', {
2259
+ method: 'POST', headers: { 'content-type': 'application/json' },
2260
+ body: JSON.stringify({ ns: state.ns }),
2261
+ }).then(function (r) { return r.json() }).then(function (d) {
2262
+ if (d && d.ok) { hint('下载完成 ✓'); state.localModel = null; paintBody(); loadLocalModel() }
2263
+ else { hint('下载失败: ' + ((d && d.error) || JSON.stringify(d))); dlBtn.disabled = false }
2264
+ }).catch(function (e) { hint('下载异常: ' + e.message); dlBtn.disabled = false })
2265
+ }
2266
+ }
1996
2267
  // ⚙️ 出站方式: 读当前账号(ns)配置
1997
2268
  function loadOutMode() {
1998
2269
  if (state.tab !== 'out') return
@@ -2823,6 +3094,16 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2823
3094
  }).join('')
2824
3095
  + '</div>'
2825
3096
  }
3097
+ // ⚙ 单会话设置 · 次级标签栏(2026-09-13 主人要求: 智能回复独立成子标签, 另留一个空标签当样板)
3098
+ function sessionSubTabs() {
3099
+ var st = state.stTab || 'lm'
3100
+ return '<div class="dk-row" style="gap:4px;margin:4px 0 6px;flex-wrap:wrap;border-bottom:1px dashed #e2d9ff;padding-bottom:6px">'
3101
+ + [['lm', '🧠 智能回复'], ['blank', '📄 空白样板']]
3102
+ .map(function (s) {
3103
+ return '<button class="dk-btn' + (st === s[0] ? ' on' : '') + '" data-sttab="' + s[0] + '">' + s[1] + '</button>'
3104
+ }).join('')
3105
+ + '</div>'
3106
+ }
2826
3107
  // 🚀 M3 群发面板(确认草稿 + 任务列表); 「📇群组管理」与「📤群发·广播」共用(2026-09-10 抽函数)
2827
3108
  function broadcastPanel() {
2828
3109
  if (!state.bcDraft && !(state.bcTasks && state.bcTasks.length)) return ''
@@ -2901,6 +3182,7 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2901
3182
  + '<button data-t="mute" class="' + (state.tab === 'mute' ? 'on' : '') + '">🔇 禁言</button>'
2902
3183
  + '<button data-t="out" class="' + (state.tab === 'out' ? 'on' : '') + '">⚙️ 出站</button>'
2903
3184
  + '<button data-t="send" class="' + (['send','broadcast','bp','card'].indexOf(state.tab) >= 0 ? 'on' : '') + '">📤 群发</button>'
3185
+ + '<button data-t="session" class="' + (state.tab === 'session' ? 'on' : '') + '">⚙ 单会话设置</button>'
2904
3186
  + '</div>'
2905
3187
  var body = tabs
2906
3188
  var status = state.msg ? '<div class="dk-msg" style="color:#2f9e44;margin:4px 0">' + esc(state.msg) + '</div>' : ''
@@ -3068,6 +3350,100 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
3068
3350
  body += '详细主动=聊天行为与适配主动完全一样, 但额外把 AI 的工具调用(🔧 工具调用)与工具结果也推到 QQ —— 能在 QQ 上看她正在干什么; 消息会明显变多, 看完记得切回适配主动。'
3069
3351
  body += '被动=始终以「回复你那条」发出, 连发约4~5条后会被QQ吞掉。完全不出站=本机静默, 不向QQ发任何回复(AI 可用工具随时切回)。本开关对纯web(没绑QQ)的会话不生效。</div>'
3070
3352
  body += '<span class="dk-msg" id="dk-out-hint" style="color:#2f9e44;margin:4px 0"></span>'
3353
+ } else if (state.tab === 'session') {
3354
+ // ⚙ 单会话设置(2026-09-13 主人定): 放"每个会话单独生效"的设置。
3355
+ // 次级标签: 🧠 智能回复 / 📄 空白样板(留作以后加新设置的模板)。
3356
+ body += sessionSubTabs()
3357
+ var st = state.stTab || 'lm'
3358
+ if (st === 'blank') {
3359
+ body += '<div style="border:1px dashed #cfc7ee;border-radius:8px;padding:20px 14px;margin:8px 0;text-align:center;line-height:2">'
3360
+ + '<div style="font-size:15px;font-weight:700">📄 空白样板</div>'
3361
+ + '<div style="font-size:12px;color:#888">这块先空着 —— 以后要加"每个会话单独生效"的设置, 照这个子标签加就行。<br>'
3362
+ + '(做法: 加一个子标签值 + 一段渲染分支 + 需要的 host API, 就能长出一个新的子页)</div>'
3363
+ + '</div>'
3364
+ } else {
3365
+ var lm = state.localModel
3366
+ body += '<div class="dk-msg" style="line-height:1.6">让机器人在本机跑一个 23MB 的中文小模型(<b>零 token</b>): 价值评分(她该不该开口) + 表情包语义搜索 —— <b>模型缺失时自动关闭, 不影响任何现有功能</b>。</div>'
3367
+ if (!lm) {
3368
+ body += '<div class="dk-empty">读取中…(若长时间不动, 点「🔄 重新检测」)</div>'
3369
+ } else {
3370
+ var lmMb = lm.bytes ? (Math.round((lm.bytes / 1048576) * 10) / 10) : 0
3371
+ var lmState = lm.available ? ('✅ 已就绪' + (lmMb ? ' · ' + lmMb + ' MB' : '')) : ('⚠️ 未安装(缺 ' + (lm.missing || []).length + ' 个文件) —— 点右边「⬇️ 下载模型」即可')
3372
+ // ⚠️ 2026-09-13 会话级: 评分模式/门槛/启用 **优先取当前会话的覆盖项**(overrides[group:xxx]),
3373
+ // 没有则继承账号级默认; 界面用"继承/已单独设置"标出来。
3374
+ var lmCfg = state.lmCfg || {}
3375
+ var ovKeyV = curOvKey()
3376
+ var ov = (ovKeyV && lmCfg.overrides && lmCfg.overrides[ovKeyV]) || {}
3377
+ var hasOv = !!(typeof ov.enabled === 'boolean' || ov.valueGate || typeof ov.valueMinScore === 'number')
3378
+ var pickGate = function (v) { return (v === 'off' || v === 'log' || v === 'block') ? v : '' }
3379
+ var gateV = pickGate(ov.valueGate) || pickGate(lmCfg.valueGate) || pickGate(lm.valueGate) || 'log'
3380
+ var minV = (typeof ov.valueMinScore === 'number') ? ov.valueMinScore
3381
+ : ((typeof lmCfg.valueMinScore === 'number') ? lmCfg.valueMinScore
3382
+ : (typeof lm.valueMinScore === 'number' ? lm.valueMinScore : 0.5))
3383
+ var onV = (typeof ov.enabled === 'boolean') ? ov.enabled
3384
+ : ((typeof lmCfg.enabled === 'boolean') ? lmCfg.enabled : (lm.enabled !== false))
3385
+ body += '<div class="dk-row" style="font-weight:700;font-size:13px;margin:10px 0 2px">🧠 智能回复(本地小模型) · bge-small-zh-v1.5</div>'
3386
+ // 本会话标识 + 继承状态(让"单会话"名副其实: 一眼看出这套值是谁的)
3387
+ body += '<div class="dk-row" style="gap:8px;align-items:center;flex-wrap:wrap;margin:0 0 4px">'
3388
+ + '<span class="dk-msg" style="font-size:12px;color:#7c6bd6">本会话: ' + esc(curHitLabel()) + '</span>'
3389
+ + '<span class="dk-msg" style="font-size:11px;color:' + (hasOv ? '#2f9e44' : '#999') + '">'
3390
+ + (hasOv ? '✔ 已单独设置' : '(暂用账号默认)') + '</span>'
3391
+ + (hasOv ? '<button class="dk-btn" id="dk-lm-reset" style="font-size:11px;padding:1px 6px" title="删除本会话的单独设置, 恢复继承账号默认">↩ 恢复继承</button>' : '')
3392
+ + '</div>'
3393
+ body += '<div class="dk-row" style="gap:8px;flex-wrap:wrap;align-items:center">'
3394
+ + '<label style="display:inline-flex;align-items:center;gap:4px;font-size:12px;cursor:pointer"><input type="checkbox" id="dk-lm-on"' + (onV ? ' checked' : '') + '> 启用智能回复(省 token)</label>'
3395
+ + '<span class="dk-msg" style="font-size:12px;color:' + (lm.available ? '#2f9e44' : '#c23131') + '">' + esc(lmState) + '</span>'
3396
+ + '</div>'
3397
+ body += '<div class="dk-row" style="gap:6px;margin:4px 0;flex-wrap:wrap">'
3398
+ + '<input class="qqs-inp" id="dk-lm-dir" style="flex:1;min-width:200px" placeholder="模型目录(留空=默认 ~/.dsh/models/bge-small-zh)" value="' + esc(lm.modelDir || '') + '">'
3399
+ + '<button class="dk-btn" id="dk-lm-save">💾 保存</button>'
3400
+ + '<button class="dk-btn" id="dk-lm-check">🔄 重新检测</button>'
3401
+ + (lm.available ? '' : '<button class="dk-btn ok" id="dk-lm-dl">⬇️ 下载模型(自动)</button>')
3402
+ + '</div>'
3403
+ // 评分模式 + 门槛(2026-09-13): off=不评分 / log=只记录(默认观察期) / block=低分不唤醒
3404
+ body += '<div class="dk-row" style="gap:8px;flex-wrap:wrap;align-items:center;margin:6px 0 2px">'
3405
+ + '<span style="font-size:12px;color:#666">评分模式:</span>'
3406
+ + [['off', '不评分'], ['log', '只记录(先观察)'], ['block', '低分不唤醒']].map(function (m2) {
3407
+ return '<label style="display:inline-flex;align-items:center;gap:4px;font-size:12px;cursor:pointer"><input type="radio" name="dk-lm-gate" value="' + m2[0] + '"' + (gateV === m2[0] ? ' checked' : '') + '> ' + m2[1] + '</label>'
3408
+ }).join('')
3409
+ + '<span style="font-size:12px;color:#666">门槛:</span>'
3410
+ + '<input class="qqs-inp" id="dk-lm-min" type="number" step="0.01" min="0" max="1" style="width:78px" value="' + minV + '">'
3411
+ + '</div>'
3412
+ body += '<span class="dk-msg" id="dk-lm-hint" style="color:#2f9e44;margin:2px 0"></span>'
3413
+ // 📊 最近评分(观察期): 让主人直接看到"她是怎么被叫醒的"
3414
+ // 2026-09-13 二次修: 列表抽成 scoreListHtml + 独立 div, 由 refreshScoreList 做**局部刷新**
3415
+ // (只换这个 div 的内容, 不整块 paintBody) → 既不闪, 又能自动更新。
3416
+ body += '<div class="dk-row" style="font-weight:700;font-size:13px;margin:10px 0 2px;gap:8px;align-items:center;flex-wrap:wrap">'
3417
+ + '<span>📊 最近评分(观察期)</span>'
3418
+ + '<span class="dk-msg" style="font-size:11px;color:#7c6bd6" id="dk-vs-scope">' + esc(curHitLabel()) + '</span>'
3419
+ + '<button class="dk-btn" id="dk-vs-reload" style="font-size:12px;padding:1px 8px">🔄 刷新</button>'
3420
+ + '<span class="dk-msg" style="font-size:11px;color:#999">每 10 秒自动更新</span>'
3421
+ + '</div>'
3422
+ body += '<div id="dk-vs-box">' + scoreListHtml(state.valueScores) + '</div>'
3423
+ // ✍️ 样例库编辑(2026-09-13 主人问"哪里写样本"): 直接改 jsonl, 保存后下一条消息生效
3424
+ body += '<div class="dk-row" style="font-weight:700;font-size:13px;margin:12px 0 2px;gap:8px;align-items:center;flex-wrap:wrap">'
3425
+ + '<span>✍️ 样例库(决定她的开口标准)</span>'
3426
+ + '<button class="dk-btn" id="dk-vs-edit-toggle" style="font-size:12px;padding:1px 8px">' + (state.vsOpen ? '收起' : '展开编辑') + '</button>'
3427
+ + '<button class="dk-btn ok" id="dk-vs-aiwrite" style="font-size:12px;padding:1px 8px" title="模拟一条用户消息唤醒她, 让她自己读聊天记录写样例(样例库位置+写法一起发过去)">🤖 让ai写(有聊天记录最好)</button>'
3428
+ + '<span class="dk-msg" style="font-size:11px;color:#999" id="dk-vs-stat"></span>'
3429
+ + '</div>'
3430
+ body += '<div id="dk-vs-editor" style="display:' + (state.vsOpen ? 'block' : 'none') + '">'
3431
+ + '<textarea class="qqs-area" id="dk-vs-text" style="width:100%;min-height:170px;font-family:monospace;font-size:11px" placeholder=\'一行一条 JSON: {"m":"群消息文本","y":1} (y=1 她会想接话 / y=0 她不会理)\'></textarea>'
3432
+ + '<div class="dk-row" style="gap:6px;margin-top:4px;align-items:center;flex-wrap:wrap">'
3433
+ + '<button class="dk-btn ok" id="dk-vs-save">💾 保存样例库</button>'
3434
+ + '<span class="dk-msg" id="dk-vs-hint" style="color:#2f9e44"></span>'
3435
+ + '</div>'
3436
+ + '<div class="dk-msg" style="font-size:11px;color:#888;line-height:1.6">一行一条 JSON。保存后<b>下一条群消息</b>就按新样例判断(自动重载, 约 0.3 秒)。'
3437
+ + '想让她自己学 —— 群里说一句「<b>从最近的聊天记录总结一批样本</b>」, 她会读真实聊天记录提炼后写进来 ✧</div>'
3438
+ + '</div>'
3439
+ body += '<div class="dk-msg" style="font-size:12px;color:#888;line-height:1.6">'
3440
+ + (lm.available
3441
+ ? '模型加载是懒加载: 第一次用到(有群消息需要评分)才载入, 约 0.3 秒, 之后常驻内存(~150MB)。'
3442
+ : '点「⬇️ 下载模型」自动拉取(约 23MB, hf-mirror 优先/官方兜底, 1~2 分钟); 也可以自己下载后放进上面目录: '
3443
+ + '<code>onnx/model_quantized.onnx</code> · <code>tokenizer.json</code> · <code>tokenizer_config.json</code> · <code>config.json</code>')
3444
+ + '</div>'
3445
+ }
3446
+ }
3071
3447
  } else if (state.tab === 'card') {
3072
3448
  // 📝 自定义 markdown 卡片(2026-09-10): 源码 + 实时预览 + 按钮配置 → 发送到选中群/私聊
3073
3449
  body += sendSubTabs()
@@ -3192,6 +3568,7 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
3192
3568
  // 📝 卡片: 必须在 paintBody 内绑定(2026-09-10 修复: 原在 tab 点击处先 bind 后 paintBody,
3193
3569
  // 绑的是即将被替换的旧 DOM → 发送/存为事件无反应、预览不渲染)
3194
3570
  if (state.tab === 'card') bindCardEvents()
3571
+ if (state.tab === 'session') bindLocalModel() // ⚠️ 只绑定: 加载由切 tab 时触发(防重绘循环)
3195
3572
  // 📤 广播子页: 绑定群发按钮 + 刷新任务列表(任务列表静默拉取, 不重绘 → 防死循环)
3196
3573
  if (state.tab === 'broadcast') { bindBroadcastEvents(); loadBroadcastTasks(false, true); startBroadcastAdvance() }
3197
3574
  // 分组(host 持久化, 与 AI 共用): 进选目标/广播页时拉一次, 保证多浏览器与 AI 侧看到的是同一份
@@ -3208,6 +3585,7 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
3208
3585
  state.tab = btn.getAttribute('data-t'); state.msg = ''
3209
3586
  if (state.tab === 'join') loadJoins(); else if (state.tab === 'mute') loadMutes()
3210
3587
  else if (state.tab === 'out') loadOutMode()
3588
+ else if (state.tab === 'session') { loadLocalModel(); bindLocalModel() }
3211
3589
  else if (state.tab === 'bp') loadBotplay()
3212
3590
  else if (state.tab === 'chat') { state.chatItems = []; state.chatErr = ''; }
3213
3591
  paintBody()
@@ -3625,6 +4003,33 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
3625
4003
  return '<a class="dk-link" href="' + u + '" target="_blank" rel="noopener noreferrer" title="' + u + '">' + u + '</a>' + trail
3626
4004
  })
3627
4005
  }
4006
+ // ── 媒体地址归一(2026-09-13 主人要求: dock 聊天界面图片要**同时兼容 url 与本地路径/图库链接**) ──
4007
+ // 入站消息的图片已改为给**本地路径**(省 token、看图不用再下载), 浏览器直接拿 D:\… 当然加载不出来,
4008
+ // 于是统一转成同源直出 URL: /api/qqbot-settings/chat/raw-media?p=<本机路径>。
4009
+ // http(s) / 已是 /api/... 的(含 sticker-img?id=xxx 图库链接) 一律原样。
4010
+ function chatMediaURL(u) {
4011
+ var s = String(u || '').trim()
4012
+ if (!s) return s
4013
+ if (/^(https?:|\/\/|\/api\/|data:|blob:)/i.test(s)) return s
4014
+ if (/^[A-Za-z]:[\\/]/.test(s) || /^\\\\/.test(s) || s.charAt(0) === '/') {
4015
+ return '/api/qqbot-settings/chat/raw-media?p=' + encodeURIComponent(s)
4016
+ }
4017
+ return s
4018
+ }
4019
+ // 图库兜底: 消息里记的本地路径是"当时"的(candidate→library 搬层后就失效),
4020
+ // 但文件名正是图库 id(12 位 sha1) → 加载失败时再用 sticker-img?id= 试一次(按 id 解析, 与层无关)
4021
+ function chatStickerFallback(src) {
4022
+ var s = String(src || '')
4023
+ if (s.indexOf('/chat/raw-media?p=') >= 0) {
4024
+ try { s = decodeURIComponent(s.split('p=')[1] || '') } catch (e) { /* 保持原样 */ }
4025
+ }
4026
+ var base = s.replace(/[?#].*$/, '').split(/[\\/]/).pop() || ''
4027
+ var idm = base.match(/^([0-9a-f]{12})\.([a-z0-9]{2,5})$/i)
4028
+ if (!idm) return ''
4029
+ var dd = ''
4030
+ try { dd = curDataDir() || '' } catch (e) { dd = '' }
4031
+ return '/api/qqbot-settings/sticker-img?id=' + encodeURIComponent(idm[1]) + (dd ? '&dataDir=' + encodeURIComponent(dd) : '')
4032
+ }
3628
4033
  function renderChatList(keepScrollOffset) {
3629
4034
  var box = document.getElementById('dk-chat-box')
3630
4035
  if (!box) return
@@ -3655,10 +4060,17 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
3655
4060
  var u = typeof m === 'string' ? m : (m && m.url)
3656
4061
  var k = typeof m === 'string' ? 'image' : ((m && m.kind) || 'image')
3657
4062
  if (!u) return ''
4063
+ u = chatMediaURL(u) // 本机路径 → 同源直出 URL(兼容 url / 本地路径 / 图库链接)
3658
4064
  if (k === 'file') return '<a class="dk-file" href="' + esc(u) + '" target="_blank" rel="noopener" title="' + esc(u) + '">📎 ' + esc((m && m.name) || '文件') + '</a>'
3659
4065
  if (k === 'voice') return '<audio class="dk-audio" controls preload="metadata" src="' + esc(u) + '"></audio>'
3660
4066
  if (k === 'video') return '<video class="dk-video" controls preload="metadata" src="' + esc(u) + '"></video>'
3661
- 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>\'">'
4067
+ // 兜底链(按序试): ① 后端给的 QQ 链接(台账兜底, 本地文件被清了也能显示)
4068
+ // ② 图库 id 链接 sticker-img?id=(路径搬层后的旧路径救不回时用)
4069
+ var fbs = []
4070
+ if (m && m.fb && String(m.fb) !== String(u)) fbs.push(String(m.fb))
4071
+ var sf = chatStickerFallback(m, u)
4072
+ if (sf && sf !== u && fbs.indexOf(sf) < 0) fbs.push(sf)
4073
+ return '<img class="dk-img" loading="lazy" data-lb="' + esc(u) + '"' + (fbs.length ? ' data-fb="' + esc(JSON.stringify(fbs)) + '"' : '') + ' src="' + esc(u) + '" referrerpolicy="no-referrer" alt="[图片]">'
3662
4074
  }).join('')
3663
4075
  var txt = it.text ? chatRenderText(it.text) : ''
3664
4076
  return '<div class="dk-crow ' + (isOut ? 'out' : 'in') + '">'
@@ -3673,9 +4085,22 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
3673
4085
  box.innerHTML = html
3674
4086
  // 图片: 单击/右键 → 灯箱放大(右键不再弹浏览器菜单, 避免误触发 onerror 变加载失败)
3675
4087
  box.querySelectorAll('img.dk-img').forEach(function (img) {
3676
- var lb = img.getAttribute('data-lb') || img.src
3677
- img.onclick = function (e) { e.preventDefault(); e.stopPropagation(); openLightbox(lb) }
3678
- img.oncontextmenu = function (e) { e.preventDefault(); e.stopPropagation(); openLightbox(lb) }
4088
+ var lbOf = function () { return img.getAttribute('data-lb') || img.src }
4089
+ img.onclick = function (e) { e.preventDefault(); e.stopPropagation(); openLightbox(lbOf()) }
4090
+ img.oncontextmenu = function (e) { e.preventDefault(); e.stopPropagation(); openLightbox(lbOf()) }
4091
+ // 加载失败 → 按兜底链依次重试(QQ 链接 → 图库 id 链接) ② 全都失败才灰字提示
4092
+ img.onerror = function () {
4093
+ var list = []
4094
+ try { list = JSON.parse(img.getAttribute('data-fb') || '[]') || [] } catch (e) { list = [] }
4095
+ var i = Number(img.getAttribute('data-fbi') || '0')
4096
+ if (Array.isArray(list) && i < list.length) {
4097
+ img.setAttribute('data-fbi', String(i + 1))
4098
+ img.setAttribute('data-lb', String(list[i]))
4099
+ img.src = String(list[i])
4100
+ return
4101
+ }
4102
+ img.outerHTML = '<span style="color:#888">[图加载失败]</span>'
4103
+ }
3679
4104
  })
3680
4105
  // 滚动: 刷新/切目标 → 滚到底; 上滚加载更早 → 保持视口
3681
4106
  if (keepScrollOffset == null) box.scrollTop = box.scrollHeight
@@ -11,4 +11,19 @@ export declare const inject: string[];
11
11
  export declare function setChannelBridge(b: QQChannel | undefined): void;
12
12
  /** 装载本插件时把 qqChannel 一并注入(由 dsh-qqbot setup 提供) */
13
13
  export declare function apply(ctx: Context): Promise<void>;
14
+ /**
15
+ * host 面板用(2026-09-13 主人要的「让ai写」按钮):
16
+ * **照抄定时任务的做法** —— 伪造一条消息直接调 handleInbound 唤醒该群/私聊的 AI 回合
17
+ * (见 features/scheduler.ts fireTask: 与 SDK 消息同形, messageId 空 → 出站自动走主动推送;
18
+ * senderName 用中性名、不打假 (@you), 防污染主人交互记忆)。
19
+ *
20
+ * 为什么不复用高层轮子: wakeSessionAgent 要先配"群组管理器会话"; injectSynthetic 是塞聚合窗口(要等人停口);
21
+ * 而面板按钮要的是**立刻触发** —— 与"定时任务到点必须触发回合"同理, 直调 handleInbound 最贴合。
22
+ *
23
+ * host 半边(settings-host.js) 通过 import('@zaofan/dsh-qqbot/channel-tools') 调用(manager 只在插件侧)。
24
+ */
25
+ export declare function askAiToWriteSamples(ns: string, scope: 'group' | 'c2c', peerId: string): Promise<{
26
+ ok: boolean;
27
+ msg: string;
28
+ }>;
14
29
  //# 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;AAGnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAwElE,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;AAkFD,mDAAmD;AACnD,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAivDvD"}
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;AAGnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAyElE,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;AAkFD,mDAAmD;AACnD,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAivDvD;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,OAAO,GAAG,KAAK,EACtB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAwCvC"}
@@ -21,6 +21,7 @@ import { readGroupMembers } from './features/chat-ledger.js';
21
21
  import * as broadcastQueue from './features/broadcast.js';
22
22
  import { wakeSessionAgent, safeAppendUserMessage } from './features/group-hub.js';
23
23
  import { managersOf, findManagerByPeer, findManagerBySessionId } from './features/session-registry.js';
24
+ import { handleInbound } from './transport/inbound.js';
24
25
  /** 诊断日志路径: 默认关闭; 需要排查时设环境变量 QQBOT_DIAG_FILE 指向日志文件 */
25
26
  const DIAG_FILE = process.env.QQBOT_DIAG_FILE || '';
26
27
  /** 大文件异步发送阈值: 本地文件 >= 5MB 时走后台任务(SDK 分片上传耗时, 避免阻塞 LLM 回合) */
@@ -455,7 +456,7 @@ export async function apply(ctx) {
455
456
  created = r.status === 'new';
456
457
  }
457
458
  else if (existsSync(sticker)) {
458
- const r = store.importLocalFile(sticker);
459
+ const r = await store.importLocalFile(sticker); // 2026-09-13 起 async(内部要算感知哈希 dHash)
459
460
  if (r.status === 'error')
460
461
  return { ok: false, msg: `导入失败: ${r.error}` };
461
462
  id = r.id;
@@ -2143,4 +2144,61 @@ export async function apply(ctx) {
2143
2144
  extDiag(`加载异常: ${err instanceof Error ? err.message : String(err)}`);
2144
2145
  }
2145
2146
  }
2147
+ /**
2148
+ * host 面板用(2026-09-13 主人要的「让ai写」按钮):
2149
+ * **照抄定时任务的做法** —— 伪造一条消息直接调 handleInbound 唤醒该群/私聊的 AI 回合
2150
+ * (见 features/scheduler.ts fireTask: 与 SDK 消息同形, messageId 空 → 出站自动走主动推送;
2151
+ * senderName 用中性名、不打假 (@you), 防污染主人交互记忆)。
2152
+ *
2153
+ * 为什么不复用高层轮子: wakeSessionAgent 要先配"群组管理器会话"; injectSynthetic 是塞聚合窗口(要等人停口);
2154
+ * 而面板按钮要的是**立刻触发** —— 与"定时任务到点必须触发回合"同理, 直调 handleInbound 最贴合。
2155
+ *
2156
+ * host 半边(settings-host.js) 通过 import('@zaofan/dsh-qqbot/channel-tools') 调用(manager 只在插件侧)。
2157
+ */
2158
+ export async function askAiToWriteSamples(ns, scope, peerId) {
2159
+ try {
2160
+ const managers = managersOf();
2161
+ if (managers.length === 0)
2162
+ return { ok: false, msg: '还没有活跃的机器人实例' };
2163
+ const nsOf = (m) => String(m?.settingsNs ?? '');
2164
+ const mgr = managers.find((m) => nsOf(m) === ns) ?? managers[0];
2165
+ if (!mgr)
2166
+ return { ok: false, msg: '找不到该机器人实例' };
2167
+ if (!peerId)
2168
+ return { ok: false, msg: '请先在面板上方选一个群/私聊目标' };
2169
+ const cfg = mgr.config;
2170
+ if (!cfg)
2171
+ return { ok: false, msg: '该实例配置未就绪' };
2172
+ const root = mgr.dataRoot || '';
2173
+ const prompt = [
2174
+ '【样例库维护】主人点了面板上的「让ai写(有聊天记录最好)」, 请你来维护本地小模型的"开口标准"样例库。',
2175
+ `· 样例库(你要写的): ${root}\\.qqbot\\value-samples.jsonl —— 一行一条 {"m":"群消息文本","y":1} / y=0; 1=你会想接话, 0=你不会理`,
2176
+ `· 评分记录(你来读): ${root}\\.qqbot\\value-scores.jsonl —— 一行一条 JSON, 字段: text=消息原文, score=本地小模型给的分, worth=是否判值得接, min=本会话门槛, gate=当时生效的模式(block=低分不唤醒/log=只记录不拦), conf=置信度(越接近1越有把握), mention=是否被@, top=最近邻样例, agg=聚合条数`,
2177
+ '做法: ①读评分记录最近 100~200 条(用文件工具) ②挑出标注可疑的 —— 尤其 conf<0.5(地图上没这类样本)、mention=true 却 worth=false(被点名却没打算回)、内容与分数明显不符的',
2178
+ '③对照真人群聊语境, 提炼 20~40 条新样例 ④用文件工具把它们**追加**写入样例库(不要覆盖整库) ⑤回报加了几条、都补了哪类。',
2179
+ '样例写法(一行一条 JSON): {"m":"消息文本","y":1或0} —— y=1 她会想接话(提问/求助/@她/她接得住的梗) / y=0 她不会理(群友互聊/短应答/表情)。',
2180
+ ].join('\n');
2181
+ const now = new Date();
2182
+ const pad = (n) => String(n).padStart(2, '0');
2183
+ const ts = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
2184
+ const fakeMsg = {
2185
+ kind: scope,
2186
+ senderId: scope === 'c2c' ? peerId : 'master',
2187
+ senderName: '样例库维护',
2188
+ content: `[面板任务 ${ts}] ${prompt}`,
2189
+ messageId: '',
2190
+ timestamp: now.toISOString(),
2191
+ groupOpenid: scope === 'group' ? peerId : undefined,
2192
+ msgType: 0,
2193
+ attachments: undefined,
2194
+ };
2195
+ const noop = () => { };
2196
+ const logger = { info: noop, warn: noop, debug: noop, error: noop };
2197
+ await handleInbound(fakeMsg, mgr, cfg, logger, undefined);
2198
+ return { ok: true, msg: `已伪造一条消息唤醒 ${scope === 'group' ? '群' : '私聊'} …${peerId.slice(-6)} 的 AI 回合, 稍等她回复 ✧` };
2199
+ }
2200
+ catch (e) {
2201
+ return { ok: false, msg: `异常: ${e instanceof Error ? e.message : String(e)}` };
2202
+ }
2203
+ }
2146
2204
  //# sourceMappingURL=channel-tools.js.map