dsh-skill-browser 1.9.11 → 2.0.1
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/CHANGELOG.md +36 -0
- package/README.md +7 -0
- package/lib/client.js +1147 -137
- package/lib/groups.js +171 -21
- package/lib/index.js +141 -2
- package/lib/mcp.js +853 -0
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +85 -86
- package/lib/client.js.bak-6colimp-20261028-230915 +0 -3748
- package/lib/client.js.bak-auth12-20261029-020107 +0 -3749
- package/lib/client.js.bak-auth12b-20261029-020140 +0 -3749
- package/lib/client.js.bak-auth12c-20261029-020206 +0 -3749
- package/lib/client.js.bak-auth12d-20261029-020232 +0 -3747
- package/lib/client.js.bak-auto-20261028-171351 +0 -3669
- package/lib/client.js.bak-auto2-20261028-171422 +0 -3676
- package/lib/client.js.bak-auto3-20261028-171545 +0 -3676
- package/lib/client.js.bak-followball-20261028-224743 +0 -3720
- package/lib/client.js.bak-gearico-20261028-230937 +0 -3748
- package/lib/client.js.bak-glow-20261030 +0 -3676
- package/lib/client.js.bak-grid-20261028-220142 +0 -3680
- package/lib/client.js.bak-guide-20261029-003108 +0 -3750
- package/lib/client.js.bak-guide2-20261029-003213 +0 -3751
- package/lib/client.js.bak-guide3-20261029-003236 +0 -3763
- package/lib/client.js.bak-guide4-20261029-003249 +0 -3768
- package/lib/client.js.bak-guidefc-20261029-003535 +0 -3775
- package/lib/client.js.bak-guideico-20261029-013809 +0 -3776
- package/lib/client.js.bak-guideorder-20261029-004345 +0 -3775
- package/lib/client.js.bak-headbtn-20261028-232114 +0 -3750
- package/lib/client.js.bak-menu6col-20261028-225126 +0 -3741
- package/lib/client.js.bak-menu6col2-20261028-225348 +0 -3741
- package/lib/client.js.bak-menu6col2-20261028-225527 +0 -3741
- package/lib/client.js.bak-monobtn-20261028-230502 +0 -3741
- package/lib/client.js.bak-navsimple-20261029-015235 +0 -3786
- package/lib/client.js.bak-nobadge-20261029-015354 +0 -3784
- package/lib/client.js.bak-nodrag-20261028-170713 +0 -3651
- package/lib/client.js.bak-official-20261028-171240 +0 -3653
- package/lib/client.js.bak-official2-20261028-171304 +0 -3668
- package/lib/client.js.bak-renumber-20261028-215518 +0 -3681
- package/lib/client.js.bak-sbgap-20261029-013144 +0 -3776
- package/lib/client.js.bak-sbgap-20261029-013202 +0 -3776
- package/lib/client.js.bak-sbrowh-20261029-012307 +0 -3776
- package/lib/client.js.bak-sbrowh-20261029-012333 +0 -3776
- package/lib/client.js.bak-scope-sb-20261029-011711 +0 -3776
- package/lib/client.js.bak-skinclarity-20261029-011027 +0 -3776
- package/lib/client.js.bak-skincols-20261028-224614 +0 -3720
- package/lib/client.js.bak-star-20261028-231205 +0 -3748
- package/lib/client.js.bak-star-20261028-231233 +0 -3748
- package/lib/client.js.bak-sync-20261028-220538 +0 -3680
package/lib/groups.js
CHANGED
|
@@ -27,6 +27,8 @@ import { categoriesMeta } from "./scan.js";
|
|
|
27
27
|
// askModel() so a host without the package still loads this plugin (the AI
|
|
28
28
|
// buttons then report "llm service unavailable" instead of breaking everything).
|
|
29
29
|
var _llmMod = null;
|
|
30
|
+
// 记录不支持 reasoningEffort 的 provider(如 magpie 聚合网关),后续直接不传该参数。
|
|
31
|
+
var _noReasoning = Object.create(null);
|
|
30
32
|
// llm is an OPTIONAL capability: read it through ctx.get() so the plugin still
|
|
31
33
|
// loads on a host/profile that has no llm service (official rule: reserve
|
|
32
34
|
// ctx.<name> for declared injections, use ctx.get() for optional services).
|
|
@@ -48,6 +50,48 @@ async function loadLlm() {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
var STORE_FILE = join(homedir(), ".dsh", "data", "dsh-skill-browser", "groups.json");
|
|
53
|
+
// AI 分组结果缓存(按技能目录集指纹,24h 有效);只存分组结构,不含技能正文。
|
|
54
|
+
var CLASSIFY_CACHE_FILE = join(homedir(), ".dsh", "data", "dsh-skill-browser", "ai-classify-cache.json");
|
|
55
|
+
var CLASSIFY_CACHE_TTL = 24 * 3600 * 1000;
|
|
56
|
+
function cacheHash(str) {
|
|
57
|
+
var h = 5381;
|
|
58
|
+
for (var i = 0; i < str.length; i++) h = ((h << 5) + h + str.charCodeAt(i)) | 0;
|
|
59
|
+
return (h >>> 0).toString(36);
|
|
60
|
+
}
|
|
61
|
+
function loadClassifyCache() {
|
|
62
|
+
try {
|
|
63
|
+
if (!existsSync(CLASSIFY_CACHE_FILE)) return {};
|
|
64
|
+
return JSON.parse(String(readFileSync(CLASSIFY_CACHE_FILE, "utf8")).replace(/^\uFEFF/, "")) || {};
|
|
65
|
+
} catch (e) { return {}; }
|
|
66
|
+
}
|
|
67
|
+
function saveClassifyCache(map) {
|
|
68
|
+
try {
|
|
69
|
+
mkdirSync(dirname(CLASSIFY_CACHE_FILE), { recursive: true });
|
|
70
|
+
writeFileSync(CLASSIFY_CACHE_FILE, JSON.stringify(map), "utf8");
|
|
71
|
+
} catch (e) { /* best-effort */ }
|
|
72
|
+
}
|
|
73
|
+
// 清除 AI 分组结果缓存(只动缓存文件,绝不碰任何技能文件)。
|
|
74
|
+
export function clearClassifyCache() {
|
|
75
|
+
try {
|
|
76
|
+
if (existsSync(CLASSIFY_CACHE_FILE)) writeFileSync(CLASSIFY_CACHE_FILE, "{}", "utf8");
|
|
77
|
+
return { ok: true };
|
|
78
|
+
} catch (e) { return { ok: false, error: String((e && e.message) || e) }; }
|
|
79
|
+
}
|
|
80
|
+
// 并行分批分类总开关:设 false 即退回「串行单次」原方案(回退用,单点切换)。
|
|
81
|
+
var PARALLEL_CLASSIFY = true;
|
|
82
|
+
// 合并多批结果:按「规范化大类名」归并去重(同名大类的 subs 合并)。
|
|
83
|
+
function mergeDomainsByName(list) {
|
|
84
|
+
var order = [], byKey = {};
|
|
85
|
+
for (var i = 0; i < list.length; i++) {
|
|
86
|
+
var d = list[i] || {};
|
|
87
|
+
var key = String(d.name || "").replace(/\s+/g, "").toLowerCase() || ("__" + i);
|
|
88
|
+
var t = byKey[key];
|
|
89
|
+
if (!t) { t = byKey[key] = { name: d.name, emoji: d.emoji, subs: [] }; order.push(key); }
|
|
90
|
+
var ss = Array.isArray(d.subs) ? d.subs : [];
|
|
91
|
+
for (var j = 0; j < ss.length; j++) t.subs.push(ss[j]);
|
|
92
|
+
}
|
|
93
|
+
return order.map(function (k) { return byKey[k]; });
|
|
94
|
+
}
|
|
51
95
|
// Two levels (domain -> sub-group) means a 109-skill library can legitimately
|
|
52
96
|
// need 10 domains x 5 sub-groups, so the cap has to be generous.
|
|
53
97
|
var MAX_GROUPS = 160;
|
|
@@ -448,12 +492,14 @@ export async function resolveModelRoute(ctx, cfg) {
|
|
|
448
492
|
return route;
|
|
449
493
|
}
|
|
450
494
|
var tried = [];
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
tried.push("agentDefaultModel");
|
|
495
|
+
// v2.0.9 用户要求:优先「当前对话模型」(live agent session selection),
|
|
496
|
+
// 其次才退 DSH 默认模型,最后目录兜底 —— 这样 AI 分组跟随你当前对话用的模型。
|
|
454
497
|
var r3 = routeFromAgent(ctx);
|
|
455
498
|
if (r3) return r3;
|
|
456
499
|
tried.push("agent.session");
|
|
500
|
+
var r2 = routeFromDefaultModel(ctx);
|
|
501
|
+
if (r2) return r2;
|
|
502
|
+
tried.push("agentDefaultModel");
|
|
457
503
|
var r4 = await routeFromLlmCatalog(ctx);
|
|
458
504
|
if (r4) return r4;
|
|
459
505
|
tried.push("llm.listProviders");
|
|
@@ -486,13 +532,14 @@ export async function describeModelRoute(ctx, cfg) {
|
|
|
486
532
|
}
|
|
487
533
|
|
|
488
534
|
|
|
489
|
-
async function askModel(ctx, cfg, system, userText, purpose, maxTokens) {
|
|
535
|
+
export async function askModel(ctx, cfg, system, userText, purpose, maxTokens, opts) {
|
|
490
536
|
// resolveModelRoute is async: stage 4 may query the host's provider catalog.
|
|
491
537
|
var route = await resolveModelRoute(ctx, cfg);
|
|
492
538
|
var llm = await loadLlm();
|
|
493
539
|
var messages = [
|
|
494
540
|
llm.createUserMessage({ content: [{ type: "text", text: userText }], source: { kind: "dsh-skill-browser" } })
|
|
495
541
|
];
|
|
542
|
+
var o2 = opts || {};
|
|
496
543
|
var options = {
|
|
497
544
|
provider: route.provider,
|
|
498
545
|
model: route.model,
|
|
@@ -500,9 +547,11 @@ async function askModel(ctx, cfg, system, userText, purpose, maxTokens) {
|
|
|
500
547
|
system: system,
|
|
501
548
|
maxTokens: maxTokens || 2400,
|
|
502
549
|
purpose: purpose,
|
|
503
|
-
signal: AbortSignal.timeout(120000)
|
|
550
|
+
signal: AbortSignal.timeout(o2.timeoutMs || 120000)
|
|
504
551
|
};
|
|
505
|
-
|
|
552
|
+
// AI 分组/封面属于结构化整理任务,不需要深度推理:显式压到 low 可明显缩短等待。
|
|
553
|
+
if (o2.reasoningEffort && !_noReasoning[String(route.provider)]) options.reasoningEffort = String(o2.reasoningEffort);
|
|
554
|
+
else if (route.reasoningEffort) options.reasoningEffort = route.reasoningEffort;
|
|
506
555
|
try {
|
|
507
556
|
var sid = ctx.agent && ctx.agent.session && ctx.agent.session.id;
|
|
508
557
|
if (sid) options.sessionId = String(sid);
|
|
@@ -518,8 +567,39 @@ async function askModel(ctx, cfg, system, userText, purpose, maxTokens) {
|
|
|
518
567
|
|
|
519
568
|
var llmSvc = getLlm(ctx);
|
|
520
569
|
if (!llmSvc) throw new Error("宿主未提供 llm 服务,AI 功能不可用");
|
|
521
|
-
|
|
522
|
-
|
|
570
|
+
|
|
571
|
+
// 一次完整尝试:跑流 + 检查 finish。finish 里的 failure 也是错误来源
|
|
572
|
+
// (UNSUPPORTED_REASONING_EFFORT 就是从 finish 抛出的,必须一起判)。
|
|
573
|
+
async function attemptOnce(opts2) {
|
|
574
|
+
var asm = new llm.BlockAssembler();
|
|
575
|
+
for await (var ch of llmSvc.stream(opts2)) asm.push(ch);
|
|
576
|
+
var fin = asm.finish;
|
|
577
|
+
if (fin && fin.kind && fin.kind !== "stop") {
|
|
578
|
+
var fl = fin.failure;
|
|
579
|
+
var dt = fl ? [fl.code ? "[" + String(fl.code) + "]" : "", String(fl.message || fl)].filter(Boolean).join(" ") : "(无 failure 详情)";
|
|
580
|
+
var e2 = new Error("模型流异常结束(" + String(fin.kind) + "): " + dt);
|
|
581
|
+
e2.code = fl && fl.code ? String(fl.code) : "LLM_FINISH_" + String(fin.kind).toUpperCase();
|
|
582
|
+
throw e2;
|
|
583
|
+
}
|
|
584
|
+
return asm;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
var assembler;
|
|
588
|
+
try {
|
|
589
|
+
assembler = await attemptOnce(options);
|
|
590
|
+
} catch (eTry) {
|
|
591
|
+
var sig = String((eTry && (eTry.code || eTry.message)) || eTry);
|
|
592
|
+
// 该 provider 不支持 reasoningEffort(实测 magpie 报 UNSUPPORTED_REASONING_EFFORT):
|
|
593
|
+
// 去掉参数重试,并记住这个 provider,后续调用直接不传,避免每次都白跑一轮。
|
|
594
|
+
if (options.reasoningEffort && /REASONING_EFFORT|reasoning.?effort|unsupported/i.test(sig)) {
|
|
595
|
+
_noReasoning[String(route.provider)] = true;
|
|
596
|
+
delete options.reasoningEffort;
|
|
597
|
+
if (ctx.logger && ctx.logger.warn) ctx.logger.warn('dsh-skill-browser: provider "' + route.provider + '" 不支持 reasoningEffort,已自动去掉并重试');
|
|
598
|
+
assembler = await attemptOnce(options);
|
|
599
|
+
} else {
|
|
600
|
+
throw eTry;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
523
603
|
var finish = assembler.finish;
|
|
524
604
|
if (finish && finish.kind && finish.kind !== "stop") {
|
|
525
605
|
// finish shape is { kind, failure } — NOT { error }.
|
|
@@ -579,6 +659,10 @@ var CLASSIFY_SYSTEM =
|
|
|
579
659
|
// to the user first, then calls applyPlan() with mode replace|merge - "如果对 AI
|
|
580
660
|
// 分组满意就替换原来的组" must be an explicit user decision, never a side effect.
|
|
581
661
|
export async function aiClassify(ctx, cfg, skills, opts) {
|
|
662
|
+
// 【铁律·用户强制】AI 分组是纯只读流程:只读取技能元数据(frontmatter),
|
|
663
|
+
// 绝不修改 / 移动 / 重命名 / 删除任何技能文件(SKILL.md 或技能目录)。
|
|
664
|
+
// 本流程只写 ① 结果缓存 ai-classify-cache.json;分组方案不落盘(落盘由用户确认后
|
|
665
|
+
// 的 applyPlan 单独写 groups.json,同样不碰任何技能文件)。
|
|
582
666
|
if (!getLlm(ctx)) throw new Error("宿主未提供 llm 服务,AI 功能不可用");
|
|
583
667
|
var o = opts || {};
|
|
584
668
|
var mode = o.mode === "merge" ? "merge" : "replace";
|
|
@@ -602,16 +686,32 @@ export async function aiClassify(ctx, cfg, skills, opts) {
|
|
|
602
686
|
}
|
|
603
687
|
}
|
|
604
688
|
|
|
689
|
+
// ── AI 分组结果缓存(同一批技能 + 同 mode 24h 内直接复用,避免重复调模型)──
|
|
690
|
+
// 指纹只用技能目录名集合,不涉及任何技能内容,因此缓存同样不读取正文。
|
|
691
|
+
var cacheKey = mode + ":" + cacheHash(pool.map(function (x) { return x.dir; }).sort().join("|"));
|
|
692
|
+
var _classifyCache = loadClassifyCache();
|
|
693
|
+
var _cacheHit = _classifyCache[cacheKey];
|
|
694
|
+
if (_cacheHit && _cacheHit.plan && (Date.now() - Number(_cacheHit.ts || 0)) < CLASSIFY_CACHE_TTL) {
|
|
695
|
+
if (ctx.logger && ctx.logger.info) ctx.logger.info("dsh-skill-browser: ai-classify cache hit (" + pool.length + " skills)");
|
|
696
|
+
return { ok: true, plan: _cacheHit.plan, mode: mode, stats: _cacheHit.stats, model: _cacheHit.model || null, cached: true };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// 【硬约束·用户强制】AI 分组的模型输入只允许「元数据白名单」:
|
|
700
|
+
// 目录名 dir + 中文名 zh + frontmatter description(各截前 60 字)+ 现分类 cat。
|
|
701
|
+
// 严禁读取 / 拼入 SKILL.md 正文(body / content / fullText 等)。即使上游误传
|
|
702
|
+
// 了正文字段,这里也一律忽略;并用行长断言兜底,防止正文被拼进 prompt。
|
|
605
703
|
function buildList(arr) {
|
|
606
704
|
var out = [];
|
|
607
705
|
for (var i = 0; i < arr.length; i++) {
|
|
608
706
|
var s = arr[i];
|
|
609
|
-
var line = s.dir;
|
|
610
|
-
var zh = String(s.zh || "").trim();
|
|
611
|
-
var desc = String(s.desc || "").trim();
|
|
612
|
-
if (zh) line += " | " + zh
|
|
613
|
-
else if (desc) line += " | " + desc
|
|
707
|
+
var line = String(s.dir || "");
|
|
708
|
+
var zh = String(s.zh || "").trim().slice(0, 60);
|
|
709
|
+
var desc = String(s.desc || "").trim().slice(0, 60);
|
|
710
|
+
if (zh) line += " | " + zh;
|
|
711
|
+
else if (desc) line += " | " + desc;
|
|
614
712
|
if (s.cat && s.cat !== "other") line += " | 现分类:" + (catLabelOf[s.cat] || s.cat);
|
|
713
|
+
// 兜底断言:单行只应是元数据(正常 < 160 字);超长说明混入了正文,拒绝。
|
|
714
|
+
if (line.length > 200) throw new Error("AI 分组输入疑似包含技能正文,已拒绝(只允许元数据)");
|
|
615
715
|
out.push(line);
|
|
616
716
|
}
|
|
617
717
|
return out;
|
|
@@ -632,7 +732,7 @@ export async function aiClassify(ctx, cfg, skills, opts) {
|
|
|
632
732
|
list.join("\n");
|
|
633
733
|
// A full 109-skill tree easily exceeds 4k tokens; truncation would parse-fail
|
|
634
734
|
// and surface as "AI 分组坏了".
|
|
635
|
-
var res = await askModel(ctx, cfg, CLASSIFY_SYSTEM, userText, "skill-browser-classify",
|
|
735
|
+
var res = await askModel(ctx, cfg, CLASSIFY_SYSTEM, userText, "skill-browser-classify", 6000, { reasoningEffort: "low", timeoutMs: 150000 });
|
|
636
736
|
var data = extractJson(res.text);
|
|
637
737
|
var raw = data && Array.isArray(data.domains) ? data.domains : [];
|
|
638
738
|
if (!raw.length && data && Array.isArray(data.groups)) raw = legacyToDomains(data.groups);
|
|
@@ -703,9 +803,50 @@ export async function aiClassify(ctx, cfg, skills, opts) {
|
|
|
703
803
|
return out;
|
|
704
804
|
}
|
|
705
805
|
|
|
706
|
-
// Pass 1:
|
|
707
|
-
|
|
708
|
-
|
|
806
|
+
// Pass 1: 分批并行分类(提速)。PARALLEL_CLASSIFY=false 即退回串行单次(回退开关)。
|
|
807
|
+
// - pool ≥ 45 才分;每批 ~40、最多 4 批并行 Promise.all;
|
|
808
|
+
// - 合并时按「同名大类」归并去重(mergeDomainsByName);
|
|
809
|
+
// - 任一批失败 → 自动回退串行单次,保证可用性(并行只是加速,不是唯一路径)。
|
|
810
|
+
var ranParallel = false, ranBatches = 1;
|
|
811
|
+
async function classifyWhole() {
|
|
812
|
+
if (!PARALLEL_CLASSIFY || pool.length < 45) {
|
|
813
|
+
ranParallel = false; ranBatches = 1;
|
|
814
|
+
var one = await classifyOnce(pool, []);
|
|
815
|
+
return { raw: one.raw, route: one.route };
|
|
816
|
+
}
|
|
817
|
+
var BATCH = 40, nB = Math.ceil(pool.length / BATCH);
|
|
818
|
+
if (nB > 4) { nB = 4; BATCH = Math.ceil(pool.length / nB); }
|
|
819
|
+
var batches = [];
|
|
820
|
+
for (var bi = 0; bi < nB; bi++) batches.push(pool.slice(bi * BATCH, bi * BATCH + BATCH));
|
|
821
|
+
var parts = await Promise.all(batches.map(function (bt) { return classifyOnce(bt, []); }));
|
|
822
|
+
var route0 = null, all = [];
|
|
823
|
+
for (var pi = 0; pi < parts.length; pi++) {
|
|
824
|
+
if (parts[pi].route) route0 = parts[pi].route;
|
|
825
|
+
var rr0 = parts[pi].raw || [];
|
|
826
|
+
for (var pj = 0; pj < rr0.length; pj++) all.push(rr0[pj]);
|
|
827
|
+
}
|
|
828
|
+
ranParallel = true; ranBatches = nB;
|
|
829
|
+
return { raw: mergeDomainsByName(all), route: route0 };
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
var p1;
|
|
833
|
+
try {
|
|
834
|
+
p1 = await classifyWhole();
|
|
835
|
+
} catch (eP1) {
|
|
836
|
+
ranParallel = false; ranBatches = 1;
|
|
837
|
+
var msg1 = String((eP1 && eP1.message) || eP1);
|
|
838
|
+
if (ctx.logger && ctx.logger.warn) ctx.logger.warn("dsh-skill-browser: classify batch failed, fallback serial: " + msg1);
|
|
839
|
+
try {
|
|
840
|
+
var fb = await classifyOnce(pool, []);
|
|
841
|
+
p1 = { raw: fb.raw, route: fb.route };
|
|
842
|
+
} catch (eP1b) {
|
|
843
|
+
if (/abort|timeout|中断/i.test(String((eP1b && eP1b.message) || eP1b))) {
|
|
844
|
+
var fb2 = await classifyOnce(pool, []);
|
|
845
|
+
p1 = { raw: fb2.raw, route: fb2.route };
|
|
846
|
+
} else { throw eP1b; }
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
routeUsed = p1.route || routeUsed;
|
|
709
850
|
if (!p1.raw.length) throw new Error("模型没有给出任何分组");
|
|
710
851
|
absorb(p1.raw);
|
|
711
852
|
|
|
@@ -713,7 +854,8 @@ export async function aiClassify(ctx, cfg, skills, opts) {
|
|
|
713
854
|
// Re-ask only about the leftovers so they get real semantic groups instead of
|
|
714
855
|
// one "其他" bin.
|
|
715
856
|
var left = leftoversOf();
|
|
716
|
-
|
|
857
|
+
// 只有遗漏较多时才补第二次调用(每次调用几十秒,少一次就快一倍)。
|
|
858
|
+
if (left.length > 20 && plan.domains.length) {
|
|
717
859
|
var names = [];
|
|
718
860
|
for (var cn = 0; cn < plan.domains.length; cn++) names.push(plan.domains[cn].name);
|
|
719
861
|
try {
|
|
@@ -753,13 +895,21 @@ export async function aiClassify(ctx, cfg, skills, opts) {
|
|
|
753
895
|
subCount += plan.domains[a].subs.length;
|
|
754
896
|
for (var b = 0; b < plan.domains[a].subs.length; b++) covered += plan.domains[a].subs[b].skills.length;
|
|
755
897
|
}
|
|
756
|
-
|
|
898
|
+
var _result = {
|
|
757
899
|
ok: true,
|
|
758
900
|
plan: plan,
|
|
759
901
|
mode: mode,
|
|
760
902
|
stats: { skills: pool.length, domains: plan.domains.length, subs: subCount, covered: covered },
|
|
761
|
-
model: routeUsed
|
|
903
|
+
model: routeUsed,
|
|
904
|
+
parallel: ranParallel,
|
|
905
|
+
batches: ranBatches
|
|
762
906
|
};
|
|
907
|
+
// 写入缓存(best-effort;plan 只含分组结构,不含任何技能正文)。
|
|
908
|
+
try {
|
|
909
|
+
_classifyCache[cacheKey] = { ts: Date.now(), mode: mode, model: routeUsed, stats: _result.stats, plan: plan };
|
|
910
|
+
saveClassifyCache(_classifyCache);
|
|
911
|
+
} catch (eC) { /* ignore */ }
|
|
912
|
+
return _result;
|
|
763
913
|
}
|
|
764
914
|
|
|
765
915
|
// Turn a flat [{"name","skills":[...]}] reply into the two-level shape.
|
|
@@ -948,7 +1098,7 @@ export async function aiCover(ctx, cfg, group, sampleSkills) {
|
|
|
948
1098
|
"分组名称:" + group.name + "\n" +
|
|
949
1099
|
(sample ? "包含技能:" + sample + "\n" : "") +
|
|
950
1100
|
"请为这个分组生成一张 200x200 的 SVG 封面图。";
|
|
951
|
-
var res = await askModel(ctx, cfg, COVER_SYSTEM, userText, "skill-browser-cover", 2000);
|
|
1101
|
+
var res = await askModel(ctx, cfg, COVER_SYSTEM, userText, "skill-browser-cover", 2000, { reasoningEffort: "low", timeoutMs: 60000 });
|
|
952
1102
|
var svg = cleanSvg(res.text);
|
|
953
1103
|
var saved = setGroupCover(group.id, svg);
|
|
954
1104
|
if (!saved.ok) throw new Error(saved.error || "封面保存失败");
|
package/lib/index.js
CHANGED
|
@@ -45,8 +45,17 @@ var listGroups = grMod.listGroups, createGroup = grMod.createGroup,
|
|
|
45
45
|
describeModelRoute = grMod.describeModelRoute;
|
|
46
46
|
var keywordClassify = kwMod.keywordClassify;
|
|
47
47
|
|
|
48
|
+
// ── v2.0.0 MCP 管理子模块(同样走 RELOAD_V 热加载) ──
|
|
49
|
+
var mcpMod = await import("./mcp.js" + RELOAD_V);
|
|
50
|
+
var collectMcp = mcpMod.collectMcp, setMcpEnabled = mcpMod.setMcpEnabled,
|
|
51
|
+
reconnectMcp = mcpMod.reconnectMcp, recordMcpCall = mcpMod.recordMcpCall,
|
|
52
|
+
getMcpStats = mcpMod.getMcpStats, clearMcpStats = mcpMod.clearMcpStats,
|
|
53
|
+
exportMcp = mcpMod.exportMcp,
|
|
54
|
+
detectMcpResidue = mcpMod.detectMcpResidue, cleanMcpResidue = mcpMod.cleanMcpResidue,
|
|
55
|
+
scanLocalMcp = mcpMod.scanLocalMcp, mcpAiDiagnose = mcpMod.mcpAiDiagnose, mcpAiFixApply = mcpMod.mcpAiFixApply;
|
|
56
|
+
|
|
48
57
|
var PREFIX = "/dsh-skill-browser";
|
|
49
|
-
export var VERSION = "
|
|
58
|
+
export var VERSION = "2.0.0";
|
|
50
59
|
var CACHE_TTL_MS = 30 * 1000;
|
|
51
60
|
var DIAG_FILE = join(homedir(), ".dsh", "data", "dsh-skill-browser", "diag.log");
|
|
52
61
|
var CONFIG_FILE = join(homedir(), ".dsh", "data", "dsh-skill-browser", "config.json");
|
|
@@ -278,6 +287,34 @@ export function createApi(ctx, config) {
|
|
|
278
287
|
ctx.logger.warn("dsh-skill-browser: tools/result listener failed: " + (e && e.message));
|
|
279
288
|
}
|
|
280
289
|
|
|
290
|
+
// ── v2.0.0 MCP 调用统计:官方 tools/result 事件,按 mcp__<server>__ 前缀归类 ──
|
|
291
|
+
var offMcp = null;
|
|
292
|
+
try {
|
|
293
|
+
offMcp = ctx.on("tools/result", function (exec, result) {
|
|
294
|
+
try {
|
|
295
|
+
var nm = exec && exec.name;
|
|
296
|
+
if (typeof nm !== "string" || nm.indexOf("mcp__") !== 0) return;
|
|
297
|
+
var seg = nm.split("__");
|
|
298
|
+
var server = seg.length >= 3 && seg[1] ? seg[1] : "(unknown)";
|
|
299
|
+
var ok = !(result && result.isError);
|
|
300
|
+
var err = "";
|
|
301
|
+
if (!ok && result) {
|
|
302
|
+
if (result.error && typeof result.error === "string") err = result.error;
|
|
303
|
+
else if (Array.isArray(result.content)) {
|
|
304
|
+
for (var ii = 0; ii < result.content.length; ii++) {
|
|
305
|
+
var blk = result.content[ii];
|
|
306
|
+
if (blk && typeof blk.text === "string") { err = blk.text; break; }
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
recordMcpCall(server, nm, ok, err, 0);
|
|
311
|
+
} catch (e2) { /* 统计失败绝不影响工具调用本身 */ }
|
|
312
|
+
});
|
|
313
|
+
ctx.logger.info("dsh-skill-browser: mcp call recorder armed (tools/result)");
|
|
314
|
+
} catch (e) {
|
|
315
|
+
ctx.logger.warn("dsh-skill-browser: mcp stats listener failed: " + (e && e.message));
|
|
316
|
+
}
|
|
317
|
+
|
|
281
318
|
function buildPayload() {
|
|
282
319
|
var skills = scanSkills(root, ZH);
|
|
283
320
|
// Per-category counts, emitted in CATS order so the client renders a
|
|
@@ -603,6 +640,11 @@ function getLedgerPayload(force) {
|
|
|
603
640
|
}
|
|
604
641
|
return;
|
|
605
642
|
}
|
|
643
|
+
if (action === "clear-classify-cache") {
|
|
644
|
+
var ccRes = grMod.clearClassifyCache ? grMod.clearClassifyCache() : { ok: false, error: "groups 模块不支持清缓存" };
|
|
645
|
+
sendJson(res, ccRes.ok ? 200 : 500, ccRes);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
606
648
|
if (action === "ai-classify") {
|
|
607
649
|
var skills = scanSkills(root, ZH);
|
|
608
650
|
if (!skills.length) { sendJson(res, 400, { ok: false, error: "技能库为空或未找到技能" }); return; }
|
|
@@ -900,11 +942,108 @@ function getLedgerPayload(force) {
|
|
|
900
942
|
return;
|
|
901
943
|
}
|
|
902
944
|
|
|
945
|
+
// ── v2.0.0 MCP 管理:列表 / 状态 / 工具清单 / 统计 / 启停 / 重连 / 导出 ──
|
|
946
|
+
function readMcpBody(cb) {
|
|
947
|
+
var chunks = [];
|
|
948
|
+
req.on("data", function (c) { chunks.push(c); });
|
|
949
|
+
req.on("end", function () {
|
|
950
|
+
var obj = {};
|
|
951
|
+
try { obj = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"); } catch (e) { obj = {}; }
|
|
952
|
+
cb(obj);
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
if (path === PREFIX + "/mcp/local-scan") {
|
|
956
|
+
try { sendJson(res, 200, scanLocalMcp(ctx)); }
|
|
957
|
+
catch (e) { sendJson(res, 500, { ok: false, error: String((e && e.message) || e) }); }
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (path === PREFIX + "/mcp/ai-diagnose") {
|
|
961
|
+
mcpAiDiagnose(ctx).then(function (r) { sendJson(res, r && r.ok ? 200 : 500, r); })
|
|
962
|
+
.catch(function (e) { sendJson(res, 500, { ok: false, error: String((e && e.message) || e) }); });
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (path === PREFIX + "/mcp/ai-fix/apply" && req.method === "POST") {
|
|
966
|
+
readBody(req, function (body) {
|
|
967
|
+
mcpAiFixApply(ctx, (body && body.actions) || []).then(function (r) { sendJson(res, 200, r); })
|
|
968
|
+
.catch(function (e) { sendJson(res, 500, { ok: false, error: String((e && e.message) || e) }); });
|
|
969
|
+
});
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (path === PREFIX + "/mcp/servers") {
|
|
973
|
+
try { sendJson(res, 200, collectMcp(ctx, /(^|&)(refresh=1|t=)/.test(query))); }
|
|
974
|
+
catch (e) {
|
|
975
|
+
ctx.logger.warn("dsh-skill-browser: mcp list failed " + (e && e.message));
|
|
976
|
+
sendJson(res, 500, { ok: false, error: String((e && e.message) || e) });
|
|
977
|
+
}
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if (path === PREFIX + "/mcp/stats") {
|
|
981
|
+
try { sendJson(res, 200, getMcpStats({ since: qp(query, "since") })); }
|
|
982
|
+
catch (e) {
|
|
983
|
+
ctx.logger.warn("dsh-skill-browser: mcp stats failed " + (e && e.message));
|
|
984
|
+
sendJson(res, 500, { ok: false, error: String((e && e.message) || e) });
|
|
985
|
+
}
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (path === PREFIX + "/mcp/stats/clear" && req.method === "POST") {
|
|
989
|
+
try { sendJson(res, 200, clearMcpStats()); }
|
|
990
|
+
catch (e) { sendJson(res, 500, { ok: false, error: String((e && e.message) || e) }); }
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (path === PREFIX + "/mcp/export") {
|
|
994
|
+
try { sendJson(res, 200, exportMcp(ctx, { mask: /(^|&)mask=1(&|$)/.test(query) })); }
|
|
995
|
+
catch (e) {
|
|
996
|
+
ctx.logger.warn("dsh-skill-browser: mcp export failed " + (e && e.message));
|
|
997
|
+
sendJson(res, 500, { ok: false, error: String((e && e.message) || e) });
|
|
998
|
+
}
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (path === PREFIX + "/mcp/residue") {
|
|
1002
|
+
try { sendJson(res, 200, detectMcpResidue(ctx)); }
|
|
1003
|
+
catch (e) {
|
|
1004
|
+
ctx.logger.warn("dsh-skill-browser: mcp residue detect failed " + (e && e.message));
|
|
1005
|
+
sendJson(res, 500, { ok: false, error: String((e && e.message) || e) });
|
|
1006
|
+
}
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (path === PREFIX + "/mcp/residue/clean" && req.method === "POST") {
|
|
1010
|
+
try {
|
|
1011
|
+
var cres = cleanMcpResidue(ctx);
|
|
1012
|
+
ctx.logger.info("dsh-skill-browser: mcp residue cleaned=" + cres.cleaned);
|
|
1013
|
+
sendJson(res, 200, cres);
|
|
1014
|
+
} catch (e) {
|
|
1015
|
+
ctx.logger.warn("dsh-skill-browser: mcp residue clean failed " + (e && e.message));
|
|
1016
|
+
sendJson(res, 500, { ok: false, error: String((e && e.message) || e) });
|
|
1017
|
+
}
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if ((path === PREFIX + "/mcp/enable" || path === PREFIX + "/mcp/disable") && req.method === "POST") {
|
|
1021
|
+
var wantEnable = path.indexOf("/mcp/enable") >= 0;
|
|
1022
|
+
readMcpBody(function (body) {
|
|
1023
|
+
var r;
|
|
1024
|
+
try { r = setMcpEnabled(ctx, String(body.id || ""), wantEnable); }
|
|
1025
|
+
catch (e) { r = { ok: false, error: String((e && e.message) || e) }; }
|
|
1026
|
+
sendJson(res, r.ok ? 200 : 500, r);
|
|
1027
|
+
});
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (path === PREFIX + "/mcp/reconnect" && req.method === "POST") {
|
|
1031
|
+
readMcpBody(function (body) {
|
|
1032
|
+
Promise.resolve(reconnectMcp(ctx, String(body.id || ""))).then(function (r) {
|
|
1033
|
+
sendJson(res, r && r.ok ? 200 : 500, r);
|
|
1034
|
+
}).catch(function (e) { sendJson(res, 500, { ok: false, error: String((e && e.message) || e) }); });
|
|
1035
|
+
});
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
903
1039
|
sendJson(res, 404, { ok: false, error: "unknown path" });
|
|
904
1040
|
};
|
|
905
1041
|
|
|
906
1042
|
return {
|
|
907
1043
|
handler: handler,
|
|
908
|
-
dispose: function () {
|
|
1044
|
+
dispose: function () {
|
|
1045
|
+
try { if (offTools) offTools(); } catch (e) {}
|
|
1046
|
+
try { if (offMcp) offMcp(); } catch (e) {}
|
|
1047
|
+
}
|
|
909
1048
|
};
|
|
910
1049
|
}
|