dsh-armory 2.2.1 → 2.3.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/README.en.md +26 -22
- package/README.md +29 -21
- package/docs/installation.md +0 -2
- package/lib/client.js +2495 -744
- package/lib/community-meta.js +222 -0
- package/lib/expert-market.js +119 -23
- package/lib/index.js +276 -92
- package/lib/loadout.js +178 -0
- package/lib/mcp-config.js +0 -171
- package/lib/mcp-oauth.js +2 -4
- package/lib/npm-downloads.js +262 -0
- package/lib/suites-registry.js +10 -4
- package/lib/tool-ledger.js +93 -0
- package/lib/typert.js +13 -10
- package/lib/wb-awesome-market.js +706 -214
- package/lib/wb-install-state.js +10 -4
- package/lib/wb-market.js +99 -27
- package/lib/wb-registry.js +32 -31
- package/lib/whats-new-en.js +31 -0
- package/lib/whats-new.js +37 -0
- package/package.json +2 -2
- package/docs/mcp-market-design.md +0 -617
- package/lib/mcp-catalog-en.js +0 -244
- package/lib/mcp-catalog.js +0 -806
- package/lib/wb-plugin-market.js +0 -291
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/typert.js
CHANGED
|
@@ -33,7 +33,6 @@ const mv = (method, params, resultSym) => ({
|
|
|
33
33
|
const nameParam = { name: 'name', wire: 'name', source: 'json', codec: codec('String', $name) }
|
|
34
34
|
const specParam = { name: 'spec', wire: 'spec', source: 'json', codec: codec('Spec', $any) }
|
|
35
35
|
const payloadParam = { name: 'payload', wire: 'payload', source: 'json', codec: codec('ImportPayload', $any) }
|
|
36
|
-
const builtinInstallParam = { name: 'payload', wire: 'payload', source: 'json', codec: codec('BuiltinInstallPayload', $any) }
|
|
37
36
|
|
|
38
37
|
export const TYPERT = {
|
|
39
38
|
package: 'dsh-armory',
|
|
@@ -46,8 +45,6 @@ export const TYPERT = {
|
|
|
46
45
|
mv('disable', [nameParam], 'NoteResult'),
|
|
47
46
|
mv('reconnect', [nameParam], 'NoteResult'),
|
|
48
47
|
mv('tools', [payloadParam], 'ToolsResult'),
|
|
49
|
-
mv('builtins', [], 'BuiltinCatalogResult'),
|
|
50
|
-
mv('installBuiltins', [builtinInstallParam], 'BuiltinInstallResult'),
|
|
51
48
|
mv('add', [specParam], 'NoteResult'),
|
|
52
49
|
mv('update', [specParam], 'NoteResult'),
|
|
53
50
|
mv('removeServer', [nameParam], 'NoteResult'),
|
|
@@ -63,7 +60,6 @@ export const TYPERT = {
|
|
|
63
60
|
mv('updateWorkspaceServer', [payloadParam], 'NoteResult'),
|
|
64
61
|
mv('removeWorkspaceServer', [payloadParam], 'NoteResult'),
|
|
65
62
|
mv('setWorkspaceExclude', [payloadParam], 'NoteResult'),
|
|
66
|
-
mv('installWorkspaceBuiltins', [builtinInstallParam], 'BuiltinInstallResult'),
|
|
67
63
|
mv('previewWorkspaceImport', [payloadParam], 'ImportPreview'),
|
|
68
64
|
mv('importWorkspaceJson', [payloadParam], 'ImportResult'),
|
|
69
65
|
mv('revealWorkspaceServer', [payloadParam], 'RevealResult'),
|
|
@@ -92,11 +88,13 @@ export const TYPERT = {
|
|
|
92
88
|
// lib/tool-ledger.js 校验,与 wb* 系列同一宽容度。
|
|
93
89
|
mv('toolTailor', [payloadParam], 'TailorReport'),
|
|
94
90
|
// WorkBuddy 市场直连(连接器 MCP + SkillHub 技能)。payload 全部 $any:数据形状由
|
|
95
|
-
// lib/wb-market.js 校验,契约层与
|
|
91
|
+
// lib/wb-market.js 校验,契约层与 importJson 同一宽容度。
|
|
96
92
|
mv('wbConnectorCatalog', [], 'WbCatalogResult'),
|
|
97
93
|
// 顶栏「市场」chip 常驻计数:只读磁盘缓存,零网络;无缓存返回 { total: null }。
|
|
98
94
|
mv('wbConnectorCachedCount', [], 'WbCachedCountResult'),
|
|
99
95
|
mv('wbConnectorPreview', [payloadParam], 'WbPreviewResult'),
|
|
96
|
+
// 市场连接器卡片头像:payload {connectorId},出 {ok, dataUrl}。卡片进视口才调。
|
|
97
|
+
mv('wbConnectorIcon', [payloadParam], 'WbIconResult'),
|
|
100
98
|
mv('wbConnectorInstall', [payloadParam], 'NoteResult'),
|
|
101
99
|
mv('wbSkillCatalog', [payloadParam], 'WbSkillListResult'),
|
|
102
100
|
mv('wbSkillDetail', [payloadParam], 'WbSkillDetailResult'),
|
|
@@ -106,21 +104,26 @@ export const TYPERT = {
|
|
|
106
104
|
// T03/R4:多市场注册表(读 / 增改)。payload $any,形状由 lib/wb-registry.js 校验。
|
|
107
105
|
mv('wbMarketRegistry', [], 'WbMarketRegistryResult'),
|
|
108
106
|
mv('wbMarketRegistryUpsert', [payloadParam], 'WbMarketRegistryResult'),
|
|
109
|
-
// T03/R5:codebuddy 插件市场(列表 / 详情)。payload $any,形状由 lib/wb-plugin-market.js 校验。
|
|
110
|
-
mv('wbPluginCatalog', [], 'WbPluginCatalogResult'),
|
|
111
|
-
mv('wbPluginDetail', [payloadParam], 'WbPluginDetailResult'),
|
|
112
107
|
// T04/R8:安装状态登记与查询(宿主侧落盘,客户端「已安装」筛选的数据源)。
|
|
113
108
|
mv('wbInstallState', [], 'InstallStateResult'),
|
|
114
109
|
mv('wbInstallStateRecord', [payloadParam], 'NoteResult'),
|
|
115
110
|
mv('wbInstallStateForget', [payloadParam], 'NoteResult'),
|
|
116
|
-
// T05/R9:插件市场更新探测(只做条件请求,不落新包)。
|
|
117
|
-
mv('wbPluginUpdateCheck', [], 'WbUpdateHintResult'),
|
|
118
111
|
// T06/Awesome:DSH 社区插件市场(awesome-dsh-plugins,数据层 lib/wb-awesome-market.js)。
|
|
119
112
|
mv('wbAwesomeCatalog', [payloadParam], 'AwesomeCatalogResult'),
|
|
120
113
|
mv('wbAwesomeDetail', [payloadParam], 'AwesomeDetailResult'),
|
|
121
114
|
// README 预览:点击卡片按需取一次(npm registry / GitHub raw),落盘缓存 7 天。
|
|
122
115
|
mv('wbAwesomeReadme', [payloadParam], 'AwesomeReadmeResult'),
|
|
123
116
|
mv('wbAwesomeUpdateCheck', [], 'WbUpdateHintResult'),
|
|
117
|
+
// 社区元信息:打开详情时按需查 GitHub 仓库(维护者 / 最近提交 / issue / star),
|
|
118
|
+
// 6 小时缓存。数据层 lib/community-meta.js,payload $any({repos, refresh})。
|
|
119
|
+
mv('wbRepoMeta', [payloadParam], 'WbRepoMetaResult'),
|
|
120
|
+
// npm 近 30 天下载量:插件卡片底栏的「↓N」。目录里没有这个字段,只能去 npm 统计接口取,
|
|
121
|
+
// 7 天缓存 + 按可见卡片懒加载。数据层 lib/npm-downloads.js,payload $any({packages, refresh})。
|
|
122
|
+
mv('wbNpmDownloads', [payloadParam], 'WbNpmDownloadsResult'),
|
|
123
|
+
// 装备清单:导出本机全局配置 / 与贴入的清单做差异。payload $any,形状由
|
|
124
|
+
// lib/loadout.js 校验;只读,不安装、不写盘、不接收任何凭据。
|
|
125
|
+
mv('wbLoadoutExport', [payloadParam], 'LoadoutResult'),
|
|
126
|
+
mv('wbLoadoutDiff', [payloadParam], 'LoadoutDiffResult'),
|
|
124
127
|
// WorkBuddy 专家直连(召唤预设 + 捆绑技能)。payload 全部 $any:数据形状由
|
|
125
128
|
// lib/expert-market.js 校验,与 wb* 系列同一宽容度。
|
|
126
129
|
mv('wbExpertCatalog', [], 'WbExpertCatalogResult'),
|