dsh-armory 2.2.1 → 2.3.0
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.en.md +6 -8
- package/README.md +6 -8
- package/lib/client.js +57 -0
- package/lib/index.js +58 -1
- package/lib/mcp-oauth.js +1 -1
- package/lib/tool-ledger.js +93 -0
- package/lib/whats-new-en.js +10 -0
- package/lib/whats-new.js +13 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -2,14 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
[中文](README.md) | English
|
|
4
4
|
|
|
5
|
-
> **The all-in-one control board for DeepSeek Harness**: MCP servers ·
|
|
6
|
-
>
|
|
7
|
-
> upgraded from a pure MCP panel into a **MCP + Skills (with custom suites) + Experts + Plugins + Markets + Tool Tailor** board.
|
|
5
|
+
> **The all-in-one control board for DeepSeek Harness**: MCP servers · skills · experts · plugins · tool-usage ledger · four markets — browse, search, install and summon in one place.
|
|
6
|
+
> Forked from [Imzl-zl/dsh-mcp-manager-ui](https://github.com/Imzl-zl/dsh-mcp-manager-ui) (MIT) and extended into a full armory.
|
|
8
7
|
>
|
|
9
|
-
> **New in v2.
|
|
10
|
-
> **New in v2.1.0**: community plugins now render their README in place (fetched on demand, cached to disk for 7 days, `javascript:` pseudo-links downgraded to plain text), and two long-standing traps are fixed — the instant-open snapshot no longer shadows the real list, and update probing no longer reports failures on machines where the primary source is unreachable. See the [changelog](.github/release-notes/v2.1.0.md).
|
|
11
|
-
> **New in v2.2.0**: bundle skills into custom suites and expand the whole set as `/name` tokens from the composer menu; uninstall installed skills from the board (with source/path guards) and watch counts refresh live after market installs; the Tool Tailor gains a sampled "wire tax" trend so you can tell whether a registry change actually took effect; SkillHub's downloads sort falls back to in-page sorting because the upstream hangs. See the [changelog](.github/release-notes/v2.2.0.md).
|
|
12
|
-
> **New in v2.2.1**: the panel now follows DSH's own language setting (Settings -> General -> Language). Switching the host to English updates the sidebar, tabs, toolbar, dialogs and update notices immediately - no restart, and no dependency on the OS locale; release notes and the built-in MCP catalog are translated as well, and non-Chinese locales fall back to English. See the [changelog](.github/release-notes/v2.2.1.md).
|
|
8
|
+
> **New in v2.3.0**: the Tool Tailor gains a skill ledger that shows which skills were actually used and which were registered but never called. See the [changelog](.github/release-notes/v2.3.0.md).
|
|
13
9
|
|
|
14
10
|
<p align="center">
|
|
15
11
|
<a href="https://github.com/2026heshao/dsh-armory/stargazers"><img src="https://img.shields.io/github/stars/2026heshao/dsh-armory?style=social" alt="stars"></a>
|
|
@@ -38,7 +34,7 @@ Restart DSH, then open **Skills · MCP · Experts** from the global panel list a
|
|
|
38
34
|
|---|---|
|
|
39
35
|
| MCP config means hand-editing YAML with no feedback on whether it connects | Visual panel: status, tool schemas, enable/disable, reconnect — global & per-project scopes, project changes apply to live sessions instantly |
|
|
40
36
|
| Installing skills means hunting for `SKILL.md` repos everywhere | **Direct markets**: SkillHub (190k+ skills), WorkBuddy connectors (316), 47 first-party-verified built-ins — search, filter, one-click install with per-file sha256 verification |
|
|
41
|
-
| Every MCP's tool descriptions eat context tokens; zero-use tools sit there for free | **Tool Tailor**: scans local session logs for call counts / errors / latency
|
|
37
|
+
| Every MCP's tool descriptions eat context tokens; zero-use tools sit there for free | **Tool Tailor**: scans local session logs for per-tool call counts / errors / latency, plus a skill-level usage ledger; prune dead tools with one click |
|
|
42
38
|
| Migrating from Claude/Cursor/VS Code means copying config by hand | Detects local configs from 11 clients and imports them with a preview |
|
|
43
39
|
| OAuth-protected or key-protected servers and plugins just fail, with no hint why | **Plugin market** (CodeBuddy official + DSH community list) with per-entry source and installability, **one-click MCP OAuth** (RFC 9728 → 8414 → 7591 → 7636 PKCE → 8707, three callback fallbacks ending at "paste a token"), and a **credential prompt before install** that fills `${...}` placeholders so the server works on first connect |
|
|
44
40
|
|
|
@@ -78,6 +74,8 @@ Transport, connection parameters, call timeout and config source at a glance; be
|
|
|
78
74
|
|
|
79
75
|
Four metric cards for the 30-day window (total calls / tools seen / zero-call MCP tools / overall error rate), one row per tool with calls, errors, error rate, Schema context tax and last use, and a persistent tailor suggestion at the bottom. Shows all tools with real call counts by default; "zero-call only (N)" narrows the view. Below the cards sits a **second trend**: the tool list actually sent to the model in every request (`request/header`), sampled per day, together with "how many times that list changed inside the window" and the timestamp of the latest one. When local logs are shorter than the selected window, the note bar says so plainly ("window N days, calls I could read span only M days"), so "this number hasn't moved in days" is attributable on screen instead of looking like a broken ledger:
|
|
80
76
|
|
|
77
|
+
Below the tool table is a new **skill ledger**: calls are grouped by the skill name in the `skill` tool's arguments, with calls / errors / last use per skill, and registered skills that were never invoked are added as zero-call rows — so it is obvious which skills are registered but unused. Calls whose arguments carry no skill name are counted separately on screen; when the registry is unavailable, the panel lists only skills seen in logs instead of faking a zero.
|
|
78
|
+
|
|
81
79
|

|
|
82
80
|
|
|
83
81
|
### Experts — summoned list
|
package/README.md
CHANGED
|
@@ -2,14 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
中文 | [English](README.en.md)
|
|
4
4
|
|
|
5
|
-
> **DeepSeek Harness 的一体化管理看板**:MCP
|
|
6
|
-
>
|
|
7
|
-
> 从纯 MCP 管理面板升级为 **MCP + 技能(含自定义套件)+ 专家 + 插件 + 市场 + 工具账本** 看板。
|
|
5
|
+
> **DeepSeek Harness 的一体化管理看板**:MCP · 技能 · 专家 · 插件 · 工具账本 · 四大市场,浏览、搜索、一键安装、一键召唤。
|
|
6
|
+
> fork 自 [Imzl-zl/dsh-mcp-manager-ui](https://github.com/Imzl-zl/dsh-mcp-manager-ui)(MIT),现已扩展为完整军械库。
|
|
8
7
|
>
|
|
9
|
-
>
|
|
10
|
-
> **v2.1.0 起**:社区插件点开即可就地读 README(按需拉取 + 7 天落盘缓存 + `javascript:` 伪协议链接降级),并修掉「秒开快照顶掉真实目录」「更新探测误报失败」两处。详见 [更新日志](.github/release-notes/v2.1.0.md)。
|
|
11
|
-
> **v2.2.0 起**:技能可以打包成「自定义套件」,召唤菜单点一次即展开为整组 `/name`;Skill 看板支持卸载已安装技能,市场装 / 卸后角标与列表实时刷新;工具裁缝新增按天采样的「真实下发税」趋势,能直接看出改注册表有没有生效;SkillHub 下载量排序改为页内本地排序,修掉上游挂起导致的空白。详见 [更新日志](.github/release-notes/v2.2.0.md)。
|
|
12
|
-
> **v2.2.1 起**:面板语言真正跟随 DSH「设置 → 通用 → 语言」——宿主切 English 后侧栏、分类、工具栏、弹窗与更新提示即时变英文,无需重启,也不再依赖系统语言;更新说明与内置 MCP 目录同步提供英文,非中文语言统一回落英文。详见 [更新日志](.github/release-notes/v2.2.1.md)。
|
|
8
|
+
> **最新 v2.3.0**:工具裁缝新增「技能账本」,直接看出哪些技能真被用过、哪些注册了却一次没调用。详见 [更新日志](.github/release-notes/v2.3.0.md)。
|
|
13
9
|
|
|
14
10
|
<p align="center">
|
|
15
11
|
<a href="https://github.com/2026heshao/dsh-armory/stargazers"><img src="https://img.shields.io/github/stars/2026heshao/dsh-armory?style=social" alt="stars"></a>
|
|
@@ -38,7 +34,7 @@ dsh plugin --profile web add github:2026heshao/dsh-armory
|
|
|
38
34
|
|---|---|
|
|
39
35
|
| MCP 配置要手改 YAML,改完不知道连没连上 | 可视化面板:状态、工具 Schema、启停、重连,全局与项目双作用域,项目级改完即对存活会话生效 |
|
|
40
36
|
| 装技能要去各处翻 `SKILL.md` 仓库 | **市场直连**:SkillHub 19 万+ 技能、WorkBuddy 316 个连接器、47 个一手核实的内置服务,搜索/筛选/一键安装,逐文件 sha256 校验 |
|
|
41
|
-
| 每个 MCP 的工具描述都在吃上下文 token,零调用的工具白白占位 |
|
|
37
|
+
| 每个 MCP 的工具描述都在吃上下文 token,零调用的工具白白占位 | **工具裁缝**:扫描本机会话日志统计每个工具的调用/错误/延迟,并给出技能维度的使用账本;死工具清单一键禁用 |
|
|
42
38
|
| 从 Claude/Cursor/VS Code 迁移要逐条抄配置 | 自动检出 11 种客户端的本机配置,预览后一键导入 |
|
|
43
39
|
| 插件散在宿主自带、CodeBuddy 官方、社区清单三处,格式与可装性各不相同 | **插件页**:三个来源并进一个可搜索目录并标明可装性,安装统一走宿主 `pluginManager`,装完明确提示需重启 |
|
|
44
40
|
| 远端 MCP 要 OAuth,而官方 client 只认静态 headers,只能手搓 token | **一键授权**:按 RFC 9728/8414/7591/7636/8707 走完发现 → 注册 → PKCE → 回调 → 换 token,回调通道三级降级(loopback / 粘贴回调地址 / 手贴 token),到期自动刷新 |
|
|
@@ -64,6 +60,8 @@ dsh plugin --profile web add github:2026heshao/dsh-armory
|
|
|
64
60
|
|
|
65
61
|
30 天窗口四张指标卡(调用总次数 / 出现过的工具 / 零调用 MCP 工具 / 整体错误率),逐工具列出调用、错误、错误率、Schema 上下文税与最后使用时间,底部常驻裁缝建议。默认显示全部工具与真实调用次数,需要时点「只看零调用(N)」收窄。指标卡下方是**第二条趋势**:按天采样日志里每次请求真实下发的工具清单(`request/header`)并算出它的税,同时给出「窗口内下发清单变过几次」和最近一次的时间;说明栏还会在本机日志短于所选窗口时直接印出「窗口 N 天,读到的调用记录只跨 M 天」,让「数字几天没变」这件事在屏幕上就能归因:
|
|
66
62
|
|
|
63
|
+
工具表下方新增**技能账本**:按 `skill` 工具调用参数里的技能名统计调用 / 错误 / 最后使用,并把当前注册表中零调用的技能补成 0 行——哪些技能注册了却一次没被调用,一眼可见。参数里没有技能名的调用单独计数上屏;注册表不可用时只列出日志里出现过的技能,不伪造 0。
|
|
64
|
+
|
|
67
65
|

|
|
68
66
|
|
|
69
67
|
### 专家 — 已召唤清单
|
package/lib/client.js
CHANGED
|
@@ -500,6 +500,12 @@ const EN_US_MESSAGES = {
|
|
|
500
500
|
"扫描中…": "Scanning…",
|
|
501
501
|
"扫描后给出": "After the scan: ",
|
|
502
502
|
"技能": "Skills",
|
|
503
|
+
"技能(skill 工具)": "Skills (skill tool)",
|
|
504
|
+
"技能账本:窗口内共 {0} 次 skill 调用;注册表 {1} 个技能中 {2} 个一次没被调用": "Skill ledger: {0} skill calls in the window; {2} of the {1} registered skills were never called",
|
|
505
|
+
"技能账本:窗口内共 {0} 次 skill 调用;注册表不可用,只列出日志里出现过的 {1} 个": "Skill ledger: {0} skill calls in the window; the registry is unavailable, so only the {1} seen in logs are listed",
|
|
506
|
+
"一次没被调用": "never called",
|
|
507
|
+
"有 {0} 次 skill 调用的参数里没有技能名,未计入上表": "{0} skill calls carried no skill name in their arguments and are not counted above",
|
|
508
|
+
"暂无技能调用": "No skill calls yet",
|
|
503
509
|
"技能 / MCP / 专家 快速调用": "Skills / MCPs / Experts quick invoke",
|
|
504
510
|
"技能 · MCP · 专家": "Skills · MCPs · Experts",
|
|
505
511
|
"技能 · MCP · 专家看板": "Skills · MCPs · Experts board",
|
|
@@ -5270,6 +5276,54 @@ const ToolTailor = ({ call, timer, report: reportProp, onReport, seedPending })
|
|
|
5270
5276
|
: null;
|
|
5271
5277
|
|
|
5272
5278
|
const tools = report && Array.isArray(report.tools) ? report.tools : [];
|
|
5279
|
+
// §6.4 技能维度:与工具表同一份账本、同一次扫描,只是把 skill 的参数里的名字拆出来。
|
|
5280
|
+
// 这一节回答的是工具表回答不了的问题——「注册了却一次没被调用」的是哪些技能。
|
|
5281
|
+
const skillRows = report && Array.isArray(report.skills) ? report.skills : [];
|
|
5282
|
+
const skillSection = (() => {
|
|
5283
|
+
// 没有报告时不留空壳;有报告但一条技能调用都没有、也没有解不出的调用时同样不占版面。
|
|
5284
|
+
if (!report) return null;
|
|
5285
|
+
const unparsed = typeof report.skillCallsUnparsed === 'number' ? report.skillCallsUnparsed : 0;
|
|
5286
|
+
if (skillRows.length === 0 && unparsed === 0) return null;
|
|
5287
|
+
const counted = typeof report.skillCalls === 'number' ? report.skillCalls : skillRows.reduce((sum, row) => sum + (row.calls || 0), 0);
|
|
5288
|
+
const summary = report.skillsRegistryAvailable === true
|
|
5289
|
+
? t('技能账本:窗口内共 {0} 次 skill 调用;注册表 {1} 个技能中 {2} 个一次没被调用', [fmtInt(counted), fmtInt(report.skillRegisteredCount), fmtInt(report.skillNeverUsedCount)])
|
|
5290
|
+
: t('技能账本:窗口内共 {0} 次 skill 调用;注册表不可用,只列出日志里出现过的 {1} 个', [fmtInt(counted), fmtInt(skillRows.length)]);
|
|
5291
|
+
return h('div', null,
|
|
5292
|
+
h('div', { className: 'dsh-mcp-tailor-note' }, h('b', null, t('技能(skill 工具)')), summary),
|
|
5293
|
+
h('div', { className: 'dsh-mcp-tailor-table' },
|
|
5294
|
+
h('div', { className: 'dsh-mcp-tailor-row head' },
|
|
5295
|
+
h('span', { className: 't-name' }, t('技能')),
|
|
5296
|
+
h('span', { className: 't-num' }, t('调用')),
|
|
5297
|
+
h('span', { className: 't-num' }, t('错误 (率)')),
|
|
5298
|
+
h('span', { className: 't-tax' }),
|
|
5299
|
+
h('span', null, t('最后使用')),
|
|
5300
|
+
h('span', { className: 't-op' }),
|
|
5301
|
+
),
|
|
5302
|
+
skillRows.length === 0
|
|
5303
|
+
? h('div', { className: 'dsh-mcp-tailor-empty' }, t('暂无技能调用'))
|
|
5304
|
+
: skillRows.map((skill) => {
|
|
5305
|
+
const isDead = skill.calls === 0;
|
|
5306
|
+
const errRate = skill.errorRate || 0;
|
|
5307
|
+
return h('div', { key: skill.name, className: 'dsh-mcp-tailor-row' + (isDead ? ' dead' : '') },
|
|
5308
|
+
h('span', { className: 't-name', title: skill.name },
|
|
5309
|
+
skill.name,
|
|
5310
|
+
isDead ? h('span', { className: 't-badge' }, t('一次没被调用')) : null,
|
|
5311
|
+
errRate >= 0.5 ? h('span', { className: 't-badge err' }, t('高错误率')) : null,
|
|
5312
|
+
),
|
|
5313
|
+
h('span', { className: 't-num' }, tailorNum(skill.calls, report)),
|
|
5314
|
+
h('span', { className: 't-num' + (errRate >= 0.5 ? ' bad' : '') },
|
|
5315
|
+
skill.errors > 0 ? fmtInt(skill.errors) + ' (' + fmtPct(errRate) + ')' : fmtInt(skill.errors)),
|
|
5316
|
+
h('span', { className: 't-tax' }),
|
|
5317
|
+
h('span', { className: 't-last' }, fmtLast(skill.lastUsedMs, report.generatedAt)),
|
|
5318
|
+
h('span', { className: 't-op' }),
|
|
5319
|
+
);
|
|
5320
|
+
}),
|
|
5321
|
+
),
|
|
5322
|
+
unparsed > 0
|
|
5323
|
+
? h('div', { className: 'dsh-mcp-tailor-note' }, t('有 {0} 次 skill 调用的参数里没有技能名,未计入上表', [fmtInt(unparsed)]))
|
|
5324
|
+
: null,
|
|
5325
|
+
);
|
|
5326
|
+
})();
|
|
5273
5327
|
const shown = tools
|
|
5274
5328
|
.filter((tool) => (onlyDead ? tool.calls === 0 : true))
|
|
5275
5329
|
.sort((a, b) => (b.calls - a.calls) || ((b.schemaTokens || 0) - (a.schemaTokens || 0)) || String(a.name).localeCompare(String(b.name)));
|
|
@@ -5472,6 +5526,9 @@ const ToolTailor = ({ call, timer, report: reportProp, onReport, seedPending })
|
|
|
5472
5526
|
);
|
|
5473
5527
|
}),
|
|
5474
5528
|
),
|
|
5529
|
+
// §6.4 技能账本。放在工具表**下方**而不是另一页:读者是同一个问题——「谁在干活、谁在占位」
|
|
5530
|
+
// 的第二个切面。零调用的技能行沿用工具表 `.dead` 的底色约定,读法不必重新学一遍。
|
|
5531
|
+
skillSection,
|
|
5475
5532
|
deadMcpActionable.length
|
|
5476
5533
|
? h('div', { className: 'dsh-mcp-tailor-note' },
|
|
5477
5534
|
h('b', null, t('裁缝建议:')),
|
package/lib/index.js
CHANGED
|
@@ -962,7 +962,64 @@ async function buildToolTailorReport(ctx, payload) {
|
|
|
962
962
|
}
|
|
963
963
|
}
|
|
964
964
|
const schemas = [...ctx.tools.schemas()].map(projectToolSchema);
|
|
965
|
-
|
|
965
|
+
const report = await scanSessionLedger({ days, mcpServerNames, schemas, disabledServers });
|
|
966
|
+
return attachRegisteredSkills(ctx, report);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* §6.4 技能账本的第二半:把「注册表里存在」与「日志里真的被调用过」合起来。
|
|
971
|
+
*
|
|
972
|
+
* 只看日志能回答「谁被用了」,但回答不了最要紧的那一问——**注册了却一次没被用过的是哪些**。
|
|
973
|
+
* 那一问的答案正是「该不该留」的依据(本机实测 32 个技能里 21 个一次没调用过),而这批名字
|
|
974
|
+
* 只能来自 `ctx.skills` 注册表。scope 解析复用看板那条 `withSkillScope`(默认预设代言),
|
|
975
|
+
* 免得这里另写一套、漏掉预设层的发现范围。
|
|
976
|
+
*
|
|
977
|
+
* 注册表缺席时(老宿主 / 预设层没挂 skill-filesystem)**不伪造 0**:把
|
|
978
|
+
* `skillsRegistryAvailable` 置 false、`skillNeverUsedCount` 置 null,界面据此只说
|
|
979
|
+
* 「日志里出现过的技能」,而不是暗示「其余技能都没被用过」——后者会是一句假话。
|
|
980
|
+
*/
|
|
981
|
+
async function attachRegisteredSkills(ctx, report) {
|
|
982
|
+
const observed = Array.isArray(report?.skills) ? report.skills : [];
|
|
983
|
+
let registered = [];
|
|
984
|
+
try {
|
|
985
|
+
const listed = await withSkillScope(ctx, {}, async (registry, options) => {
|
|
986
|
+
if (typeof registry.list !== "function") return undefined;
|
|
987
|
+
return registry.list(options);
|
|
988
|
+
});
|
|
989
|
+
if (Array.isArray(listed)) {
|
|
990
|
+
registered = listed.filter((skill) => typeof skill?.name === "string" && skill.name !== "");
|
|
991
|
+
}
|
|
992
|
+
} catch {
|
|
993
|
+
// 注册表读不了不挡工具账本:技能那一节退化为「只有日志里出现过的」。
|
|
994
|
+
}
|
|
995
|
+
const available = registered.length > 0;
|
|
996
|
+
const registeredNames = new Set(registered.map((skill) => skill.name));
|
|
997
|
+
const observedNames = new Set(observed.map((skill) => skill.name));
|
|
998
|
+
const merged = available
|
|
999
|
+
? [
|
|
1000
|
+
...observed.map((skill) => ({ ...skill, registered: registeredNames.has(skill.name) })),
|
|
1001
|
+
...registered
|
|
1002
|
+
.filter((skill) => !observedNames.has(skill.name))
|
|
1003
|
+
.map((skill) => ({
|
|
1004
|
+
name: skill.name,
|
|
1005
|
+
calls: 0,
|
|
1006
|
+
errors: 0,
|
|
1007
|
+
errorRate: 0,
|
|
1008
|
+
firstUsedMs: null,
|
|
1009
|
+
lastUsedMs: null,
|
|
1010
|
+
registered: true,
|
|
1011
|
+
})),
|
|
1012
|
+
].sort((a, b) => (b.calls - a.calls) || String(a.name).localeCompare(String(b.name)))
|
|
1013
|
+
: observed.map((skill) => ({ ...skill, registered: null }));
|
|
1014
|
+
return {
|
|
1015
|
+
...report,
|
|
1016
|
+
skills: merged,
|
|
1017
|
+
skillsRegistryAvailable: available,
|
|
1018
|
+
skillRegisteredCount: available ? registered.length : null,
|
|
1019
|
+
// 「一次没被调用」只在注册表可用时才有意义(否则分母本身是未知的)。
|
|
1020
|
+
skillNeverUsedCount: available ? merged.filter((skill) => skill.calls === 0).length : null,
|
|
1021
|
+
skillCount: merged.length,
|
|
1022
|
+
};
|
|
966
1023
|
}
|
|
967
1024
|
|
|
968
1025
|
// 全局 MCP 列表核心(list / getWorkspaceView 共用)。
|
package/lib/mcp-oauth.js
CHANGED
|
@@ -29,7 +29,7 @@ import { homedir } from "node:os";
|
|
|
29
29
|
import { join } from "node:path";
|
|
30
30
|
|
|
31
31
|
/** 插件版本:DCR 的 clientInfo 与 UA 共用一处,避免两边各自漂移。 */
|
|
32
|
-
const PLUGIN_VERSION = "2.
|
|
32
|
+
const PLUGIN_VERSION = "2.3.0";
|
|
33
33
|
const USER_AGENT = `dsh-armory/${PLUGIN_VERSION} (+mcp-oauth)`;
|
|
34
34
|
const NET_TIMEOUT_MS = 15_000;
|
|
35
35
|
/** 授权流程整体存活时间:超过就判过期,避免会话表无限增长。 */
|
package/lib/tool-ledger.js
CHANGED
|
@@ -29,6 +29,8 @@ import { setImmediate } from "node:timers";
|
|
|
29
29
|
|
|
30
30
|
const SESSIONS_DIR_SEGMENTS = [".dsh", "sessions"];
|
|
31
31
|
const MCP_PREFIX = "mcp__";
|
|
32
|
+
/** §6.4 宿主里「参数带名字」的内置工具:技能调用都从这里走。 */
|
|
33
|
+
const SKILL_TOOL_NAME = "skill";
|
|
32
34
|
/** 每处理这么多文件让出一次事件循环,避免大库扫描把宿主卡住。 */
|
|
33
35
|
const YIELD_EVERY = 16;
|
|
34
36
|
|
|
@@ -60,6 +62,14 @@ const TEXT_CACHE_MAX_ENTRIES = 32;
|
|
|
60
62
|
*/
|
|
61
63
|
const WIRE_TAX_MAX_KEYS = 64;
|
|
62
64
|
|
|
65
|
+
/**
|
|
66
|
+
* §6.4 `callId → 技能名` 的映射上限。它只在「等结果回来把错误算到技能头上」这一件事上用,
|
|
67
|
+
* 而工具账本本来就为每个工具记着 `seenCallIds`,这里只是同一件事的技能版本。上限的作用与
|
|
68
|
+
* WIRE_TAX_MAX_KEYS 同理:日志长度不可控时不能让它无界增长;超出后停止登记新映射,
|
|
69
|
+
* 已登记的照常配对,未配对的错误数最多偏少(不会偏多)。
|
|
70
|
+
*/
|
|
71
|
+
const SKILL_CALL_MAP_MAX = 20_000;
|
|
72
|
+
|
|
63
73
|
|
|
64
74
|
/** 把字节数格式化成 MB 文案,只用于诊断信息。 */
|
|
65
75
|
function formatMB(bytes) {
|
|
@@ -235,6 +245,33 @@ function emptyToolRecord() {
|
|
|
235
245
|
return { calls: 0, results: 0, errors: 0, seenCallIds: new Set(), firstUsedMs: null, lastUsedMs: null };
|
|
236
246
|
}
|
|
237
247
|
|
|
248
|
+
/** §6.4 技能维度的调用账目:`skill` 工具每调一次,参数里就写着一个技能名。 */
|
|
249
|
+
function emptySkillRecord() {
|
|
250
|
+
return { calls: 0, errors: 0, seenResultIds: new Set(), firstUsedMs: null, lastUsedMs: null };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 从 `skill` 工具的调用参数里取技能名。
|
|
255
|
+
* 参数是**字符串化的 JSON**(`{"name":"grill"}`),解不出来就不是一次可归属的调用:
|
|
256
|
+
* 这类情况必须单独计数(`skillCallsUnparsed`)而不是静默丢弃,否则界面上会表现为
|
|
257
|
+
* 「skill 有 N 次调用、但按技能的表是空的」这种自相矛盾的读数。
|
|
258
|
+
* @param {unknown} rawArguments
|
|
259
|
+
* @returns {string|null}
|
|
260
|
+
*/
|
|
261
|
+
function skillNameOf(rawArguments) {
|
|
262
|
+
if (typeof rawArguments !== "string") return null;
|
|
263
|
+
let parsed;
|
|
264
|
+
try {
|
|
265
|
+
parsed = JSON.parse(rawArguments);
|
|
266
|
+
} catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
const name = parsed?.name;
|
|
270
|
+
if (typeof name !== "string") return null;
|
|
271
|
+
const trimmed = name.trim();
|
|
272
|
+
return trimmed === "" ? null : trimmed;
|
|
273
|
+
}
|
|
274
|
+
|
|
238
275
|
/**
|
|
239
276
|
* 按前缀归属把工具名分类到 MCP 服务器。与 mcp-registry.js 的 toolInventory 同一判定:
|
|
240
277
|
* `mcp__<server>__<tool>`;服务器名自身可能含下划线,取**最长**匹配的归属,匹配到多个
|
|
@@ -295,6 +332,13 @@ export async function scanSessionLedger({ days, mcpServerNames, schemas, disable
|
|
|
295
332
|
// 桶只存「窗口内的相对天序号」(0 = 最早一天,windowDays-1 = 今天),
|
|
296
333
|
// 由界面负责渲染;返回绝对日期反而让前端多一次时区换算的机会出错。
|
|
297
334
|
const dayBuckets = new Array(windowDays).fill(0);
|
|
335
|
+
// §6.4 技能维度:`skill` 工具是宿主里唯一「参数里带名字」的内置工具,所以同一个 callId
|
|
336
|
+
// 既属于工具 `skill`,也属于某个具体技能。两本账各记各的,互不覆盖——工具表看「skill 被
|
|
337
|
+
// 调了 59 次」,技能表看「grill 12 次、剩下 20 个一次没有」,两者回答的是不同的问题。
|
|
338
|
+
const skillUsage = new Map(); // skillName -> record
|
|
339
|
+
const skillCallOwner = new Map(); // callId -> skillName(只为把错误算到技能头上)
|
|
340
|
+
let totalSkillCalls = 0;
|
|
341
|
+
let skillCallsUnparsed = 0;
|
|
298
342
|
// §6.1 真实下发税的按天采样。`dayTaxKeys` 每个日桶只存**当天最后一次**请求的指纹(数字与
|
|
299
343
|
// 短串,不持有工具数组引用 —— 365 个日桶各留一份 234 项清单会常驻几十 MB,宿主主进程里不
|
|
300
344
|
// 可接受);`wireTaxByKey` 存每个不同清单的税,只在指纹首次出现时算一次,上限 WIRE_TAX_MAX_KEYS
|
|
@@ -416,10 +460,42 @@ export async function scanSessionLedger({ days, mcpServerNames, schemas, disable
|
|
|
416
460
|
if (record.firstUsedMs === null || time < record.firstUsedMs) record.firstUsedMs = time;
|
|
417
461
|
if (record.lastUsedMs === null || time > record.lastUsedMs) record.lastUsedMs = time;
|
|
418
462
|
}
|
|
463
|
+
// §6.4 同一件事的技能版本:`skill` 的参数里带着技能名,按名字再记一笔。
|
|
464
|
+
// 解不出名字的调用单独计数,界面据此说明「有一部分调用无法归属」,
|
|
465
|
+
// 而不是让技能表的合计悄悄小于工具表里的 skill 调用数。
|
|
466
|
+
if (name === SKILL_TOOL_NAME) {
|
|
467
|
+
const skillName = skillNameOf(event.data?.arguments);
|
|
468
|
+
if (skillName === null) {
|
|
469
|
+
skillCallsUnparsed += 1;
|
|
470
|
+
} else {
|
|
471
|
+
let skillRecord = skillUsage.get(skillName);
|
|
472
|
+
if (!skillRecord) {
|
|
473
|
+
skillRecord = emptySkillRecord();
|
|
474
|
+
skillUsage.set(skillName, skillRecord);
|
|
475
|
+
}
|
|
476
|
+
skillRecord.calls += 1;
|
|
477
|
+
totalSkillCalls += 1;
|
|
478
|
+
if (callId && skillCallOwner.size < SKILL_CALL_MAP_MAX) skillCallOwner.set(callId, skillName);
|
|
479
|
+
if (time !== null) {
|
|
480
|
+
if (skillRecord.firstUsedMs === null || time < skillRecord.firstUsedMs) skillRecord.firstUsedMs = time;
|
|
481
|
+
if (skillRecord.lastUsedMs === null || time > skillRecord.lastUsedMs) skillRecord.lastUsedMs = time;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
419
485
|
} else if (event.type === "tool/result") {
|
|
420
486
|
const message = event.data?.message;
|
|
421
487
|
const callId = typeof message?.toolCallId === "string" ? message.toolCallId : null;
|
|
422
488
|
if (!callId) continue;
|
|
489
|
+
// §6.4 技能错误先归账:结果事件只带 callId,工具那本账要靠遍历
|
|
490
|
+
// usage 反查,技能这本账直接查映射,成本更低且不受「调用在窗口外」影响。
|
|
491
|
+
const skillOwner = skillCallOwner.get(callId);
|
|
492
|
+
if (skillOwner !== undefined) {
|
|
493
|
+
const skillRecord = skillUsage.get(skillOwner);
|
|
494
|
+
if (skillRecord && !skillRecord.seenResultIds.has(callId)) {
|
|
495
|
+
skillRecord.seenResultIds.add(callId);
|
|
496
|
+
if (message.isError === true) skillRecord.errors += 1;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
423
499
|
// 与 call 配对:同一 callId 的后续 result(重试/续写)不再重复计。
|
|
424
500
|
let owner = null;
|
|
425
501
|
for (const record of usage.values()) {
|
|
@@ -604,6 +680,23 @@ export async function scanSessionLedger({ days, mcpServerNames, schemas, disable
|
|
|
604
680
|
// 没有真实下发记录时为 null —— 界面必须把「不知道」和「差 0」分开呈现。
|
|
605
681
|
wireTaxDivergenceTokens: wireTaxLatest === null ? null : taxTotalTokens - wireTaxLatest.tokens,
|
|
606
682
|
tools,
|
|
683
|
+
// §6.4 技能维度账目。工具表回答「skill 这个工具被调了几次」,这里回答「哪些技能真的
|
|
684
|
+
// 被用过、各用了几次」——两者的合计**不要求相等**:参数解不出名字的调用单独进
|
|
685
|
+
// skillCallsUnparsed,让差异自己显形,而不是被悄悄吞掉。
|
|
686
|
+
// 排序与工具表同口径:调用多的在前,同调用数按名字排,保证两次扫描的先后一致。
|
|
687
|
+
skills: [...skillUsage]
|
|
688
|
+
.map(([name, record]) => ({
|
|
689
|
+
name,
|
|
690
|
+
calls: record.calls,
|
|
691
|
+
errors: record.errors,
|
|
692
|
+
errorRate: record.calls > 0 ? record.errors / record.calls : 0,
|
|
693
|
+
firstUsedMs: record.firstUsedMs,
|
|
694
|
+
lastUsedMs: record.lastUsedMs,
|
|
695
|
+
}))
|
|
696
|
+
.sort((a, b) => (b.calls - a.calls) || String(a.name).localeCompare(String(b.name))),
|
|
697
|
+
skillCalls: totalSkillCalls,
|
|
698
|
+
skillCallsUnparsed,
|
|
699
|
+
skillCount: skillUsage.size,
|
|
607
700
|
};
|
|
608
701
|
}
|
|
609
702
|
|
package/lib/whats-new-en.js
CHANGED
|
@@ -379,6 +379,16 @@ export const WHATS_NEW_EN = {
|
|
|
379
379
|
text: "Third-party content (connectors, skills, expert descriptions and community READMEs) stays in the publisher's original language - that is data, not UI copy",
|
|
380
380
|
},
|
|
381
381
|
],
|
|
382
|
+
'2.3.0': [
|
|
383
|
+
{
|
|
384
|
+
text: '**Skill ledger**: the Tool Tailor now parses the skill name from `skill` calls in the same log scan, tracks calls / errors / last use per skill, and adds zero-call rows for registered skills that were never invoked - see at a glance which skills are registered but unused',
|
|
385
|
+
where: 'Tool Tailor -> below the tool table -> "Skills (skill tool)"',
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
text: 'Calls whose arguments carry no skill name are counted separately instead of being dropped; when the registry is unavailable, the panel lists only skills seen in logs instead of faking a zero',
|
|
389
|
+
where: 'Tool Tailor -> note under the skill table',
|
|
390
|
+
},
|
|
391
|
+
],
|
|
382
392
|
};
|
|
383
393
|
|
|
384
394
|
/**
|
package/lib/whats-new.js
CHANGED
|
@@ -432,4 +432,17 @@ export const WHATS_NEW = [
|
|
|
432
432
|
},
|
|
433
433
|
],
|
|
434
434
|
},
|
|
435
|
+
{
|
|
436
|
+
version: '2.3.0',
|
|
437
|
+
items: [
|
|
438
|
+
{
|
|
439
|
+
text: '**技能账本**:工具裁缝在同一趟扫描里解析 `skill` 调用的技能名,按技能统计调用 / 错误 / 最后使用,并把注册表中零调用的技能补成 0 行——哪些技能注册了却一次没被调用,现在一眼能看到',
|
|
440
|
+
where: '工具裁缝 → 工具表下方「技能(skill 工具)」',
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
text: '参数里没有技能名的调用单独计数上屏,不静默丢弃;注册表不可用时不伪造 0,只列出日志里出现过的技能',
|
|
444
|
+
where: '工具裁缝 → 技能表下方说明',
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
},
|
|
435
448
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-armory",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Unified Skill + MCP + Expert + Plugin board for DeepSeek Harness (desktop app and Web GUI) — custom skill suites, skill install/uninstall with live refresh, global panel page, JSON import, WorkBuddy connector & SkillHub & Expert & CodeBuddy plugin & awesome-dsh-plugins market installs, MCP OAuth authorization, tool-usage tailor with wire-tax trends, and profile-backed Host Remote management.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|