dsh-skill-browser 1.8.7 → 1.9.9
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 +178 -0
- package/README-zh.md +190 -0
- package/lib/client.js +1723 -108
- package/lib/groups.js +956 -0
- package/lib/index.js +377 -22
- package/lib/keyword-groups.js +200 -0
- package/lib/ledger-stats.js +203 -0
- package/lib/ledger-writer.js +56 -1
- package/lib/scan.js +16 -0
- package/lib/zh.js +117 -1
- package/package.json +9 -3
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// ── v1.9.7: 关键词快速分组(零 token、毫秒级)────────────────────────
|
|
2
|
+
// 用户反馈 AI 分组「太满」而且慢。本模块用**技能目录名的关键词正则**直接切
|
|
3
|
+
// 「大类 → 子类」两级方案,不调用任何模型:
|
|
4
|
+
// · 零 token、零网络,110+ 技能 < 5ms 出方案
|
|
5
|
+
// · 结果完全确定,同样的技能库永远得到同样的分组
|
|
6
|
+
// · 规则写死可审计,随时能按用户口味加一行
|
|
7
|
+
// · 每个子类硬上限 8 项,避免"一屏塞满"
|
|
8
|
+
//
|
|
9
|
+
// 规则表 RULES 是**有序**的:从上往下第一条命中即生效(first-match-wins)。
|
|
10
|
+
// 所以「具体」的规则必须排在「笼统」的规则之前。
|
|
11
|
+
//
|
|
12
|
+
// 输出结构与 groups.js 的 aiClassify() 完全一致,前端复用同一个方案弹层:
|
|
13
|
+
// { ok, plan:{ domains:[ { name, emoji, subs:[ { name, emoji, skills:[dir] } ] } ] },
|
|
14
|
+
// stats:{ skills, domains, subs, covered }, mode:"keyword", unmatched:[] }
|
|
15
|
+
|
|
16
|
+
var MAX_SUB = 8;
|
|
17
|
+
|
|
18
|
+
var DOMAINS = [
|
|
19
|
+
{ key: "fluent", name: "Fluent/CFD 仿真", emoji: "🌀" },
|
|
20
|
+
{ key: "dsh", name: "DSH 工具链", emoji: "🛠️" },
|
|
21
|
+
{ key: "skill", name: "技能库治理", emoji: "🧠" },
|
|
22
|
+
{ key: "agent", name: "多智能体与长任务", emoji: "🤝" },
|
|
23
|
+
{ key: "gateway", name: "网关与反代", emoji: "🌐" },
|
|
24
|
+
{ key: "install", name: "软件安装与下载", emoji: "💾" },
|
|
25
|
+
{ key: "system", name: "系统与文件", emoji: "🖥️" },
|
|
26
|
+
{ key: "dev", name: "开发与发布", emoji: "⌨️" },
|
|
27
|
+
{ key: "doc", name: "文档与邮件", emoji: "📄" },
|
|
28
|
+
{ key: "image", name: "图片与视觉", emoji: "🖼️" },
|
|
29
|
+
{ key: "life", name: "网盘与生活", emoji: "🎨" }
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
var RULES = [
|
|
33
|
+
// ── Fluent / CFD(16 条,按用途切细,每子类 ≤8)────────────────
|
|
34
|
+
{ dom: "fluent", sub: "SpaceClaim 几何操作", emoji: "📐", re: /^spaceclaim-/ },
|
|
35
|
+
{ dom: "fluent", sub: "几何参数与模型", emoji: "📏",
|
|
36
|
+
re: /^(geometry-|fluent-debug-tank|fluent-domain-count|fluent-update-regions|ansys-local-workflows)/ },
|
|
37
|
+
{ dom: "fluent", sub: "画网格", emoji: "🕸️", re: /^fluent-mesh/ },
|
|
38
|
+
{ dom: "fluent", sub: "求解设置与初始化", emoji: "⚙️",
|
|
39
|
+
re: /^(fluent-setfile-|fluent-initialization|fluent-discretization-|fluent-precompute-|fluent-run-continuity|fluent-session-connect|fluent-gui-open)/ },
|
|
40
|
+
{ dom: "fluent", sub: "物理模型与扭矩", emoji: "🌀",
|
|
41
|
+
re: /^(fluent-mrf-|fluent-aeration-|fluent-pbm-|fluent-torque-|impeller-circulation|torque-power-)/ },
|
|
42
|
+
{ dom: "fluent", sub: "后处理·云图与矢量", emoji: "🌈",
|
|
43
|
+
re: /^(fluent-contour-|fluent-line-surface-|fluent-liquid-surface-|fluent-shear-|fluent-animation-)/ },
|
|
44
|
+
{ dom: "fluent", sub: "后处理·xyplot 曲线", emoji: "📈", re: /^fluent-xyplot-/ },
|
|
45
|
+
{ dom: "fluent", sub: "后处理·整理与对比", emoji: "📊",
|
|
46
|
+
re: /^(fluent-postprocess-|fluent-data-postprocess-|pbm-data-)/ },
|
|
47
|
+
{ dom: "fluent", sub: "运行监控与进程", emoji: "📡",
|
|
48
|
+
re: /^(fluent-operation-monitor|fluent-run-health-check|fluent-midrun-save|fluent-detached-|fluent-process-|fluent-iter-monitor|fluent-monitor-report)/ },
|
|
49
|
+
{ dom: "fluent", sub: "故障排查", emoji: "🔍",
|
|
50
|
+
re: /^(fluent-run-failure-|fluent-divergence-|fluent-path-check|fluent-path-index-repair|fluent-prev-run-vdisplay)/ },
|
|
51
|
+
{ dom: "fluent", sub: "主线与铁律", emoji: "🧭",
|
|
52
|
+
re: /^(fluent-universal-mainline|fluent-control-laws|fluent-quick-reference|fluent-cfd$)/ },
|
|
53
|
+
{ dom: "fluent", sub: "查证与经验", emoji: "📚",
|
|
54
|
+
re: /^(fluent-doc-query|fluent-experience|fluent-first-reference|fluent-growth-loop|fluent-experiment-first)/ },
|
|
55
|
+
{ dom: "fluent", sub: "步骤与产物规范", emoji: "📝",
|
|
56
|
+
re: /^(fluent-artifact-path|fluent-case-save-discipline|fluent-step-plan|fluent-simulation-template|fluent-subagent-parallel)/ },
|
|
57
|
+
{ dom: "fluent", sub: "多工况流程", emoji: "🔁",
|
|
58
|
+
re: /^(fluent-muti-|fluent-workflow-change|fluent-case-workflow-)/ },
|
|
59
|
+
{ dom: "fluent", sub: "文件与备份", emoji: "🗂️",
|
|
60
|
+
re: /^(fluent-case-|fluent-file-|fluent-junk-cleanup)/ },
|
|
61
|
+
{ dom: "fluent", sub: "主线与铁律", emoji: "🧭", re: /^fluent-/ },
|
|
62
|
+
|
|
63
|
+
// ── DSH 工具链 ──────────────────────────────────────────────────
|
|
64
|
+
{ dom: "dsh", sub: "全局配置", emoji: "⚙️", re: /^dsh-config$/ },
|
|
65
|
+
{ dom: "dsh", sub: "启动与恢复", emoji: "🚑", re: /^dsh-recovery$/ },
|
|
66
|
+
{ dom: "dsh", sub: "插件发布与同步", emoji: "🚀", re: /^dsh-plugin-publish-/ },
|
|
67
|
+
{ dom: "dsh", sub: "插件版本巡检", emoji: "📦", re: /^dsh-plugin-update-/ },
|
|
68
|
+
{ dom: "dsh", sub: "插件开发", emoji: "🧩", re: /^dsh-plugin-/ },
|
|
69
|
+
{ dom: "dsh", sub: "插件发现与安装", emoji: "🔌", re: /^(find-plugins|dsh-market)/ },
|
|
70
|
+
{ dom: "dsh", sub: "全局配置", emoji: "⚙️", re: /^dsh-/ },
|
|
71
|
+
|
|
72
|
+
// ── 技能库治理 ──────────────────────────────────────────────────
|
|
73
|
+
{ dom: "skill", sub: "技能库治理", emoji: "🧠", re: /^skill-/ },
|
|
74
|
+
|
|
75
|
+
// ── 多智能体与长任务 ────────────────────────────────────────────
|
|
76
|
+
{ dom: "agent", sub: "团队与预设", emoji: "🤝", re: /^(agent-team-|agent-preset-)/ },
|
|
77
|
+
{ dom: "agent", sub: "长任务与并行", emoji: "🎯", re: /^(goal-auto-continuation|task-parallel-)/ },
|
|
78
|
+
|
|
79
|
+
// ── 网关与反代 ──────────────────────────────────────────────────
|
|
80
|
+
{ dom: "gateway", sub: "接入与额度", emoji: "🌐",
|
|
81
|
+
re: /^(reverse-proxy-|agent2api-|lobsterai-)/ },
|
|
82
|
+
|
|
83
|
+
// ── 软件安装与下载 ──────────────────────────────────────────────
|
|
84
|
+
{ dom: "install", sub: "软件安装配置", emoji: "💾", re: /^software-install-/ },
|
|
85
|
+
{ dom: "install", sub: "镜像与下载", emoji: "⬇️", re: /^download-mirror-/ },
|
|
86
|
+
|
|
87
|
+
// ── 系统与文件 ──────────────────────────────────────────────────
|
|
88
|
+
{ dom: "system", sub: "删除与文件卫生", emoji: "🧹", re: /^(delete-to-recyclebin-|desktop-file-hygiene-)/ },
|
|
89
|
+
{ dom: "system", sub: "Windows 排障", emoji: "🖥️", re: /^(windows-toolkit|diagnose-windows)/ },
|
|
90
|
+
|
|
91
|
+
// ── 开发与发布 ──────────────────────────────────────────────────
|
|
92
|
+
{ dom: "dev", sub: "GitHub 发布", emoji: "🚀", re: /^github-publish/ },
|
|
93
|
+
{ dom: "dev", sub: "浏览器自动化", emoji: "🎭", re: /^playwright/ },
|
|
94
|
+
{ dom: "dev", sub: "前端与设计", emoji: "🎨", re: /^(frontend-design|design$)/ },
|
|
95
|
+
|
|
96
|
+
// ── 文档与邮件 ──────────────────────────────────────────────────
|
|
97
|
+
{ dom: "doc", sub: "文档处理", emoji: "📄", re: /^(document-toolkit|office)/ },
|
|
98
|
+
{ dom: "doc", sub: "批量邮件", emoji: "✉️", re: /^mail-bulk-/ },
|
|
99
|
+
|
|
100
|
+
// ── 图片与视觉 ──────────────────────────────────────────────────
|
|
101
|
+
{ dom: "image", sub: "电子印章", emoji: "🔖", re: /^image-electronic-seal/ },
|
|
102
|
+
{ dom: "image", sub: "壁纸与相册", emoji: "🖼️", re: /^(wallpaper-|image-gallery)/ },
|
|
103
|
+
{ dom: "image", sub: "本地出图", emoji: "🎨", re: /^qwen21-/ },
|
|
104
|
+
|
|
105
|
+
// ── 网盘与生活 ──────────────────────────────────────────────────
|
|
106
|
+
{ dom: "life", sub: "网盘", emoji: "☁️", re: /^(quarkclouddrive|baidu)/ },
|
|
107
|
+
{ dom: "life", sub: "生活杂项", emoji: "🎈", re: /^(misc-lowfreq-|maoxt-|weather)/ }
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
// 兜底:上面全没命中的技能,按 scan.js 的 cat 归入对应大类,子类名「未归类」。
|
|
111
|
+
var CAT_FALLBACK = {
|
|
112
|
+
fluent: { dom: "fluent", sub: "主线与铁律", emoji: "🧭" },
|
|
113
|
+
dsh: { dom: "dsh", sub: "全局配置", emoji: "⚙️" },
|
|
114
|
+
dev: { dom: "dev", sub: "前端与设计", emoji: "🎨" },
|
|
115
|
+
system: { dom: "system", sub: "Windows 排障", emoji: "🖥️" },
|
|
116
|
+
image: { dom: "image", sub: "壁纸与相册", emoji: "🖼️" },
|
|
117
|
+
output: { dom: "agent", sub: "长任务与并行", emoji: "🎯" }
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
function domMeta(key) {
|
|
121
|
+
for (var i = 0; i < DOMAINS.length; i++) if (DOMAINS[i].key === key) return DOMAINS[i];
|
|
122
|
+
return { key: "other", name: "其他技能", emoji: "📦" };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function matchRule(dir) {
|
|
126
|
+
for (var i = 0; i < RULES.length; i++) if (RULES[i].re.test(dir)) return RULES[i];
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 一个子类超过 MAX_SUB 时,拆成「名称 1」「名称 2」…,保证一屏不满。
|
|
131
|
+
function splitSub(name, emoji, dirs) {
|
|
132
|
+
var out = [];
|
|
133
|
+
if (dirs.length <= MAX_SUB) { out.push({ name: name, emoji: emoji, skills: dirs }); return out; }
|
|
134
|
+
var part = 1;
|
|
135
|
+
for (var i = 0; i < dirs.length; i += MAX_SUB) {
|
|
136
|
+
out.push({
|
|
137
|
+
name: name + " " + part,
|
|
138
|
+
emoji: emoji,
|
|
139
|
+
skills: dirs.slice(i, i + MAX_SUB)
|
|
140
|
+
});
|
|
141
|
+
part++;
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 纯函数:给一组 {dir, cat, ...} 技能,返回两级分组方案。
|
|
147
|
+
// 不读盘、不写盘、不联网 —— 调用方拿 plan 去走既有的 applyPlan()。
|
|
148
|
+
export function keywordClassify(skills) {
|
|
149
|
+
var list = Array.isArray(skills) ? skills : [];
|
|
150
|
+
var byDom = {};
|
|
151
|
+
var unmatched = [];
|
|
152
|
+
|
|
153
|
+
for (var i = 0; i < list.length; i++) {
|
|
154
|
+
var s = list[i] || {};
|
|
155
|
+
var dir = String(s.dir || "").trim();
|
|
156
|
+
if (!dir) continue;
|
|
157
|
+
var rule = matchRule(dir);
|
|
158
|
+
if (!rule) {
|
|
159
|
+
var fb = CAT_FALLBACK[String(s.cat || "")] || { dom: "life", sub: "未归类", emoji: "📦" };
|
|
160
|
+
rule = { dom: fb.dom, sub: "未归类", emoji: "📦" };
|
|
161
|
+
unmatched.push(dir);
|
|
162
|
+
}
|
|
163
|
+
if (!byDom[rule.dom]) byDom[rule.dom] = { subs: {}, order: [] };
|
|
164
|
+
var d = byDom[rule.dom];
|
|
165
|
+
if (!d.subs[rule.sub]) { d.subs[rule.sub] = { emoji: rule.emoji || "📁", dirs: [] }; d.order.push(rule.sub); }
|
|
166
|
+
d.subs[rule.sub].dirs.push(dir);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
var domains = [];
|
|
170
|
+
for (var m = 0; m < DOMAINS.length; m++) {
|
|
171
|
+
var key = DOMAINS[m].key;
|
|
172
|
+
if (!byDom[key]) continue;
|
|
173
|
+
var g = byDom[key];
|
|
174
|
+
var subs = [];
|
|
175
|
+
for (var t = 0; t < g.order.length; t++) {
|
|
176
|
+
var sb = g.subs[g.order[t]];
|
|
177
|
+
var parts = splitSub(g.order[t], sb.emoji, sb.dirs);
|
|
178
|
+
for (var p = 0; p < parts.length; p++) subs.push(parts[p]);
|
|
179
|
+
}
|
|
180
|
+
if (subs.length) domains.push({ name: DOMAINS[m].name, emoji: DOMAINS[m].emoji, subs: subs });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
var subCount = 0, covered = 0;
|
|
184
|
+
for (var a = 0; a < domains.length; a++) {
|
|
185
|
+
subCount += domains[a].subs.length;
|
|
186
|
+
for (var b = 0; b < domains[a].subs.length; b++) covered += domains[a].subs[b].skills.length;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
plan: { domains: domains },
|
|
192
|
+
stats: { skills: covered, domains: domains.length, subs: subCount, covered: covered },
|
|
193
|
+
mode: "keyword",
|
|
194
|
+
unmatched: unmatched
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function keywordDomains() { return DOMAINS; }
|
|
199
|
+
export function keywordRules() { return RULES; }
|
|
200
|
+
export var KEYWORD_MAX_SUB = MAX_SUB;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// dsh-skill-browser — 调用台账统计聚合(纯函数,无外部依赖)
|
|
2
|
+
//
|
|
3
|
+
// 数据源:技能失效-自动登记.jsonl(每行一条 skill 工具调用结果)
|
|
4
|
+
// {"ts":"2026-09-03T19:18:04.963Z","skill":"geometry-backup","ok":true,
|
|
5
|
+
// "kind":"auto-tool-result","callId":"call_x","error":""}
|
|
6
|
+
//
|
|
7
|
+
// 本模块只做「聚合」:按起止日期过滤 → 按 日/周/月/年 分桶 → 按技能排行。
|
|
8
|
+
// 不读文件、不碰网络,方便单测。
|
|
9
|
+
|
|
10
|
+
var MAX_BUCKETS = 400;
|
|
11
|
+
|
|
12
|
+
function pad2(n) { return n < 10 ? "0" + n : String(n); }
|
|
13
|
+
|
|
14
|
+
// 取 ISO 时间串的本地日期部分 YYYY-MM-DD(ts 是 UTC,用 Date 转本地)
|
|
15
|
+
export function dayKeyOf(ts) {
|
|
16
|
+
var d = new Date(String(ts || ""));
|
|
17
|
+
if (isNaN(d.getTime())) return "";
|
|
18
|
+
return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 周一为一周起点
|
|
22
|
+
export function weekStartOf(dayKey) {
|
|
23
|
+
var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(dayKey || ""));
|
|
24
|
+
if (!m) return "";
|
|
25
|
+
var d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
26
|
+
var dow = d.getDay(); // 0=周日
|
|
27
|
+
var back = dow === 0 ? 6 : dow - 1;
|
|
28
|
+
d.setDate(d.getDate() - back);
|
|
29
|
+
return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function addDays(dayKey, n) {
|
|
33
|
+
var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(dayKey || ""));
|
|
34
|
+
if (!m) return "";
|
|
35
|
+
var d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
36
|
+
d.setDate(d.getDate() + n);
|
|
37
|
+
return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function monthKeyOf(dayKey) { return String(dayKey || "").slice(0, 7); }
|
|
41
|
+
function yearKeyOf(dayKey) { return String(dayKey || "").slice(0, 4); }
|
|
42
|
+
|
|
43
|
+
// 桶 key + 展示标签
|
|
44
|
+
function bucketOf(dayKey, gran) {
|
|
45
|
+
if (gran === "year") { var y = yearKeyOf(dayKey); return { key: y, label: y }; }
|
|
46
|
+
if (gran === "month") { var mo = monthKeyOf(dayKey); return { key: mo, label: mo }; }
|
|
47
|
+
if (gran === "week") {
|
|
48
|
+
var ws = weekStartOf(dayKey);
|
|
49
|
+
return { key: ws, label: ws.slice(5) + "~" + addDays(ws, 6).slice(5) };
|
|
50
|
+
}
|
|
51
|
+
return { key: dayKey, label: dayKey.slice(5) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 下一个桶(用于补空洞)
|
|
55
|
+
function nextBucketKey(key, gran) {
|
|
56
|
+
if (gran === "year") return String(Number(key) + 1);
|
|
57
|
+
if (gran === "month") {
|
|
58
|
+
var m = /^(\d{4})-(\d{2})$/.exec(key); if (!m) return "";
|
|
59
|
+
var y = Number(m[1]), mo = Number(m[2]);
|
|
60
|
+
mo++; if (mo > 12) { mo = 1; y++; }
|
|
61
|
+
return y + "-" + pad2(mo);
|
|
62
|
+
}
|
|
63
|
+
return addDays(key, gran === "week" ? 7 : 1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function normalizeGran(v) {
|
|
67
|
+
var g = String(v || "").toLowerCase();
|
|
68
|
+
if (g === "year" || g === "month" || g === "week" || g === "day") return g;
|
|
69
|
+
return "day";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 记录规整:只保留需要的字段,非法 ts 丢弃
|
|
73
|
+
export function normalizeRecords(raw) {
|
|
74
|
+
var out = [];
|
|
75
|
+
var list = Object.prototype.toString.call(raw) === "[object Array]" ? raw : [];
|
|
76
|
+
for (var i = 0; i < list.length; i++) {
|
|
77
|
+
var r = list[i] || {};
|
|
78
|
+
var day = dayKeyOf(r.ts);
|
|
79
|
+
if (!day) continue;
|
|
80
|
+
var skill = String(r.skill == null ? "" : r.skill).trim();
|
|
81
|
+
if (!skill) continue;
|
|
82
|
+
out.push({
|
|
83
|
+
ts: String(r.ts),
|
|
84
|
+
day: day,
|
|
85
|
+
skill: skill,
|
|
86
|
+
ok: r.ok === false ? false : true,
|
|
87
|
+
kind: String(r.kind == null ? "" : r.kind)
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
out.sort(function (a, b) { return a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0; });
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 主聚合入口
|
|
95
|
+
// opts: { from, to, gran, top, sort }
|
|
96
|
+
export function buildStats(rawRecords, opts) {
|
|
97
|
+
var o = opts || {};
|
|
98
|
+
var gran = normalizeGran(o.gran);
|
|
99
|
+
var records = normalizeRecords(rawRecords);
|
|
100
|
+
|
|
101
|
+
// 全量时间边界(不受筛选影响,用于前端提示「数据可用区间」)
|
|
102
|
+
var dataFirst = records.length ? records[0].day : "";
|
|
103
|
+
var dataLast = records.length ? records[records.length - 1].day : "";
|
|
104
|
+
|
|
105
|
+
var from = String(o.from || "").trim();
|
|
106
|
+
var to = String(o.to || "").trim();
|
|
107
|
+
if (from && to && from > to) { var t = from; from = to; to = t; }
|
|
108
|
+
|
|
109
|
+
var picked = [];
|
|
110
|
+
for (var i = 0; i < records.length; i++) {
|
|
111
|
+
var r = records[i];
|
|
112
|
+
if (from && r.day < from) continue;
|
|
113
|
+
if (to && r.day > to) continue;
|
|
114
|
+
picked.push(r);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---- 总计 ----
|
|
118
|
+
var okCount = 0, failCount = 0;
|
|
119
|
+
var skillAgg = Object.create(null);
|
|
120
|
+
var daySet = Object.create(null);
|
|
121
|
+
for (var j = 0; j < picked.length; j++) {
|
|
122
|
+
var x = picked[j];
|
|
123
|
+
if (x.ok) okCount++; else failCount++;
|
|
124
|
+
daySet[x.day] = 1;
|
|
125
|
+
var a = skillAgg[x.skill];
|
|
126
|
+
if (!a) { a = skillAgg[x.skill] = { skill: x.skill, calls: 0, ok: 0, fail: 0, last: "", first: "" }; }
|
|
127
|
+
a.calls++;
|
|
128
|
+
if (x.ok) a.ok++; else a.fail++;
|
|
129
|
+
if (!a.first) a.first = x.ts;
|
|
130
|
+
a.last = x.ts;
|
|
131
|
+
}
|
|
132
|
+
var calls = picked.length;
|
|
133
|
+
var activeDays = Object.keys(daySet).length;
|
|
134
|
+
|
|
135
|
+
var totals = {
|
|
136
|
+
calls: calls,
|
|
137
|
+
ok: okCount,
|
|
138
|
+
fail: failCount,
|
|
139
|
+
rate: calls ? Math.round((okCount / calls) * 1000) / 10 : 0,
|
|
140
|
+
skills: Object.keys(skillAgg).length,
|
|
141
|
+
days: activeDays,
|
|
142
|
+
avgPerDay: activeDays ? Math.round((calls / activeDays) * 10) / 10 : 0
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// ---- 分桶(补空洞)----
|
|
146
|
+
var buckets = [];
|
|
147
|
+
if (picked.length) {
|
|
148
|
+
var map = Object.create(null);
|
|
149
|
+
var order = [];
|
|
150
|
+
for (var k = 0; k < picked.length; k++) {
|
|
151
|
+
var b = bucketOf(picked[k].day, gran);
|
|
152
|
+
if (!b.key) continue;
|
|
153
|
+
var cur = map[b.key];
|
|
154
|
+
if (!cur) { cur = map[b.key] = { key: b.key, label: b.label, calls: 0, ok: 0, fail: 0 }; order.push(b.key); }
|
|
155
|
+
cur.calls++;
|
|
156
|
+
if (picked[k].ok) cur.ok++; else cur.fail++;
|
|
157
|
+
}
|
|
158
|
+
order.sort();
|
|
159
|
+
// 补空洞:从首个桶走到末个桶
|
|
160
|
+
var firstKey = order[0], lastKey = order[order.length - 1];
|
|
161
|
+
var walk = firstKey, guard = 0;
|
|
162
|
+
while (walk && walk <= lastKey && guard < MAX_BUCKETS) {
|
|
163
|
+
if (map[walk]) buckets.push(map[walk]);
|
|
164
|
+
else buckets.push({ key: walk, label: bucketOf(gran === "day" ? walk : (gran === "year" ? walk + "-01-01" : (gran === "month" ? walk + "-01" : walk)), gran).label, calls: 0, ok: 0, fail: 0 });
|
|
165
|
+
walk = nextBucketKey(walk, gran);
|
|
166
|
+
guard++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---- 排行 ----
|
|
171
|
+
var ranks = [];
|
|
172
|
+
for (var sk in skillAgg) if (Object.prototype.hasOwnProperty.call(skillAgg, sk)) ranks.push(skillAgg[sk]);
|
|
173
|
+
var sort = String(o.sort || "calls");
|
|
174
|
+
var cmp = {
|
|
175
|
+
calls: function (a, b) { return b.calls - a.calls || b.fail - a.fail || (a.skill < b.skill ? -1 : 1); },
|
|
176
|
+
fails: function (a, b) { return b.fail - a.fail || b.calls - a.calls || (a.skill < b.skill ? -1 : 1); },
|
|
177
|
+
rate: function (a, b) { return (a.ok / a.calls) - (b.ok / b.calls) || b.calls - a.calls; },
|
|
178
|
+
last: function (a, b) { return a.last < b.last ? 1 : a.last > b.last ? -1 : 0; },
|
|
179
|
+
name: function (a, b) { return a.skill < b.skill ? -1 : a.skill > b.skill ? 1 : 0; }
|
|
180
|
+
}[sort] || null;
|
|
181
|
+
ranks.sort(cmp || function (a, b) { return b.calls - a.calls; });
|
|
182
|
+
for (var q = 0; q < ranks.length; q++) {
|
|
183
|
+
ranks[q].rank = q + 1;
|
|
184
|
+
ranks[q].rate = ranks[q].calls ? Math.round((ranks[q].ok / ranks[q].calls) * 1000) / 10 : 0;
|
|
185
|
+
}
|
|
186
|
+
var top = Number(o.top);
|
|
187
|
+
if (!(top > 0)) top = 0;
|
|
188
|
+
if (top && ranks.length > top) ranks = ranks.slice(0, top);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
gran: gran,
|
|
193
|
+
from: from,
|
|
194
|
+
to: to,
|
|
195
|
+
sort: sort,
|
|
196
|
+
dataFirst: dataFirst,
|
|
197
|
+
dataLast: dataLast,
|
|
198
|
+
totals: totals,
|
|
199
|
+
buckets: buckets,
|
|
200
|
+
ranks: ranks,
|
|
201
|
+
rankTotal: Object.keys(skillAgg).length
|
|
202
|
+
};
|
|
203
|
+
}
|
package/lib/ledger-writer.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// 台账 md 内插件维护区块用标记包围,幂等追加:
|
|
17
17
|
// <!-- dsh-skill-browser:auto-ledger:v1 BEGIN --> ... <!-- dsh-skill-browser:auto-ledger:v1 END -->
|
|
18
18
|
|
|
19
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, appendFileSync, readdirSync } from "node:fs";
|
|
19
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, appendFileSync, readdirSync, statSync, openSync, readSync, closeSync } from "node:fs";
|
|
20
20
|
import { join, dirname } from "node:path";
|
|
21
21
|
|
|
22
22
|
var LEDGER_REL = ["skill-master", "references", "技能生效-失效台账.md"];
|
|
@@ -223,6 +223,61 @@ export function readAutoLog(root, skillFilter) {
|
|
|
223
223
|
return out;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
// ── v1.9.6 性能:自动登记 jsonl 的增量读取 ────────────────────
|
|
227
|
+
// 问题:原 readAutoLog 每次调用都 readFileSync 整个 jsonl(986+ 行且持续增长),
|
|
228
|
+
// 台账看板每次切粒度/改排序都会重读一遍,导致卡顿与内存抖动。
|
|
229
|
+
// 方案:①用 stat 探针判断文件是否变化(size+mtimeMs),未变化直接复用解析结果;
|
|
230
|
+
// ②需要"最近 N 条"时只从文件尾部读,不读全文。
|
|
231
|
+
export function autoLogStat(root) {
|
|
232
|
+
var p = autoLogFilePath(root);
|
|
233
|
+
try {
|
|
234
|
+
if (!existsSync(p)) return { path: p, exists: false, size: 0, mtimeMs: 0 };
|
|
235
|
+
var st = statSync(p);
|
|
236
|
+
return { path: p, exists: true, size: st.size, mtimeMs: st.mtimeMs };
|
|
237
|
+
} catch (e) {
|
|
238
|
+
return { path: p, exists: false, size: 0, mtimeMs: 0 };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 只读文件尾部若干字节(按行切齐),用于"最近 N 条"场景,避免全量读盘
|
|
243
|
+
export function readAutoLogTail(root, limit, skillFilter) {
|
|
244
|
+
var n = Math.max(1, Number(limit) || 30);
|
|
245
|
+
var p = autoLogFilePath(root);
|
|
246
|
+
if (!existsSync(p)) return [];
|
|
247
|
+
var text = "";
|
|
248
|
+
try {
|
|
249
|
+
var st = statSync(p);
|
|
250
|
+
// 每行约 130 字节,按需多读 40% 余量;不足则退回全量
|
|
251
|
+
var want = Math.min(st.size, Math.max(4096, n * 190));
|
|
252
|
+
var fd = openSync(p, "r");
|
|
253
|
+
try {
|
|
254
|
+
var buf = Buffer.alloc(want);
|
|
255
|
+
readSync(fd, buf, 0, want, Math.max(0, st.size - want));
|
|
256
|
+
text = buf.toString("utf8");
|
|
257
|
+
} finally {
|
|
258
|
+
try { closeSync(fd); } catch (e2) {}
|
|
259
|
+
}
|
|
260
|
+
if (want < st.size) {
|
|
261
|
+
var nl = text.indexOf("\n");
|
|
262
|
+
if (nl >= 0) text = text.slice(nl + 1); // 丢掉可能被截断的首行
|
|
263
|
+
}
|
|
264
|
+
} catch (e) {
|
|
265
|
+
try { text = readFileSync(p, "utf8"); } catch (e3) { return []; }
|
|
266
|
+
}
|
|
267
|
+
var out = [];
|
|
268
|
+
var lines = text.split("\n");
|
|
269
|
+
for (var i = 0; i < lines.length; i++) {
|
|
270
|
+
var line = lines[i].trim();
|
|
271
|
+
if (!line) continue;
|
|
272
|
+
try {
|
|
273
|
+
var obj = JSON.parse(line);
|
|
274
|
+
if (skillFilter && obj.skill !== skillFilter) continue;
|
|
275
|
+
out.push(obj);
|
|
276
|
+
} catch (e4) { /* 跳过坏行/截断行 */ }
|
|
277
|
+
}
|
|
278
|
+
return out.length > n ? out.slice(out.length - n) : out;
|
|
279
|
+
}
|
|
280
|
+
|
|
226
281
|
// ── 内部:队列与写盘 ────────────────────────────────────
|
|
227
282
|
function enqueue(state, entry) {
|
|
228
283
|
state.queue.push(entry);
|
package/lib/scan.js
CHANGED
|
@@ -69,10 +69,25 @@ export function categorize(dirName) {
|
|
|
69
69
|
return { id: cat, sub: sub };
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// v1.9.0: resolve one skill's version string.
|
|
73
|
+
// Order: SKILL.md frontmatter `version` -> sibling package.json `version` -> "".
|
|
74
|
+
// "" means "not yet versioned" (the panel renders a fallback badge).
|
|
75
|
+
function readSkillVersion(root, dirName, fm) {
|
|
76
|
+
var v = String((fm && fm.version) || "").trim();
|
|
77
|
+
if (v) return v;
|
|
78
|
+
try {
|
|
79
|
+
var pkgPath = join(root, dirName, "package.json");
|
|
80
|
+
if (!existsSync(pkgPath)) return "";
|
|
81
|
+
var pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
82
|
+
return String((pkg && pkg.version) || "").trim();
|
|
83
|
+
} catch (e) { return ""; }
|
|
84
|
+
}
|
|
85
|
+
|
|
72
86
|
// Scan one skill root directory. Returns an array of skill summaries,
|
|
73
87
|
// sorted by directory name. Skips non-directories, `_`-prefixed and
|
|
74
88
|
// `.`-prefixed entries, and directories without a SKILL.md.
|
|
75
89
|
// v1.7.2: each entry also carries `disabled` (read from frontmatter).
|
|
90
|
+
// v1.9.0: each entry also carries `version` ("" = not yet versioned).
|
|
76
91
|
export function scanSkills(root, zhDict) {
|
|
77
92
|
var out = [];
|
|
78
93
|
if (!root || !existsSync(root)) return out;
|
|
@@ -93,6 +108,7 @@ export function scanSkills(root, zhDict) {
|
|
|
93
108
|
zh: (zhDict && zhDict[e.name]) || "",
|
|
94
109
|
cat: cat.id,
|
|
95
110
|
sub: cat.sub,
|
|
111
|
+
version: readSkillVersion(root, e.name, fm),
|
|
96
112
|
mtime: safeMtime(skillFile),
|
|
97
113
|
disabled: fmSaysDisabled(fm)
|
|
98
114
|
});
|
package/lib/zh.js
CHANGED
|
@@ -137,5 +137,121 @@ export var UI_TEXT = {
|
|
|
137
137
|
aggMenuTitleCat: "分类批量启停 — 仅作用于当前分类",
|
|
138
138
|
aggMenuStat: "{scope} · 共 {n} 项 · 已停用 {x} 项",
|
|
139
139
|
aggCatStat: "停用 {x}/{y}",
|
|
140
|
-
aggCancelText: "取消"
|
|
140
|
+
aggCancelText: "取消",
|
|
141
|
+
// ── v1.9.0:版本号 / 视图分组 / AI 分组与封面 ──
|
|
142
|
+
versionLabel: "版本",
|
|
143
|
+
versionNone: "暂未编版本号",
|
|
144
|
+
versionTip: "该技能 SKILL.md frontmatter 与 package.json 都没有声明 version",
|
|
145
|
+
groupsTitle: "我的分组",
|
|
146
|
+
groupsHint: "分组只是视图归档,不会移动任何技能文件",
|
|
147
|
+
groupNew: "+ 新建分组",
|
|
148
|
+
groupAi: "🤖 AI 自动分组",
|
|
149
|
+
groupAll: "全部",
|
|
150
|
+
groupUngrouped: "未分组",
|
|
151
|
+
groupEmpty: "还没有分组,点「+ 新建分组」或「🤖 AI 自动分组」",
|
|
152
|
+
groupNamePrompt: "分组名称",
|
|
153
|
+
groupNamePlaceholder: "例如:Fluent 仿真、逆向工具、文档处理",
|
|
154
|
+
groupDeleteConfirm: "删除分组「{name}」?技能文件不会受影响,只是取消归档。",
|
|
155
|
+
groupCreated: "已创建分组:{name}",
|
|
156
|
+
groupRenamed: "已重命名:{name}",
|
|
157
|
+
groupDeleted: "已删除分组:{name}",
|
|
158
|
+
groupAssignTip: "放进这个分组(只改视图,不动文件)",
|
|
159
|
+
groupMove: "移动",
|
|
160
|
+
groupMoveTitle: "移动到分组",
|
|
161
|
+
groupMoveNone: "移出分组",
|
|
162
|
+
groupAssigned: "已把 {name} 移入「{group}」",
|
|
163
|
+
groupUnassigned: "已把 {name} 移出分组",
|
|
164
|
+
groupAiRunning: "AI 正在分析技能库并分组,请稍候…",
|
|
165
|
+
groupAiDone: "AI 已建 {n} 个分组",
|
|
166
|
+
groupAiFail: "AI 分组失败:{err}",
|
|
167
|
+
groupCover: "AI 生成封面",
|
|
168
|
+
groupCoverRunning: "正在生成封面…",
|
|
169
|
+
groupCoverDone: "封面已生成",
|
|
170
|
+
groupCoverFail: "封面生成失败:{err}",
|
|
171
|
+
groupRename: "重命名",
|
|
172
|
+
groupDelete: "删除分组",
|
|
173
|
+
groupManageTitle: "分组管理",
|
|
174
|
+
aiNoLlm: "AI 不可用:宿主未提供 llm 服务",
|
|
175
|
+
modelUsing: "AI 分组将使用:{model}",
|
|
176
|
+
modelSourceDefault: "DSH 默认模型",
|
|
177
|
+
modelSourceAgent: "当前对话模型",
|
|
178
|
+
modelSourceConfig: "插件配置",
|
|
179
|
+
modelSourceCatalog: "宿主可用模型",
|
|
180
|
+
modelNone: "AI 不可用:没找到任何可用模型,请到 DSH 设置 → 模型 选一个默认模型",
|
|
181
|
+
modelProbeTip: "点一下检测 AI 分组会用哪个模型(不会真的调模型)",
|
|
182
|
+
hotReload: "热重载",
|
|
183
|
+
hotReloadTip: "热重载:重新加载插件代码,不重启桌面端",
|
|
184
|
+
hotReloadRunning: "正在热重载…",
|
|
185
|
+
hotReloadDone: "热重载完成(v{v} · 第 {n} 代)",
|
|
186
|
+
hotReloadFail: "热重载失败:{e}",
|
|
187
|
+
hotReloadRefresh: "代码已换新,正在刷新界面…",
|
|
188
|
+
cardMore: "加载更多(还有 {n} 条)",
|
|
189
|
+
detailHint: "点开才动态加载全文,列表只做静态摘要",
|
|
190
|
+
lstatTitle: "调用台账",
|
|
191
|
+
lstatQuickToday: "今天",
|
|
192
|
+
lstatQuick7: "近7天",
|
|
193
|
+
lstatQuick30: "近30天",
|
|
194
|
+
lstatQuickMonth: "本月",
|
|
195
|
+
lstatQuickPrevMonth: "上月",
|
|
196
|
+
lstatQuickYear: "今年",
|
|
197
|
+
lstatQuickAll: "全部",
|
|
198
|
+
lstatFrom: "起",
|
|
199
|
+
lstatTo: "止",
|
|
200
|
+
lstatGran: "粒度",
|
|
201
|
+
lstatGranDay: "日",
|
|
202
|
+
lstatGranWeek: "周",
|
|
203
|
+
lstatGranMonth: "月",
|
|
204
|
+
lstatGranYear: "年",
|
|
205
|
+
lstatCalls: "调用",
|
|
206
|
+
lstatSkills: "活跃技能",
|
|
207
|
+
lstatDays: "活跃天数",
|
|
208
|
+
lstatRate: "成功率",
|
|
209
|
+
lstatAvg: "日均",
|
|
210
|
+
lstatTrend: "调用趋势",
|
|
211
|
+
lstatRank: "技能调用排行",
|
|
212
|
+
lstatColRank: "#",
|
|
213
|
+
lstatColSkill: "技能",
|
|
214
|
+
lstatColVer: "版本",
|
|
215
|
+
lstatColCalls: "调用",
|
|
216
|
+
lstatColOk: "成功",
|
|
217
|
+
lstatColFail: "失败",
|
|
218
|
+
lstatColRate: "成功率",
|
|
219
|
+
lstatColLast: "最后调用",
|
|
220
|
+
lstatSortTip: "点表头切换排序",
|
|
221
|
+
lstatEmptyRange: "该区间没有调用记录",
|
|
222
|
+
lstatEmptyAll: "还没有任何调用记录 —— 用一次技能后这里就会出现数据",
|
|
223
|
+
lstatLoading: "台账统计加载中…",
|
|
224
|
+
lstatFail: "台账统计加载失败:{err}",
|
|
225
|
+
lstatDataRange: "数据区间 {from} ~ {to}",
|
|
226
|
+
lstatNoData: "无数据",
|
|
227
|
+
lstatManual: "人工台账",
|
|
228
|
+
lstatAuto: "自动登记流水",
|
|
229
|
+
lstatAutoTip: "插件监听 skill 工具调用结果自动记录 · 最近 30 条",
|
|
230
|
+
lstatCollapse: "展开原始流水",
|
|
231
|
+
lstatExpand: "收起原始流水",
|
|
232
|
+
lstatTopN: "仅显示前 {n}",
|
|
233
|
+
// ── v1.9.3:两级分组(总-分)与 AI 方案确认 ──
|
|
234
|
+
groupSubsOf: "子类",
|
|
235
|
+
groupWhole: "整个大类",
|
|
236
|
+
groupNewSub: "在此大类下新建子类",
|
|
237
|
+
groupCurTag: "当前",
|
|
238
|
+
groupKidsN: "{n} 子类",
|
|
239
|
+
groupKw: "⚡ 关键词分组",
|
|
240
|
+
groupKwTip: "本地关键词规则直接分组:零 token、毫秒级、结果固定",
|
|
241
|
+
groupAiTip: "调用模型重新归纳分组:慢,但更懂语义",
|
|
242
|
+
groupPlanTitleKw: "⚡ 关键词分组方案(确认后才会写入)",
|
|
243
|
+
groupPlanTitle: "🤖 AI 分组方案(确认后才会写入)",
|
|
244
|
+
groupPlanStat: "共 {d} 个大类 · {s} 个子类 · 覆盖 {n} 个技能",
|
|
245
|
+
groupPlanReplace: "替换:清掉旧的 AI 分组,按此方案重建",
|
|
246
|
+
groupPlanMerge: "追加:保留现有分组,只补未归类的技能",
|
|
247
|
+
groupApplyRunning: "正在写入分组…",
|
|
248
|
+
groupApplyDone: "已应用:{d} 个大类 / {s} 个子类",
|
|
249
|
+
groupApplyFail: "应用分组失败:{err}",
|
|
250
|
+
groupSubCreated: "子类已创建:{name}",
|
|
251
|
+
groupSubPrompt: "在「{p}」下新建子类,输入名称:",
|
|
252
|
+
groupSubFail: "新建子类失败",
|
|
253
|
+
groupModeTree: "🌳 树状",
|
|
254
|
+
groupModeFlat: "▦ 平铺",
|
|
255
|
+
groupModeTreeTip: "树状:每个大类一行,子类常驻缩进挂在下层,层层展开一眼看全",
|
|
256
|
+
groupModeFlatTip: "平铺:大类排成卡片,点开才显示子类(v1.9.3 原样)"
|
|
141
257
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-skill-browser",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.9.9",
|
|
4
|
+
"description": "技能浏览器:把 ~/.dsh/skills 里的技能做成可视化面板(搜索/分类/启停/详情/调用台账)。v1.9.0 自定义分组+版本号+AI 分组+AI 封面;v1.9.1 调用台账看板;v1.9.2 全皮肤自适应配色;v1.9.3 两级分组+滚动条+悬浮球层级;v1.9.4 面板⚡热重载;v1.9.5 吸顶表头不透明;v1.9.6 详情层隔离+分片渲染+增量读取;v1.9.7 关键词快速分组;v1.9.8 树状/平铺双模式分组栏;v1.9.9 零配置模型自动发现+全皮肤文字可见性加固。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -65,7 +65,13 @@
|
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
68
|
-
"@deepseek-ai/dsh": ">=0.1.7-rc.1"
|
|
68
|
+
"@deepseek-ai/dsh": ">=0.1.7-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-llm": "^0.1.7-rc.1 || ^0.2.0-rc.1"
|
|
70
|
+
},
|
|
71
|
+
"peerDependenciesMeta": {
|
|
72
|
+
"@deepseek-ai/dsh-llm": {
|
|
73
|
+
"optional": true
|
|
74
|
+
}
|
|
69
75
|
},
|
|
70
76
|
"devDependencies": {
|
|
71
77
|
"@deepseek-ai/cordis": "^4.0.2",
|