@zaofan/dsh-qqbot 1.10.14 → 1.10.15
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 +25 -0
- package/dist/channel-tools.d.ts +6 -3
- package/dist/channel-tools.d.ts.map +1 -1
- package/dist/channel-tools.js +226 -18
- package/dist/channel-tools.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/features/botplay-ext.d.ts +4 -0
- package/dist/features/botplay-ext.d.ts.map +1 -1
- package/dist/features/botplay-ext.js +4 -1
- package/dist/features/botplay-ext.js.map +1 -1
- package/dist/features/md-media-rewrite.d.ts +17 -0
- package/dist/features/md-media-rewrite.d.ts.map +1 -0
- package/dist/features/md-media-rewrite.js +151 -0
- package/dist/features/md-media-rewrite.js.map +1 -0
- package/dist/features/md-media.d.ts +47 -0
- package/dist/features/md-media.d.ts.map +1 -0
- package/dist/features/md-media.js +219 -0
- package/dist/features/md-media.js.map +1 -0
- package/dist/features/reasoning-bus.d.ts +36 -0
- package/dist/features/reasoning-bus.d.ts.map +1 -0
- package/dist/features/reasoning-bus.js +65 -0
- package/dist/features/reasoning-bus.js.map +1 -0
- package/dist/gateway/bootstrap.d.ts.map +1 -1
- package/dist/gateway/bootstrap.js +36 -1
- package/dist/gateway/bootstrap.js.map +1 -1
- package/dist/transport/events.d.ts +4 -0
- package/dist/transport/events.d.ts.map +1 -1
- package/dist/transport/events.js +3 -0
- package/dist/transport/events.js.map +1 -1
- package/dist/transport/outbound.d.ts.map +1 -1
- package/dist/transport/outbound.js +45 -1
- package/dist/transport/outbound.js.map +1 -1
- package/examples/ext-outbound/report-relay.mjs +109 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -728,3 +728,28 @@ autoRecall:
|
|
|
728
728
|
## License
|
|
729
729
|
|
|
730
730
|
[MIT](./LICENSE)
|
|
731
|
+
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
## 正文图组:markdown 图片直接发
|
|
735
|
+
|
|
736
|
+
DSH 前端会把**回复正文里的 markdown 图片**渲染成图;本插件把同一段文本**原样转发到 QQ** ——
|
|
737
|
+
本地路径会自动上传成 `qqbot-media` 引用(读文件头补真实宽高,配额满 100 张自动删最早),不需要调用任何工具。
|
|
738
|
+
|
|
739
|
+
```markdown
|
|
740
|
+

|
|
741
|
+
|
|
742
|
+
第一幕:本鲸宣布今晚不睡了
|
|
743
|
+
|
|
744
|
+

|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
**一行一张、连续几行就是图组**(小剧场 / 分镜 / 图文并排),QQ 侧按顺序渲染成一屏。
|
|
748
|
+
|
|
749
|
+
| DSH 前端 | QQ 群 |
|
|
750
|
+
| :---: | :---: |
|
|
751
|
+
|  |  |
|
|
752
|
+
|
|
753
|
+
配合表情包库最顺手:`list_stickers` 搜到图 → 把本地路径写进正文 → 图组直接出来,比连发 `send_media` 更像翻漫画。
|
|
754
|
+
|
|
755
|
+
失败策略:正文里图传不上去会**原样留在文字里**(不阻断本条);而 `send_md` 卡片是“图传失败就整条不发”,两者别混用。
|
package/dist/channel-tools.d.ts
CHANGED
|
@@ -111,13 +111,16 @@ export declare function syncContextTools(agentCtx: unknown, shouldHave: boolean,
|
|
|
111
111
|
export declare function ensureExtToolsGlobal(globalCtx: unknown): void;
|
|
112
112
|
export declare function ensureContextToolsGlobal(globalCtx: unknown): void;
|
|
113
113
|
/**
|
|
114
|
-
* 按会话**实时剪枝**三个上下文工具的可见性(2026-10-09
|
|
114
|
+
* 按会话**实时剪枝**三个上下文工具的可见性(2026-10-09 起;2026-10-21 按 `dsh-computer-use` 的实测修)。
|
|
115
115
|
*
|
|
116
116
|
* 语义:**开了「无上下文模式 + 智能判断」**的会话 → 看得见那三个工具;
|
|
117
117
|
* 否则该会话**连工具名都看不到**(模型清单里没有 = 零 token),而**注册表纹丝不动**。
|
|
118
|
-
* 面板一改开关 → 下一次 pre-step
|
|
118
|
+
* 面板一改开关 → 下一次 pre-step(下一回合 / 下一 step)即生效。
|
|
119
119
|
*
|
|
120
|
-
*
|
|
120
|
+
* 2026-10-21 修两处(对齐 `dsh-plugins/dsh-computer-use/internal/schema-visibility.mjs`):
|
|
121
|
+
* ① `ctx.tools` 在未 inject 时会抛 ⇒ 必须退化到 `ctx.get('tools')`,否则"剪枝"整段被 catch 吞掉;
|
|
122
|
+
* ② `restrict()` 的返回值是**函数**,释放必须走 {@link liftRestriction}。
|
|
123
|
+
* 幂等:同一作用域状态没变化就直接 return,不重复设/撤 restriction(避免无谓变更通知与抖动)。
|
|
121
124
|
*/
|
|
122
125
|
export declare function restrictContextTools(agentCtx: unknown, shouldHave: boolean, env?: {
|
|
123
126
|
dataRoot?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-tools.d.ts","sourceRoot":"","sources":["../src/channel-tools.ts"],"names":[],"mappings":"
|
|
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
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* 复制进共享 node_modules,也不得在 dependencies 里自带 dsh-tools(避免双包)。
|
|
9
9
|
*/
|
|
10
10
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
11
|
+
import { uploadMarkdownImage, listMarkdownImages, deleteMarkdownImage } from './features/md-media.js';
|
|
12
|
+
import { setMaterialTargetFactory } from './features/md-media-rewrite.js';
|
|
11
13
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
12
14
|
import { join, dirname } from 'node:path';
|
|
13
15
|
import { homedir } from 'node:os';
|
|
@@ -169,6 +171,19 @@ function findChannelByTag(tag) {
|
|
|
169
171
|
* @param tag 人设 id(如 whale)
|
|
170
172
|
* @param dataRoot 插件数据根(群台账在这里)
|
|
171
173
|
*/
|
|
174
|
+
// 2026-10-21: 出站自动传图需要 groupAdmin 句柄;取**任一**桥(不依赖当前会话)
|
|
175
|
+
setMaterialTargetFactory(() => {
|
|
176
|
+
try {
|
|
177
|
+
// 2026-10-21 修:channelBridges 实测是空的(同文件 204 行注释也写了),要用 managersOf()
|
|
178
|
+
for (const m of managersOf()) {
|
|
179
|
+
const cc = m.groupAdmin;
|
|
180
|
+
if (cc)
|
|
181
|
+
return { apiCall: cc.apiCall.bind(cc) };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch { /* ignore */ }
|
|
185
|
+
return undefined;
|
|
186
|
+
});
|
|
172
187
|
function buildWorkspaceQqPack(tag, dataRoot) {
|
|
173
188
|
const hit = findChannelByTag(tag);
|
|
174
189
|
if (!hit)
|
|
@@ -290,9 +305,63 @@ function buildWorkspaceQqPack(tag, dataRoot) {
|
|
|
290
305
|
},
|
|
291
306
|
sendMarkdown: (scope, id, md) => (typeof sender?.sendMarkdown === 'function' && id ? Promise.resolve(sender.sendMarkdown(toTarget(scope, id), String(md))).then(() => true).catch(() => false) : Promise.resolve(false)),
|
|
292
307
|
sendImage: (scope, id, src) => (typeof sender?.sendMedia === 'function' && id ? Promise.resolve(sender.sendMedia(toTarget(scope, id), 'image', src)).then(() => true).catch(() => false) : Promise.resolve(false)),
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
308
|
+
// 2026-10-22(主人定):素材上传等本来**不需要群管理权限** —— 这里直接用机器人自己的
|
|
309
|
+
// access_token 通道(与 api/group-admin.ts 同源),不再借 groupAdmin,也不依赖本实例是否启用群管理。
|
|
310
|
+
apiCall: (() => {
|
|
311
|
+
const cred = globalThis.__qqbotAppCred;
|
|
312
|
+
const appId = String(mgr?.config?.appId || cred?.appId || '');
|
|
313
|
+
const appSecret = String(mgr?.config?.appSecret || cred?.appSecret || '');
|
|
314
|
+
if (!appId || !appSecret)
|
|
315
|
+
return undefined;
|
|
316
|
+
const g = globalThis;
|
|
317
|
+
if (!g.__qqbotBotToken)
|
|
318
|
+
g.__qqbotBotToken = new Map();
|
|
319
|
+
const cache = g.__qqbotBotToken;
|
|
320
|
+
const token = async () => {
|
|
321
|
+
const hit = cache.get(appId);
|
|
322
|
+
if (hit && Date.now() < hit.at)
|
|
323
|
+
return hit.t;
|
|
324
|
+
try {
|
|
325
|
+
const res = await fetch('https://bots.qq.com/app/getAppAccessToken', {
|
|
326
|
+
method: 'POST',
|
|
327
|
+
headers: { 'content-type': 'application/json' },
|
|
328
|
+
body: JSON.stringify({ appId, clientSecret: appSecret }),
|
|
329
|
+
});
|
|
330
|
+
const j = (await res.json());
|
|
331
|
+
if (!j.access_token)
|
|
332
|
+
return undefined;
|
|
333
|
+
cache.set(appId, { t: j.access_token, at: Date.now() + ((j.expires_in ?? 7200) - 60) * 1000 });
|
|
334
|
+
return j.access_token;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
return async (method, path, body) => {
|
|
341
|
+
const tk = await token();
|
|
342
|
+
if (!tk)
|
|
343
|
+
return { ok: false, err: { code: 'no-token', human: '取不到 access_token(appId/appSecret 缺失或网络失败)' } };
|
|
344
|
+
try {
|
|
345
|
+
const res = await fetch('https://api.sgroup.qq.com' + path, {
|
|
346
|
+
method,
|
|
347
|
+
headers: { Authorization: 'QQBot ' + tk, 'content-type': 'application/json' },
|
|
348
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
349
|
+
});
|
|
350
|
+
const text = await res.text();
|
|
351
|
+
let data = {};
|
|
352
|
+
try {
|
|
353
|
+
data = text ? JSON.parse(text) : {};
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
data = { raw: text };
|
|
357
|
+
}
|
|
358
|
+
return { ok: res.ok, data, err: res.ok ? undefined : { code: res.status, human: text.slice(0, 200) } };
|
|
359
|
+
}
|
|
360
|
+
catch (e) {
|
|
361
|
+
return { ok: false, err: { code: 'net', human: e?.message ?? String(e) } };
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
})(),
|
|
296
365
|
};
|
|
297
366
|
}
|
|
298
367
|
/** 全局「智能判断」开关(由 session-manager 初始化时注入;照 setChannelBridge 同款做法) */
|
|
@@ -2336,6 +2405,91 @@ export async function apply(ctx) {
|
|
|
2336
2405
|
}
|
|
2337
2406
|
},
|
|
2338
2407
|
});
|
|
2408
|
+
/** 素材管理(2026-10-21 新接口 /v2/media-images/*:本地图 → qqbot-media:// 引用串) */
|
|
2409
|
+
function anyAdminOf(exec) {
|
|
2410
|
+
try {
|
|
2411
|
+
const s = findSessionRec(channelOf(exec), exec);
|
|
2412
|
+
const client = s?.ch?.manager?.groupAdmin;
|
|
2413
|
+
const root = String(s?.ch?.manager?.config?.dataRoot ?? '');
|
|
2414
|
+
if (client)
|
|
2415
|
+
return { client, dataRoot: root };
|
|
2416
|
+
for (const b of channelBridges) {
|
|
2417
|
+
try {
|
|
2418
|
+
const c = b.manager.groupAdmin;
|
|
2419
|
+
if (c)
|
|
2420
|
+
return { client: c, dataRoot: String(b.manager.config?.dataRoot ?? '') };
|
|
2421
|
+
}
|
|
2422
|
+
catch { /* 跳过坏桥 */ }
|
|
2423
|
+
}
|
|
2424
|
+
return undefined;
|
|
2425
|
+
}
|
|
2426
|
+
catch {
|
|
2427
|
+
return undefined;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
const materialTool = defineTool({
|
|
2431
|
+
name: 'qq_material',
|
|
2432
|
+
description: 'QQ 素材管理(Markdown 图片): 把**本地图片**传到 QQ 平台换 qqbot-media:// 引用串,可直接写进 markdown 消息(不再需要公网图床)。限制: 单张 ≤10MB、png/jpeg/gif/webp、单机器人最多 100 张、图片滑动过期 90 天(每次被引用顺延)。action=upload 传图 / list 看已传列表 / delete 删除释放配额。',
|
|
2433
|
+
parameters: {
|
|
2434
|
+
action: { type: 'string', enum: ['upload', 'list', 'delete'], description: '默认 upload' },
|
|
2435
|
+
image: { type: 'string', description: 'upload: 本地图片绝对路径' },
|
|
2436
|
+
image_id: { type: 'string', description: 'delete: 图片 ID(list 里能看到)' },
|
|
2437
|
+
cursor: { type: 'string', description: 'list: 翻页游标(首页不传)' },
|
|
2438
|
+
},
|
|
2439
|
+
output: {
|
|
2440
|
+
schema: {
|
|
2441
|
+
type: 'object',
|
|
2442
|
+
properties: { ok: { type: 'boolean' }, msg: { type: 'string' }, url: { type: 'string' }, markdown: { type: 'string' } },
|
|
2443
|
+
additionalProperties: false,
|
|
2444
|
+
},
|
|
2445
|
+
render: (_a, v) => [{ type: 'text', text: String(v.msg || (v.ok ? 'ok' : '失败')) }],
|
|
2446
|
+
},
|
|
2447
|
+
execute: async (args, exec) => {
|
|
2448
|
+
const mat = anyAdminOf(exec);
|
|
2449
|
+
if (!mat)
|
|
2450
|
+
return { ok: false, msg: '拿不到机器人实例(没有已连接的本)' };
|
|
2451
|
+
const target = { apiCall: mat.client.apiCall.bind(mat.client) };
|
|
2452
|
+
const action = String(args.action ?? 'upload').trim() || 'upload';
|
|
2453
|
+
if (action === 'upload') {
|
|
2454
|
+
const r = await uploadMarkdownImage(target, String(args.image ?? '').trim());
|
|
2455
|
+
if (!r.ok)
|
|
2456
|
+
return { ok: false, msg: '上传失败: ' + String(r.msg || '') };
|
|
2457
|
+
const name = String(args.image ?? '').split(/[\\/]/).pop() || 'image';
|
|
2458
|
+
const dim = r.width && r.height ? ' #' + r.width + 'px #' + r.height + 'px' : '';
|
|
2459
|
+
const markdown = ' + ')';
|
|
2460
|
+
const head = (r.cached ? '✅ 命中本地缓存,未重复上传' : '✅ 已上传')
|
|
2461
|
+
+ '(' + Math.round((r.bytes ?? 0) / 1024) + 'KB'
|
|
2462
|
+
+ (r.width ? ',' + r.width + '×' + r.height : '') + ')';
|
|
2463
|
+
return { ok: true, url: String(r.url), markdown, msg: head + '\n' + markdown };
|
|
2464
|
+
}
|
|
2465
|
+
if (action === 'list') {
|
|
2466
|
+
const r = await listMarkdownImages(target, String(args.cursor ?? ''));
|
|
2467
|
+
if (!r.ok)
|
|
2468
|
+
return { ok: false, msg: '查询失败: ' + String(r.msg || '') };
|
|
2469
|
+
const d = (r.data ?? {});
|
|
2470
|
+
const list = Array.isArray(d.images) ? d.images : [];
|
|
2471
|
+
const lines = list.slice(0, 20).map((x, i) => {
|
|
2472
|
+
const exp = x.expires_at ? ' | 到期 ' + new Date(Number(x.expires_at) * 1000).toISOString().slice(0, 10) : '';
|
|
2473
|
+
return i + 1 + '. ' + String(x.file_name || '?') + ' | ' + String(x.image_id || '') + exp;
|
|
2474
|
+
});
|
|
2475
|
+
const more = d.next_cursor ? '(还有下一页,cursor=' + d.next_cursor + ')' : '';
|
|
2476
|
+
return {
|
|
2477
|
+
ok: true,
|
|
2478
|
+
msg: '共 ' + String(d.total ?? list.length) + ' 张' + more + (lines.length ? '\n' + lines.join('\n') : '\n(列表为空)'),
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
if (action === 'delete') {
|
|
2482
|
+
const id = String(args.image_id ?? '').trim();
|
|
2483
|
+
if (!id)
|
|
2484
|
+
return { ok: false, msg: 'delete 需要 image_id' };
|
|
2485
|
+
const r = await deleteMarkdownImage(target, id);
|
|
2486
|
+
return r.ok
|
|
2487
|
+
? { ok: true, msg: '✅ 已删除 ' + id + '(配额已释放;已发出的消息不受影响)' }
|
|
2488
|
+
: { ok: false, msg: '删除失败: ' + String(r.msg || '') };
|
|
2489
|
+
}
|
|
2490
|
+
return { ok: false, msg: '未知 action: ' + action + '(可选 upload / list / delete)' };
|
|
2491
|
+
},
|
|
2492
|
+
});
|
|
2339
2493
|
const recallBatchTool = defineTool({
|
|
2340
2494
|
name: 'group_recall_batch',
|
|
2341
2495
|
description: '群管理(写,危险): 批量撤回消息 + 可选批量禁言发送者。'
|
|
@@ -2760,6 +2914,7 @@ export async function apply(ctx) {
|
|
|
2760
2914
|
{ name: 'group_recall_batch', tool: recallBatchTool },
|
|
2761
2915
|
{ name: 'recall_keywords', tool: recallKeywordsTool },
|
|
2762
2916
|
{ name: 'group_kick', tool: kickMemberTool },
|
|
2917
|
+
{ name: 'qq_material', tool: materialTool },
|
|
2763
2918
|
{ name: 'id_lookup', tool: idLookupTool },
|
|
2764
2919
|
{ name: 'session_list', tool: sessionListTool },
|
|
2765
2920
|
{ name: 'session_wake', tool: sessionWakeTool },
|
|
@@ -3717,6 +3872,11 @@ export function ensureExtToolsGlobal(globalCtx) {
|
|
|
3717
3872
|
})();
|
|
3718
3873
|
}
|
|
3719
3874
|
export function ensureContextToolsGlobal(globalCtx) {
|
|
3875
|
+
// 2026-10-21 修(trace 实证):restrict 报 "names unknown global tools" => 我们注册的层不是「全局层」。
|
|
3876
|
+
// cordis 的 ctx.root 才是根作用域;拿不到就退回传入值(行为不变)。
|
|
3877
|
+
const rootOfGlobal = (globalCtx.root ?? globalCtx);
|
|
3878
|
+
if (rootOfGlobal !== globalCtx)
|
|
3879
|
+
traceToolReg('[ctxtool-reg] 用 root ctx 注册(不同于传入的 ctx)');
|
|
3720
3880
|
try {
|
|
3721
3881
|
if (!globalCtx || typeof globalCtx !== 'object')
|
|
3722
3882
|
return;
|
|
@@ -3725,7 +3885,7 @@ export function ensureContextToolsGlobal(globalCtx) {
|
|
|
3725
3885
|
// 表现就是"全局注册压根没发生"(trace 里只有别的报错、没有"全局注册 N 件")。
|
|
3726
3886
|
// 正解:**用 cordis 的运行时取服务 `ctx.get('tools')`**(不需要 inject 声明)——
|
|
3727
3887
|
// 本插件别处也是这个姿势(见 ext-capabilities 给扩展工具的 kernel 文档:`ctx.get('tools')`)。
|
|
3728
|
-
const ctxObj =
|
|
3888
|
+
const ctxObj = rootOfGlobal;
|
|
3729
3889
|
let toolsObj;
|
|
3730
3890
|
try {
|
|
3731
3891
|
toolsObj = ctxObj.tools;
|
|
@@ -3763,14 +3923,55 @@ export function ensureContextToolsGlobal(globalCtx) {
|
|
|
3763
3923
|
}
|
|
3764
3924
|
/** 每个 agent 作用域当前的可见性状态(幂等用:状态没变就不动) */
|
|
3765
3925
|
const ctxRestrictState = new WeakMap();
|
|
3926
|
+
/** 释放 restriction 的唯一正确姿势(2026-10-21,抄 dsh-computer-use/internal/schema-visibility.mjs):
|
|
3927
|
+
* cordis 的 `ctx.effect(fn)` 返回的是**函数**(`Disposable<T> = () => T`),
|
|
3928
|
+
* 对函数调 `.dispose?.()` 会**静默变成空操作** —— 限制挂上去、再也没摘下来。
|
|
3929
|
+
* 所以函数优先,`.dispose()` 只作兼容。
|
|
3930
|
+
*/
|
|
3931
|
+
function liftRestriction(disposable) {
|
|
3932
|
+
if (typeof disposable === 'function') {
|
|
3933
|
+
try {
|
|
3934
|
+
disposable();
|
|
3935
|
+
}
|
|
3936
|
+
catch { /* ignore */ }
|
|
3937
|
+
return;
|
|
3938
|
+
}
|
|
3939
|
+
const d = disposable;
|
|
3940
|
+
if (d && typeof d.dispose === 'function') {
|
|
3941
|
+
try {
|
|
3942
|
+
d.dispose();
|
|
3943
|
+
}
|
|
3944
|
+
catch { /* ignore */ }
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
/** 取某作用域的 tools 服务:`ctx.tools` 在未声明 inject 时会抛,退化到 cordis 运行时 `ctx.get('tools')`。 */
|
|
3948
|
+
function toolsOfScope(agentCtx) {
|
|
3949
|
+
const c = agentCtx;
|
|
3950
|
+
try {
|
|
3951
|
+
const t = c?.tools;
|
|
3952
|
+
if (t && typeof t === 'object')
|
|
3953
|
+
return t;
|
|
3954
|
+
}
|
|
3955
|
+
catch { /* 未 inject 时读 ctx.tools 会抛 */ }
|
|
3956
|
+
try {
|
|
3957
|
+
const t = c?.get?.('tools');
|
|
3958
|
+
if (t && typeof t === 'object')
|
|
3959
|
+
return t;
|
|
3960
|
+
}
|
|
3961
|
+
catch { /* ignore */ }
|
|
3962
|
+
return undefined;
|
|
3963
|
+
}
|
|
3766
3964
|
/**
|
|
3767
|
-
* 按会话**实时剪枝**三个上下文工具的可见性(2026-10-09
|
|
3965
|
+
* 按会话**实时剪枝**三个上下文工具的可见性(2026-10-09 起;2026-10-21 按 `dsh-computer-use` 的实测修)。
|
|
3768
3966
|
*
|
|
3769
3967
|
* 语义:**开了「无上下文模式 + 智能判断」**的会话 → 看得见那三个工具;
|
|
3770
3968
|
* 否则该会话**连工具名都看不到**(模型清单里没有 = 零 token),而**注册表纹丝不动**。
|
|
3771
|
-
* 面板一改开关 → 下一次 pre-step
|
|
3969
|
+
* 面板一改开关 → 下一次 pre-step(下一回合 / 下一 step)即生效。
|
|
3772
3970
|
*
|
|
3773
|
-
*
|
|
3971
|
+
* 2026-10-21 修两处(对齐 `dsh-plugins/dsh-computer-use/internal/schema-visibility.mjs`):
|
|
3972
|
+
* ① `ctx.tools` 在未 inject 时会抛 ⇒ 必须退化到 `ctx.get('tools')`,否则"剪枝"整段被 catch 吞掉;
|
|
3973
|
+
* ② `restrict()` 的返回值是**函数**,释放必须走 {@link liftRestriction}。
|
|
3974
|
+
* 幂等:同一作用域状态没变化就直接 return,不重复设/撤 restriction(避免无谓变更通知与抖动)。
|
|
3774
3975
|
*/
|
|
3775
3976
|
export function restrictContextTools(agentCtx, shouldHave, env) {
|
|
3776
3977
|
try {
|
|
@@ -3784,27 +3985,34 @@ export function restrictContextTools(agentCtx, shouldHave, env) {
|
|
|
3784
3985
|
const st = ctxRestrictState.get(key);
|
|
3785
3986
|
if (st && st.shouldHave === shouldHave)
|
|
3786
3987
|
return; // ← 状态一致:不动(这是"不抖"的关键)
|
|
3787
|
-
if (st?.dispose)
|
|
3788
|
-
|
|
3789
|
-
st.dispose();
|
|
3790
|
-
}
|
|
3791
|
-
catch { /* ignore */ }
|
|
3792
|
-
}
|
|
3793
|
-
const toolsObj = agentCtx.tools;
|
|
3794
|
-
const restrict = toolsObj?.restrict;
|
|
3988
|
+
if (st?.dispose)
|
|
3989
|
+
liftRestriction(st.dispose);
|
|
3795
3990
|
let dispose;
|
|
3796
|
-
if (!shouldHave
|
|
3991
|
+
if (!shouldHave) {
|
|
3992
|
+
const toolsObj = toolsOfScope(agentCtx);
|
|
3993
|
+
const restrict = toolsObj?.restrict;
|
|
3994
|
+
if (typeof restrict !== 'function') {
|
|
3995
|
+
traceToolReg('[ctxtool-reg] 剪枝失败: 拿不到 tools.restrict(ctx.tools 与 ctx.get("tools") 都取不到)');
|
|
3996
|
+
ctxRestrictState.set(key, { shouldHave });
|
|
3997
|
+
return;
|
|
3998
|
+
}
|
|
3797
3999
|
try {
|
|
3798
4000
|
const d = restrict.call(toolsObj, { deny: [...CTX_TOOL_NAMES] });
|
|
3799
4001
|
if (typeof d === 'function')
|
|
3800
4002
|
dispose = d;
|
|
4003
|
+
else if (d && typeof d.dispose === 'function') {
|
|
4004
|
+
const dd = d;
|
|
4005
|
+
dispose = () => { dd.dispose(); };
|
|
4006
|
+
}
|
|
4007
|
+
traceToolReg('[ctxtool-reg] 剪枝已挂 deny=' + CTX_TOOL_NAMES.join(',') + ' 返回形态=' + (typeof d));
|
|
3801
4008
|
}
|
|
3802
4009
|
catch (e) {
|
|
3803
|
-
traceToolReg('[ctxtool-reg] restrict
|
|
4010
|
+
traceToolReg('[ctxtool-reg] restrict 抛错: ' + String(e?.message ?? e).slice(0, 160));
|
|
3804
4011
|
}
|
|
3805
4012
|
}
|
|
3806
4013
|
ctxRestrictState.set(key, dispose ? { shouldHave, dispose } : { shouldHave });
|
|
3807
|
-
traceToolReg(`[ctxtool-reg] ${shouldHave ? '放开(本会话继承全局三个工具)' : '剪枝(本会话看不到三个工具)'}
|
|
4014
|
+
traceToolReg(`[ctxtool-reg] ${shouldHave ? '放开(本会话继承全局三个工具)' : '剪枝(本会话看不到三个工具)'}`
|
|
4015
|
+
+ ` dispose=${dispose ? '有' : '无'} dataRoot=${String(env?.dataRoot ?? '')} sk=${String(env?.sessionKey ?? '')}`);
|
|
3808
4016
|
}
|
|
3809
4017
|
catch (e) {
|
|
3810
4018
|
traceToolReg('[ctxtool-reg] 剪枝异常: ' + String(e?.message ?? e).slice(0, 160));
|