dsh-tool-folder 0.2.2 → 0.2.4
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 +42 -0
- package/lib/index.js +105 -28
- package/lib/ux.js +285 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -149,6 +149,48 @@ were never unregistered, so nothing else changes.
|
|
|
149
149
|
|
|
150
150
|
## Changelog
|
|
151
151
|
|
|
152
|
+
### v0.2.4 — P1: batch tools_schema + sibling closure (2026-08-31)
|
|
153
|
+
1. **Batch expand** — `tools_schema` accepts `names: [...]` to expand several
|
|
154
|
+
tools in one call (`{ found, results: [...] }`); a single `name` keeps the
|
|
155
|
+
legacy 6-field shape byte-for-byte.
|
|
156
|
+
2. **Sibling closure** (`closureSize`, default 0 = off, mirrors the
|
|
157
|
+
`maxWarmTools` compat precedent): when enabled, expanding a tool also lists
|
|
158
|
+
lightweight sibling entries (name + 120-char description) from the same
|
|
159
|
+
server prefix, so the model can batch-expand what it actually needs —
|
|
160
|
+
kills the "expanded A, now I need B" round-trip. `closure: false` opts out
|
|
161
|
+
per call.
|
|
162
|
+
3. Output contract moved to a pure function (`buildSchemaResponse`, ux.js) —
|
|
163
|
+
11 new tests pin single/batch/missing/siblings branches (94 total green).
|
|
164
|
+
4. Two-axis audit (Standards + Spec): 1 Critical fixed during review — the
|
|
165
|
+
single/batch routing condition was inverted (old `name`-only calls would
|
|
166
|
+
have returned the batch shape); contract tests now catch that class
|
|
167
|
+
instantly. `siblingClosure` dead code removed.
|
|
168
|
+
|
|
169
|
+
### v0.2.3 — fold-ux: search fallback catalog + query expansion + stable results + discovery stats (2026-08-30)
|
|
170
|
+
1. **Zero-match fallback catalog** (`fallbackCatalogSize`, default 30): when
|
|
171
|
+
`tools_search` finds nothing, it returns a name-sorted lightweight catalog
|
|
172
|
+
(name + 80-char description) instead of an empty list — the model never
|
|
173
|
+
concludes a capability doesn't exist just because its wording missed.
|
|
174
|
+
`total` reports the real catalog size (not the listed slice); the output
|
|
175
|
+
carries `fallback: true`. `0` restores the legacy empty result.
|
|
176
|
+
2. **CN→EN query expansion** (`queryExpandEnabled`, default on): a 48-term
|
|
177
|
+
high-frequency intent table appends English synonyms to the search text
|
|
178
|
+
(search/find, file, image/ocr, database, deploy, schedule…). Only adds
|
|
179
|
+
terms, never rewrites the original query; exact-name priority still uses
|
|
180
|
+
the raw query, so precise lookups are byte-identical to v0.2.2.
|
|
181
|
+
3. **Stable results** (`createSearchCache`, 60s TTL / LRU 200): the same query
|
|
182
|
+
returns the same result set across turns — kills per-turn candidate drift.
|
|
183
|
+
4. **Discovery stats** (`recordDiscoveries`): every found tool is counted and
|
|
184
|
+
persisted (30s-throttled, merged into tool-folder-state.json); on restart
|
|
185
|
+
the top-5 are logged so low-discovery tools can be identified and their
|
|
186
|
+
descriptions improved (Anthropic's "monitor discovery rate" loop).
|
|
187
|
+
5. 83 tests green (11 new: ux module).
|
|
188
|
+
6. Three-axis audit (Standards / Spec / compatibility): 1 P1 fixed — warm and
|
|
189
|
+
discovery state now merge-write the shared state file instead of clobbering
|
|
190
|
+
each other; P2 hardening — `tools_search` execute got a whole-body
|
|
191
|
+
fail-open catch, fallback `total` semantics clarified, comments corrected
|
|
192
|
+
to process-level scope.
|
|
193
|
+
|
|
152
194
|
### v0.2.2 — tiered disclosure budget + warm LRU + incremental semantic cache (2026-08-30)
|
|
153
195
|
1. **Tiered disclosure budget** (`disclosureBudget`, default 0 = off): caps how
|
|
154
196
|
many tool names + descriptions are disclosed per request. Over budget, tools
|
package/lib/index.js
CHANGED
|
@@ -32,10 +32,11 @@ import fs from "node:fs";
|
|
|
32
32
|
import path from "node:path";
|
|
33
33
|
import z from "@deepseek-ai/schemastery";
|
|
34
34
|
import { buildIndex, search } from "./bm25.js";
|
|
35
|
-
import { compressTool, normalizeDescription
|
|
35
|
+
import { compressTool, normalizeDescription } from "./schema.js";
|
|
36
36
|
import { selectionCoverage, isHotTool, prioritizeExact, touchWarm, evictWarm, missingMetaTools } from "./metrics.js";
|
|
37
37
|
import { tierForBudget } from "./budget.js";
|
|
38
38
|
import { readState, writeState } from "./storage.js";
|
|
39
|
+
import { expandQuery, fallbackCatalog, createSearchCache, recordDiscoveries, serializeDiscoveries, deserializeDiscoveries, siblingClosure, buildSchemaResponse } from "./ux.js";
|
|
39
40
|
import { ChainGuard, verdict } from "./chainguard.js";
|
|
40
41
|
import { matchCategory } from "./category.js";
|
|
41
42
|
import { toonifyValue } from "./toonify.js";
|
|
@@ -83,6 +84,12 @@ export const Config = z.object({
|
|
|
83
84
|
.description("分级披露预算(字节):0=关闭。开启后可见工具 schema 估算字节超预算时,动态段按 完整schema→name+desc→仅name→折叠进catalog 逐级降级(core/hot 永不降级)"),
|
|
84
85
|
maxWarmTools: z.number().min(0).max(50).default(0)
|
|
85
86
|
.description("warm LRU 上限:tools_search/schema 发现或折叠后被调用的工具进入 warm 集合,下一轮注入(完整 schema);达上限逐出最久未用;0=关闭(默认关,无配置升级逐字节等价,显式开启才启用)"),
|
|
87
|
+
fallbackCatalogSize: z.number().min(0).max(200).default(30)
|
|
88
|
+
.description("P0: tools_search 零匹配时回退的轻目录条数(name+80字描述,名字典序);0=关闭回退(返回空=旧行为)"),
|
|
89
|
+
queryExpandEnabled: z.boolean().default(true)
|
|
90
|
+
.description("P0: 中文意图→英文同义词扩展(只加词不改原词,扩大 BM25/语义召回;精确名匹配不受影响)"),
|
|
91
|
+
closureSize: z.number().min(0).max(20).default(0)
|
|
92
|
+
.description("P1: tools_schema 展开时附带同 server 兄弟工具的轻目录条数(name+120字描述,名字典序);0=关闭(默认关=无配置升级输出逐字段等价,显式开启才启用,与 maxWarmTools 先例一致)"),
|
|
86
93
|
});
|
|
87
94
|
|
|
88
95
|
const DEFAULTS = {
|
|
@@ -119,6 +126,9 @@ const DEFAULTS = {
|
|
|
119
126
|
embedModel: "bge-m3",
|
|
120
127
|
disclosureBudget: 0, // v0.2.2: 分级披露预算(0=关闭)
|
|
121
128
|
maxWarmTools: 0, // v0.2.2: warm LRU 上限(0=关闭,默认关=无配置升级逐字节等价)
|
|
129
|
+
fallbackCatalogSize: 30, // P0: 零匹配回退轻目录条数(0=关闭)
|
|
130
|
+
queryExpandEnabled: true, // P0: 中文→英文 query 扩展
|
|
131
|
+
closureSize: 0, // P1: tools_schema 同 server 兄弟闭包(默认关=逐字段等价,显式开启)
|
|
122
132
|
};
|
|
123
133
|
|
|
124
134
|
function feedbackPath(cfg) {
|
|
@@ -330,7 +340,8 @@ export function apply(ctx, config) {
|
|
|
330
340
|
};
|
|
331
341
|
const persistWarmState = () => {
|
|
332
342
|
if (!warmEnabled) return;
|
|
333
|
-
|
|
343
|
+
// merge 写:与 discoveries 共用 state 文件,整写会互抹(Standards 轴 P1)。
|
|
344
|
+
const payload = { ...readState(activeStateFile, {}), warm: [...warm.entries()], ts: Date.now() };
|
|
334
345
|
let ok = writeState(activeStateFile, payload);
|
|
335
346
|
if (!ok) {
|
|
336
347
|
// 首选目录不可写 → 降到 ~/.dsh/state/
|
|
@@ -340,6 +351,39 @@ export function apply(ctx, config) {
|
|
|
340
351
|
};
|
|
341
352
|
restoreWarm();
|
|
342
353
|
|
|
354
|
+
// P0-④: 发现率统计(独立于 warm 门控——maxWarmTools=0 时统计照常工作)。
|
|
355
|
+
// 与 warm 共用同一个 state 文件;落盘节流 30s(工具输出非注入面,频率低)。
|
|
356
|
+
let discoveries = new Map();
|
|
357
|
+
let lastPersistTs = 0;
|
|
358
|
+
const restoreDiscoveries = () => {
|
|
359
|
+
let cloud = readState(activeStateFile, null);
|
|
360
|
+
if (!cloud) {
|
|
361
|
+
activeStateFile = fallbackStateFile();
|
|
362
|
+
cloud = readState(activeStateFile, null);
|
|
363
|
+
}
|
|
364
|
+
discoveries = deserializeDiscoveries(cloud && Array.isArray(cloud.discoveries) ? cloud.discoveries : null);
|
|
365
|
+
};
|
|
366
|
+
const persistDiscoveries = () => {
|
|
367
|
+
const now = Date.now();
|
|
368
|
+
if (now - lastPersistTs < 30000) return; // 节流:30s 内只写一次
|
|
369
|
+
lastPersistTs = now;
|
|
370
|
+
// merge 写:与 warm 共用 state 文件,整写会互抹(Standards 轴 P1)。
|
|
371
|
+
const payload = { ...readState(activeStateFile, {}), discoveries: serializeDiscoveries(discoveries), ts: now };
|
|
372
|
+
let ok = writeState(activeStateFile, payload);
|
|
373
|
+
if (!ok) {
|
|
374
|
+
activeStateFile = fallbackStateFile();
|
|
375
|
+
writeState(activeStateFile, payload);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
restoreDiscoveries();
|
|
379
|
+
if (discoveries.size > 0) {
|
|
380
|
+
const top = serializeDiscoveries(discoveries, 5).map((e) => `${e.name}(${e.count})`).join(", ");
|
|
381
|
+
logger.info("discovery stats restored: %d tools, top: %s", discoveries.size, top);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// P0-②: 检索结果缓存(同 query 60s 内稳定;LRU 200 条;进程级共享)。
|
|
385
|
+
const searchCache = createSearchCache(200, 60000);
|
|
386
|
+
|
|
343
387
|
// "mcp__viking__remember" → "mcp__viking"; plain tools ("tool-bash") stay
|
|
344
388
|
// as themselves (single segment survives slice(0,2)).
|
|
345
389
|
const serverOf = (name) => String(name).split("__").slice(0, 2).join("__");
|
|
@@ -440,6 +484,7 @@ export function apply(ctx, config) {
|
|
|
440
484
|
},
|
|
441
485
|
},
|
|
442
486
|
total: { type: "integer", required: true },
|
|
487
|
+
fallback: { type: "boolean", description: "true = 零匹配,结果回退为按名字典序的轻目录" },
|
|
443
488
|
},
|
|
444
489
|
};
|
|
445
490
|
const toolsSearch = defineTool({
|
|
@@ -461,15 +506,27 @@ export function apply(ctx, config) {
|
|
|
461
506
|
output: { schema: outputSchema, render: (_a, v) => [{ type: "text", text: JSON.stringify(v, null, 2) }] },
|
|
462
507
|
isConcurrencySafe: () => true,
|
|
463
508
|
async execute(args) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
509
|
+
// 整体 fail-open 兜底(Spec 轴 P2):任何未来 helper 意外抛错 → 空结果
|
|
510
|
+
// 而非硬失败,保持"折叠绝不误伤能力"的底线。
|
|
511
|
+
try {
|
|
512
|
+
const q = String(args.query || "");
|
|
513
|
+
const lim = args.limit ?? 8;
|
|
514
|
+
if (!index) return { query: q, results: [], total: 0 };
|
|
515
|
+
// P0-②: 同 query 内容指纹的结果缓存——重复搜索返回同一结果集(漂移的
|
|
516
|
+
// 最小修)。进程级共享(非单会话隔离);空 query("*" 语义)不走缓存。
|
|
517
|
+
const cacheKey = `${lim}|${q}`;
|
|
518
|
+
if (q) {
|
|
519
|
+
const cached = searchCache.get(cacheKey);
|
|
520
|
+
if (cached) return cached;
|
|
521
|
+
}
|
|
522
|
+
// P0-③: 中文意图→英文同义词扩展(只加词不改原词;精确名匹配用原 q)。
|
|
523
|
+
const eq = cfg.queryExpandEnabled !== false ? expandQuery(q) : q;
|
|
467
524
|
// v0.2.1: exact-name > name-substring > BM25 priority (deterministic
|
|
468
525
|
// fallback chain, borrowed from Hermes-style tool search).
|
|
469
526
|
const bm25Hits = prioritizeExact(
|
|
470
527
|
allTools,
|
|
471
528
|
q,
|
|
472
|
-
search(index,
|
|
529
|
+
search(index, eq, lim * 2).map((i) => ({ id: allTools[i].name })),
|
|
473
530
|
);
|
|
474
531
|
let fused = bm25Hits;
|
|
475
532
|
// Semantic leg (P0-1): RRF-fuse BM25 with local bge-m3 similarity.
|
|
@@ -480,7 +537,7 @@ export function apply(ctx, config) {
|
|
|
480
537
|
const sem = await getSemanticIndex();
|
|
481
538
|
if (sem?.available) {
|
|
482
539
|
const docs = allTools.map((t) => ({ id: t.name, text: `${t.name} ${t.description || ""}` }));
|
|
483
|
-
const semHits = await searchSemantic(sem.vectors, docs,
|
|
540
|
+
const semHits = await searchSemantic(sem.vectors, docs, eq);
|
|
484
541
|
fused = rrfFuse(bm25Hits, semHits.slice(0, lim * 2), lim);
|
|
485
542
|
}
|
|
486
543
|
} catch (e) {
|
|
@@ -488,7 +545,7 @@ export function apply(ctx, config) {
|
|
|
488
545
|
}
|
|
489
546
|
}
|
|
490
547
|
const byName = new Map(allTools.map((t) => [t.name, t]));
|
|
491
|
-
|
|
548
|
+
let results = fused.map((h) => {
|
|
492
549
|
const t = byName.get(h.id) || {};
|
|
493
550
|
return {
|
|
494
551
|
name: h.id,
|
|
@@ -496,13 +553,31 @@ export function apply(ctx, config) {
|
|
|
496
553
|
server: serverOf(h.id),
|
|
497
554
|
};
|
|
498
555
|
});
|
|
556
|
+
// P0-①: 零匹配回退轻目录(name+80字描述,名字典序)——根治「搜词不对
|
|
557
|
+
// 就以为没有该能力」。fallbackCatalogSize=0 关闭(返回空=旧行为)。
|
|
558
|
+
// total 在回退时=全量目录大小(而非列出条数),避免模型误以为目录总容量=limit。
|
|
559
|
+
let fallback = false;
|
|
560
|
+
if (results.length === 0 && cfg.fallbackCatalogSize > 0) {
|
|
561
|
+
results = fallbackCatalog(allTools, cfg.fallbackCatalogSize, serverOf);
|
|
562
|
+
fallback = true;
|
|
563
|
+
}
|
|
499
564
|
// v0.2.2: 被搜到的工具名进入 warm 集合,下一轮注入完整 schema。
|
|
500
565
|
touchWarmNames(results.map((r) => r.name));
|
|
501
|
-
|
|
566
|
+
// P0-④: 发现率统计(节流落盘,低频工具名单反哺描述改进)。
|
|
567
|
+
recordDiscoveries(discoveries, results.map((r) => r.name));
|
|
568
|
+
persistDiscoveries();
|
|
569
|
+
const out = {
|
|
502
570
|
query: q,
|
|
503
571
|
results,
|
|
504
|
-
total:
|
|
572
|
+
total: fallback ? allTools.length : results.length,
|
|
573
|
+
...(fallback ? { fallback: true } : {}),
|
|
505
574
|
};
|
|
575
|
+
if (q) searchCache.set(cacheKey, out);
|
|
576
|
+
return out;
|
|
577
|
+
} catch (e) {
|
|
578
|
+
logger.warn("tools_search failed (%s) — empty results", e?.message);
|
|
579
|
+
return { query: String(args.query || ""), results: [], total: 0 };
|
|
580
|
+
}
|
|
506
581
|
},
|
|
507
582
|
});
|
|
508
583
|
try {
|
|
@@ -525,15 +600,24 @@ export function apply(ctx, config) {
|
|
|
525
600
|
const schemaTool = defineTool({
|
|
526
601
|
name: "tools_schema",
|
|
527
602
|
description:
|
|
528
|
-
"Get the complete parameter schema of one
|
|
603
|
+
"Get the complete parameter schema of one or more tools by name. Use after " +
|
|
529
604
|
"tools_search narrows candidates, or before calling a tool whose exact " +
|
|
530
605
|
"arguments you need. Returns the full JSON schema of parameters plus the " +
|
|
531
|
-
"full description
|
|
606
|
+
"full description; sibling tools from the same server are listed alongside " +
|
|
607
|
+
"(lightweight) so you can batch-expand what you need.",
|
|
532
608
|
parameters: {
|
|
533
609
|
name: {
|
|
534
610
|
type: "string",
|
|
535
|
-
|
|
536
|
-
|
|
611
|
+
description: "Exact tool name, e.g. 'mcp__viking__remember' (either name or names).",
|
|
612
|
+
},
|
|
613
|
+
names: {
|
|
614
|
+
type: "array",
|
|
615
|
+
items: { type: "string" },
|
|
616
|
+
description: "Batch: exact tool names to expand in one call (either name or names).",
|
|
617
|
+
},
|
|
618
|
+
closure: {
|
|
619
|
+
type: "boolean",
|
|
620
|
+
description: "Include lightweight sibling tools from the same server (default true).",
|
|
537
621
|
},
|
|
538
622
|
},
|
|
539
623
|
output: {
|
|
@@ -542,21 +626,14 @@ export function apply(ctx, config) {
|
|
|
542
626
|
},
|
|
543
627
|
isConcurrencySafe: () => true,
|
|
544
628
|
async execute(args) {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
629
|
+
// P1: 批量展开(names 数组)+ 同 server 兄弟闭包(closureSize 条轻目录)。
|
|
630
|
+
// 输出契约全部在 buildSchemaResponse(纯函数,lib/ux.js 可单测)。
|
|
631
|
+
const res = buildSchemaResponse(allTools, args, { closureSize: cfg.closureSize, serverOf });
|
|
548
632
|
// v0.2.2: 展开过 schema 的工具名进入 warm 集合,下一轮保持注入。
|
|
549
|
-
touchWarmNames(
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
server: serverOf(t.name),
|
|
554
|
-
description: t.description || "",
|
|
555
|
-
// C4 defense: even a schema containing `const: -0` must come back as
|
|
556
|
-
// lossless JSON.
|
|
557
|
-
parameters: sanitizeLossless(t.parameters ?? {}),
|
|
558
|
-
required: Array.isArray(t.parameters?.required) ? t.parameters.required : [],
|
|
559
|
-
};
|
|
633
|
+
touchWarmNames(
|
|
634
|
+
Array.isArray(res.results) ? res.results.map((r) => r.name) : res.found && res.name ? [res.name] : [],
|
|
635
|
+
);
|
|
636
|
+
return res;
|
|
560
637
|
},
|
|
561
638
|
});
|
|
562
639
|
try {
|
package/lib/ux.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ux.js — P0 折叠体验改进(2026-08-30 第二轮调研落地,来源见 AGENTS.md §0.3)。
|
|
3
|
+
*
|
|
4
|
+
* 四个纯函数/工厂,全部不触碰注入面(每轮 prompt 字节零变化,兼容红线保持):
|
|
5
|
+
* ① expandQuery 中文高频意图 → 追加英文检索词(只加词不改原词;BM25 召回
|
|
6
|
+
* 更宽,精确名匹配 prioritizeExact 仍用原 query,不受影响)
|
|
7
|
+
* ② fallbackCatalog tools_search 零匹配时回退的全量轻目录(name + 80 字描述,
|
|
8
|
+
* 按名字典序稳定排序)——根治「搜词不对就以为没有」
|
|
9
|
+
* ③ createSearchCache 同 query 内容指纹的结果缓存(固定 TTL + LRU 上限)——
|
|
10
|
+
* 同会话重复搜索返回同一结果集(漂移的最小修)
|
|
11
|
+
* ④ recordDiscoveries 发现率统计(name→count,落盘可恢复)——低频工具名单
|
|
12
|
+
* 反哺描述改进(官方「监控发现率」建议的本地等价物)
|
|
13
|
+
* P1 ⑤ siblingClosure 同 server 兄弟闭包 ⑥ buildSchemaResponse tools_schema
|
|
14
|
+
* 输出契约(单数/批量/siblings/missing 全分支,可单测)
|
|
15
|
+
*/
|
|
16
|
+
import { sanitizeLossless } from "./schema.js";
|
|
17
|
+
|
|
18
|
+
/* ------------------------- ① query 扩展词表 ------------------------- */
|
|
19
|
+
|
|
20
|
+
/** 中文高频意图 → 英文检索词。只做「包含即追加」,不做分词/替换。 */
|
|
21
|
+
const EXPAND = {
|
|
22
|
+
搜索: "search find lookup",
|
|
23
|
+
查找: "search find lookup",
|
|
24
|
+
文件: "file files read write edit",
|
|
25
|
+
图片: "image picture photo vision ocr",
|
|
26
|
+
截图: "screenshot screen capture image",
|
|
27
|
+
识别: "ocr recognize extract text",
|
|
28
|
+
文字: "text ocr recognize",
|
|
29
|
+
数据库: "database db sql query",
|
|
30
|
+
查询: "query select fetch search",
|
|
31
|
+
网络: "network web http fetch url",
|
|
32
|
+
浏览器: "browser chrome page navigate",
|
|
33
|
+
下载: "download fetch get",
|
|
34
|
+
上传: "upload put push",
|
|
35
|
+
发布: "publish deploy release",
|
|
36
|
+
部署: "deploy release publish",
|
|
37
|
+
测试: "test run verify",
|
|
38
|
+
运行: "run execute start",
|
|
39
|
+
执行: "run execute command",
|
|
40
|
+
命令: "command shell exec terminal",
|
|
41
|
+
安装: "install setup configure",
|
|
42
|
+
配置: "config configure setup",
|
|
43
|
+
错误: "error fail exception debug",
|
|
44
|
+
修复: "fix repair patch debug",
|
|
45
|
+
删除: "delete remove rm",
|
|
46
|
+
创建: "create new make init",
|
|
47
|
+
修改: "edit update modify change",
|
|
48
|
+
查看: "view show list inspect read",
|
|
49
|
+
列表: "list enumerate show",
|
|
50
|
+
记忆: "memory remember recall viking",
|
|
51
|
+
技能: "skill search catalog",
|
|
52
|
+
工具: "tool search schema",
|
|
53
|
+
定时: "schedule cron timer",
|
|
54
|
+
提醒: "remind schedule alert",
|
|
55
|
+
邮件: "email mail send",
|
|
56
|
+
键盘: "keyboard type input",
|
|
57
|
+
鼠标: "mouse click move",
|
|
58
|
+
窗口: "window open close",
|
|
59
|
+
进程: "process task list kill",
|
|
60
|
+
服务: "service server daemon",
|
|
61
|
+
日志: "log trace tail",
|
|
62
|
+
监控: "monitor watch observe",
|
|
63
|
+
版本: "version release tag git",
|
|
64
|
+
提交: "commit push git",
|
|
65
|
+
分支: "branch git merge",
|
|
66
|
+
仓库: "repo repository git",
|
|
67
|
+
会话: "session agent",
|
|
68
|
+
对话: "chat session message",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 中文意图 → 扩展查询串:命中的每个中文词追加其英文同义词组到尾部。
|
|
73
|
+
* 无中文(或词表未命中)时原样返回——旧行为零变化。
|
|
74
|
+
* @param {string} q
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
export function expandQuery(q) {
|
|
78
|
+
const s = String(q || "").trim();
|
|
79
|
+
if (!s) return s;
|
|
80
|
+
const extra = [];
|
|
81
|
+
for (const [cn, en] of Object.entries(EXPAND)) {
|
|
82
|
+
if (s.includes(cn)) extra.push(en);
|
|
83
|
+
}
|
|
84
|
+
return extra.length ? `${s} ${extra.join(" ")}` : s;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* ------------------------- ② 零匹配回退轻目录 ------------------------- */
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 轻目录:按名字典序排序取前 limit 个(name + 描述截 80 + server)。
|
|
91
|
+
* limit<=0 → [](关闭回退,行为与旧版一致)。
|
|
92
|
+
* @param {Array<{name:string,description?:string}>} allTools
|
|
93
|
+
* @param {number} limit
|
|
94
|
+
* @param {(name:string)=>string} [serverOf]
|
|
95
|
+
* @returns {Array<{name:string,description:string,server:string}>}
|
|
96
|
+
*/
|
|
97
|
+
export function fallbackCatalog(allTools, limit, serverOf) {
|
|
98
|
+
const lim = Math.max(0, Number(limit) || 0);
|
|
99
|
+
if (!lim || !Array.isArray(allTools) || allTools.length === 0) return [];
|
|
100
|
+
const svc = typeof serverOf === "function" ? serverOf : () => "";
|
|
101
|
+
return [...allTools]
|
|
102
|
+
.sort((a, b) => String(a.name).localeCompare(String(b.name)))
|
|
103
|
+
.slice(0, lim)
|
|
104
|
+
.map((t) => ({
|
|
105
|
+
name: String(t.name || ""),
|
|
106
|
+
description: String(t.description || "").slice(0, 80),
|
|
107
|
+
server: svc(t.name),
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* ------------------------- ③ 检索结果缓存 ------------------------- */
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 结果缓存工厂:同 key 在 TTL 内返回同一结果(确定性),超 TTL 或超上限逐出。
|
|
115
|
+
* @param {number} [maxSize] LRU 上限(默认 200)
|
|
116
|
+
* @param {number} [ttlMs] 有效期(默认 60s)
|
|
117
|
+
*/
|
|
118
|
+
export function createSearchCache(maxSize = 200, ttlMs = 60000) {
|
|
119
|
+
const map = new Map();
|
|
120
|
+
return {
|
|
121
|
+
get(key) {
|
|
122
|
+
const e = map.get(key);
|
|
123
|
+
if (!e) return null;
|
|
124
|
+
if (Date.now() - e.ts > ttlMs) {
|
|
125
|
+
map.delete(key);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return e.value;
|
|
129
|
+
},
|
|
130
|
+
set(key, value) {
|
|
131
|
+
map.set(key, { ts: Date.now(), value });
|
|
132
|
+
if (map.size > maxSize) {
|
|
133
|
+
const oldest = map.keys().next().value;
|
|
134
|
+
if (oldest !== undefined) map.delete(oldest);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
size() {
|
|
138
|
+
return map.size;
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ------------------------- ④ 发现率统计 ------------------------- */
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 记录一次发现的工具名(name→count)。
|
|
147
|
+
* @param {Map<string,number>} stats
|
|
148
|
+
* @param {Array<string>} names
|
|
149
|
+
* @returns {number} 当前去重工具数
|
|
150
|
+
*/
|
|
151
|
+
export function recordDiscoveries(stats, names) {
|
|
152
|
+
const list = Array.isArray(names) ? names.filter(Boolean) : [];
|
|
153
|
+
for (const n of list) stats.set(n, (stats.get(n) || 0) + 1);
|
|
154
|
+
return stats.size;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 序列化为按 count 降序的数组(cap 截断),供落盘。
|
|
159
|
+
* @param {Map<string,number>} stats
|
|
160
|
+
* @param {number} [cap]
|
|
161
|
+
*/
|
|
162
|
+
export function serializeDiscoveries(stats, cap = 200) {
|
|
163
|
+
return [...stats.entries()]
|
|
164
|
+
.sort((a, b) => b[1] - a[1])
|
|
165
|
+
.slice(0, Math.max(1, cap))
|
|
166
|
+
.map(([name, count]) => ({ name, count }));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 从落盘数组恢复 Map;坏条目跳过(fail-safe)。
|
|
171
|
+
* @param {Array<{name:string,count:number}>} [list]
|
|
172
|
+
*/
|
|
173
|
+
export function deserializeDiscoveries(list) {
|
|
174
|
+
const m = new Map();
|
|
175
|
+
for (const e of list || []) {
|
|
176
|
+
if (e && typeof e.name === "string" && e.name.length > 0 && Number.isFinite(e.count) && e.count > 0) {
|
|
177
|
+
m.set(e.name, e.count);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return m;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* ------------------------- P1: 依赖闭包 ------------------------- */
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 同 server 兄弟闭包(graph-tool-call 的本地简化:server 前缀 = 依赖分组代理)。
|
|
187
|
+
* 展开目标工具时顺带返回同 server 的其它工具轻目录(name+120 字描述),
|
|
188
|
+
* 模型看到兄弟名字后可批量展开——减少「展开 A 发现还要 B」的往返税。
|
|
189
|
+
* ponytail: server 前缀是粗粒度依赖近似;精确 producer 图需要宿主工具元数据,
|
|
190
|
+
* 真需要时再按参数类型启发补。
|
|
191
|
+
* @param {Array<{name:string,description?:string}>} allTools
|
|
192
|
+
* @param {Array<string>} expandedNames 已展开的工具名
|
|
193
|
+
* @param {number} limit 附带兄弟总数上限(0=关闭)
|
|
194
|
+
* @param {(name:string)=>string} [serverOf] server 前缀提取
|
|
195
|
+
* @returns {Array<{name:string,description:string}>}
|
|
196
|
+
*/
|
|
197
|
+
export function siblingClosure(allTools, expandedNames, limit, serverOf) {
|
|
198
|
+
const lim = Math.max(0, Number(limit) || 0);
|
|
199
|
+
if (!lim || !Array.isArray(allTools) || !Array.isArray(expandedNames) || expandedNames.length === 0) return [];
|
|
200
|
+
const svc = typeof serverOf === "function" ? serverOf : (n) => String(n).split("__").slice(0, 2).join("__");
|
|
201
|
+
const servers = new Set(expandedNames.map((n) => svc(String(n))).filter(Boolean));
|
|
202
|
+
if (servers.size === 0) return [];
|
|
203
|
+
const skip = new Set(expandedNames);
|
|
204
|
+
const out = [];
|
|
205
|
+
for (const t of [...allTools].sort((a, b) => String(a.name).localeCompare(String(b.name)))) {
|
|
206
|
+
if (out.length >= lim) break;
|
|
207
|
+
const nm = String(t.name || "");
|
|
208
|
+
if (!nm || skip.has(nm)) continue;
|
|
209
|
+
if (servers.has(svc(nm))) {
|
|
210
|
+
out.push({ name: nm, description: String(t.description || "").slice(0, 120) });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* ------------------------- P1: tools_schema 输出契约 ------------------------- */
|
|
217
|
+
|
|
218
|
+
const defaultServerOf = (name) => String(name).split("__").slice(0, 2).join("__");
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* tools_schema 的纯输出构造(从 index.js execute 提取,契约可单测——single 判定
|
|
222
|
+
* 写反曾逃过 87 条纯函数测试,根因是 execute 无测试;提取后契约分支全部可钉死)。
|
|
223
|
+
* 单数调用(name 或 names 单元素,去重后 1 个)→ 旧版逐字段结构;
|
|
224
|
+
* 批量(去重后 >1 个)→ { found, results: [...], siblings?, missing? }。
|
|
225
|
+
* @param {Array<{name:string,description?:string,parameters?:object}>} allTools
|
|
226
|
+
* @param {{name?:string,names?:Array<string>,closure?:boolean}} args
|
|
227
|
+
* @param {{closureSize?:number,serverOf?:Function}} [opts]
|
|
228
|
+
*/
|
|
229
|
+
export function buildSchemaResponse(allTools, args, opts = {}) {
|
|
230
|
+
const closureSize = Math.max(0, Number(opts.closureSize) || 0);
|
|
231
|
+
const svc = typeof opts.serverOf === "function" ? opts.serverOf : defaultServerOf;
|
|
232
|
+
const primary = String(args?.name || "");
|
|
233
|
+
const batch = (Array.isArray(args?.names) ? args.names : []).map((n) => String(n)).filter(Boolean);
|
|
234
|
+
const want = [...new Set([...batch, ...(primary ? [primary] : [])])];
|
|
235
|
+
if (want.length === 0) return { found: false, reason: "no name" };
|
|
236
|
+
const expanded = [];
|
|
237
|
+
const missing = [];
|
|
238
|
+
for (const n of want) {
|
|
239
|
+
const t = (allTools || []).find((x) => x && x.name === n);
|
|
240
|
+
if (!t) {
|
|
241
|
+
missing.push(n);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
expanded.push({
|
|
245
|
+
name: t.name,
|
|
246
|
+
server: svc(t.name),
|
|
247
|
+
description: t.description || "",
|
|
248
|
+
// C4 defense: even a schema containing `const: -0` must come back as
|
|
249
|
+
// lossless JSON.
|
|
250
|
+
parameters: sanitizeLossless(t.parameters ?? {}),
|
|
251
|
+
required: Array.isArray(t.parameters?.required) ? t.parameters.required : [],
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
// ⚠️ 判定只看 want.length——旧实现用 !primary 导致只传 name 的调用误走批量结构。
|
|
255
|
+
const single = want.length === 1;
|
|
256
|
+
if (single) {
|
|
257
|
+
if (expanded.length === 0) return { name: want[0], found: false, reason: "unknown tool" };
|
|
258
|
+
const e = expanded[0];
|
|
259
|
+
const out = {
|
|
260
|
+
name: e.name,
|
|
261
|
+
found: true,
|
|
262
|
+
server: e.server,
|
|
263
|
+
description: e.description,
|
|
264
|
+
parameters: e.parameters,
|
|
265
|
+
required: e.required,
|
|
266
|
+
};
|
|
267
|
+
if (closureSize > 0 && args?.closure !== false) {
|
|
268
|
+
out.siblings = siblingClosure(allTools, [e.name], closureSize, svc);
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
const out = {
|
|
273
|
+
found: expanded.length > 0,
|
|
274
|
+
...(expanded.length > 0
|
|
275
|
+
? {
|
|
276
|
+
results: expanded,
|
|
277
|
+
...(closureSize > 0 && args?.closure !== false
|
|
278
|
+
? { siblings: siblingClosure(allTools, expanded.map((e) => e.name), closureSize, svc) }
|
|
279
|
+
: {}),
|
|
280
|
+
}
|
|
281
|
+
: {}),
|
|
282
|
+
...(missing.length > 0 ? { missing } : {}),
|
|
283
|
+
};
|
|
284
|
+
return out;
|
|
285
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tool-folder",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Fold the DSH tool surface per request + ChainGuard firewall (high-risk block + exfil-chain detection + anti-obfuscation) + BM25/bge-m3 hybrid tools_search. Shrinks schema tokens 80-90% while keeping selection accuracy. v0.2.0 adds a semantic retrieval leg (local Ollama bge-m3, RRF hybrid) and ChainGuard obfuscation detection (concat-rebuild / -enc / certutil / bitsadmin / homoglyphs).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|
|
@@ -45,4 +45,4 @@
|
|
|
45
45
|
".": "./lib/index.js",
|
|
46
46
|
"./lib/*": "./lib/*"
|
|
47
47
|
}
|
|
48
|
-
}
|
|
48
|
+
}
|