@zaofan/dsh-qqbot 1.10.47 → 1.11.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.
Files changed (45) hide show
  1. package/README.md +40 -3
  2. package/client/qqbot-settings.js +694 -12
  3. package/dist/features/ext-capabilities.d.ts +2 -0
  4. package/dist/features/ext-capabilities.d.ts.map +1 -1
  5. package/dist/features/ext-capabilities.js +38 -0
  6. package/dist/features/ext-capabilities.js.map +1 -1
  7. package/dist/features/ext-reload.d.ts +2 -2
  8. package/dist/features/ext-reload.d.ts.map +1 -1
  9. package/dist/features/ext-reload.js +30 -8
  10. package/dist/features/ext-reload.js.map +1 -1
  11. package/dist/features/ext-ui-seed.d.ts +25 -0
  12. package/dist/features/ext-ui-seed.d.ts.map +1 -0
  13. package/dist/features/ext-ui-seed.js +187 -0
  14. package/dist/features/ext-ui-seed.js.map +1 -0
  15. package/dist/features/ext-ui.d.ts +42 -0
  16. package/dist/features/ext-ui.d.ts.map +1 -0
  17. package/dist/features/ext-ui.js +207 -0
  18. package/dist/features/ext-ui.js.map +1 -0
  19. package/dist/features/extension-store.d.ts.map +1 -1
  20. package/dist/features/extension-store.js +3 -0
  21. package/dist/features/extension-store.js.map +1 -1
  22. package/dist/gateway/bootstrap.d.ts.map +1 -1
  23. package/dist/gateway/bootstrap.js +18 -1
  24. package/dist/gateway/bootstrap.js.map +1 -1
  25. package/dist/gateway/middleware-setup.d.ts.map +1 -1
  26. package/dist/gateway/middleware-setup.js +11 -1
  27. package/dist/gateway/middleware-setup.js.map +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +131 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/session/session-manager.d.ts.map +1 -1
  32. package/dist/session/session-manager.js +2 -1
  33. package/dist/session/session-manager.js.map +1 -1
  34. package/docs/extension-contract.md +400 -255
  35. package/examples/ext-commands//346/211/251/345/261/225/347/244/272/344/276/213.mjs +84 -0
  36. package/examples/ext-hooks/demo-store.mjs +75 -0
  37. package/examples/ext-ui/README.md +95 -0
  38. package/examples/ext-ui/demo/dock/card.css +55 -0
  39. package/examples/ext-ui/demo/dock/card.html +95 -0
  40. package/examples/ext-ui/demo/dock/card.js +261 -0
  41. package/examples/ext-ui/demo/settings/card.css +61 -0
  42. package/examples/ext-ui/demo/settings/card.html +38 -0
  43. package/examples/ext-ui/demo/settings/card.js +87 -0
  44. package/package.json +145 -144
  45. package/settings-host.js +585 -4
@@ -576,6 +576,467 @@ window.__ModuleLoader__.load({
576
576
  return h('div', null, body, h(HintBlock, { text: f.hint }))
577
577
  }
578
578
 
579
+ // ══ 扩展 UI(2026-10-26)════════════════════════════════════════════════
580
+ // 为什么要提到模块顶层:ExtCardPage 是给 QqbotHome 用的(顶层 tab 切换),
581
+ // 原来错写在 SettingsPage 内部 → QqbotHome 里 ReferenceError: ExtCardPage is not defined,
582
+ // 整页白屏(2026-10-26 主人实测)。
583
+ // 这里自包含实现,不依赖 SettingsPage 内部的 extUiText / injectExtUiCss。
584
+ var EXT_UI_CSS_ID = 'qqs-ext-css'
585
+ function extUiEnsureCss() {
586
+ try {
587
+ if (document.getElementById(EXT_UI_CSS_ID)) return
588
+ var s = document.createElement('style')
589
+ s.id = EXT_UI_CSS_ID
590
+ s.textContent = ''
591
+ + '.qqs-ext-cards{display:flex;gap:10px;overflow-x:auto;overflow-y:hidden;'
592
+ + '-webkit-overflow-scrolling:touch;scrollbar-width:none;'
593
+ + 'scroll-snap-type:x proximity;padding:2px 0 6px}'
594
+ + '.qqs-ext-cards::-webkit-scrollbar{display:none}'
595
+ + '.qqs-ext-card{flex:0 0 auto;scroll-snap-align:start;min-width:220px;max-width:320px}'
596
+ + '.qqs-top-tabs{scrollbar-width:none}'
597
+ + '.qqs-top-tabs::-webkit-scrollbar{display:none}'
598
+ + '.qqs-ext-page-card{display:block;min-width:260px}'
599
+ document.head.appendChild(s)
600
+ // 鼠标滚轮 → 横向滚动(2026-10-26 主人实测"不能横滑"):
601
+ // 容器 overflow-x:auto 在触摸屏上能滑,但**桌面上普通滚轮是纵向的**,
602
+ // 浏览器不会自动转成横滚,所以看着"溢出却不动"。这里做一次事件委托,
603
+ // .qqs-top-tabs / .qqs-ext-cards 上把纵向滚轮折算成 scrollLeft。
604
+ if (!window.__qqsExtWheelBound) {
605
+ window.__qqsExtWheelBound = true
606
+ // 哪些容器算"可横滑条":设置页顶栏、扩展卡片条、dock 页签(★ 2026-10-26 补 dk-tab)
607
+ var isScrollBar = function (el) {
608
+ if (!el || !el.classList) return false
609
+ return el === window.__qqsTopTabs
610
+ || el.id === 'qqs-top-tabs'
611
+ || el.classList.contains('qqs-top-tabs')
612
+ || el.classList.contains('qqs-ext-cards')
613
+ || el.classList.contains('dk-tab')
614
+ }
615
+ document.addEventListener('wheel', function (e) {
616
+ var el = e.target
617
+ while (el) {
618
+ if (isScrollBar(el)) {
619
+ if (el.scrollWidth > el.clientWidth + 1 && Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
620
+ el.scrollLeft += e.deltaY
621
+ e.preventDefault()
622
+ }
623
+ return
624
+ }
625
+ el = el.parentNode
626
+ }
627
+ }, { passive: false })
628
+
629
+ // ★ 鼠标按住拖拽(桌面端最直觉的滑动方式)。
630
+ // dock 页签是字符串拼的 HTML、拿不到 ref,所以用**全局事件委托**:
631
+ // mousedown 判断落点是否在可横滑容器里,是就开始记起点。
632
+ var dragEl = null, dragX = 0, dragLeft = 0, dragMoved = false
633
+ document.addEventListener('mousedown', function (e) {
634
+ if (e.button !== 0) return
635
+ var el = e.target
636
+ while (el) {
637
+ if (isScrollBar(el) && el.scrollWidth > el.clientWidth + 1) {
638
+ dragEl = el; dragX = e.pageX; dragLeft = el.scrollLeft; dragMoved = false
639
+ el.style.cursor = 'grab'
640
+ return
641
+ }
642
+ el = el.parentNode
643
+ }
644
+ })
645
+ document.addEventListener('mousemove', function (e) {
646
+ if (!dragEl) return
647
+ var dx = e.pageX - dragX
648
+ if (Math.abs(dx) > 4) {
649
+ dragMoved = true
650
+ dragEl.style.cursor = 'grabbing'
651
+ dragEl.scrollLeft = dragLeft - dx
652
+ e.preventDefault()
653
+ }
654
+ })
655
+ document.addEventListener('mouseup', function () {
656
+ if (!dragEl) return
657
+ try { dragEl.style.cursor = '' } catch (x) { /* ignore */ }
658
+ var d = dragEl
659
+ setTimeout(function () { if (dragEl === d) dragEl = null }, 0)
660
+ // ★ 2026-10-27 修:dragMoved 必须清 —— 之前只清 dragEl、没清 dragMoved,
661
+ // 结果是「拖过一次之后所有 click 都被吞」:点按钮没反应、页签切不动,
662
+ // 看起来像"路由到别处了"(主人实测就是这个现象)。
663
+ // 留 120ms 给紧随其后的 click 消费,然后强制归零。
664
+ setTimeout(function () { dragMoved = false }, 120)
665
+ })
666
+ // 拖完拦截一次 click,避免"想滑动却切走了页签"
667
+ document.addEventListener('click', function (e) {
668
+ if (dragMoved) { dragMoved = false; e.stopPropagation(); e.preventDefault() }
669
+ }, true)
670
+ }
671
+ } catch (e) { /* ignore */ }
672
+ }
673
+ function extUiLoad(extId, f, slot) {
674
+ // slot 用于区分「同一个扩展 id 在 profile 级 / 数据根各有一张卡」的情况(见 ext-ui.ts 注释)
675
+ var u = '/api/qqbot-settings/ext-ui/file?f=' + encodeURIComponent(f) + '&id=' + encodeURIComponent(extId)
676
+ if (slot) u += '&slot=' + encodeURIComponent(slot)
677
+ return fetch(u)
678
+ .then(function (r) { return r.json() })
679
+ .then(function (j) { return j && j.ok ? String(j.text || '') : '' })
680
+ .catch(function () { return '' })
681
+ }
682
+ /**
683
+ * 给卡片脚本的统一 API(2026-10-26)
684
+ *
685
+ * @param extId 扩展 id
686
+ * @param cfgScope 'profile'(设置页卡片:全局一份)| 'data'(dock 卡片:每实例一份)
687
+ * @param dockCtx dock 卡片才有:当前实例/会话上下文(settings 卡片传 null)
688
+ *
689
+ * dock 卡片的独特之处就是**能看到当前实例和会话的真实数据**,所以这里额外给:
690
+ * dock.context() 当前实例 + 会话信息(ns/appId/dataRoot/peer/群名)
691
+ * dock.sessions() 这个机器人管过哪些会话
692
+ * dock.history(n) 当前会话最近 n 条消息
693
+ * dock.members() 群友台账(谁说过话、什么印象分)
694
+ * 这些走 /api/qqbot-settings/dock-data,scope=当前数据根(每实例一份,天然隔离)。
695
+ */
696
+ /** 读当前 web 会话 id(宿主的 localStorage,和「当前会话」检测同一个源) */
697
+ function extUiCurrentSid() {
698
+ try {
699
+ var raw = localStorage.getItem('dsh.sessions.current')
700
+ if (!raw) return ''
701
+ var o = JSON.parse(raw)
702
+ return (o && o.sessionId) ? String(o.sessionId) : ''
703
+ } catch (e) { return '' }
704
+ }
705
+ function extUiApi(extId, cfgScope, dockCtx, acctCtx) {
706
+ var scope = cfgScope === 'profile' ? 'profile' : 'data'
707
+ // ★ 2026-10-27(主人要求):设置页的卡片虽然是"全局 UI",也能读写**当前选中账号**。
708
+ // getConfig('data') / setConfig(p, 'data') 会落到上面账号下拉框选中的那个实例;
709
+ // 传 sc='profile' 仍是全局那份。第三个参数可临时指定某个账号(不传=当前选中的)。
710
+ var acctCwdOf = function (a) {
711
+ if (!a || typeof a !== 'object') return ''
712
+ return String(a.cwd || '').trim() || (a.dataRoot ? String(a.dataRoot).replace(/[\/]dshqqbot[\/]*$/, '') : '')
713
+ }
714
+ var api = {
715
+ extId: extId,
716
+ /** 当前卡片上下文里的账号(设置页=下拉框选中的;dock=当前实例) */
717
+ acct: acctCtx || (dockCtx ? { ns: dockCtx.ns, cwd: dockCtx.cwd, dataRoot: dockCtx.dataRoot } : null),
718
+ getConfig: function (s, acctOverride) {
719
+ var sc = s === 'profile' || s === 'data' ? s : scope
720
+ var a = acctOverride || acctCtx
721
+ var u = '/api/qqbot-settings/ext-config?id=' + encodeURIComponent(extId) + '&scope=' + sc
722
+ var cw = acctCwdOf(a)
723
+ if (sc === 'data' && cw) u += '&cwd=' + encodeURIComponent(cw)
724
+ return fetch(u)
725
+ .then(function (r) { return r.json() })
726
+ .then(function (j) { return (j && j.ok && j.config) || {} })
727
+ .catch(function () { return {} })
728
+ },
729
+ setConfig: function (patch, s, acctOverride) {
730
+ var sc = s === 'profile' || s === 'data' ? s : scope
731
+ var a = acctOverride || acctCtx
732
+ var cw = acctCwdOf(a)
733
+ return fetch('/api/qqbot-settings/ext-config', {
734
+ method: 'POST',
735
+ headers: { 'Content-Type': 'application/json' },
736
+ body: JSON.stringify({ id: extId, scope: sc, patch: patch || {}, cwd: sc === 'data' ? cw : '' }),
737
+ }).then(function (r) { return r.json() }).catch(function () { return null })
738
+ },
739
+ api: function (p, opts) {
740
+ // ★ 2026-10-26:改走统一 dispatch 出口 —— 扩展用 env.registerRoute() 注册的
741
+ // 自有存储接口都从这里进(settings-host 的 /api/ext/dispatch 派发)。
742
+ // ★ 2026-10-27:dock 卡片额外带 dataRoot —— hooks 是「插件启动时跑一次」,
743
+ // 其 env.dataRoot 是**插件级**的(主实例那份),不是当前机器人实例的。
744
+ // 所以数据根必须随请求传,让 handler 用 query.dataRoot 自己算路径。
745
+ var u = '/api/ext/dispatch?id=' + encodeURIComponent(extId) + '&p=' + encodeURIComponent(String(p || '/'))
746
+ if (dockCtx && dockCtx.dataRoot) u += '&dataRoot=' + encodeURIComponent(dockCtx.dataRoot)
747
+ return fetch(u, Object.assign({ headers: { 'Content-Type': 'application/json' } }, opts || {}))
748
+ .then(function (r) { return r.json() })
749
+ .catch(function () { return null })
750
+ },
751
+ toast: function (m) { try { window.alert(String(m)) } catch (e) { /* ignore */ } },
752
+ /**
753
+ * 能力调用(2026-10-27 主人要求:UI 卡片要和其余扩展对齐能力)
754
+ *
755
+ * 卡片跑在浏览器里,但 store / kernel / log 这些都在服务端 ——
756
+ * 所以走 RPC:cap('store.load') → /api/ext/dispatch?p=/__cap&fn=store.load
757
+ * 后端有白名单(EXT_CAP_ALLOW),只有列出来的能用。
758
+ *
759
+ * 白名单:store.load / store.save / appId / owners /
760
+ * kernel.get / kernel.has / log / text / markdown / appendSilent
761
+ */
762
+ cap: function (fn) {
763
+ var args = Array.prototype.slice.call(arguments, 1);
764
+ var u = '/api/ext/dispatch?id=' + encodeURIComponent(extId)
765
+ + '&p=' + encodeURIComponent('/__cap')
766
+ + '&fn=' + encodeURIComponent(String(fn || ''))
767
+ + '&args=' + encodeURIComponent(JSON.stringify(args))
768
+ if (dockCtx && dockCtx.dataRoot) u += '&dataRoot=' + encodeURIComponent(dockCtx.dataRoot)
769
+ if (dockCtx && dockCtx.cwd) u += '&cwd=' + encodeURIComponent(dockCtx.cwd)
770
+ if (acctCtx && acctCtx.cwd) u += '&cwd=' + encodeURIComponent(acctCtx.cwd)
771
+ // ★ 2026-10-27 修:peer/ns **必须实时读**,不能用挂载时的快照 ——
772
+ // 卡片挂载时「当前会话」检测还没跑完(异步),那会儿 detectedHit 是 null,
773
+ // 于是 appendSilent 拿到空 peer,永远注入失败。
774
+ try {
775
+ // ★ 2026-10-27(主人点明):只要**当前会话 id** 就够了。
776
+ // 后端 findManagerBySessionId(sid) 一步定位 manager + agent,
777
+ // 不用再猜 scope/peer/群注册表。
778
+ var sidNow = extUiCurrentSid()
779
+ if (sidNow) u += '&sid=' + encodeURIComponent(sidNow)
780
+ var live = window.__qqsDockState || {}
781
+ if (live.ns) u += '&ns=' + encodeURIComponent(live.ns)
782
+ var hit = live.detectedHit
783
+ if (hit && hit.peerId) {
784
+ u += '&peer=' + encodeURIComponent(hit.peerId)
785
+ u += '&scope=' + encodeURIComponent(hit.scope === 'c2c' ? 'c2c' : 'group')
786
+ } else if (dockCtx && dockCtx.peer && dockCtx.peer.peerId) {
787
+ u += '&peer=' + encodeURIComponent(dockCtx.peer.peerId)
788
+ u += '&scope=' + encodeURIComponent(dockCtx.peer.scope || 'group')
789
+ }
790
+ } catch (e) { /* ignore */ }
791
+ return fetch(u, { method: 'POST' })
792
+ .then(function (r) { return r.json() })
793
+ .then(function (j) { return j && j.ok ? j.data : null })
794
+ .catch(function () { return null })
795
+ },
796
+ dock: null,
797
+ }
798
+
799
+ // ── 语义化包装(内部走 cap,写起来更像其余扩展)──────────────
800
+ api.log = function (msg) { return api.cap('log', String(msg == null ? '' : msg)) }
801
+ api.store = {
802
+ load: function () { return api.cap('store.load') },
803
+ save: function (v) { return api.cap('store.save', v == null ? {} : v) },
804
+ }
805
+ api.kernel = {
806
+ /** 取宿主服务(DSH cordis Context)—— 等于扩展里的 env.kernel.get(name) */
807
+ get: function (name) { return api.cap('kernel.get', String(name == null ? '' : name)) },
808
+ has: function (name) { return api.cap('kernel.has', String(name == null ? '' : name)) },
809
+ }
810
+ api.owners = function () { return api.cap('owners') }
811
+ api.appIdOf = function () { return api.cap('appId') }
812
+ if (dockCtx) {
813
+ var q = function (what) {
814
+ // ★ 2026-10-27:必须带上 dataRoot / cwd —— 否则后端推不出当前实例的数据根,
815
+ // 会落到宿主默认(表现为"切到 3 号机读到的还是 2 号机的路径")。
816
+ var u = '/api/qqbot-settings/dock-data?what=' + encodeURIComponent(what)
817
+ if (dockCtx.ns) u += '&ns=' + encodeURIComponent(dockCtx.ns)
818
+ if (dockCtx.dataRoot) u += '&dataRoot=' + encodeURIComponent(dockCtx.dataRoot)
819
+ if (dockCtx.cwd) u += '&cwd=' + encodeURIComponent(dockCtx.cwd)
820
+ // ★ 同上:peer 实时读(挂载时可能还没检测出来)
821
+ try {
822
+ var sidNow2 = extUiCurrentSid()
823
+ if (sidNow2) u += '&sid=' + encodeURIComponent(sidNow2)
824
+ var live2 = window.__qqsDockState || {}
825
+ var hit2 = live2.detectedHit
826
+ if (hit2 && hit2.peerId) {
827
+ u += '&peer=' + encodeURIComponent(hit2.peerId)
828
+ u += '&scope=' + encodeURIComponent(hit2.scope === 'c2c' ? 'c2c' : 'group')
829
+ } else if (dockCtx.peer && dockCtx.peer.peerId) {
830
+ u += '&peer=' + encodeURIComponent(dockCtx.peer.peerId)
831
+ u += '&scope=' + encodeURIComponent(dockCtx.peer.scope || 'group')
832
+ }
833
+ } catch (e) { /* ignore */ }
834
+ return u
835
+ }
836
+ api.dock = {
837
+ ns: dockCtx.ns || '',
838
+ appId: dockCtx.appId || '',
839
+ // ★ 2026-10-27(主人问:当前会话 id 暴露了吗?—— 之前没有,现在补上)
840
+ // 就是顶栏「当前会话」那个 26421171-... ,宿主的 agent 表按它索引。
841
+ // 做成 getter:每次读都是最新的(卡片挂载时可能还没检测完,不能存快照)。
842
+ get sid() { return extUiCurrentSid() },
843
+ get peerId() {
844
+ var h = (window.__qqsDockState || {}).detectedHit
845
+ return h && h.peerId ? String(h.peerId) : ''
846
+ },
847
+ get scope() {
848
+ var h = (window.__qqsDockState || {}).detectedHit
849
+ return h && h.scope === 'c2c' ? 'c2c' : 'group'
850
+ },
851
+ // ★ 两个路径都暴露(2026-10-26 主人问"dataRoot 还是 cwd?" —— 都给了):
852
+ // dataRoot = 插件数据根(推荐:扩展自己的文件/SQLite 放这儿,跟插件数据一起)
853
+ // cwd = 当前工作区根(扩展要往用户项目目录写东西时才用)
854
+ dataRoot: dockCtx.dataRoot || '',
855
+ cwd: dockCtx.cwd || '',
856
+ peer: dockCtx.peer || null,
857
+ context: function () { return fetch(q('context')).then(function (r) { return r.json() }).catch(function () { return null }) },
858
+ sessions: function () { return fetch(q('sessions')).then(function (r) { return r.json() }).catch(function () { return null }) },
859
+ history: function (n) { return fetch(q('history') + '&limit=' + (n || 20)).then(function (r) { return r.json() }).catch(function () { return null }) },
860
+ members: function () { return fetch(q('members')).then(function (r) { return r.json() }).catch(function () { return null }) },
861
+ }
862
+ }
863
+ return api
864
+ }
865
+ /** 把一张扩展卡片用 Shadow DOM 挂进容器 */
866
+ function extUiMountCard(container, extId, cls, dockCtx, acctCtx) {
867
+ var host = document.createElement('div')
868
+ host.className = cls || 'qqs-ext-card'
869
+ var sr = host.attachShadow({ mode: 'open' })
870
+ container.appendChild(host)
871
+ var slot = dockCtx ? 'dock' : 'settings'
872
+ Promise.all([
873
+ extUiLoad(extId, 'card.html', slot),
874
+ extUiLoad(extId, 'card.css', slot),
875
+ extUiLoad(extId, 'card.js', slot),
876
+ ])
877
+ .then(function (res) {
878
+ sr.innerHTML = '<style>' + (res[1] || '') + '</style>' + (res[0] || '')
879
+ if (!res[2]) return
880
+ var api = extUiApi(extId, dockCtx ? 'data' : 'profile', dockCtx, acctCtx)
881
+ try {
882
+ new Function('qqbotExt', 'shadowRoot', res[2])(api, sr)
883
+ } catch (e) {
884
+ var d = document.createElement('div')
885
+ d.style.cssText = 'color:#c33;font-size:12px;padding:6px 0'
886
+ d.textContent = '卡片脚本出错: ' + (e && e.message ? e.message : e)
887
+ sr.appendChild(d)
888
+ }
889
+ })
890
+ return host
891
+ }
892
+ /** 扩展页签的整页视图:把它的卡片铺在内容区 */
893
+ function ExtCardPage(props) {
894
+ var extId = String((props && props.extId) || '')
895
+ var title = String((props && props.title) || extId)
896
+ // ★ 2026-10-27(主人要求):设置页的卡片虽然是"全局 UI",但也要能读写**当前选中账号**
897
+ // 那份数据 —— 即 getConfig('data') 走上面账号下拉框选中的实例。
898
+ // acct 来自 QqbotHome 的 acctx(含 ns / cwd / dataRoot)。
899
+ var acct = (props && props.acct) || null
900
+ useEffect(function () {
901
+ extUiEnsureCss()
902
+ var box = document.getElementById('qqs-ext-page')
903
+ if (!box || !extId) return
904
+ box.innerHTML = ''
905
+ extUiMountCard(box, extId, 'qqs-ext-page-card', null, acct)
906
+ return function () { try { box.innerHTML = '' } catch (e) { /* ignore */ } }
907
+ }, [extId, acct && acct.ns])
908
+ return h('div', { style: { maxWidth: 720 } },
909
+ h('h2', { style: { margin: '0 0 10px' } }, title),
910
+ h('div', { id: 'qqs-ext-page', style: { display: 'flex', flexWrap: 'wrap', gap: 12, alignItems: 'flex-start' } }))
911
+ }
912
+
913
+ /**
914
+ * dock 扩展页签的列表(2026-10-26)
915
+ * 只收 slot=dock 的卡片 —— 它们放在**数据根**(每个机器人实例各一份),
916
+ * 所以能拿到「当前实例 + 当前会话」的数据(见 extUiApi 里的 dock.*)。
917
+ * 拉一次缓存在 window,paintBody 重绘后就有页签了。
918
+ */
919
+ var _dockTabsLoaded = false
920
+ var _dockTabsRoot = '' // 上次拉列表用的 dataRoot —— 换机器人实例时要重拉
921
+ function ensureDockExtTabs(force) {
922
+ // ★ 2026-10-27 修(主人实测:切到 im-qqbot-3 仍显示 im-qqbot-2 的扩展卡片):
923
+ // dock 卡片按**机器人实例的 dataRoot** 分源,不同实例不一样。
924
+ // 原来拉列表没带实例信息、且只拉一次 → 换了实例还是旧那份。
925
+ // 现在带上当前账号的 dataDir,变了就重拉。
926
+ var st = window.__qqsDockState || {}
927
+ var ac = null
928
+ try {
929
+ var l0 = (st.accts || [])
930
+ // ★ 2026-10-27:dock 的 state 里**没有 acctId**(那是 SettingsPage 的 React state),
931
+ // 必须用 st.ns 匹配。原来写 st.acctId 永远 undefined → 落到"第一个启用的实例"
932
+ // → 不管切到哪个实例都拿 2 号机的 dataRoot(主人实测就是这个)。
933
+ if (st.ns) l0.forEach(function (a) { if (a && (a.ns === st.ns || a.id === st.ns)) ac = a })
934
+ if (!ac) l0.forEach(function (a) { if (!ac && a && !a.disabled) ac = a })
935
+ if (!ac && l0.length) ac = l0[0]
936
+ } catch (e) { ac = null }
937
+ // ★ 2026-10-27:dock 卡片按**机器人实例的 dataRoot** 分源,不同实例不一样。
938
+ // 注意不能用 ac.dataDir —— 那是「图库目录」({dataRoot}/表情包),
939
+ // 扩展 UI 在 {dataRoot}/.qqbot-extensions/ui/,差一级就扫不到。
940
+ // 优先用 accounts 回给我们的 dataRoot;老版本没这个字段时从 cwd 推。
941
+ var root = ''
942
+ if (ac) {
943
+ root = String(ac.dataRoot || '').trim()
944
+ if (!root && ac.cwd) root = String(ac.cwd).replace(/[\\/]+$/, '') + '/dshqqbot'
945
+ }
946
+ if (_dockTabsLoaded && !force && root === _dockTabsRoot) return
947
+ _dockTabsLoaded = true
948
+ _dockTabsRoot = root
949
+ var u = '/api/qqbot-settings/ext-ui/list'
950
+ if (root) u += '?dataRoot=' + encodeURIComponent(root)
951
+ fetch(u)
952
+ .then(function (r) { return r.json() })
953
+ .then(function (j) {
954
+ if (!j || !j.ok) return
955
+ var list = (j.items || []).filter(function (x) { return x.slot === 'dock' })
956
+ window.__qqsDockTabs = list
957
+ // paintBody 是 dock 闭包内的函数,模块级拿不到 —— 由它自己挂到 window 上
958
+ if (typeof window.__qqsPaintBody === 'function') {
959
+ try { window.__qqsPaintBody() } catch (e) { /* ignore */ }
960
+ }
961
+ })
962
+ .catch(function () { _dockTabsLoaded = false }) // 失败允许下次重试
963
+ }
964
+ /** 把一张 dock 扩展卡片挂进面板(带当前实例/会话上下文) */
965
+ function mountDockExtTab(bodyEl, extId) {
966
+ extUiEnsureCss()
967
+ var box = document.createElement('div')
968
+ box.id = 'qqs-dock-ext'
969
+ box.style.cssText = 'display:block'
970
+ bodyEl.appendChild(box)
971
+ mountDockCard(box, extId)
972
+ }
973
+ /** 真挂载:先取一次上下文(拿到 dataRoot / cwd),再挂卡片 —— 卡片里 dock.dataRoot 可直接同步读 */
974
+ function mountDockCard(container, extId) {
975
+ // ★ 2026-10-27:必须在这里也装上 CSS + 滚轮/拖拽监听。
976
+ // 原来只在 mountDockExtTab 里调,但实际走的是本函数 —— 结果监听器根本没绑,
977
+ // 表现就是「dock 页签既拖不动也滚不动」(主人实测)。
978
+ // extUiEnsureCss 是幂等的,多调几次没副作用。
979
+ try { extUiEnsureCss() } catch (e) { /* ignore */ }
980
+ // ★ 2026-10-27 修:state 是 dock 闭包里的变量,**模块级拿不到**!
981
+ // 原来写 `typeof state !== 'undefined' ? state : {}` 永远拿到 {},
982
+ // 于是 ns 是空、没传给后端 → 后端用默认 NS(im-qqbot) → 读到别的实例的配置,
983
+ // 卡片里显示的 appId/cwd 全错(主人实测)。改从 window 上取。
984
+ var st = (window.__qqsDockState && typeof window.__qqsDockState === 'object') ? window.__qqsDockState : {}
985
+ // ★ 2026-10-27:dataRoot/cwd 必须**先从账号列表取**,不能等 context 返回后再填 ——
986
+ // 否则第一次请求必然没带上,后端只能落到宿主默认(表现为"切到 3 号机还是 2 号机的路径")。
987
+ var ac = null
988
+ try {
989
+ var l0 = (st.accts || [])
990
+ // ★ 2026-10-27:dock 的 state 里**没有 acctId**(那是 SettingsPage 的 React state),
991
+ // 必须用 st.ns 匹配。原来写 st.acctId 永远 undefined → 落到"第一个启用的实例"
992
+ // → 不管切到哪个实例都拿 2 号机的 dataRoot(主人实测就是这个)。
993
+ if (st.ns) l0.forEach(function (a) { if (a && (a.ns === st.ns || a.id === st.ns)) ac = a })
994
+ if (!ac) l0.forEach(function (a) { if (!ac && a && !a.disabled) ac = a })
995
+ if (!ac && l0.length) ac = l0[0]
996
+ } catch (e) { ac = null }
997
+ var root0 = ''
998
+ if (ac) {
999
+ root0 = String(ac.dataRoot || '').trim()
1000
+ if (!root0 && ac.cwd) root0 = String(ac.cwd).replace(/[\\/]+$/, '') + '/dshqqbot'
1001
+ }
1002
+ // ★ 2026-10-27 修(主人实测:静默注入 diag 里 peer 是空的):
1003
+ // 原来用 st.gid —— 那是「📇 群组管理」里选的目标群,**不一定是当前会话**。
1004
+ // 扩展卡片要"往当前会话注入",必须用顶栏那个「当前会话」(state.detectedHit),
1005
+ // 和「🤖 让 AI 写」按钮完全一致(它用的就是 detectedHit.peerId)。
1006
+ var __hit = st.detectedHit || null
1007
+ var peer = null
1008
+ if (__hit && __hit.peerId) {
1009
+ peer = { scope: __hit.scope === 'c2c' ? 'c2c' : 'group', peerId: String(__hit.peerId), name: __hit.name || '' }
1010
+ } else if (st.gid) {
1011
+ peer = { scope: 'group', peerId: String(st.gid) }
1012
+ }
1013
+ var base = {
1014
+ ns: st.ns || '',
1015
+ appId: (ac && ac.appId) || '',
1016
+ dataRoot: root0,
1017
+ cwd: (ac && ac.cwd) || '',
1018
+ peer: peer,
1019
+ }
1020
+ var u = '/api/qqbot-settings/dock-data?what=context'
1021
+ if (base.ns) u += '&ns=' + encodeURIComponent(base.ns)
1022
+ if (base.dataRoot) u += '&dataRoot=' + encodeURIComponent(base.dataRoot)
1023
+ if (base.cwd) u += '&cwd=' + encodeURIComponent(base.cwd)
1024
+ if (peer) u += '&peer=' + encodeURIComponent(peer.peerId) + '&scope=' + peer.scope
1025
+ fetch(u)
1026
+ .then(function (r) { return r.json() })
1027
+ .then(function (j) {
1028
+ if (j && j.ok && j.context) {
1029
+ base.appId = j.context.appId || ''
1030
+ base.dataRoot = j.context.dataRoot || ''
1031
+ base.cwd = j.context.cwd || ''
1032
+ }
1033
+ })
1034
+ .catch(function () { /* 取不到就用空值,卡片自己再 dock.context() */ })
1035
+ .then(function () {
1036
+ extUiMountCard(container, extId, 'qqs-dock-ext-card', base)
1037
+ })
1038
+ }
1039
+
579
1040
  function SettingsPage(props) {
580
1041
  var close = props && props.close
581
1042
  // 多账号: 当前操作账号的 settings 命名空间(主=im-qqbot 不带参, 其它=实例 id)
@@ -711,6 +1172,94 @@ window.__ModuleLoader__.load({
711
1172
  q2 ? h('span', { onClick: function () { setQ2('') }, style: { fontSize: 12, color: T.text3, cursor: 'pointer' } }, '清空') : null,
712
1173
  h('span', { style: { fontSize: 11, color: T.text3, whiteSpace: 'nowrap' } }, '程序算的相似度 · 本地即时')))
713
1174
 
1175
+ // ── 扩展 UI 卡片(2026-10-26)─────────────────────────────────────────
1176
+ // 扩展在 {dataRoot}/.qqbot-extensions/ui/<id>/ 放 card.html / card.css / card.js,
1177
+ // 这里拉过来用 **Shadow DOM** 挂到设置页底部。扫描与安全校验见 src/features/ext-ui.ts。
1178
+ // 用 Shadow DOM 的两个理由:
1179
+ // ① 扩展写的 CSS 不会污染这个 535KB 单文件里的任何样式(一条全局 button{} 就能改全站)
1180
+ // ② 扩展脚本报错只白自己那张卡,不整页崩
1181
+ var EXT_UI_LIST = '/api/qqbot-settings/ext-ui/list'
1182
+ var EXT_UI_FILE = '/api/qqbot-settings/ext-ui/file?f='
1183
+ function extUiText(extId, f) {
1184
+ return fetch(EXT_UI_FILE + encodeURIComponent(f) + '&id=' + encodeURIComponent(extId))
1185
+ .then(function (r) { return r.json() })
1186
+ .then(function (j) { return j && j.ok ? String(j.text || '') : '' })
1187
+ .catch(function () { return '' })
1188
+ }
1189
+ function extUiApi(extId) {
1190
+ return {
1191
+ extId: extId,
1192
+ // 扩展自己的只读/只写接口走 /api/ext/<id>/…(由扩展自己注册,本插件不代管)
1193
+ api: function (p, opts) {
1194
+ return fetch('/api/ext/' + encodeURIComponent(extId) + String(p || ''),
1195
+ Object.assign({ headers: { 'Content-Type': 'application/json' } }, opts || {}))
1196
+ .then(function (r) { return r.json() })
1197
+ .catch(function () { return null })
1198
+ },
1199
+ toast: function (m) { try { window.alert(String(m)) } catch (e) { /* ignore */ } },
1200
+ }
1201
+ }
1202
+ function injectExtUiCss() {
1203
+ if (document.getElementById('qqs-ext-css')) return
1204
+ var s = document.createElement('style')
1205
+ s.id = 'qqs-ext-css'
1206
+ // 横向滑动:抄 .dk-tab 那套已在机器上跑熟的写法(overflow-x + 惯性 + 隐藏滚动条)。
1207
+ // ★ flex:0 0 auto 是关键 —— 少了这句卡片会被 flex 压成一条缝,这是最常见的坑。
1208
+ // scroll-snap 让"滑一下正好停在卡片边缘",不会卡在半张卡中间。
1209
+ s.textContent = ''
1210
+ + '.qqs-ext-cards{display:flex;gap:10px;overflow-x:auto;overflow-y:hidden;'
1211
+ + '-webkit-overflow-scrolling:touch;scrollbar-width:none;'
1212
+ + 'scroll-snap-type:x proximity;padding:2px 0 6px}'
1213
+ + '.qqs-ext-cards::-webkit-scrollbar{display:none}'
1214
+ + '.qqs-ext-card{flex:0 0 auto;scroll-snap-align:start;min-width:220px;max-width:320px}'
1215
+ // 顶部页签条:内置四页 + 扩展页签,扩展多了横向滑动(2026-10-26)
1216
+ + '.qqs-top-tabs{scrollbar-width:none}'
1217
+ + '.qqs-top-tabs::-webkit-scrollbar{display:none}'
1218
+ // 扩展整页视图:让卡片在页内能撑开(卡片自身宽度由 :host 决定)
1219
+ + '.qqs-ext-page-card{display:block}'
1220
+ document.head.appendChild(s)
1221
+ }
1222
+ function mountExtUiCards() {
1223
+ injectExtUiCss()
1224
+ var box = document.getElementById('qqs-ext-cards')
1225
+ if (!box) return
1226
+ fetch(EXT_UI_LIST).then(function (r) { return r.json() }).then(function (j) {
1227
+ if (!j || !j.ok) return
1228
+ var list = (j.items || []).filter(function (x) { return x.slot === 'settings' })
1229
+ if (!list.length) { box.style.display = 'none'; return }
1230
+ box.style.display = ''
1231
+ // 幂等:卡片数量没变就不重挂(避免每次 setState 都把扩展的 DOM 冲掉)
1232
+ var sig = list.map(function (x) { return x.extId }).join(',')
1233
+ if (box.getAttribute('data-sig') === sig) return
1234
+ box.setAttribute('data-sig', sig)
1235
+ box.innerHTML = ''
1236
+ list.forEach(function (it) {
1237
+ var host = document.createElement('div')
1238
+ host.className = 'qqs-ext-card'
1239
+ var sr = host.attachShadow({ mode: 'open' })
1240
+ box.appendChild(host)
1241
+ Promise.all([
1242
+ extUiText(it.extId, 'card.html'),
1243
+ it.files && it.files.indexOf('card.css') >= 0 ? extUiText(it.extId, 'card.css') : Promise.resolve(''),
1244
+ it.files && it.files.indexOf('card.js') >= 0 ? extUiText(it.extId, 'card.js') : Promise.resolve(''),
1245
+ ]).then(function (res) {
1246
+ sr.innerHTML = '<style>' + (res[1] || '') + '</style>' + (res[0] || '')
1247
+ if (!res[2]) return
1248
+ try {
1249
+ // Function 而不是 eval:作用域干净,且把 qqbotExt 作为形参传进去
1250
+ new Function('qqbotExt', 'shadowRoot', res[2])(extUiApi(it.extId), sr)
1251
+ } catch (e) {
1252
+ var d = document.createElement('div')
1253
+ d.style.cssText = 'color:#c33;font-size:12px;padding:4px 0'
1254
+ d.textContent = '卡片脚本出错: ' + (e && e.message ? e.message : e)
1255
+ sr.appendChild(d)
1256
+ }
1257
+ })
1258
+ })
1259
+ }).catch(function () { /* 静默:扩展 UI 挂了不该影响设置页本体 */ })
1260
+ }
1261
+ setTimeout(function () { try { mountExtUiCards() } catch (e) { /* ignore */ } }, 0)
1262
+
714
1263
  return h('div', { style: { maxWidth: 720 } },
715
1264
  h('h2', { style: { margin: '0 0 6px' } }, 'QQ 机器人设置'),
716
1265
  h('p', { style: { fontSize: 12, color: T.text3, margin: '0 0 10px' } }, '改完点「保存」立刻生效(不用重启)。看不懂的项放着别动就行;想恢复"不限制"就把数字填 0、勾选取消、列表清空。只提交你改过的字段——没碰的项不会被动。'),
@@ -721,7 +1270,9 @@ window.__ModuleLoader__.load({
721
1270
  h(SButton, { variant: 'primary', onClick: doSave }, dirtyN ? ('保存(' + dirtyN + ' 项改动)') : '保存'),
722
1271
  h(SButton, { variant: 'ghost', onClick: load }, '放弃修改(重新读取)'),
723
1272
  msg ? h('span', { style: { fontSize: 13, color: T.text2 } }, msg) : null,
724
- close ? h(SButton, { variant: 'ghost', onClick: close }, '完成') : null))
1273
+ close ? h(SButton, { variant: 'ghost', onClick: close }, '完成') : null),
1274
+ // ★ 扩展卡片挂载点:横向可滑动,样式见文件末尾注入的 .qqs-ext-cards
1275
+ h('div', { id: 'qqs-ext-cards', className: 'qqs-ext-cards' }))
725
1276
  }
726
1277
 
727
1278
  // ── ⑥ QQ 群管理(单开大卡片; P3+ 设计系统版: 横排 Tab + 状态条 + 表格 + 空状态 + 预留接口位) ──
@@ -1539,6 +2090,21 @@ window.__ModuleLoader__.load({
1539
2090
  })
1540
2091
  return function () { alive = false }
1541
2092
  }, [accts, acctId])
2093
+ // ── 扩展 UI 页签(2026-10-26):slot=settings 的扩展挂到顶部 tab 右边 ──
2094
+ // 数据来自 /api/qqbot-settings/ext-ui/list(profile 级目录,全 profile 一份)。
2095
+ var [extTabs, setExtTabs] = useState([])
2096
+ useEffect(function () {
2097
+ var alive = true
2098
+ fetch('/api/qqbot-settings/ext-ui/list')
2099
+ .then(function (r) { return r.json() })
2100
+ .then(function (j) {
2101
+ if (!alive || !j || !j.ok) return
2102
+ var list = (j.items || []).filter(function (x) { return x.slot === 'settings' })
2103
+ setExtTabs(list)
2104
+ })
2105
+ .catch(function () { /* 扩展列表拉不到不影响设置页 */ })
2106
+ return function () { alive = false }
2107
+ }, [])
1542
2108
  if (!tab) return h('div', null, '…')
1543
2109
  function choose(t) { setTab(t); try { localStorage.setItem(TAB_KEY, t) } catch (e) {} }
1544
2110
  function chooseAcct(id) { setAcctId(id); try { localStorage.setItem(ACCT_KEY, id) } catch (e) {} }
@@ -1547,12 +2113,28 @@ window.__ModuleLoader__.load({
1547
2113
  }
1548
2114
  var ac = currentAcct()
1549
2115
  // 传给按账号视图的上下文: 设置按 ns, 图库按 dataDir, 定时按 schedDir
1550
- var acctx = ac ? { id: ac.id, ns: ac.ns, dataDir: ac.dataDir, schedDir: ac.schedDir } : null
2116
+ // ★ 2026-10-27:补上 cwd / dataRoot —— 扩展卡片要能对"当前选中的账号"
2117
+ // 读写它自己那份配置(getConfig('data')),光有 dataDir(图库目录)不够。
2118
+ var acctx = ac ? {
2119
+ id: ac.id, ns: ac.ns, dataDir: ac.dataDir, schedDir: ac.schedDir,
2120
+ cwd: ac.cwd || '', dataRoot: ac.dataRoot || '', appId: ac.appId || '',
2121
+ } : null
1551
2122
  // key=账号: 切号即强制子页整页重挂(清掉旧账号 state, 全部重新拉取)
1552
2123
  var subKey = ac ? ac.id : 'main'
1553
2124
  var sub = tab === 'gallery' ? h(GalleryPage, { key: subKey, close: props && props.close, acct: acctx })
1554
2125
  : tab === 'timers' ? h(TimersPage, { key: subKey, close: props && props.close, acct: acctx })
1555
2126
  : tab === 'accounts' ? h(AccountsPage, { close: props && props.close })
2127
+ : String(tab).indexOf('ext:') === 0
2128
+ ? h(ExtCardPage, {
2129
+ key: String(tab).slice(4) + '|' + subKey, // 切账号即重挂(同其他子页)
2130
+ extId: String(tab).slice(4),
2131
+ acct: acctx, // ★ 卡片据此可对当前账号读写
2132
+ title: (function () {
2133
+ var hit = null
2134
+ extTabs.forEach(function (x) { if (x.extId === String(tab).slice(4)) hit = x })
2135
+ return hit ? hit.title : String(tab).slice(4)
2136
+ })(),
2137
+ })
1556
2138
  : h(SettingsPage, { key: subKey, close: props && props.close, acct: acctx })
1557
2139
  var acctLine = (accts && accts.length > 1) || (accts && accts.length === 1)
1558
2140
  return h('div', { style: { width: '100%' } },
@@ -1568,16 +2150,65 @@ window.__ModuleLoader__.load({
1568
2150
  ac ? h('span', { style: { fontSize: 12, color: '#6d28d9', whiteSpace: 'nowrap' } }, ac.dataDir ? '数据目录: ' + ac.dataDir : '') : null,
1569
2151
  h('span', { style: { flex: 1 } }),
1570
2152
  h('span', { style: { fontSize: 12, color: '#8a97ad' } }, '设置/图库/定时 都跟随上面选的机器人')),
1571
- h('div', { style: { position: 'sticky', top: 0, zIndex: 30, background: 'transparent', padding: '10px 4px 12px', display: 'flex', gap: 10, flexWrap: 'nowrap', alignItems: 'stretch', marginBottom: 12 } },
1572
- [['settings', '设置', '#ff5d5d'], ['gallery', '表情包图库', '#ffd97a'], ['timers', '定时任务', '#5b9dff'], ['accounts', '账号与预设', '#b197fc']].map(function (t) {
2153
+ // ★ 2026-10-26 修「页签条不能横滑」:原来把 `position:sticky` 和 `overflow-x:auto`
2154
+ // 放在**同一个元素**上 —— sticky 会创建新的格式化上下文,导致滚动容器的
2155
+ // scrollWidth 计算异常(看着溢出、实际判定为没溢出),于是滚不动。
2156
+ // 拆成两层:外层只管 sticky + 裁剪,内层只管横向滚动。
2157
+ // ⚠️ 外层必须**自己受宽度约束**(width/maxWidth/minWidth:0)——
2158
+ // 否则它会被内容撑开,内层 width:100% 也跟着等于内容宽,永远不产生溢出。
2159
+ h('div', { style: { position: 'sticky', top: 0, zIndex: 30, background: 'transparent', overflow: 'hidden', marginBottom: 12, width: '100%', maxWidth: '100%', minWidth: 0, boxSizing: 'border-box' } },
2160
+ h('div', { id: 'qqs-top-tabs', className: 'qqs-top-tabs',
2161
+ // ★ 2026-10-26:id 查不到(设置页可能在 Shadow DOM 里,document.getElementById 拿不到),
2162
+ // 改用 React ref 直接抓元素,并把「鼠标按住拖拽」绑上 —— 桌面端之前只能靠滚轮。
2163
+ ref: function (el) {
2164
+ if (!el) return
2165
+ try { window.__qqsTopTabs = el } catch (e) {}
2166
+ if (el.__qqsDragBound) return
2167
+ el.__qqsDragBound = true
2168
+ var down = false, startX = 0, startLeft = 0, moved = false
2169
+ el.addEventListener('mousedown', function (e) {
2170
+ if (e.button !== 0) return
2171
+ // 点在按钮上时不要立刻开始拖(否则点选页签会失效)
2172
+ down = true; moved = false
2173
+ startX = e.pageX; startLeft = el.scrollLeft
2174
+ })
2175
+ el.addEventListener('mousemove', function (e) {
2176
+ if (!down) return
2177
+ var dx = e.pageX - startX
2178
+ if (Math.abs(dx) > 4) {
2179
+ moved = true
2180
+ el.style.cursor = 'grabbing'
2181
+ el.scrollLeft = startLeft - dx
2182
+ e.preventDefault()
2183
+ }
2184
+ })
2185
+ var endDrag = function () {
2186
+ if (!down) return
2187
+ down = false
2188
+ el.style.cursor = ''
2189
+ }
2190
+ el.addEventListener('mouseup', endDrag)
2191
+ el.addEventListener('mouseleave', endDrag)
2192
+ // 拖过之后拦截一次 click,避免"想滑动却误切页签"
2193
+ el.addEventListener('click', function (e) {
2194
+ if (moved) { moved = false; e.stopPropagation(); e.preventDefault() }
2195
+ }, true)
2196
+ },
2197
+ style: { display: 'flex', gap: 10, flexWrap: 'nowrap', alignItems: 'stretch', padding: '10px 4px 12px', overflowX: 'auto', overflowY: 'hidden', WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none', width: '100%', boxSizing: 'border-box', cursor: 'grab', userSelect: 'none' } },
2198
+ // 内置四页 + 扩展页签(2026-10-26):扩展 tab 用青蓝色区分,放在最右边。
2199
+ // 容器从「四个 flex:1 平分」改成「按钮 flex:0 0 auto + 容器横向可滑」——
2200
+ // 否则扩展一多,按钮会被压成一条缝(这跟 .dk-tab 那套是同一个道理)。
2201
+ [['settings', '设置', '#ff5d5d'], ['gallery', '表情包图库', '#ffd97a'], ['timers', '定时任务', '#5b9dff'], ['accounts', '账号与预设', '#b197fc']]
2202
+ .concat(extTabs.map(function (x) { return ['ext:' + x.extId, x.title, '#38bdf8'] }))
2203
+ .map(function (t) {
1573
2204
  var on = tab === t[0]
1574
2205
  var n = t[0] === 'gallery' ? badges.untagged : t[0] === 'timers' ? badges.timers : 0
1575
2206
  var col = t[2]
1576
2207
  return h('button', { key: t[0], onClick: function () { choose(t[0]) },
1577
2208
  style: {
1578
- flex: 1, minWidth: 0, cursor: 'pointer', font: 'inherit',
2209
+ flex: '0 0 auto', minWidth: 104, cursor: 'pointer', font: 'inherit',
1579
2210
  color: 'var(--qqs-text,#171a21)', fontSize: 14, textAlign: 'center',
1580
- whiteSpace: 'nowrap', padding: '9px 8px', borderRadius: 14,
2211
+ whiteSpace: 'nowrap', padding: '9px 14px', borderRadius: 14,
1581
2212
  border: '1px solid ' + (on ? col : col + 'a6'),
1582
2213
  background: 'linear-gradient(165deg,' + col + (on ? '66' : '40') + ',' + col + (on ? '1f' : '14') + ')',
1583
2214
  boxShadow: on
@@ -1588,7 +2219,7 @@ window.__ModuleLoader__.load({
1588
2219
  transition: 'all .15s', outline: 'none',
1589
2220
  } },
1590
2221
  t[1], badge(n))
1591
- })),
2222
+ }))),
1592
2223
  sub)
1593
2224
  }
1594
2225
 
@@ -2103,7 +2734,7 @@ var QQS_THEME_CSS = [
2103
2734
  // · 群管理(发消息/审批入群/禁言) → 悬浮球点开成操作台
2104
2735
  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}"
2105
2736
  // ── 💬 聊天视图样式(dock 追加段, 2026-09-07): QQ 风格气泡, 群友左(bot)右 ──
2106
- 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-cbubble a.dk-link{color:#1a66d6;text-decoration:underline;word-break:break-all;cursor:pointer}#qqs-dock-panel .dk-crow.out .dk-cbubble a.dk-link{color:#eaf3ff}#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:visible}#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-dock-panel .dk-composer{position:relative}#qqs-dock-panel .dk-at-pop{position:absolute;left:6px;bottom:calc(100% - 4px);z-index:30;min-width:200px;max-width:90%;max-height:190px;overflow-y:auto;background:#fff;border:1px solid #e0e4ea;border-radius:10px;box-shadow:0 8px 24px rgba(30,40,80,.16);padding:4px;display:none}#qqs-dock-panel .dk-at-item{display:flex;align-items:center;gap:6px;padding:5px 9px;border-radius:7px;cursor:pointer;font-size:12px;color:#1f2329;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#qqs-dock-panel .dk-at-item.on,#qqs-dock-panel .dk-at-item:hover{background:#eef3ff;color:#2b5fd0}#qqs-dock-panel .dk-at-empty{color:#aaa;font-size:12px;padding:6px 9px}#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}#qqs-dock-panel .dk-tab{overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;scrollbar-width:none}#qqs-dock-panel .dk-tab::-webkit-scrollbar{display:none}#qqs-dock-panel .dk-tab button{white-space:nowrap;flex:0 0 auto}@media (max-width:560px){#qqs-dock-panel:not(.dk-full){left:8px!important;right:8px!important;width:auto!important;max-width:calc(100vw - 16px)!important;bottom:96px!important}#qqs-dock-panel .dk-h{font-size:13px;padding:8px 10px;gap:6px}#qqs-dock-panel .dk-b{padding:8px 10px;font-size:13px}#qqs-dock-panel .dk-tab button{font-size:12px;padding:7px 10px}#qqs-dock-panel .dk-btn{font-size:12px;padding:6px 10px}#qqs-dock-panel .dk-row{gap:6px;margin:5px 0}#qqs-dock-panel select.qqs-sel,#qqs-dock-panel input.qqs-txt,#qqs-dock-panel textarea.qqs-txt{padding:7px 9px;font-size:16px}#qqs-dock-panel .dk-list{max-height:46vh}#qqs-dock-panel .dk-item{padding:8px 6px;gap:6px}}@media (max-width:560px){#qqs-dock-panel.dk-full{border-radius:0}#qqs-dock-panel.dk-full .dk-b{padding-bottom:calc(8px + env(safe-area-inset-bottom,0px))}#qqs-dock-panel.dk-full .dk-tab{position:sticky;top:0;background:#fff;z-index:2}}@media (max-width:560px){#qqs-dock-ball{width:46px;height:46px;line-height:46px;font-size:21px}}#qqs-dock-panel .dk-chat-wrap{position:relative}#qqs-dock-panel .dk-jump-latest{display:none;position:absolute;right:12px;bottom:76px;z-index:6;align-items:center;gap:4px;padding:5px 13px;font-size:12px;font-weight:600;color:#fff;cursor:pointer;border:none;border-radius:15px;background:linear-gradient(180deg,#69a6ff,#3d7df5);box-shadow:0 3px 12px rgba(30,60,140,.32);transition:transform .12s,box-shadow .12s}#qqs-dock-panel .dk-jump-latest.on{display:inline-flex}#qqs-dock-panel .dk-jump-latest:hover{background:linear-gradient(180deg,#5c9bfa,#2f6fe6);box-shadow:0 4px 16px rgba(30,60,140,.4)}#qqs-dock-panel .dk-jump-latest:active{transform:scale(.95)}"
2737
+ 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-cbubble a.dk-link{color:#1a66d6;text-decoration:underline;word-break:break-all;cursor:pointer}#qqs-dock-panel .dk-crow.out .dk-cbubble a.dk-link{color:#eaf3ff}#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:visible}#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-dock-panel .dk-composer{position:relative}#qqs-dock-panel .dk-at-pop{position:absolute;left:6px;bottom:calc(100% - 4px);z-index:30;min-width:200px;max-width:90%;max-height:190px;overflow-y:auto;background:#fff;border:1px solid #e0e4ea;border-radius:10px;box-shadow:0 8px 24px rgba(30,40,80,.16);padding:4px;display:none}#qqs-dock-panel .dk-at-item{display:flex;align-items:center;gap:6px;padding:5px 9px;border-radius:7px;cursor:pointer;font-size:12px;color:#1f2329;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#qqs-dock-panel .dk-at-item.on,#qqs-dock-panel .dk-at-item:hover{background:#eef3ff;color:#2b5fd0}#qqs-dock-panel .dk-at-empty{color:#aaa;font-size:12px;padding:6px 9px}#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}#qqs-dock-panel .dk-tab{overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;scrollbar-width:none}#qqs-dock-panel .dk-tab::-webkit-scrollbar{display:none}#qqs-dock-panel .dk-tab button{white-space:nowrap;flex:0 0 auto}@media (max-width:560px){#qqs-dock-panel:not(.dk-full){left:8px!important;right:8px!important;width:auto!important;max-width:calc(100vw - 16px)!important;bottom:96px!important}#qqs-dock-panel .dk-h{font-size:13px;padding:8px 10px;gap:6px}#qqs-dock-panel .dk-b{padding:8px 10px;font-size:13px}#qqs-dock-panel .dk-tab button{font-size:12px;padding:7px 10px}#qqs-dock-panel .dk-btn{font-size:12px;padding:6px 10px}#qqs-dock-panel .dk-row{gap:6px;margin:5px 0}#qqs-dock-panel select.qqs-sel,#qqs-dock-panel input.qqs-txt,#qqs-dock-panel textarea.qqs-txt{padding:7px 9px;font-size:16px}#qqs-dock-panel .dk-list{max-height:46vh}#qqs-dock-panel .dk-item{padding:8px 6px;gap:6px}}@media (max-width:560px){#qqs-dock-panel.dk-full{border-radius:0}#qqs-dock-panel.dk-full .dk-b{padding-bottom:calc(8px + env(safe-area-inset-bottom,0px))}#qqs-dock-panel.dk-full .dk-tab{position:relative;background:#fff;z-index:2}}@media (max-width:560px){#qqs-dock-ball{width:46px;height:46px;line-height:46px;font-size:21px}}#qqs-dock-panel .dk-chat-wrap{position:relative}#qqs-dock-panel .dk-jump-latest{display:none;position:absolute;right:12px;bottom:76px;z-index:6;align-items:center;gap:4px;padding:5px 13px;font-size:12px;font-weight:600;color:#fff;cursor:pointer;border:none;border-radius:15px;background:linear-gradient(180deg,#69a6ff,#3d7df5);box-shadow:0 3px 12px rgba(30,60,140,.32);transition:transform .12s,box-shadow .12s}#qqs-dock-panel .dk-jump-latest.on{display:inline-flex}#qqs-dock-panel .dk-jump-latest:hover{background:linear-gradient(180deg,#5c9bfa,#2f6fe6);box-shadow:0 4px 16px rgba(30,60,140,.4)}#qqs-dock-panel .dk-jump-latest:active{transform:scale(.95)}"
2107
2738
  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) {} };
2108
2739
  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) {} }
2109
2740
  function startQqDock(sessionsSvc) {
@@ -2330,6 +2961,9 @@ var QQS_THEME_CSS = [
2330
2961
 
2331
2962
  // ── 面板状态(每个实例独立保存, 切回不丢) ──
2332
2963
  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, bpExt: null, bpExtBusy: '', bpReq: '', bpReqHint: '', bpSrcText: null, bpSrcPath: '', rosterSel: {}, rosterScope: 'all', rosterQ: '', hubSid: '', hubRev: undefined, hubBusy: '', hubMsg: '', gaEnabled: false, gaPoll: false, gaPollWake: true, gaHubNotify: true, gaNotifyGroup: true, gaInterval: 5, gaMinCount: 1, gaMsg: '', gaBusy: '', bcDraft: null, bcTasks: null, cardMd: '', cardBtns: '', cardGid: '', cardBusy: '', cardQ: '', tgGroups: [], tgCur: '', hammerSub: 'mute', arOn: false, arMode: 'keep', arKws: '', arKinds: [], arMute: 0, arDry: false, arMsg: '', qunLogged: null, qunUin: '', qunGroups: [], qunGc: '', qunKw: '', qunMembers: [], qunCount: 0, qunQr: false, qunMsg: '', qunBusy: false, qunLoading: false }
2964
+ // ★ 2026-10-27:把 state 暴露到 window —— 模块级的 mountDockCard 拿不到这个闭包变量,
2965
+ // 扩展卡片要读当前 ns / gid 只能从这里取。
2966
+ try { window.__qqsDockState = state } catch (e) { /* ignore */ }
2333
2967
  // 🗂 自定义目标分组(仿 QQ 分组): 2026-09-12 起**host 持久化**({dataRoot}/.qqbot/target-groups.json),
2334
2968
  // localStorage 只当秒开缓存 —— 这样 **AI 与主人共用同一份分组**(agent 侧 broadcast_send 可直接写分组名群发),
2335
2969
  // 顺带修掉"换个浏览器分组就没了"的老毛病。
@@ -3941,6 +4575,16 @@ var QQS_THEME_CSS = [
3941
4575
  }
3942
4576
  function paintBody() {
3943
4577
  if (!panel || !open) return
4578
+ // 让模块级的 ensureDockExtTabs 能触发重绘(它拿不到这个闭包)
4579
+ try { window.__qqsPaintBody = paintBody } catch (e) { /* ignore */ }
4580
+ // ★ 2026-10-27 修(主人问:上方明明已命中当前会话,为什么扩展还拿不到):
4581
+ // `state` 是**每次渲染新建的对象** —— 只在初始化时赋一次 window.__qqsDockState 指针,
4582
+ // 后续 render 换了新 state,window 上那个还指着旧对象(detectedHit 永远是 null)。
4583
+ // 每次都刷新指针,扩展卡片才能读到当前的 detectedHit / ns / gid。
4584
+ try { window.__qqsDockState = state } catch (e) { /* ignore */ }
4585
+ // ★ 2026-10-27:每次重绘都确保滚轮/拖拽监听已装(幂等)。
4586
+ // 不能只靠"挂扩展卡片时"装 —— 那样不开扩展页签就永远是死的。
4587
+ try { extUiEnsureCss() } catch (e) { /* ignore */ }
3944
4588
  var b = panel.querySelector('.dk-b')
3945
4589
  if (!b) return
3946
4590
  var groupOpts = state.groups.map(function (g) {
@@ -3954,6 +4598,7 @@ var QQS_THEME_CSS = [
3954
4598
  var label = c.name ? (c.name + ' (' + tail + ')') : ('私聊 ' + tail)
3955
4599
  return '<option value="' + esc(c.id) + '"' + (c.id === state.sendTo ? ' selected' : '') + '>' + esc(label) + '</option>'
3956
4600
  }).join('')
4601
+ ensureDockExtTabs()
3957
4602
  var tabs = '<div class="dk-tab">'
3958
4603
  + '<button data-t="roster" class="' + (state.tab === 'roster' ? 'on' : '') + '">📇 群组管理</button>'
3959
4604
  + '<button data-t="chat" class="' + (state.tab === 'chat' ? 'on' : '') + '">💬 聊天</button>'
@@ -3962,6 +4607,10 @@ var QQS_THEME_CSS = [
3962
4607
  + '<button data-t="out" class="' + (state.tab === 'out' ? 'on' : '') + '">⚙️ 出站</button>'
3963
4608
  + '<button data-t="send" class="' + (['send','broadcast','bp','card'].indexOf(state.tab) >= 0 ? 'on' : '') + '">📤 群发</button>'
3964
4609
  + '<button data-t="session" class="' + (state.tab === 'session' ? 'on' : '') + '">⚙ 单会话设置</button>'
4610
+ // ★ 2026-10-26 扩展页签(slot=dock):只收数据根下的 ui/<id>/,每个实例各一份
4611
+ + (window.__qqsDockTabs || []).map(function (x) {
4612
+ return '<button data-t="ext:' + esc(x.extId) + '" class="' + (state.tab === 'ext:' + x.extId ? 'on' : '') + '">' + esc(x.title || x.extId) + '</button>'
4613
+ }).join('')
3965
4614
  + '</div>'
3966
4615
  var body = tabs
3967
4616
  var status = state.msg ? '<div class="dk-msg" style="color:#2f9e44;margin:4px 0">' + esc(state.msg) + '</div>' : ''
@@ -4144,6 +4793,10 @@ var QQS_THEME_CSS = [
4144
4793
  body += '详细主动=聊天行为与适配主动完全一样, 但额外把 AI 的工具调用(🔧 工具调用)与工具结果也推到 QQ —— 能在 QQ 上看她正在干什么; 消息会明显变多, 看完记得切回适配主动。'
4145
4794
  body += '被动=始终以「回复你那条」发出, 连发约4~5条后会被QQ吞掉。完全不出站=本机静默, 不向QQ发任何回复(AI 可用工具随时切回)。本开关对纯web(没绑QQ)的会话不生效。</div>'
4146
4795
  body += '<span class="dk-msg" id="dk-out-hint" style="color:#2f9e44;margin:4px 0"></span>'
4796
+ } else if (String(state.tab || '').indexOf('ext:') === 0) {
4797
+ // ★ 2026-10-26 扩展页签(slot=dock):把卡片用 Shadow DOM 挂进面板,
4798
+ // 并带上当前实例/会话上下文(dock.* 那套数据能力就靠它)。
4799
+ body += '<div id="qqs-dock-ext-host" style="padding:2px 0"></div>'
4147
4800
  } else if (state.tab === 'session') {
4148
4801
  // ⚙ 单会话设置(2026-09-13 主人定): 放"每个会话单独生效"的设置。
4149
4802
  // 次级标签: 🧠 智能回复 / 📄 空白样板(留作以后加新设置的模板)。
@@ -4472,6 +5125,13 @@ var QQS_THEME_CSS = [
4472
5125
  + '<input class="qqs-txt" id="dk-bindname" placeholder="备注(可选)" value="' + esc(state.bindName) + '" style="width:110px">'
4473
5126
  + '<button class="dk-btn" id="dk-bind">绑定</button></div>'
4474
5127
  b.innerHTML = body
5128
+ // ★ 2026-10-26:扩展页签的卡片在 DOM 写入之后才挂(Shadow DOM 需要真实容器)
5129
+ if (String(state.tab || '').indexOf('ext:') === 0) {
5130
+ try {
5131
+ var extHost = b.querySelector('#qqs-dock-ext-host')
5132
+ if (extHost) mountDockCard(extHost, String(state.tab).slice(4))
5133
+ } catch (e) { /* 扩展崩了不该影响面板 */ }
5134
+ }
4475
5135
  bindBodyEvents()
4476
5136
  bindPanelDrag()
4477
5137
  if (state.tab === 'send') applyTargetFilter(panel, state)
@@ -6891,14 +7551,36 @@ var QQS_THEME_CSS = [
6891
7551
  loading: 'lazy',
6892
7552
  className: 'qqs-mk-cover',
6893
7553
  onError: function (e) {
7554
+ // ★ 2026-10-26:封面图原来只有 jsdelivr → gh-proxy 两级,中间挂一个就白板。
7555
+ // 改成四级降级链,全失败才隐藏(别破版):
7556
+ // jsdelivr → gh-proxy → ghproxy.net → raw.githubusercontent.com
7557
+ // 用 data-tries 记进度;img 不重渲染就不会丢这个属性。
6894
7558
  try {
6895
7559
  var el = e.target;
6896
7560
  var cur = String(el.src || '');
6897
- if (cur.indexOf('cdn.jsdelivr.net') >= 0) {
6898
- // 换 gh-proxy 再试一次
6899
- el.src = cur.replace('https://cdn.jsdelivr.net/gh/', 'https://gh-proxy.com/https://raw.githubusercontent.com/');
7561
+ var n = parseInt(el.getAttribute('data-tries') || '0', 10);
7562
+ var next = '';
7563
+ if (cur.indexOf('cdn.jsdelivr.net/gh/') >= 0) {
7564
+ // owner/repo@branch/path → 还原成 raw 地址
7565
+ var tail = cur.split('cdn.jsdelivr.net/gh/')[1] || '';
7566
+ var m = tail.match(/^([^@]+)@([^/]+)\/(.*)$/);
7567
+ if (m) {
7568
+ var raw = 'https://raw.githubusercontent.com/' + m[1] + '/' + m[2] + '/' + m[3];
7569
+ next = n === 0 ? ('https://gh-proxy.com/' + raw)
7570
+ : n === 1 ? ('https://ghproxy.net/' + raw)
7571
+ : raw;
7572
+ }
7573
+ } else if (cur.indexOf('gh-proxy.com/') >= 0) {
7574
+ var r2 = cur.replace('https://gh-proxy.com/', '');
7575
+ next = n <= 1 ? ('https://ghproxy.net/' + r2) : r2;
7576
+ } else if (cur.indexOf('ghproxy.net/') >= 0) {
7577
+ next = cur.replace('https://ghproxy.net/', '');
7578
+ }
7579
+ if (next && n < 3) {
7580
+ el.setAttribute('data-tries', String(n + 1));
7581
+ el.src = next;
6900
7582
  } else {
6901
- el.style.display = 'none'; // 都失败就藏掉,别破版
7583
+ el.style.display = 'none'; // 四个源都不通 → 藏掉,别破版
6902
7584
  }
6903
7585
  } catch (x) {}
6904
7586
  },