@zaofan/dsh-qqbot 1.11.11 → 1.11.12

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.
@@ -416,6 +416,7 @@ window.__ModuleLoader__.load({
416
416
  { k: 'groupAdmin.manageGroup', kw: '群管理 默认群 group_openid 目标群', t: 'str', label: '对话内默认管理群 group_openid(web 对话时群工具用它; QQ 群会话自动用当前群)', wide: true, def: '' },
417
417
  { k: 'groupAdmin.watchJoinRequests', kw: '入群申请 事件 提醒 订阅', t: 'bool', label: '实时接收"入群申请"事件并自动提醒(勾选后需重启才生效)', def: false },
418
418
  { k: 'groupAdmin.notifyInGroup', kw: '入群申请 提醒 群内 通知', t: 'bool', label: '收到入群申请时在该群内发提醒消息', def: true },
419
+ { k: 'extMetaTool', kw: '扩展工具 元工具 gain 按需 领取 token 省', t: 'bool', label: '扩展工具按需取用(默认开: 全局只挂 gain_qq_tools 元工具, 非 QQ 会话需要时自己领取; 关掉=全部扩展工具全量暴露)', def: true },
419
420
  { t: 'custom', render: function (ctx) { return h(GroupAdminPanel, { key: 'ga-' + (ctx.ns || 'main'), ns: ctx.ns || undefined }) } },
420
421
  ],
421
422
  },
@@ -89,25 +89,6 @@ export declare function syncContextTools(agentCtx: unknown, shouldHave: boolean,
89
89
  dataRoot?: string;
90
90
  sessionKey?: string;
91
91
  }, reminderText?: string): void;
92
- /**
93
- * 把"三个上下文工具 + QQ 群管理工具"**全局注册一次、永不注销**。
94
- *
95
- * 幂等靠 globalThis 记句柄:跨热刷(模块重新 import)依然有效,避免热刷后重复注册被宿主判重名。
96
- * ⚠️ 可见性不在这里决定 —— 交给 {@link restrictContextTools} 按会话剪枝。
97
- */
98
- /**
99
- * 把 `expose: 'all'` 的扩展工具注册到**插件根作用域**(2026-10-10 主人要的"暴露给全部会话")。
100
- *
101
- * 为什么必须挂在这一层:扩展工具平时由 session-manager 在**每个 QQ 会话**上用 `agentCtx` 注册,
102
- * 而 web / 终端等**非 QQ 会话不走那条链** ⇒ 压根看不到它们。想让它们跨通道,
103
- * 只能挂到插件根 —— 宿主会让所有会话继承插件作用域的工具(`context_*` 三工具当年也是为此才改成全局)。
104
- *
105
- * 命名:`<name>@<人设 id>`,并且**每个账号实例各注册一份**(实例各有自己的 preset / appId / 账号数据);
106
- * 执行时由注册名里的 tag 反查账号包(见 buildWorkspaceQqPack)—— **名字即归属**。
107
- *
108
- * 幂等:句柄存 globalThis(跨热刷可见),已注册过的 `name@tag` 不重复注册。
109
- * fail-soft:拿不到 tools 服务 / 没有实例 / 目录读不到,一律静默跳过,绝不影响主流程。
110
- */
111
92
  export declare function ensureExtToolsGlobal(globalCtx: unknown): void;
112
93
  export declare function ensureContextToolsGlobal(globalCtx: unknown): void;
113
94
  /**
@@ -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":"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"}
@@ -3764,25 +3764,196 @@ export function syncContextTools(agentCtx, shouldHave, env, reminderText) {
3764
3764
  * ⚠️ 2026-10-10:`qq_group_admin` 也在这里 —— 它跟 context_* 一样是**QQ 通道专属**,
3765
3765
  * 全局注册后若不在非 QQ 会话里 deny 掉,会白占每个会话的 token(主人要求剪枝覆盖非 QQ 会话)。 */
3766
3766
  const CTX_TOOL_NAMES = ['context_memo', 'context_compact', 'context_drop', 'qq_group_admin'];
3767
- /**
3768
- * 把"三个上下文工具 + QQ 群管理工具"**全局注册一次、永不注销**。
3769
- *
3770
- * 幂等靠 globalThis 记句柄:跨热刷(模块重新 import)依然有效,避免热刷后重复注册被宿主判重名。
3771
- * ⚠️ 可见性不在这里决定 —— 交给 {@link restrictContextTools} 按会话剪枝。
3772
- */
3773
- /**
3774
- * 把 `expose: 'all'` 的扩展工具注册到**插件根作用域**(2026-10-10 主人要的"暴露给全部会话")。
3775
- *
3776
- * 为什么必须挂在这一层:扩展工具平时由 session-manager 在**每个 QQ 会话**上用 `agentCtx` 注册,
3777
- * 而 web / 终端等**非 QQ 会话不走那条链** ⇒ 压根看不到它们。想让它们跨通道,
3778
- * 只能挂到插件根 —— 宿主会让所有会话继承插件作用域的工具(`context_*` 三工具当年也是为此才改成全局)。
3779
- *
3780
- * 命名:`<name>@<人设 id>`,并且**每个账号实例各注册一份**(实例各有自己的 preset / appId / 账号数据);
3781
- * 执行时由注册名里的 tag 反查账号包(见 buildWorkspaceQqPack)—— **名字即归属**。
3782
- *
3783
- * 幂等:句柄存 globalThis(跨热刷可见),已注册过的 `name@tag` 不重复注册。
3784
- * fail-soft:拿不到 tools 服务 / 没有实例 / 目录读不到,一律静默跳过,绝不影响主流程。
3785
- */
3767
+ /** 本轮扫描到的「可领取」扩展工具(full 名 → 条目) */
3768
+ const extToolIndex = new Map();
3769
+ /** 已领取到某个会话作用域的工具(agentCtx → full → disposer) */
3770
+ const extGranted = new WeakMap();
3771
+ const GAIN_TOOL_NAME = 'gain_qq_tools';
3772
+ /** 取当前调用方的 agent ctx(enable 的目标作用域) */
3773
+ function agentCtxOf(exec) {
3774
+ try {
3775
+ const c = exec?.agent?.ctx;
3776
+ return c && typeof c === 'object' ? c : undefined;
3777
+ }
3778
+ catch {
3779
+ return undefined;
3780
+ }
3781
+ }
3782
+ /** 把一个扩展工具定义包成 dsh 工具(与全局注册同款;抽出来给 enable 复用) */
3783
+ function buildExtTool(ent, globalCtx, logger) {
3784
+ return defineTool({
3785
+ name: ent.full,
3786
+ description: String(ent.def.description ?? '') + ' (用户扩展工具 · ' + ent.tag + ')',
3787
+ parameters: (ent.def.inputSchema ?? { type: 'object', properties: {} }),
3788
+ output: {
3789
+ schema: {
3790
+ type: 'object', additionalProperties: false,
3791
+ properties: { ok: { type: 'boolean', required: true }, msg: { type: 'string', required: true } },
3792
+ },
3793
+ render: (_a, v) => [{ type: 'text', text: v.ok ? v.msg : ('失败: ' + v.msg) }],
3794
+ },
3795
+ async execute(args, exec) {
3796
+ let pack;
3797
+ try {
3798
+ pack = buildWorkspaceQqPack(ent.tag, ent.inst.dataRoot);
3799
+ }
3800
+ catch {
3801
+ pack = undefined;
3802
+ }
3803
+ const env = {
3804
+ cwd: ent.inst.cwd || ent.inst.dataRoot,
3805
+ exec,
3806
+ ctx: globalCtx,
3807
+ logger,
3808
+ qq: pack,
3809
+ };
3810
+ try {
3811
+ const sess = findSessionRec(channelOf(exec), exec);
3812
+ if (sess) {
3813
+ env.manager = sess.ch.manager;
3814
+ env.sender = sess.ch.sender;
3815
+ env.replyTarget = sess.rec.replyTarget;
3816
+ }
3817
+ else if (pack) {
3818
+ env.sender = pack.sender;
3819
+ }
3820
+ }
3821
+ catch { /* ignore */ }
3822
+ try {
3823
+ const r = await ent.def.run(args, env);
3824
+ if (r && typeof r === 'object')
3825
+ return { ok: r.ok === true, msg: String(r.msg ?? 'done') };
3826
+ return { ok: true, msg: r === undefined || r === null ? 'done' : String(r) };
3827
+ }
3828
+ catch (err) {
3829
+ return { ok: false, msg: err instanceof Error ? err.message : String(err) };
3830
+ }
3831
+ },
3832
+ });
3833
+ }
3834
+ /** 元工具本体(全局注册一次) */
3835
+ function makeGainTool(globalCtx, logger) {
3836
+ return defineTool({
3837
+ name: GAIN_TOOL_NAME,
3838
+ description: '按需取用「QQ 实例的扩展工具」:先 list / search 看有哪些(只回名字+一句话),'
3839
+ + '再 enable 把某个装进当前会话,之后就能调用它(非 QQ 会话想往 QQ 发消息、读会话时用)。'
3840
+ + 'disable 可卸下。',
3841
+ parameters: {
3842
+ type: 'object',
3843
+ properties: {
3844
+ action: { type: 'string', enum: ['list', 'search', 'enable', 'disable'], description: 'list=列出全部 / search=按关键词搜 / enable=装到当前会话 / disable=卸下' },
3845
+ query: { type: 'string', description: 'search 用:关键词(匹配工具名与描述)' },
3846
+ tool: { type: 'string', description: 'enable/disable 用:工具名(形如 qq_send__whale-girl,可先 list 查)' },
3847
+ tag: { type: 'string', description: '可选:只看某个实例(tag)' },
3848
+ },
3849
+ required: ['action'],
3850
+ },
3851
+ output: {
3852
+ schema: {
3853
+ type: 'object', additionalProperties: false,
3854
+ properties: { ok: { type: 'boolean', required: true }, msg: { type: 'string', required: true } },
3855
+ },
3856
+ render: (_a, v) => [{ type: 'text', text: v.ok ? v.msg : ('失败: ' + v.msg) }],
3857
+ },
3858
+ async execute(args, exec) {
3859
+ const action = String(args?.action ?? 'list');
3860
+ const q = String(args?.query ?? '').trim().toLowerCase();
3861
+ const wantTool = String(args?.tool ?? '').trim();
3862
+ const wantTag = String(args?.tag ?? '').trim();
3863
+ const rows = [...extToolIndex.values()].filter((e) => (!wantTag || e.tag === wantTag));
3864
+ try {
3865
+ if (action === 'list' || action === 'search') {
3866
+ const hit = action === 'search'
3867
+ ? rows.filter((e) => !q || e.full.toLowerCase().includes(q) || String(e.description).toLowerCase().includes(q))
3868
+ : rows;
3869
+ if (!hit.length)
3870
+ return { ok: true, msg: rows.length ? '没有匹配的工具(换关键词试试)' : '当前没有可领取的扩展工具' };
3871
+ const lines = hit.map((e) => e.full + ' — ' + String(e.description || '').slice(0, 90)).join('\n');
3872
+ return { ok: true, msg: '可领取的扩展工具 ' + hit.length + ' 个(enable 时 tool 写全名):\n' + lines };
3873
+ }
3874
+ if (action === 'enable') {
3875
+ if (!wantTool)
3876
+ return { ok: false, msg: 'tool 必填(先 action=list 查名字)' };
3877
+ const ent = extToolIndex.get(wantTool) || rows.find((e) => e.baseName === wantTool) || undefined;
3878
+ if (!ent)
3879
+ return { ok: false, msg: '找不到工具: ' + wantTool + '(先 action=list)' };
3880
+ const actx = agentCtxOf(exec);
3881
+ if (!actx)
3882
+ return { ok: false, msg: '拿不到当前会话作用域(本工具只能在会话内使用)' };
3883
+ let m = extGranted.get(actx);
3884
+ if (!m) {
3885
+ m = new Map();
3886
+ extGranted.set(actx, m);
3887
+ }
3888
+ if (m.has(ent.full))
3889
+ return { ok: true, msg: '已经装好了:' + ent.full };
3890
+ const toolsObj = toolsOfScope(actx);
3891
+ const regFn = toolsObj?.register;
3892
+ if (!toolsObj || typeof regFn !== 'function')
3893
+ return { ok: false, msg: '当前会话拿不到 tools.register' };
3894
+ const tool = buildExtTool(ent, globalCtx, logger);
3895
+ const d = regFn.call(toolsObj, tool);
3896
+ if (typeof d === 'function')
3897
+ m.set(ent.full, d);
3898
+ return { ok: true, msg: '已装进当前会话:' + ent.full + '(本回合后续步骤即可调用;换会话需重新 enable)' };
3899
+ }
3900
+ if (action === 'disable') {
3901
+ const actx = agentCtxOf(exec);
3902
+ const m = actx ? extGranted.get(actx) : undefined;
3903
+ if (!m || !m.size)
3904
+ return { ok: true, msg: '当前会话没有领取过扩展工具' };
3905
+ const target = wantTool || '';
3906
+ if (target) {
3907
+ const d = m.get(target);
3908
+ if (!d)
3909
+ return { ok: false, msg: '当前会话没装: ' + target };
3910
+ try {
3911
+ d();
3912
+ }
3913
+ catch { /* ignore */ }
3914
+ m.delete(target);
3915
+ return { ok: true, msg: '已卸下:' + target };
3916
+ }
3917
+ for (const [k, d] of m) {
3918
+ try {
3919
+ d();
3920
+ }
3921
+ catch { /* ignore */ }
3922
+ void k;
3923
+ }
3924
+ m.clear();
3925
+ return { ok: true, msg: '已卸下本会话领取的全部扩展工具' };
3926
+ }
3927
+ return { ok: false, msg: '未知 action: ' + action };
3928
+ }
3929
+ catch (e) {
3930
+ return { ok: false, msg: (e instanceof Error ? e.message : String(e)) };
3931
+ }
3932
+ },
3933
+ });
3934
+ }
3935
+ /** 全局注册元工具(幂等) */
3936
+ function ensureGainToolGlobal(globalCtx, logger) {
3937
+ try {
3938
+ const g = globalThis;
3939
+ if (g.__qqbotGainToolReg)
3940
+ return;
3941
+ const rootCtx = ((globalCtx?.root) ?? globalCtx);
3942
+ const gctx = rootCtx;
3943
+ let toolsObj;
3944
+ try {
3945
+ toolsObj = gctx.get?.('tools');
3946
+ }
3947
+ catch { /* ignore */ }
3948
+ if (!toolsObj || typeof toolsObj.register !== 'function')
3949
+ return;
3950
+ const d = toolsObj.register.call(toolsObj, makeGainTool(globalCtx, logger));
3951
+ if (typeof d === 'function')
3952
+ g.__qqbotGainToolReg = d;
3953
+ gctx.logger?.info?.('[channel-tools] 已注册元工具 ' + GAIN_TOOL_NAME);
3954
+ }
3955
+ catch { /* ignore */ }
3956
+ }
3786
3957
  export function ensureExtToolsGlobal(globalCtx) {
3787
3958
  // 节流:本函数挂在 agent/pre-step(**每个 step** 都触发)—— 15 秒内不重复扫盘;
3788
3959
  // 幂等保证:即便节流窗口外重复进入,已注册的 name@tag 也会被跳过。
@@ -3834,6 +4005,16 @@ export function ensureExtToolsGlobal(globalCtx) {
3834
4005
  if (!g.__qqbotExtToolsGlobal)
3835
4006
  g.__qqbotExtToolsGlobal = new Map();
3836
4007
  const reg = g.__qqbotExtToolsGlobal;
4008
+ // 开关(默认开 = 按需取用):关掉后回到"扩展工具全部全局注册"的旧行为
4009
+ let metaMode = true;
4010
+ try {
4011
+ const firstMgr = managersOf()[0];
4012
+ if (firstMgr?.config && firstMgr.config.extMetaTool === false)
4013
+ metaMode = false;
4014
+ }
4015
+ catch { /* ignore */ }
4016
+ extToolIndex.clear();
4017
+ extGlobDiag('metaMode=' + String(metaMode));
3837
4018
  for (const inst of insts) {
3838
4019
  let defs = [];
3839
4020
  try {
@@ -3857,6 +4038,12 @@ export function ensureExtToolsGlobal(globalCtx) {
3857
4038
  extGlobDiag('SKIP illegal global name: ' + full);
3858
4039
  continue;
3859
4040
  }
4041
+ // 2026-10-28:**按需取用模式**(默认)——只登记到索引,不注册到全局。
4042
+ // 会话需要时由 gain_qq_tools.enable 装进它自己的 agent 作用域。
4043
+ if (metaMode) {
4044
+ extToolIndex.set(full, { full, tag: inst.tag, baseName: def.name, description: String(def.description ?? ''), def: def, inst });
4045
+ continue;
4046
+ }
3860
4047
  if (reg.has(full))
3861
4048
  continue;
3862
4049
  try {
@@ -3924,6 +4111,9 @@ export function ensureExtToolsGlobal(globalCtx) {
3924
4111
  }
3925
4112
  }
3926
4113
  }
4114
+ if (metaMode)
4115
+ ensureGainToolGlobal(globalCtx, gctx.logger);
4116
+ extGlobDiag('index=' + extToolIndex.size + ' 可领取');
3927
4117
  }
3928
4118
  catch (e) {
3929
4119
  extGlobDiag('EXCEPTION ' + ((e instanceof Error ? e.message : String(e)) || '').slice(0, 200));