@zaofan/dsh-qqbot 1.11.8 → 1.11.9

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.
@@ -2760,6 +2760,34 @@ var QQS_THEME_CSS = [
2760
2760
  document.body.appendChild(wrap)
2761
2761
  var ball = wrap.querySelector('#qqs-dock-ball')
2762
2762
  var badge = wrap.querySelector('#qqs-dock-badge')
2763
+ // ── 2026-10-28(主人定):悬浮球**只在 QQ 会话里显示** ──
2764
+ // 判定沿用 session-lookup(state.detectedHit 的同一个接口),未命中就不显示。
2765
+ // 为什么还要轮询:会话切换(web 侧点开另一个会话)不会通知这里;
2766
+ // 只比 sessionId 字符串,变了才发请求,开销可忽略。
2767
+ var _dockSid = ''
2768
+ var _dockHit = null
2769
+ function refreshDockVisibility() {
2770
+ var sid = ''
2771
+ try { sid = curSessionId() } catch (eS) { sid = '' }
2772
+ // 同一会话、已知结论 → 直接套用(不发请求,也不闪)
2773
+ if (sid === _dockSid && _dockHit !== null) { wrap.style.display = _dockHit ? '' : 'none'; return }
2774
+ _dockSid = sid
2775
+ if (!sid) { _dockHit = false; wrap.style.display = 'none'; return }
2776
+ // 会话变了:先按"未命中"藏起来,拿到结论再显示(宁可晚一点,也别让它乱闪)
2777
+ _dockHit = null
2778
+ wrap.style.display = 'none'
2779
+ qqHitOf(sid, function (hit) {
2780
+ if (sid !== _dockSid) return // 查询期间又切了会话:这次结论作废
2781
+ _dockHit = hit
2782
+ wrap.style.display = hit ? '' : 'none'
2783
+ })
2784
+ }
2785
+ try {
2786
+ refreshDockVisibility()
2787
+ // 事件驱动(2026-10-28,主人嫌 2s 轮询费性能):不再 setInterval ——
2788
+ // apply() 里的 refreshQqUi 在「sessions.list 变化 / 窗口聚焦 / 页面重新可见」时调这里。
2789
+ window.__qqsRefreshDock = refreshDockVisibility
2790
+ } catch (eVis) { /* 显隐失败不影响球本身 */ }
2763
2791
  var panel = null
2764
2792
  var open = false
2765
2793
  // ⚠️ 2026-09-27 修复「拖到屏幕外后球永久消失」:拖拽时本来有 clamp(行1749),
@@ -6718,6 +6746,31 @@ var QQS_THEME_CSS = [
6718
6746
  return ''
6719
6747
  }
6720
6748
 
6749
+ /**
6750
+ * 当前会话是不是 QQ 会话(2026-10-28)。
6751
+ * 同一次查询在窗口内共享 —— 悬浮球显隐、扩展页签显隐都走这里,不重复打后端。
6752
+ * ⚠️ 否定结论只缓存 2 秒:宿主刚启动时后端可能还没就绪,长缓存会让悬浮球一直不出现。
6753
+ */
6754
+ function qqHitOf(sid, cb) {
6755
+ if (!sid) { try { cb(false) } catch (e0) {} ; return }
6756
+ var c = (window.__qqsHitCache = window.__qqsHitCache || {})
6757
+ var e = c[sid]
6758
+ if (e && Date.now() - e.at < (e.hit ? 10000 : 2000)) { try { cb(e.hit) } catch (e1) {} ; return }
6759
+ try {
6760
+ fetch('/api/qqbot-settings/session-lookup?sessionId=' + encodeURIComponent(sid))
6761
+ .then(function (r) { return r.json() })
6762
+ .then(function (d) {
6763
+ var hit = !!(d && d.hits && d.hits.length)
6764
+ c[sid] = { at: Date.now(), hit: hit }
6765
+ try { cb(hit) } catch (e2) {}
6766
+ })
6767
+ .catch(function () { try { cb(false) } catch (e3) {} })
6768
+ } catch (e4) { try { cb(false) } catch (e5) {} }
6769
+ }
6770
+
6771
+ // (2026-10-28)页签显隐不用 DOM 方案了 —— 见 apply() 里的 setExtTabVisible:
6772
+ // `slots.inject` 的返回值是 disposer,注销后宿主自己会刷新页签列表。
6773
+
6721
6774
  function curWorkspaceCwd() {
6722
6775
  try {
6723
6776
  var svc = _extSessionsSvc
@@ -7762,14 +7815,28 @@ var QQS_THEME_CSS = [
7762
7815
  function (props) { return h(QqbotHome, { close: props && props.close }) })
7763
7816
  })
7764
7817
  })
7765
- // 「QQ机器人扩展」会话页签(2026-10-24):与「对话/轨迹/记忆/白板看板」并列
7766
- safe('conversation view', function () {
7767
- slots.inject('conversation.view', function () {
7768
- return slots.register(
7769
- { name: 'conversation.view', id: 'qqbot-ext', order: 81, label: 'QQ机器人扩展' },
7770
- ExtPanel)
7771
- })
7772
- })
7818
+ // 「QQ机器人扩展」会话页签:**只在 QQ 会话里注入**(2026-10-28 主人定)
7819
+ // 为什么不用 DOM 隐藏:`slots.inject(key, cb)` 的返回值是 disposer(已核 ui-renderer 源码:
7820
+ // `return () => { disposeController(); }`),注销后宿主 `slots.subscribe("conversation.view")`
7821
+ // 会自动刷新页签列表 —— 比按文案找按钮稳得多。
7822
+ var _extTabDisposed = null
7823
+ function setExtTabVisible(show) {
7824
+ if (show) {
7825
+ if (_extTabDisposed) return
7826
+ try {
7827
+ _extTabDisposed = slots.inject('conversation.view', function () {
7828
+ return slots.register(
7829
+ { name: 'conversation.view', id: 'qqbot-ext', order: 81, label: 'QQ机器人扩展' },
7830
+ ExtPanel)
7831
+ }) || null
7832
+ if (!_extTabDisposed) _extTabDisposed = function () {}
7833
+ } catch (eTab) { _extTabDisposed = null }
7834
+ } else {
7835
+ if (!_extTabDisposed) return
7836
+ try { _extTabDisposed() } catch (eTab2) {}
7837
+ _extTabDisposed = null
7838
+ }
7839
+ }
7773
7840
  safe('approval float', startApprovalFloat)
7774
7841
  // sessions 服务(宿主根服务): 权威读 web 当前会话(current)与 cwd(agent id === session id)
7775
7842
  var sessionsSvc = null
@@ -7777,6 +7844,44 @@ var QQS_THEME_CSS = [
7777
7844
  if (!sessionsSvc) { try { sessionsSvc = ctx.get && ctx.get('sessions') } catch (e) {} }
7778
7845
  _extSessionsSvc = sessionsSvc // 供 ExtPanel 读当前工作区 cwd
7779
7846
  safe('dock', function () { startQqDock(sessionsSvc) })
7847
+ // ── 统一刷新:页签 + 悬浮球,**事件驱动,不轮询**(2026-10-28)──
7848
+ // 信号源:`sessions.list`(ObservableSnapshot,宿主在左侧会话列表切换时就会推)
7849
+ // + window focus / 页面可见性(兜底,切回标签页时补一次)。
7850
+ var _qqUiRetry = 0
7851
+ var refreshQqUi = function () {
7852
+ // dock 球的显隐也走这同一个信号(否则切会话时页签跟走了、球还赖着不动)
7853
+ try { if (typeof window !== 'undefined' && typeof window.__qqsRefreshDock === 'function') window.__qqsRefreshDock() } catch (eDock) {}
7854
+ var sid = ''
7855
+ try { sid = curSessionId() } catch (eSid) { sid = '' }
7856
+ // 有限次重试(≈7 秒内 6 次):宿主刚启动时"当前会话"或后端 lookup 可能还没就绪,
7857
+ // 一次性判空就再也不显示球了(2026-10-28 主人实测)。不是长期轮询。
7858
+ function retryLater() {
7859
+ if (_qqUiRetry >= 6) return
7860
+ _qqUiRetry++
7861
+ setTimeout(function () { refreshQqUi() }, 1200)
7862
+ }
7863
+ if (!sid) { setExtTabVisible(false); retryLater(); return }
7864
+ qqHitOf(sid, function (hit) {
7865
+ var now = ''
7866
+ try { now = curSessionId() } catch (eNow) {}
7867
+ if (now !== sid) return // 查询期间切走了:别把旧结论套到新会话上
7868
+ setExtTabVisible(!!hit)
7869
+ if (hit) { _qqUiRetry = 0; return }
7870
+ retryLater()
7871
+ })
7872
+ }
7873
+ safe('qq ui refresh', function () {
7874
+ refreshQqUi()
7875
+ try {
7876
+ if (sessionsSvc && sessionsSvc.list && typeof sessionsSvc.list.subscribe === 'function') {
7877
+ sessionsSvc.list.subscribe(function () { refreshQqUi() })
7878
+ }
7879
+ } catch (eSub) {}
7880
+ try {
7881
+ window.addEventListener('focus', refreshQqUi)
7882
+ document.addEventListener('visibilitychange', function () { if (!document.hidden) refreshQqUi() })
7883
+ } catch (eWin) {}
7884
+ })
7780
7885
  }
7781
7886
 
7782
7887
  exports.inject = ['slots', 'sessions']
@@ -110,6 +110,24 @@ export declare function syncContextTools(agentCtx: unknown, shouldHave: boolean,
110
110
  */
111
111
  export declare function ensureExtToolsGlobal(globalCtx: unknown): void;
112
112
  export declare function ensureContextToolsGlobal(globalCtx: unknown): void;
113
+ /**
114
+ * 按会话同步「上下文压缩三件套 + QQ 群管理」的**注册状态**(幂等)。
115
+ *
116
+ * @param agentCtx 该 QQ 会话的 agent 作用域(`record.agentCtx` 或 `agent.ctx`)
117
+ * @param opts.ctxTools 要不要 context_memo / context_compact / context_drop(跟「无上下文·智能判断」开关)
118
+ * @param opts.groupAdmin 要不要 qq_group_admin(跟 groupAdmin.enabled)
119
+ * @param opts.dataRoot 会话坐标(工具执行时反查数据根用)
120
+ * @param opts.sessionKey 会话坐标(备忘类工具要求非空)
121
+ *
122
+ * 失败策略:拿不到 `tools.register` 或注册抛错时**不记状态**(写 `__retry`),下一次 pre-step 重试;
123
+ * 绝不影响会话主流程。
124
+ */
125
+ export declare function syncChannelScopedTools(agentCtx: unknown, opts: {
126
+ ctxTools: boolean;
127
+ groupAdmin: boolean;
128
+ dataRoot?: string;
129
+ sessionKey?: string;
130
+ }): void;
113
131
  export declare function applyExtDisabledRestrict(agentCtx: unknown, dataRoot: string, extToolNames: string[]): void;
114
132
  export declare function restrictContextTools(agentCtx: unknown, shouldHave: boolean, env?: {
115
133
  dataRoot?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"channel-tools.d.ts","sourceRoot":"","sources":["../src/channel-tools.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"channel-tools.d.ts","sourceRoot":"","sources":["../src/channel-tools.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAInD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AA8FlE,6DAA6D;AAC7D,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,eAAO,MAAM,IAAI,wBAAwB,CAAC;AAI1C,eAAO,MAAM,MAAM,UAAY,CAAC;AAoOhC,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAiC;AAkD5F,wCAAwC;AACxC,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAErH;AACD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAE9E;AACD,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI,CAI/D;AAkFD,mDAAmD;AACnD,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAo8EvD;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,OAAO,GAAG,KAAK,EACtB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAkBvC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,OAAO,GAAG,KAAK,EACtB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CA+BvC;AAOD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM;;;;;;;;;IAE1C;AAED,yCAAyC;AACzC,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAExF;AAYD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAExD;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAKxI;AA4RD,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAyI;AACzL,wBAAgB,eAAe,IAAI,MAAM,CAA2F;AA8CpI;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CA+CtJ;AA0BD;;;;;GAKG;AACH;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CAkH7D;AAED,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CAwCjE;AA+ED;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,OAAO,EACjB,IAAI,EAAE;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GACvF,IAAI,CA4CN;AAID,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,IAAI,CAsC1G;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA0CnI"}
@@ -4047,6 +4047,83 @@ function toolsOfScope(agentCtx) {
4047
4047
  *
4048
4048
  * 幂等:restrict 返回 dispose;每次 apply 先撤销上一次的,再按当前 disabled 重挂。
4049
4049
  */
4050
+ // ══════════════════════════════════════════════════════════════════════════
4051
+ // 2026-10-28(主人定,对应 issue #26):上下文/群管理工具改为**按 QQ 会话注册**
4052
+ // ══════════════════════════════════════════════════════════════════════════
4053
+ // 旧做法(2026-10-09 起):「插件根作用域全局注册一次 + pre-step 逐会话 restrict deny」。
4054
+ // · 当初改成全局,是为了避开"每步先注销再重注册 → 工具表反复变动 → 宿主每步发
4055
+ // 「工具已更新·移除/新增」"的抖动(那批通知还进上下文,纯烧 token)。
4056
+ // · 代价是:① 第一次 pre-step 之前,无关工作区的会话也能看到这四个名字;
4057
+ // ② `restrict` 只过滤**继承来的**工具,全局层本身就是泄漏源,剪枝只是事后补救。
4058
+ //
4059
+ // 现在照 `mountChannelTools` 的路子(**per-agent register**),配一个幂等状态机:
4060
+ // · 状态没变 → 一根手指都不动(不产生任何变更通知,同样不抖);
4061
+ // · 状态变了 → 先撤旧的、再按新状态注册(面板一改开关,下一 step 生效)。
4062
+ // 非 QQ 会话根本不经过这里 ⇒ 天生看不到,零窗口期、零 token。
4063
+ const ctxScopeState = new WeakMap();
4064
+ /**
4065
+ * 按会话同步「上下文压缩三件套 + QQ 群管理」的**注册状态**(幂等)。
4066
+ *
4067
+ * @param agentCtx 该 QQ 会话的 agent 作用域(`record.agentCtx` 或 `agent.ctx`)
4068
+ * @param opts.ctxTools 要不要 context_memo / context_compact / context_drop(跟「无上下文·智能判断」开关)
4069
+ * @param opts.groupAdmin 要不要 qq_group_admin(跟 groupAdmin.enabled)
4070
+ * @param opts.dataRoot 会话坐标(工具执行时反查数据根用)
4071
+ * @param opts.sessionKey 会话坐标(备忘类工具要求非空)
4072
+ *
4073
+ * 失败策略:拿不到 `tools.register` 或注册抛错时**不记状态**(写 `__retry`),下一次 pre-step 重试;
4074
+ * 绝不影响会话主流程。
4075
+ */
4076
+ export function syncChannelScopedTools(agentCtx, opts) {
4077
+ try {
4078
+ if (!agentCtx || typeof agentCtx !== 'object')
4079
+ return;
4080
+ const key = agentCtx;
4081
+ // 会话坐标每次都刷新(工具执行时靠它定位数据根/会话)
4082
+ if (opts.dataRoot) {
4083
+ ctxEnvMap.set(key, { dataRoot: String(opts.dataRoot), sessionKey: String(opts.sessionKey ?? '') });
4084
+ }
4085
+ const sig = `${opts.ctxTools ? '1' : '0'}${opts.groupAdmin ? '1' : '0'}`;
4086
+ const st = ctxScopeState.get(key);
4087
+ if (st && st.sig === sig)
4088
+ return; // 状态一致 → 不动(这是"不抖"的关键)
4089
+ const toolsObj = toolsOfScope(agentCtx);
4090
+ const reg = toolsObj?.register;
4091
+ if (!toolsObj || typeof reg !== 'function') {
4092
+ traceToolReg('[scoped-tool] 拿不到 tools.register,跳过(下次再试)');
4093
+ return;
4094
+ }
4095
+ // 先撤旧的(换开关后不能叠加)
4096
+ if (st)
4097
+ for (const d of st.disposers) {
4098
+ try {
4099
+ d();
4100
+ }
4101
+ catch { /* ignore */ }
4102
+ }
4103
+ const want = [
4104
+ ...(opts.ctxTools ? ctxToolsRef() : []),
4105
+ ...(opts.groupAdmin ? qunAdminRef() : []),
4106
+ ];
4107
+ const disposers = [];
4108
+ for (const t of want) {
4109
+ try {
4110
+ const d = reg.call(toolsObj, t);
4111
+ if (typeof d === 'function')
4112
+ disposers.push(d);
4113
+ }
4114
+ catch { /* 单个工具失败不影响其它 */ }
4115
+ }
4116
+ // 全成功才认这个 sig;否则标 __retry,下一 step 重来
4117
+ const okAll = disposers.length === want.length;
4118
+ ctxScopeState.set(key, { sig: okAll ? sig : `__retry-${sig}`, disposers });
4119
+ traceToolReg(`[scoped-tool] 按会话注册 ctx=${opts.ctxTools ? 1 : 0} groupAdmin=${opts.groupAdmin ? 1 : 0}`
4120
+ + ` want=${want.length} ok=${disposers.length}`
4121
+ + ` dataRoot=${String(opts.dataRoot ?? '')} sk=${String(opts.sessionKey ?? '')}`);
4122
+ }
4123
+ catch (e) {
4124
+ traceToolReg('[scoped-tool] 异常: ' + String(e?.message ?? e).slice(0, 160));
4125
+ }
4126
+ }
4050
4127
  let __extRestrictDispose;
4051
4128
  export function applyExtDisabledRestrict(agentCtx, dataRoot, extToolNames) {
4052
4129
  try {