@zaofan/dsh-qqbot 1.10.48 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -3
- package/client/qqbot-settings.js +487 -30
- package/dist/features/ext-capabilities.d.ts +2 -0
- package/dist/features/ext-capabilities.d.ts.map +1 -1
- package/dist/features/ext-capabilities.js +38 -0
- package/dist/features/ext-capabilities.js.map +1 -1
- package/dist/features/ext-reload.d.ts +2 -2
- package/dist/features/ext-reload.d.ts.map +1 -1
- package/dist/features/ext-reload.js +30 -8
- package/dist/features/ext-reload.js.map +1 -1
- package/dist/features/ext-ui-seed.d.ts +25 -0
- package/dist/features/ext-ui-seed.d.ts.map +1 -0
- package/dist/features/ext-ui-seed.js +187 -0
- package/dist/features/ext-ui-seed.js.map +1 -0
- package/dist/features/ext-ui.d.ts +3 -5
- package/dist/features/ext-ui.d.ts.map +1 -1
- package/dist/features/ext-ui.js +34 -12
- package/dist/features/ext-ui.js.map +1 -1
- package/dist/features/extension-store.d.ts.map +1 -1
- package/dist/features/extension-store.js +3 -0
- package/dist/features/extension-store.js.map +1 -1
- package/dist/gateway/bootstrap.d.ts.map +1 -1
- package/dist/gateway/bootstrap.js +18 -1
- package/dist/gateway/bootstrap.js.map +1 -1
- package/dist/gateway/middleware-setup.d.ts.map +1 -1
- package/dist/gateway/middleware-setup.js +11 -1
- package/dist/gateway/middleware-setup.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +131 -1
- package/dist/index.js.map +1 -1
- package/dist/session/session-manager.d.ts.map +1 -1
- package/dist/session/session-manager.js +2 -1
- package/dist/session/session-manager.js.map +1 -1
- package/docs/extension-contract.md +400 -255
- package/examples/ext-commands//346/211/251/345/261/225/347/244/272/344/276/213.mjs +84 -0
- package/examples/ext-hooks/demo-store.mjs +75 -0
- package/examples/ext-ui/demo/dock/card.css +55 -0
- package/examples/ext-ui/demo/dock/card.html +95 -0
- package/examples/ext-ui/demo/dock/card.js +261 -0
- package/examples/ext-ui/demo/settings/card.css +61 -0
- package/examples/ext-ui/demo/settings/card.html +38 -0
- package/examples/ext-ui/demo/settings/card.js +87 -0
- package/package.json +145 -144
- package/settings-host.js +6693 -6244
- package/examples/ext-ui/local-llm/card.css +0 -66
- package/examples/ext-ui/local-llm/card.html +0 -38
- package/examples/ext-ui/local-llm/card.js +0 -59
package/client/qqbot-settings.js
CHANGED
|
@@ -603,13 +603,19 @@ window.__ModuleLoader__.load({
|
|
|
603
603
|
// .qqs-top-tabs / .qqs-ext-cards 上把纵向滚轮折算成 scrollLeft。
|
|
604
604
|
if (!window.__qqsExtWheelBound) {
|
|
605
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
|
+
}
|
|
606
615
|
document.addEventListener('wheel', function (e) {
|
|
607
616
|
var el = e.target
|
|
608
|
-
while (el
|
|
609
|
-
|
|
610
|
-
var hit = el.id === 'qqs-top-tabs'
|
|
611
|
-
|| (el.classList && (el.classList.contains('qqs-top-tabs') || el.classList.contains('qqs-ext-cards')))
|
|
612
|
-
if (hit) {
|
|
617
|
+
while (el) {
|
|
618
|
+
if (isScrollBar(el)) {
|
|
613
619
|
if (el.scrollWidth > el.clientWidth + 1 && Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
|
|
614
620
|
el.scrollLeft += e.deltaY
|
|
615
621
|
e.preventDefault()
|
|
@@ -619,35 +625,259 @@ window.__ModuleLoader__.load({
|
|
|
619
625
|
el = el.parentNode
|
|
620
626
|
}
|
|
621
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)
|
|
622
670
|
}
|
|
623
671
|
} catch (e) { /* ignore */ }
|
|
624
672
|
}
|
|
625
|
-
function extUiLoad(extId, f) {
|
|
626
|
-
|
|
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)
|
|
627
678
|
.then(function (r) { return r.json() })
|
|
628
679
|
.then(function (j) { return j && j.ok ? String(j.text || '') : '' })
|
|
629
680
|
.catch(function () { return '' })
|
|
630
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
|
+
}
|
|
631
865
|
/** 把一张扩展卡片用 Shadow DOM 挂进容器 */
|
|
632
|
-
function extUiMountCard(container, extId, cls) {
|
|
866
|
+
function extUiMountCard(container, extId, cls, dockCtx, acctCtx) {
|
|
633
867
|
var host = document.createElement('div')
|
|
634
868
|
host.className = cls || 'qqs-ext-card'
|
|
635
869
|
var sr = host.attachShadow({ mode: 'open' })
|
|
636
870
|
container.appendChild(host)
|
|
637
|
-
|
|
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
|
+
])
|
|
638
877
|
.then(function (res) {
|
|
639
878
|
sr.innerHTML = '<style>' + (res[1] || '') + '</style>' + (res[0] || '')
|
|
640
879
|
if (!res[2]) return
|
|
641
|
-
var api =
|
|
642
|
-
extId: extId,
|
|
643
|
-
api: function (p, opts) {
|
|
644
|
-
return fetch('/api/ext/' + encodeURIComponent(extId) + String(p || ''),
|
|
645
|
-
Object.assign({ headers: { 'Content-Type': 'application/json' } }, opts || {}))
|
|
646
|
-
.then(function (r) { return r.json() })
|
|
647
|
-
.catch(function () { return null })
|
|
648
|
-
},
|
|
649
|
-
toast: function (m) { try { window.alert(String(m)) } catch (e) { /* ignore */ } },
|
|
650
|
-
}
|
|
880
|
+
var api = extUiApi(extId, dockCtx ? 'data' : 'profile', dockCtx, acctCtx)
|
|
651
881
|
try {
|
|
652
882
|
new Function('qqbotExt', 'shadowRoot', res[2])(api, sr)
|
|
653
883
|
} catch (e) {
|
|
@@ -663,19 +893,150 @@ window.__ModuleLoader__.load({
|
|
|
663
893
|
function ExtCardPage(props) {
|
|
664
894
|
var extId = String((props && props.extId) || '')
|
|
665
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
|
|
666
900
|
useEffect(function () {
|
|
667
901
|
extUiEnsureCss()
|
|
668
902
|
var box = document.getElementById('qqs-ext-page')
|
|
669
903
|
if (!box || !extId) return
|
|
670
904
|
box.innerHTML = ''
|
|
671
|
-
extUiMountCard(box, extId, 'qqs-ext-page-card')
|
|
905
|
+
extUiMountCard(box, extId, 'qqs-ext-page-card', null, acct)
|
|
672
906
|
return function () { try { box.innerHTML = '' } catch (e) { /* ignore */ } }
|
|
673
|
-
}, [extId])
|
|
907
|
+
}, [extId, acct && acct.ns])
|
|
674
908
|
return h('div', { style: { maxWidth: 720 } },
|
|
675
909
|
h('h2', { style: { margin: '0 0 10px' } }, title),
|
|
676
910
|
h('div', { id: 'qqs-ext-page', style: { display: 'flex', flexWrap: 'wrap', gap: 12, alignItems: 'flex-start' } }))
|
|
677
911
|
}
|
|
678
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
|
+
|
|
679
1040
|
function SettingsPage(props) {
|
|
680
1041
|
var close = props && props.close
|
|
681
1042
|
// 多账号: 当前操作账号的 settings 命名空间(主=im-qqbot 不带参, 其它=实例 id)
|
|
@@ -1752,7 +2113,12 @@ window.__ModuleLoader__.load({
|
|
|
1752
2113
|
}
|
|
1753
2114
|
var ac = currentAcct()
|
|
1754
2115
|
// 传给按账号视图的上下文: 设置按 ns, 图库按 dataDir, 定时按 schedDir
|
|
1755
|
-
|
|
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
|
|
1756
2122
|
// key=账号: 切号即强制子页整页重挂(清掉旧账号 state, 全部重新拉取)
|
|
1757
2123
|
var subKey = ac ? ac.id : 'main'
|
|
1758
2124
|
var sub = tab === 'gallery' ? h(GalleryPage, { key: subKey, close: props && props.close, acct: acctx })
|
|
@@ -1760,8 +2126,9 @@ window.__ModuleLoader__.load({
|
|
|
1760
2126
|
: tab === 'accounts' ? h(AccountsPage, { close: props && props.close })
|
|
1761
2127
|
: String(tab).indexOf('ext:') === 0
|
|
1762
2128
|
? h(ExtCardPage, {
|
|
1763
|
-
key: String(tab).slice(4),
|
|
2129
|
+
key: String(tab).slice(4) + '|' + subKey, // 切账号即重挂(同其他子页)
|
|
1764
2130
|
extId: String(tab).slice(4),
|
|
2131
|
+
acct: acctx, // ★ 卡片据此可对当前账号读写
|
|
1765
2132
|
title: (function () {
|
|
1766
2133
|
var hit = null
|
|
1767
2134
|
extTabs.forEach(function (x) { if (x.extId === String(tab).slice(4)) hit = x })
|
|
@@ -1787,8 +2154,47 @@ window.__ModuleLoader__.load({
|
|
|
1787
2154
|
// 放在**同一个元素**上 —— sticky 会创建新的格式化上下文,导致滚动容器的
|
|
1788
2155
|
// scrollWidth 计算异常(看着溢出、实际判定为没溢出),于是滚不动。
|
|
1789
2156
|
// 拆成两层:外层只管 sticky + 裁剪,内层只管横向滚动。
|
|
1790
|
-
|
|
1791
|
-
|
|
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' } },
|
|
1792
2198
|
// 内置四页 + 扩展页签(2026-10-26):扩展 tab 用青蓝色区分,放在最右边。
|
|
1793
2199
|
// 容器从「四个 flex:1 平分」改成「按钮 flex:0 0 auto + 容器横向可滑」——
|
|
1794
2200
|
// 否则扩展一多,按钮会被压成一条缝(这跟 .dk-tab 那套是同一个道理)。
|
|
@@ -2328,7 +2734,7 @@ var QQS_THEME_CSS = [
|
|
|
2328
2734
|
// · 群管理(发消息/审批入群/禁言) → 悬浮球点开成操作台
|
|
2329
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}"
|
|
2330
2736
|
// ── 💬 聊天视图样式(dock 追加段, 2026-09-07): QQ 风格气泡, 群友左(bot)右 ──
|
|
2331
|
-
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:
|
|
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)}"
|
|
2332
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) {} };
|
|
2333
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) {} }
|
|
2334
2740
|
function startQqDock(sessionsSvc) {
|
|
@@ -2555,6 +2961,9 @@ var QQS_THEME_CSS = [
|
|
|
2555
2961
|
|
|
2556
2962
|
// ── 面板状态(每个实例独立保存, 切回不丢) ──
|
|
2557
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 */ }
|
|
2558
2967
|
// 🗂 自定义目标分组(仿 QQ 分组): 2026-09-12 起**host 持久化**({dataRoot}/.qqbot/target-groups.json),
|
|
2559
2968
|
// localStorage 只当秒开缓存 —— 这样 **AI 与主人共用同一份分组**(agent 侧 broadcast_send 可直接写分组名群发),
|
|
2560
2969
|
// 顺带修掉"换个浏览器分组就没了"的老毛病。
|
|
@@ -4166,6 +4575,16 @@ var QQS_THEME_CSS = [
|
|
|
4166
4575
|
}
|
|
4167
4576
|
function paintBody() {
|
|
4168
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 */ }
|
|
4169
4588
|
var b = panel.querySelector('.dk-b')
|
|
4170
4589
|
if (!b) return
|
|
4171
4590
|
var groupOpts = state.groups.map(function (g) {
|
|
@@ -4179,6 +4598,7 @@ var QQS_THEME_CSS = [
|
|
|
4179
4598
|
var label = c.name ? (c.name + ' (' + tail + ')') : ('私聊 ' + tail)
|
|
4180
4599
|
return '<option value="' + esc(c.id) + '"' + (c.id === state.sendTo ? ' selected' : '') + '>' + esc(label) + '</option>'
|
|
4181
4600
|
}).join('')
|
|
4601
|
+
ensureDockExtTabs()
|
|
4182
4602
|
var tabs = '<div class="dk-tab">'
|
|
4183
4603
|
+ '<button data-t="roster" class="' + (state.tab === 'roster' ? 'on' : '') + '">📇 群组管理</button>'
|
|
4184
4604
|
+ '<button data-t="chat" class="' + (state.tab === 'chat' ? 'on' : '') + '">💬 聊天</button>'
|
|
@@ -4187,6 +4607,10 @@ var QQS_THEME_CSS = [
|
|
|
4187
4607
|
+ '<button data-t="out" class="' + (state.tab === 'out' ? 'on' : '') + '">⚙️ 出站</button>'
|
|
4188
4608
|
+ '<button data-t="send" class="' + (['send','broadcast','bp','card'].indexOf(state.tab) >= 0 ? 'on' : '') + '">📤 群发</button>'
|
|
4189
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('')
|
|
4190
4614
|
+ '</div>'
|
|
4191
4615
|
var body = tabs
|
|
4192
4616
|
var status = state.msg ? '<div class="dk-msg" style="color:#2f9e44;margin:4px 0">' + esc(state.msg) + '</div>' : ''
|
|
@@ -4369,6 +4793,10 @@ var QQS_THEME_CSS = [
|
|
|
4369
4793
|
body += '详细主动=聊天行为与适配主动完全一样, 但额外把 AI 的工具调用(🔧 工具调用)与工具结果也推到 QQ —— 能在 QQ 上看她正在干什么; 消息会明显变多, 看完记得切回适配主动。'
|
|
4370
4794
|
body += '被动=始终以「回复你那条」发出, 连发约4~5条后会被QQ吞掉。完全不出站=本机静默, 不向QQ发任何回复(AI 可用工具随时切回)。本开关对纯web(没绑QQ)的会话不生效。</div>'
|
|
4371
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>'
|
|
4372
4800
|
} else if (state.tab === 'session') {
|
|
4373
4801
|
// ⚙ 单会话设置(2026-09-13 主人定): 放"每个会话单独生效"的设置。
|
|
4374
4802
|
// 次级标签: 🧠 智能回复 / 📄 空白样板(留作以后加新设置的模板)。
|
|
@@ -4697,6 +5125,13 @@ var QQS_THEME_CSS = [
|
|
|
4697
5125
|
+ '<input class="qqs-txt" id="dk-bindname" placeholder="备注(可选)" value="' + esc(state.bindName) + '" style="width:110px">'
|
|
4698
5126
|
+ '<button class="dk-btn" id="dk-bind">绑定</button></div>'
|
|
4699
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
|
+
}
|
|
4700
5135
|
bindBodyEvents()
|
|
4701
5136
|
bindPanelDrag()
|
|
4702
5137
|
if (state.tab === 'send') applyTargetFilter(panel, state)
|
|
@@ -7116,14 +7551,36 @@ var QQS_THEME_CSS = [
|
|
|
7116
7551
|
loading: 'lazy',
|
|
7117
7552
|
className: 'qqs-mk-cover',
|
|
7118
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 不重渲染就不会丢这个属性。
|
|
7119
7558
|
try {
|
|
7120
7559
|
var el = e.target;
|
|
7121
7560
|
var cur = String(el.src || '');
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
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;
|
|
7125
7582
|
} else {
|
|
7126
|
-
el.style.display = 'none'; //
|
|
7583
|
+
el.style.display = 'none'; // 四个源都不通 → 藏掉,别破版
|
|
7127
7584
|
}
|
|
7128
7585
|
} catch (x) {}
|
|
7129
7586
|
},
|