@zaofan/dsh-qqbot 1.11.11 → 1.11.13
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/client/qqbot-settings.js +8213 -8212
- package/dist/channel-tools.d.ts +4 -19
- package/dist/channel-tools.d.ts.map +1 -1
- package/dist/channel-tools.js +401 -31
- package/dist/channel-tools.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/session/session-manager.d.ts.map +1 -1
- package/dist/session/session-manager.js +41 -2
- package/dist/session/session-manager.js.map +1 -1
- package/package.json +1 -1
- package/settings-host.js +24 -0
package/dist/channel-tools.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface QQChannel {
|
|
|
8
8
|
}
|
|
9
9
|
export declare const name = "qqbot-channel-tools";
|
|
10
10
|
export declare const inject: string[];
|
|
11
|
+
/** 某实例的标识:优先**人设 id**(config.preset),没配才回落工作区目录名 */
|
|
12
|
+
export declare function instanceTagOfManager(m: unknown): string;
|
|
11
13
|
export declare function setContextlessSmartGlobal(v: boolean): void;
|
|
12
14
|
/** 读登记(session-manager 的 pre-step 用) */
|
|
13
15
|
export declare function readContextPending(dataRoot: string, sessionKey: string): {
|
|
@@ -89,25 +91,8 @@ export declare function syncContextTools(agentCtx: unknown, shouldHave: boolean,
|
|
|
89
91
|
dataRoot?: string;
|
|
90
92
|
sessionKey?: string;
|
|
91
93
|
}, reminderText?: string): void;
|
|
92
|
-
/**
|
|
93
|
-
|
|
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
|
-
*/
|
|
94
|
+
/** 按当前会话的「已领取集合」重设一次剪枝(幂等:deny 名单没变就不动) */
|
|
95
|
+
export declare function applyExtToolVisibility(agentCtx: unknown, selfTag?: string): void;
|
|
111
96
|
export declare function ensureExtToolsGlobal(globalCtx: unknown): void;
|
|
112
97
|
export declare function ensureContextToolsGlobal(globalCtx: unknown): void;
|
|
113
98
|
/**
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
1
|
+
{"version":3,"file":"channel-tools.d.ts","sourceRoot":"","sources":["../src/channel-tools.ts"],"names":[],"mappings":"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"}
|
package/dist/channel-tools.js
CHANGED
|
@@ -142,7 +142,7 @@ function workspaceTagOf(cwd) {
|
|
|
142
142
|
return 'ws' + Math.abs(hsh).toString(36).slice(0, 6);
|
|
143
143
|
}
|
|
144
144
|
/** 某实例的标识:优先**人设 id**(config.preset),没配才回落工作区目录名 */
|
|
145
|
-
function instanceTagOfManager(m) {
|
|
145
|
+
export function instanceTagOfManager(m) {
|
|
146
146
|
const cfg = m?.config;
|
|
147
147
|
const p = String(cfg?.preset ?? '').trim();
|
|
148
148
|
return p || workspaceTagOf(m?.cwd);
|
|
@@ -3764,25 +3764,335 @@ 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
|
+
/** 本轮扫描到的「可领取」扩展工具(full 名 → 条目) */
|
|
3768
|
+
const extToolIndex = new Map();
|
|
3769
|
+
/** 已领取到某个会话作用域的工具(agentCtx → full → disposer)—— metaMode 下用 */
|
|
3770
|
+
const extGranted = new WeakMap();
|
|
3767
3771
|
/**
|
|
3768
|
-
*
|
|
3772
|
+
* 「默认全剪枝 + 按需放开」的授予集合(2026-10-28,主人定)。
|
|
3769
3773
|
*
|
|
3770
|
-
*
|
|
3771
|
-
*
|
|
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)—— **名字即归属**。
|
|
3774
|
+
* 目标形态:`expose:'all'` 的工具**始终全局注册**(注册表不动、不抖),
|
|
3775
|
+
* 但每个会话**默认被 deny 掉全部**(模型清单里连名字都看不到 = 零 token),
|
|
3776
|
+
* 只有 `gain_qq_tools.enable` 过的名字才从 deny 名单里拿掉。
|
|
3782
3777
|
*
|
|
3783
|
-
*
|
|
3784
|
-
*
|
|
3778
|
+
* 复用的是仓库里已有的 `tools.restrict({ deny })` + `liftRestriction`(见 restrictContextTools 注释),
|
|
3779
|
+
* 不新造机制。
|
|
3785
3780
|
*/
|
|
3781
|
+
const extGrantSet = new WeakMap();
|
|
3782
|
+
const extRestrictState2 = new WeakMap();
|
|
3783
|
+
/** 见过的会话作用域 → 它的 selfTag(扫描完成后要按新索引**重放一次**剪枝) */
|
|
3784
|
+
const extSeenCtx = new Map();
|
|
3785
|
+
/** 按当前会话的「已领取集合」重设一次剪枝(幂等:deny 名单没变就不动) */
|
|
3786
|
+
export function applyExtToolVisibility(agentCtx, selfTag) {
|
|
3787
|
+
try {
|
|
3788
|
+
const ctx = agentCtx;
|
|
3789
|
+
if (!ctx || typeof ctx !== 'object')
|
|
3790
|
+
return;
|
|
3791
|
+
// ⚠️ 2026-10-28(主人实测:enable 一个 standard 后,自己实例那 4 个被移除):
|
|
3792
|
+
// gain_qq_tools 内部调本函数时**不传 selfTag**,原来会把 ownSuffix 算成空串 ⇒
|
|
3793
|
+
// 连带把本实例自己的那份也 deny 掉。现在:不传就**沿用上次记住的 tag**。
|
|
3794
|
+
const prevTag = extSeenCtx.get(ctx) ?? '';
|
|
3795
|
+
const tag = selfTag !== undefined ? String(selfTag) : prevTag;
|
|
3796
|
+
extSeenCtx.set(ctx, tag);
|
|
3797
|
+
const all = [...extToolIndex.keys()];
|
|
3798
|
+
if (!all.length)
|
|
3799
|
+
return; // 还没扫到东西:先不动(别把不该剪的剪了)
|
|
3800
|
+
// ⚠️ 2026-10-28(主人纠正):开关**不是**"关=不剪枝、全可见"。
|
|
3801
|
+
// 关掉元工具 = "按需取用"这条通道整个关掉 ⇒ 别人实例 expose 的工具**永远不可见**
|
|
3802
|
+
// (没有 gain 入口,也就不该偷偷放出来)。所以无论开关开/关,都照常剪:
|
|
3803
|
+
// · 自己实例那份(xxx__本tag):始终放开
|
|
3804
|
+
// · 别人那份:始终 deny;开关开着时可通过 gain_qq_tools 临时放开
|
|
3805
|
+
// ★ 2026-10-28(主人实测:关了元工具,之前领取的工具还在):开关关掉时
|
|
3806
|
+
// **忽略并清空领取记录** —— 通道一关,别人的工具就该彻底消失,不能靠残留的 granted 续命。
|
|
3807
|
+
const metaOnNow = globalThis.__qqbotExtMetaOn !== false;
|
|
3808
|
+
if (!metaOnNow)
|
|
3809
|
+
extGrantSet.delete(ctx);
|
|
3810
|
+
const granted = metaOnNow ? (extGrantSet.get(ctx) || new Set()) : new Set();
|
|
3811
|
+
// ★ 2026-10-28(主人直觉):**本实例自己的那份默认放开** —— 在 whale-girl 的会话里,
|
|
3812
|
+
// `bing_search__whale-girl` 本来就该看得见;别人的实例、以及自己没暴露的工具才要 gain 领取。
|
|
3813
|
+
const ownSuffix = tag ? ('__' + tag) : '';
|
|
3814
|
+
const deny = all.filter((n) => !granted.has(n) && !(ownSuffix && n.endsWith(ownSuffix)));
|
|
3815
|
+
const sig = (metaOnNow ? '1' : '0') + '|' + deny.join(',');
|
|
3816
|
+
const st = extRestrictState2.get(ctx);
|
|
3817
|
+
if (st && st.sig === sig)
|
|
3818
|
+
return;
|
|
3819
|
+
if (st?.dispose)
|
|
3820
|
+
liftRestriction(st.dispose);
|
|
3821
|
+
const toolsObj = toolsOfScope(ctx);
|
|
3822
|
+
const restrict = toolsObj?.restrict;
|
|
3823
|
+
if (typeof restrict !== 'function') {
|
|
3824
|
+
traceToolReg('[ext-vis] 剪枝失败: 拿不到 tools.restrict');
|
|
3825
|
+
return;
|
|
3826
|
+
}
|
|
3827
|
+
if (!deny.length) {
|
|
3828
|
+
extRestrictState2.set(ctx, { sig });
|
|
3829
|
+
return;
|
|
3830
|
+
}
|
|
3831
|
+
const d = restrict.call(toolsObj, { deny });
|
|
3832
|
+
const dispose = typeof d === 'function' ? d : undefined;
|
|
3833
|
+
extRestrictState2.set(ctx, { sig, dispose });
|
|
3834
|
+
traceToolReg('[ext-vis] deny=' + deny.length + ' 件 已领取=' + granted.size);
|
|
3835
|
+
}
|
|
3836
|
+
catch (e) {
|
|
3837
|
+
traceToolReg('[ext-vis] 异常: ' + String(e?.message ?? e).slice(0, 140));
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
const GAIN_TOOL_NAME = 'gain_qq_tools';
|
|
3841
|
+
/** 取当前调用方的 agent ctx(enable 的目标作用域) */
|
|
3842
|
+
function agentCtxOf(exec) {
|
|
3843
|
+
try {
|
|
3844
|
+
const c = exec?.agent?.ctx;
|
|
3845
|
+
return c && typeof c === 'object' ? c : undefined;
|
|
3846
|
+
}
|
|
3847
|
+
catch {
|
|
3848
|
+
return undefined;
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
/** 把一个扩展工具定义包成 dsh 工具(与全局注册同款;抽出来给 enable 复用) */
|
|
3852
|
+
function buildExtTool(ent, globalCtx, logger) {
|
|
3853
|
+
return defineTool({
|
|
3854
|
+
name: ent.full,
|
|
3855
|
+
description: String(ent.def.description ?? '') + ' (用户扩展工具 · ' + ent.tag + ')',
|
|
3856
|
+
parameters: (ent.def.inputSchema ?? { type: 'object', properties: {} }),
|
|
3857
|
+
output: {
|
|
3858
|
+
schema: {
|
|
3859
|
+
type: 'object', additionalProperties: false,
|
|
3860
|
+
properties: { ok: { type: 'boolean', required: true }, msg: { type: 'string', required: true } },
|
|
3861
|
+
},
|
|
3862
|
+
render: (_a, v) => [{ type: 'text', text: v.ok ? v.msg : ('失败: ' + v.msg) }],
|
|
3863
|
+
},
|
|
3864
|
+
async execute(args, exec) {
|
|
3865
|
+
let pack;
|
|
3866
|
+
try {
|
|
3867
|
+
pack = buildWorkspaceQqPack(ent.tag, ent.inst.dataRoot);
|
|
3868
|
+
}
|
|
3869
|
+
catch {
|
|
3870
|
+
pack = undefined;
|
|
3871
|
+
}
|
|
3872
|
+
const env = {
|
|
3873
|
+
cwd: ent.inst.cwd || ent.inst.dataRoot,
|
|
3874
|
+
exec,
|
|
3875
|
+
ctx: globalCtx,
|
|
3876
|
+
logger,
|
|
3877
|
+
qq: pack,
|
|
3878
|
+
};
|
|
3879
|
+
try {
|
|
3880
|
+
const sess = findSessionRec(channelOf(exec), exec);
|
|
3881
|
+
if (sess) {
|
|
3882
|
+
env.manager = sess.ch.manager;
|
|
3883
|
+
env.sender = sess.ch.sender;
|
|
3884
|
+
env.replyTarget = sess.rec.replyTarget;
|
|
3885
|
+
}
|
|
3886
|
+
else if (pack) {
|
|
3887
|
+
env.sender = pack.sender;
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
catch { /* ignore */ }
|
|
3891
|
+
try {
|
|
3892
|
+
const r = await ent.def.run(args, env);
|
|
3893
|
+
if (r && typeof r === 'object')
|
|
3894
|
+
return { ok: r.ok === true, msg: String(r.msg ?? 'done') };
|
|
3895
|
+
return { ok: true, msg: r === undefined || r === null ? 'done' : String(r) };
|
|
3896
|
+
}
|
|
3897
|
+
catch (err) {
|
|
3898
|
+
return { ok: false, msg: err instanceof Error ? err.message : String(err) };
|
|
3899
|
+
}
|
|
3900
|
+
},
|
|
3901
|
+
});
|
|
3902
|
+
}
|
|
3903
|
+
/** 元工具本体(全局注册一次) */
|
|
3904
|
+
function makeGainTool(globalCtx, logger) {
|
|
3905
|
+
return defineTool({
|
|
3906
|
+
name: GAIN_TOOL_NAME,
|
|
3907
|
+
description: '按需取用「QQ 实例的扩展工具」:先 list / search 看有哪些(只回名字+一句话),'
|
|
3908
|
+
+ '再 enable 把某个装进当前会话,之后就能调用它(非 QQ 会话想往 QQ 发消息、读会话时用)。'
|
|
3909
|
+
+ 'disable 可卸下。',
|
|
3910
|
+
// ⚠️ 2026-10-28(引擎控制台实录):defineTool 的 parameters 要的是**扁平字段表**,
|
|
3911
|
+
// ;写成 JSON Schema(type/properties/required)
|
|
3912
|
+
// 会被判 "unsupported JSON schema: parameters.type must be a value schema object" ⇒ 注册直接失败。
|
|
3913
|
+
parameters: {
|
|
3914
|
+
action: { type: 'string', required: true, enum: ['list', 'search', 'enable', 'disable'], description: 'list=列出全部 / search=按关键词搜 / enable=装到当前会话 / disable=卸下' },
|
|
3915
|
+
query: { type: 'string', description: 'search 用:关键词(匹配工具名与描述)' },
|
|
3916
|
+
tool: { type: 'string', description: 'enable/disable 用:工具名,可用逗号或空格**批量**(如 qq_send__whale-girl,bing_search__standard;先 list 查)' },
|
|
3917
|
+
tag: { type: 'string', description: '可选:只看某个实例(tag)' },
|
|
3918
|
+
limit: { type: 'integer', description: 'list/search 返回条数 1~50(默认 20)' },
|
|
3919
|
+
},
|
|
3920
|
+
output: {
|
|
3921
|
+
schema: {
|
|
3922
|
+
type: 'object', additionalProperties: false,
|
|
3923
|
+
properties: { ok: { type: 'boolean', required: true }, msg: { type: 'string', required: true } },
|
|
3924
|
+
},
|
|
3925
|
+
render: (_a, v) => [{ type: 'text', text: v.ok ? v.msg : ('失败: ' + v.msg) }],
|
|
3926
|
+
},
|
|
3927
|
+
async execute(args, exec) {
|
|
3928
|
+
const action = String(args?.action ?? 'list');
|
|
3929
|
+
const q = String(args?.query ?? '').trim().toLowerCase();
|
|
3930
|
+
const wantTools = String(args?.tool ?? '').trim()
|
|
3931
|
+
.split(/[,\s]+/).map((x) => x.trim()).filter(Boolean);
|
|
3932
|
+
const wantTag = String(args?.tag ?? '').trim();
|
|
3933
|
+
const limRaw = Number(args?.limit);
|
|
3934
|
+
const lim = Math.max(1, Math.min(50, Number.isFinite(limRaw) && limRaw > 0 ? Math.trunc(limRaw) : 20));
|
|
3935
|
+
const rows = [...extToolIndex.values()].filter((e) => (!wantTag || e.tag === wantTag));
|
|
3936
|
+
try {
|
|
3937
|
+
if (action === 'list' || action === 'search') {
|
|
3938
|
+
const hit = action === 'search'
|
|
3939
|
+
? rows.filter((e) => !q || e.full.toLowerCase().includes(q) || String(e.description).toLowerCase().includes(q))
|
|
3940
|
+
: rows;
|
|
3941
|
+
if (!hit.length)
|
|
3942
|
+
return { ok: true, msg: rows.length ? '没有匹配的工具(换关键词试试)' : '当前没有可领取的扩展工具' };
|
|
3943
|
+
const shown = hit.slice(0, lim);
|
|
3944
|
+
const lines = shown.map((e) => e.full + ' — ' + String(e.description || '').slice(0, 90)).join('\n');
|
|
3945
|
+
const more = hit.length > shown.length
|
|
3946
|
+
? ('\n……(共 ' + hit.length + ' 个,已显示前 ' + shown.length + ' 个;加 limit 或 search 缩小)')
|
|
3947
|
+
: '';
|
|
3948
|
+
return { ok: true, msg: '可领取的扩展工具 ' + hit.length + ' 个(enable 时 tool 写全名,可逗号批量):\n' + lines + more };
|
|
3949
|
+
}
|
|
3950
|
+
if (action === 'enable') {
|
|
3951
|
+
if (!wantTools.length)
|
|
3952
|
+
return { ok: false, msg: 'tool 必填(先 action=list 查名字)' };
|
|
3953
|
+
const actx = agentCtxOf(exec);
|
|
3954
|
+
if (!actx)
|
|
3955
|
+
return { ok: false, msg: '拿不到当前会话作用域(本工具只能在会话内使用)' };
|
|
3956
|
+
let gset = extGrantSet.get(actx);
|
|
3957
|
+
if (!gset) {
|
|
3958
|
+
gset = new Set();
|
|
3959
|
+
extGrantSet.set(actx, gset);
|
|
3960
|
+
}
|
|
3961
|
+
const toolsObj = toolsOfScope(actx);
|
|
3962
|
+
const regFn = toolsObj?.register;
|
|
3963
|
+
let mm = extGranted.get(actx);
|
|
3964
|
+
if (!mm) {
|
|
3965
|
+
mm = new Map();
|
|
3966
|
+
extGranted.set(actx, mm);
|
|
3967
|
+
}
|
|
3968
|
+
const okList = [];
|
|
3969
|
+
const badList = [];
|
|
3970
|
+
for (const name of wantTools) {
|
|
3971
|
+
const ent = extToolIndex.get(name) || rows.find((e) => e.baseName === name) || undefined;
|
|
3972
|
+
if (!ent) {
|
|
3973
|
+
badList.push(name);
|
|
3974
|
+
continue;
|
|
3975
|
+
}
|
|
3976
|
+
gset.add(ent.full);
|
|
3977
|
+
// 兜底:按需取用模式下工具未全局注册,得真注册一次
|
|
3978
|
+
if (!mm.has(ent.full) && toolsObj && typeof regFn === 'function') {
|
|
3979
|
+
try {
|
|
3980
|
+
const d = regFn.call(toolsObj, buildExtTool(ent, globalCtx, logger));
|
|
3981
|
+
if (typeof d === 'function')
|
|
3982
|
+
mm.set(ent.full, d);
|
|
3983
|
+
}
|
|
3984
|
+
catch { /* 已全局注册时覆盖同名,失败无妨 */ }
|
|
3985
|
+
}
|
|
3986
|
+
okList.push(ent.full);
|
|
3987
|
+
}
|
|
3988
|
+
applyExtToolVisibility(actx);
|
|
3989
|
+
const head = okList.length ? ('已放开 ' + okList.length + ' 个:' + okList.join(', ')) : '没有放开任何工具';
|
|
3990
|
+
const tail = badList.length ? (';找不到:' + badList.join(', ')) : '';
|
|
3991
|
+
return { ok: badList.length === 0, msg: head + tail + '(本会话可见;换会话要重新 enable)' };
|
|
3992
|
+
}
|
|
3993
|
+
if (action === 'disable') {
|
|
3994
|
+
const actx = agentCtxOf(exec);
|
|
3995
|
+
if (actx) {
|
|
3996
|
+
const gset = extGrantSet.get(actx);
|
|
3997
|
+
if (!gset || !gset.size)
|
|
3998
|
+
return { ok: true, msg: '当前会话没有领取过扩展工具' };
|
|
3999
|
+
if (!wantTools.length) {
|
|
4000
|
+
gset.clear();
|
|
4001
|
+
applyExtToolVisibility(actx);
|
|
4002
|
+
return { ok: true, msg: '已收回本会话领取的全部扩展工具' };
|
|
4003
|
+
}
|
|
4004
|
+
const done = [];
|
|
4005
|
+
const miss = [];
|
|
4006
|
+
for (const name of wantTools) {
|
|
4007
|
+
const ent = extToolIndex.get(name) || rows.find((e) => e.baseName === name) || undefined;
|
|
4008
|
+
const full = ent ? ent.full : name;
|
|
4009
|
+
if (gset.delete(full))
|
|
4010
|
+
done.push(full);
|
|
4011
|
+
else
|
|
4012
|
+
miss.push(name);
|
|
4013
|
+
}
|
|
4014
|
+
applyExtToolVisibility(actx);
|
|
4015
|
+
return {
|
|
4016
|
+
ok: miss.length === 0,
|
|
4017
|
+
msg: (done.length ? '已收回 ' + done.length + ' 个:' + done.join(', ') : '没有收回任何工具')
|
|
4018
|
+
+ (miss.length ? (';未领取:' + miss.join(', ')) : ''),
|
|
4019
|
+
};
|
|
4020
|
+
}
|
|
4021
|
+
const m = extGranted.get(actx);
|
|
4022
|
+
if (!m || !m.size)
|
|
4023
|
+
return { ok: true, msg: '当前会话没有领取过扩展工具' };
|
|
4024
|
+
const target = wantTools[0] ?? '';
|
|
4025
|
+
if (target) {
|
|
4026
|
+
const d = m.get(target);
|
|
4027
|
+
if (!d)
|
|
4028
|
+
return { ok: false, msg: '当前会话没装: ' + target };
|
|
4029
|
+
try {
|
|
4030
|
+
d();
|
|
4031
|
+
}
|
|
4032
|
+
catch { /* ignore */ }
|
|
4033
|
+
m.delete(target);
|
|
4034
|
+
return { ok: true, msg: '已卸下:' + target };
|
|
4035
|
+
}
|
|
4036
|
+
for (const [k, d] of m) {
|
|
4037
|
+
try {
|
|
4038
|
+
d();
|
|
4039
|
+
}
|
|
4040
|
+
catch { /* ignore */ }
|
|
4041
|
+
void k;
|
|
4042
|
+
}
|
|
4043
|
+
m.clear();
|
|
4044
|
+
return { ok: true, msg: '已卸下本会话领取的全部扩展工具' };
|
|
4045
|
+
}
|
|
4046
|
+
return { ok: false, msg: '未知 action: ' + action };
|
|
4047
|
+
}
|
|
4048
|
+
catch (e) {
|
|
4049
|
+
return { ok: false, msg: (e instanceof Error ? e.message : String(e)) };
|
|
4050
|
+
}
|
|
4051
|
+
},
|
|
4052
|
+
});
|
|
4053
|
+
}
|
|
4054
|
+
/** 全局注册/注销元工具(幂等;on=false 时**真注销**,对应设置页的热关闭) */
|
|
4055
|
+
function ensureGainToolGlobal(globalCtx, logger, on = true) {
|
|
4056
|
+
try {
|
|
4057
|
+
const g = globalThis;
|
|
4058
|
+
// ★ 2026-10-28(主人 + issue 反馈):**热关闭** —— 开关关掉时元工具必须真的消失,
|
|
4059
|
+
// 否则「非 QQ 会话零 QQ 工具」这条做不到(还会白占几十 token)。
|
|
4060
|
+
if (!on) {
|
|
4061
|
+
if (typeof g.__qqbotGainToolReg === 'function') {
|
|
4062
|
+
try {
|
|
4063
|
+
g.__qqbotGainToolReg();
|
|
4064
|
+
}
|
|
4065
|
+
catch { /* ignore */ }
|
|
4066
|
+
g.__qqbotGainToolReg = undefined;
|
|
4067
|
+
logger?.info?.('[channel-tools] 已注销元工具 ' + GAIN_TOOL_NAME);
|
|
4068
|
+
}
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
if (g.__qqbotGainToolReg)
|
|
4072
|
+
return;
|
|
4073
|
+
const rootCtx = ((globalCtx?.root) ?? globalCtx);
|
|
4074
|
+
const gctx = rootCtx;
|
|
4075
|
+
let toolsObj;
|
|
4076
|
+
try {
|
|
4077
|
+
toolsObj = gctx.get?.('tools');
|
|
4078
|
+
}
|
|
4079
|
+
catch { /* ignore */ }
|
|
4080
|
+
if (!toolsObj || typeof toolsObj.register !== 'function')
|
|
4081
|
+
return;
|
|
4082
|
+
const d = toolsObj.register.call(toolsObj, makeGainTool(globalCtx, logger));
|
|
4083
|
+
if (typeof d === 'function')
|
|
4084
|
+
g.__qqbotGainToolReg = d;
|
|
4085
|
+
gctx.logger?.info?.('[channel-tools] 已注册元工具 ' + GAIN_TOOL_NAME);
|
|
4086
|
+
}
|
|
4087
|
+
catch (e) {
|
|
4088
|
+
// ⚠️ 2026-10-28:这里原来静默 catch ⇒ "开了开关却连元工具都没有",完全查不出原因。
|
|
4089
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
4090
|
+
try {
|
|
4091
|
+
console.error('[channel-tools] 元工具注册失败: ' + msg);
|
|
4092
|
+
}
|
|
4093
|
+
catch { /* ignore */ }
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
3786
4096
|
export function ensureExtToolsGlobal(globalCtx) {
|
|
3787
4097
|
// 节流:本函数挂在 agent/pre-step(**每个 step** 都触发)—— 15 秒内不重复扫盘;
|
|
3788
4098
|
// 幂等保证:即便节流窗口外重复进入,已注册的 name@tag 也会被跳过。
|
|
@@ -3813,6 +4123,45 @@ export function ensureExtToolsGlobal(globalCtx) {
|
|
|
3813
4123
|
extDiag('[extglob] 拿不到 tools 服务, 跳过');
|
|
3814
4124
|
return;
|
|
3815
4125
|
}
|
|
4126
|
+
// 2026-10-28:元工具**先注册**,且不依赖"有没有实例"——
|
|
4127
|
+
// 上一版放在扫描末尾,若中途 early-return 就永远注册不上(实测:开了按需取用却连元工具都没有)。
|
|
4128
|
+
const metaOn = (() => {
|
|
4129
|
+
// ① **文件开关优先**(最可靠,2026-10-28 主人实测:"重启了也读不到勾选"):
|
|
4130
|
+
// settings-host 保存时写 {dataRoot}/.qqbot/ext-meta-tool.json,这里每次扫描现读;
|
|
4131
|
+
// 不依赖 config 快照,也不依赖 globalThis 有没有被写过。
|
|
4132
|
+
try {
|
|
4133
|
+
const dr = String(managersOf()[0]?.dataRoot ?? '').trim();
|
|
4134
|
+
if (dr) {
|
|
4135
|
+
const o = JSON.parse(readFileSync(join(dr, '.qqbot', 'ext-meta-tool.json'), 'utf8'));
|
|
4136
|
+
if (typeof o?.on === 'boolean')
|
|
4137
|
+
return o.on;
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
catch { /* 文件不存在 = 还没设过,往下走 */ }
|
|
4141
|
+
// ② 进程级热标记:设置页保存时 settings-host 写入(config 是启动快照,不会自己变)。
|
|
4142
|
+
try {
|
|
4143
|
+
const live = globalThis.__qqbotExtMetaTool;
|
|
4144
|
+
if (typeof live === 'boolean')
|
|
4145
|
+
return live;
|
|
4146
|
+
}
|
|
4147
|
+
catch { /* ignore */ }
|
|
4148
|
+
// ② 回退 config —— ⚠️ 必须**遍历所有实例**,不能只看 managersOf()[0]:
|
|
4149
|
+
// index.ts 的 'qqbot/settings-changed' 监听会 把
|
|
4150
|
+
// 保存值原地合并进当前实例的 config(这就是面板其它开关的热更路子);但只有**被保存的那个
|
|
4151
|
+
// 实例**里有这个字段,第一个 manager 未必是它 ⇒ 只看第 0 个会永远读到 undefined/false。
|
|
4152
|
+
try {
|
|
4153
|
+
for (const m of managersOf()) {
|
|
4154
|
+
const cfgM = m?.config;
|
|
4155
|
+
if (cfgM && cfgM.extMetaTool === true)
|
|
4156
|
+
return true;
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
catch { /* ignore */ }
|
|
4160
|
+
return false;
|
|
4161
|
+
})();
|
|
4162
|
+
// ★ 2026-10-28(主人急):**无条件注册元工具** —— 它是个极轻的入口,本来就不该被任何开关挡住。
|
|
4163
|
+
// "开了按需取用却连 gain_qq_tools 都看不到" 这种状态毫无意义:要么两个都有,要么两个都没有。
|
|
4164
|
+
ensureGainToolGlobal(globalCtx, gctx.logger, metaOn);
|
|
3816
4165
|
// 实例清单:每个账号实例注册一份(tag = 人设 id)
|
|
3817
4166
|
const insts = [];
|
|
3818
4167
|
try {
|
|
@@ -3834,6 +4183,16 @@ export function ensureExtToolsGlobal(globalCtx) {
|
|
|
3834
4183
|
if (!g.__qqbotExtToolsGlobal)
|
|
3835
4184
|
g.__qqbotExtToolsGlobal = new Map();
|
|
3836
4185
|
const reg = g.__qqbotExtToolsGlobal;
|
|
4186
|
+
// 开关(默认开 = 按需取用):关掉后回到"扩展工具全部全局注册"的旧行为
|
|
4187
|
+
// 2026-10-28(主人实测纠正):默认**保持旧行为**(全部 expose:'all' 全局注册)。
|
|
4188
|
+
// 原因:第一版把默认值设成"按需取用",结果元工具没注册上 => 所有会话一个扩展工具都没有
|
|
4189
|
+
// (主人截图:"怎么你改一下代码把自己的扩展工具都给不注册了")。
|
|
4190
|
+
// ⇒ 现在只有显式打开 extMetaTool 才进按需取用,默认一切照旧。
|
|
4191
|
+
const metaMode = metaOn; // 与上面同一个口径(含 live 标记)
|
|
4192
|
+
extToolIndex.clear();
|
|
4193
|
+
globalThis.__qqbotExtMetaOn = metaMode;
|
|
4194
|
+
extGlobDiag('metaMode=' + String(metaMode));
|
|
4195
|
+
// ── 阶段 1:扫描,**只填索引**(不注册)────────────────────────────
|
|
3837
4196
|
for (const inst of insts) {
|
|
3838
4197
|
let defs = [];
|
|
3839
4198
|
try {
|
|
@@ -3843,26 +4202,41 @@ export function ensureExtToolsGlobal(globalCtx) {
|
|
|
3843
4202
|
defs = [];
|
|
3844
4203
|
}
|
|
3845
4204
|
const exposed = defs.filter((d) => d.expose === 'all');
|
|
3846
|
-
extGlobDiag('inst=' + inst.tag + ' defs=' + defs.length + ' exposeAll=' + exposed.length
|
|
3847
|
-
if (!exposed.length)
|
|
3848
|
-
continue;
|
|
4205
|
+
extGlobDiag('inst=' + inst.tag + ' defs=' + defs.length + ' exposeAll=' + exposed.length);
|
|
3849
4206
|
for (const def of exposed) {
|
|
3850
|
-
// ⚠️
|
|
3851
|
-
// `@` 不在 LLM API 允许的工具名字符集里(^[a-zA-Z0-9_-]+$),
|
|
3852
|
-
// 一旦注册进去就是"每一轮请求都被拒"(机器人整体瘫痪)。故改用 `__`。
|
|
4207
|
+
// ⚠️ 工具名只允许 [a-zA-Z0-9_-](LLM API 硬约束,2026-10-10 事故)
|
|
3853
4208
|
const full = def.name + '__' + inst.tag;
|
|
3854
|
-
// ⚠️ 兜底闸门:工具名只允许 [a-zA-Z0-9_-](LLM API 硬约束)——
|
|
3855
|
-
// 非法名一旦注册进工具表,**每一轮请求都会被 API 整轮拒绝**(2026-10-10 事故)。
|
|
3856
4209
|
if (!/^[a-zA-Z0-9_-]+$/.test(full)) {
|
|
3857
4210
|
extGlobDiag('SKIP illegal global name: ' + full);
|
|
3858
4211
|
continue;
|
|
3859
4212
|
}
|
|
3860
|
-
|
|
4213
|
+
extToolIndex.set(full, {
|
|
4214
|
+
full, tag: inst.tag, baseName: def.name,
|
|
4215
|
+
description: String(def.description ?? ''),
|
|
4216
|
+
def: def, inst,
|
|
4217
|
+
});
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
// ── 阶段 2:**先剪枝**(此时索引已完整)──────────────────────────
|
|
4221
|
+
// ★ 2026-10-28(主人抓到抖动:"新增 7 个 → 下个 step 又移除 7 个"):
|
|
4222
|
+
// 以前是「循环里边扫描边注册」⇒ 注册发生时 deny 名单还不完整 ⇒ 模型先看到一步、下一步才被剪掉。
|
|
4223
|
+
// 现在顺序固定为 **扫描 → 剪枝 → 注册**:注册落地时 deny 已经就位,模型永远看不到那一步。
|
|
4224
|
+
for (const [c, tg] of extSeenCtx)
|
|
4225
|
+
applyExtToolVisibility(c, tg);
|
|
4226
|
+
// ── 阶段 3:全局注册 —— **始终注册**(注册表 != 可见性;可见由 deny 控制)。
|
|
4227
|
+
// ⚠️ 2026-10-28 修:原来按需取用模式下跳过注册,结果连"自己实例那份"都调不到
|
|
4228
|
+
// (deny 只能隐藏,不能凭空造出工具)。
|
|
4229
|
+
if (true) {
|
|
4230
|
+
for (const ent of extToolIndex.values()) {
|
|
4231
|
+
if (reg.has(ent.full))
|
|
3861
4232
|
continue;
|
|
4233
|
+
const def = ent.def;
|
|
4234
|
+
const inst = ent.inst;
|
|
4235
|
+
const full = ent.full;
|
|
3862
4236
|
try {
|
|
3863
4237
|
const tool = defineTool({
|
|
3864
4238
|
name: full,
|
|
3865
|
-
description: def.description + ' (用户扩展工具 · ' + inst.tag + ')',
|
|
4239
|
+
description: String(def.description ?? '') + ' (用户扩展工具 · ' + inst.tag + ')',
|
|
3866
4240
|
parameters: def.inputSchema,
|
|
3867
4241
|
output: {
|
|
3868
4242
|
schema: {
|
|
@@ -3872,7 +4246,6 @@ export function ensureExtToolsGlobal(globalCtx) {
|
|
|
3872
4246
|
render: (_a, v) => [{ type: 'text', text: v.ok ? v.msg : ('失败: ' + v.msg) }],
|
|
3873
4247
|
},
|
|
3874
4248
|
async execute(args, exec) {
|
|
3875
|
-
// 账号包:按注册名里的 tag 反查(这就是"名字即归属")
|
|
3876
4249
|
let pack;
|
|
3877
4250
|
try {
|
|
3878
4251
|
pack = buildWorkspaceQqPack(inst.tag, inst.dataRoot);
|
|
@@ -3887,7 +4260,6 @@ export function ensureExtToolsGlobal(globalCtx) {
|
|
|
3887
4260
|
logger: gctx.logger,
|
|
3888
4261
|
qq: pack,
|
|
3889
4262
|
};
|
|
3890
|
-
// 若这一次调用恰好来自 QQ 会话,则会话级上下文优先(sender/replyTarget/manager 用当前会话的)
|
|
3891
4263
|
try {
|
|
3892
4264
|
const sess = findSessionRec(channelOf(exec), exec);
|
|
3893
4265
|
if (sess) {
|
|
@@ -3915,15 +4287,13 @@ export function ensureExtToolsGlobal(globalCtx) {
|
|
|
3915
4287
|
if (typeof d === 'function')
|
|
3916
4288
|
reg.set(full, d);
|
|
3917
4289
|
extGlobDiag('OK registered ' + full);
|
|
3918
|
-
extDiag('[extglob] 全局注册 ' + full);
|
|
3919
|
-
gctx.logger?.info?.('[channel-tools] 扩展工具已暴露给全部会话: ' + full);
|
|
3920
4290
|
}
|
|
3921
4291
|
catch (e) {
|
|
3922
4292
|
extGlobDiag('FAIL register ' + full + ' :: ' + ((e instanceof Error ? e.message : String(e)) || '').slice(0, 200));
|
|
3923
|
-
extDiag('[extglob] 注册失败 ' + full + ': ' + ((e instanceof Error ? e.message : String(e)) || '').slice(0, 160));
|
|
3924
4293
|
}
|
|
3925
4294
|
}
|
|
3926
4295
|
}
|
|
4296
|
+
extGlobDiag('index=' + extToolIndex.size + ' 可领取');
|
|
3927
4297
|
}
|
|
3928
4298
|
catch (e) {
|
|
3929
4299
|
extGlobDiag('EXCEPTION ' + ((e instanceof Error ? e.message : String(e)) || '').slice(0, 200));
|