dsh-armory 2.3.0 → 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 +21 -15
- package/README.md +24 -14
- package/docs/installation.md +0 -2
- package/lib/client.js +2323 -629
- package/lib/community-meta.js +222 -0
- package/lib/expert-market.js +119 -23
- package/lib/index.js +218 -91
- 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/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 +21 -0
- package/lib/whats-new.js +24 -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/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { interpolate } from "@deepseek-ai/cordis-plugin-loader";
|
|
|
7
7
|
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
8
8
|
import { basename, dirname, join } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import {
|
|
10
|
+
import { normalizeMcpImport, readManagedMcpServers, setManagedMcpDisabled, updateManagedMcpPatch } from "./mcp-config.js";
|
|
11
11
|
import { admitImportedServers } from "./import-admission.js";
|
|
12
12
|
import { collectImportLocations, missingEnvNames, resolveImportLocation } from "./mcp-import-sources.js";
|
|
13
13
|
import { deriveMcpPhase, formatMcpLog, sanitizeMcpLog } from "./mcp-observability.js";
|
|
@@ -16,12 +16,17 @@ import { allMcpEntries, entriesForName, projectToolSchema, revisionOf, toolInven
|
|
|
16
16
|
import { removeSkillFromDisk } from "./skill-remove.js";
|
|
17
17
|
import { WHATS_NEW } from "./whats-new.js";
|
|
18
18
|
import { attachWhatsNewEnglish } from "./whats-new-en.js";
|
|
19
|
-
import { probeMcpServersAuthorization, wbConnectorCatalog, wbConnectorCachedCount, wbConnectorPreview, wbSkillCatalog, wbSkillDetail, wbSkillInstall, wbSkillInstalled, wbSkillUninstall } from "./wb-market.js";
|
|
20
|
-
// T03
|
|
19
|
+
import { probeMcpServersAuthorization, wbConnectorCatalog, wbConnectorCachedCount, wbConnectorIcon, wbConnectorPreview, wbSkillCatalog, wbSkillDetail, wbSkillInstall, wbSkillInstalled, wbSkillUninstall } from "./wb-market.js";
|
|
20
|
+
// T03/R4:多市场注册表(连接器源)的宿主侧数据层。
|
|
21
21
|
import { readRegistry, upsertMarketplace } from "./wb-registry.js";
|
|
22
|
-
import { wbPluginCatalog, wbPluginDetail, wbPluginUpdateCheck } from "./wb-plugin-market.js";
|
|
23
22
|
// T06/Awesome:DSH 社区插件市场(awesome-dsh-plugins)的宿主侧数据层。
|
|
24
23
|
import { wbAwesomeCatalog, wbAwesomeDetail, wbAwesomeReadme, wbAwesomeUpdateCheck } from "./wb-awesome-market.js";
|
|
24
|
+
// 社区元信息(维护者 / 最近提交 / issue / star):打开详情时按需查,6 小时缓存。
|
|
25
|
+
import { wbRepoMeta } from "./community-meta.js";
|
|
26
|
+
// npm 近 30 天下载量:插件卡片底栏的「↓N」,目录里没有这个字段,只能去 npm 统计接口取。
|
|
27
|
+
import { wbNpmDownloads } from "./npm-downloads.js";
|
|
28
|
+
// 装备清单:只读汇总本机已装条目,校验与差异计算保持纯函数(lib/loadout.js)。
|
|
29
|
+
import { buildLoadout, diffLoadout } from "./loadout.js";
|
|
25
30
|
import { readState as readInstallState, record as recordInstallState, forget as forgetInstallState } from "./wb-install-state.js";
|
|
26
31
|
// 自定义套件(Skill 看板):读 / 整集合覆盖保存,数据层 lib/suites-registry.js。
|
|
27
32
|
import { readSuites as readSkillSuites, saveSuites as writeSkillSuites } from "./suites-registry.js";
|
|
@@ -36,7 +41,7 @@ import {
|
|
|
36
41
|
saveManualToken as oauthSaveManualToken,
|
|
37
42
|
submitCallback as oauthSubmitCallback,
|
|
38
43
|
} from "./mcp-oauth.js";
|
|
39
|
-
import { hasTeamBridge, installExpertSkills, listExpertRecent, listExpertSkills, prepareExpertPersona, readExpertPresets, recordExpertRecent, removeExpertPreset, uninstallExpertSkills, uninstallTeamMemberPersonas, wbExpertCatalog, wbExpertDetail, writeExpertPreset } from "./expert-market.js";
|
|
44
|
+
import { hasTeamBridge, installExpertSkills, listExpertRecent, listExpertSkills, prepareExpertPersona, readExpertDisplayNames, readExpertPresets, recordExpertRecent, removeExpertPreset, uninstallExpertSkills, uninstallTeamMemberPersonas, wbExpertCatalog, wbExpertDetail, writeExpertPreset } from "./expert-market.js";
|
|
40
45
|
import { attachPersona, detachPersona, listPersonas, reconcilePersonas } from "./session-persona.js";
|
|
41
46
|
// clearTextCache 的 import 已随 toolTailor 的「清空缓存」动作一起删掉(见下方 toolTailor 注释)。
|
|
42
47
|
import { scanSessionLedger } from "./tool-ledger.js";
|
|
@@ -381,12 +386,6 @@ function externalMcpEntries(ctx, state) {
|
|
|
381
386
|
return !managedIds.has(entry.options.id) && !(!entry.options.group && managedNames.has(entry.options.config.serverName));
|
|
382
387
|
});
|
|
383
388
|
}
|
|
384
|
-
function effectiveMcpServers(ctx, state) {
|
|
385
|
-
return [
|
|
386
|
-
...state.servers,
|
|
387
|
-
...externalMcpEntries(ctx, state).map((entry) => ({ ...entry.options.config, name: entry.options.config.serverName })),
|
|
388
|
-
];
|
|
389
|
-
}
|
|
390
389
|
function revealRevision(state, groups) {
|
|
391
390
|
const values = [];
|
|
392
391
|
for (const current of state.servers) {
|
|
@@ -589,7 +588,6 @@ function summarize(entry, inventory, managed, conflict = false, lastLog = null)
|
|
|
589
588
|
// 已经是不可用的终态证据),而不是去猜 mcp-client 的日志文案;日志只用来填 lastError 的
|
|
590
589
|
// 具体原因。挂载失败(我们自己 setup 阶段就挂不上:配置求值为空、工具注册被拒)与连接
|
|
591
590
|
// 失败(mcp-client 那边的事)分开标记,不混为一谈。
|
|
592
|
-
// 已导出作为测试接缝(同 checkForUpdate)。
|
|
593
591
|
function summarizeWorkspaceRow(ctx, wsPath, server, mountError) {
|
|
594
592
|
// 诊断通路就在这里被依赖,就在这里确保开着(按 root 幂等,与全局 list/status 同一份)。
|
|
595
593
|
ensureLogCapture(ctx);
|
|
@@ -1048,10 +1046,148 @@ async function listMcpServers(ctx) {
|
|
|
1048
1046
|
};
|
|
1049
1047
|
}
|
|
1050
1048
|
|
|
1049
|
+
// ── 装备清单:本机条目收集(只读,只投影身份字段)────────────────────────
|
|
1050
|
+
// 导出与导入对比共用这一份收集逻辑,避免两个入口对「本机装了什么」给出不同答案。
|
|
1051
|
+
// 绝不把 env / headers 的值带出去:MCP 只保留键名,技能只保留名称与来源,
|
|
1052
|
+
// 专家从 profile 补丁的召唤预设读 plugin、从目录缓存补展示名,插件读安装账本。
|
|
1053
|
+
function endpointHostOf(value) {
|
|
1054
|
+
if (typeof value !== "string" || !value) return "";
|
|
1055
|
+
try {
|
|
1056
|
+
return new URL(value).host || "";
|
|
1057
|
+
} catch {
|
|
1058
|
+
return "";
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
async function collectLoadoutItems(ctx) {
|
|
1063
|
+
const items = [];
|
|
1064
|
+
let records = [];
|
|
1065
|
+
try {
|
|
1066
|
+
const state = await readInstallState();
|
|
1067
|
+
records = Array.isArray(state?.items) ? state.items : [];
|
|
1068
|
+
} catch {
|
|
1069
|
+
// 账本读不到仍然可以导出 MCP / 技能 / 专家;插件条目会缺,但不该整单失败。
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// 1) MCP:以真实注册表为准;连接器账本用于补来源,名称对不上时保留账本条目。
|
|
1073
|
+
try {
|
|
1074
|
+
const { servers } = await listMcpServers(ctx);
|
|
1075
|
+
const connectorByName = new Map();
|
|
1076
|
+
for (const record of records) {
|
|
1077
|
+
if (record?.kind !== "connector") continue;
|
|
1078
|
+
const key = String(record.key || "").trim();
|
|
1079
|
+
if (key) connectorByName.set(key, record);
|
|
1080
|
+
const rowName = String(record.name || "").trim();
|
|
1081
|
+
if (rowName) connectorByName.set(rowName, record);
|
|
1082
|
+
}
|
|
1083
|
+
const seen = new Set();
|
|
1084
|
+
for (const server of servers) {
|
|
1085
|
+
const name = String(server?.serverName || "").trim();
|
|
1086
|
+
if (!name) continue;
|
|
1087
|
+
seen.add(name);
|
|
1088
|
+
const connector = connectorByName.get(name);
|
|
1089
|
+
const configKeys = [
|
|
1090
|
+
...Object.keys(server.env || {}),
|
|
1091
|
+
...Object.keys(server.headers || {}),
|
|
1092
|
+
].filter(Boolean);
|
|
1093
|
+
const endpointHost = endpointHostOf(server.url);
|
|
1094
|
+
items.push({
|
|
1095
|
+
kind: "mcp",
|
|
1096
|
+
name,
|
|
1097
|
+
...(server.transport ? { transport: server.transport } : {}),
|
|
1098
|
+
source: connector ? `connector:${connector.key}` : "manual",
|
|
1099
|
+
...(endpointHost ? { endpointHost } : {}),
|
|
1100
|
+
...(configKeys.length ? { configKeys: [...new Set(configKeys)] } : {}),
|
|
1101
|
+
...(typeof server.enabled === "boolean" ? { enabled: server.enabled } : {}),
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
for (const record of records) {
|
|
1105
|
+
if (record?.kind !== "connector") continue;
|
|
1106
|
+
const key = String(record.key || "").trim();
|
|
1107
|
+
const name = String(record.name || key).trim();
|
|
1108
|
+
if (!name || seen.has(name) || (key && seen.has(key))) continue;
|
|
1109
|
+
seen.add(name);
|
|
1110
|
+
items.push({ kind: "mcp", name, source: `connector:${key || name}` });
|
|
1111
|
+
}
|
|
1112
|
+
} catch {
|
|
1113
|
+
// MCP 注册表不可用时跳过这一组,其余组照常导出。
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// 2) 技能:宿主 skill 注册表的全局视图;账本用于补市场来源与版本。
|
|
1117
|
+
try {
|
|
1118
|
+
const ledger = new Map();
|
|
1119
|
+
for (const record of records) {
|
|
1120
|
+
if (record?.kind === "skill") ledger.set(String(record.key || ""), record);
|
|
1121
|
+
}
|
|
1122
|
+
await withSkillScope(ctx, { scope: "global" }, async (registry, options) => {
|
|
1123
|
+
if (typeof registry.list !== "function") return;
|
|
1124
|
+
const rows = await registry.list(options);
|
|
1125
|
+
for (const skill of rows) {
|
|
1126
|
+
const name = String(skill?.name || "").trim();
|
|
1127
|
+
if (!name) continue;
|
|
1128
|
+
const record = ledger.get(name);
|
|
1129
|
+
items.push({
|
|
1130
|
+
kind: "skill",
|
|
1131
|
+
name,
|
|
1132
|
+
source: String(record?.source?.marketplace || skill.source || "local"),
|
|
1133
|
+
...(record?.version ? { version: String(record.version) } : {}),
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
} catch {
|
|
1138
|
+
// 宿主没挂 skill 注册表时跳过技能组。
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// 3) 专家:profile 补丁里的召唤预设 + 目录缓存补展示名 / 更新时间。
|
|
1142
|
+
try {
|
|
1143
|
+
const patch = await readPatch(ctx);
|
|
1144
|
+
const presets = readExpertPresets(patch.content);
|
|
1145
|
+
let experts = [];
|
|
1146
|
+
try {
|
|
1147
|
+
const catalog = await wbExpertCatalog();
|
|
1148
|
+
experts = Array.isArray(catalog?.experts) ? catalog.experts : [];
|
|
1149
|
+
} catch {
|
|
1150
|
+
// 目录拉不到就退回 plugin id 作名称。
|
|
1151
|
+
}
|
|
1152
|
+
const byPlugin = new Map(experts.map((expert) => [String(expert.plugin || ""), expert]));
|
|
1153
|
+
for (const preset of presets) {
|
|
1154
|
+
const plugin = String(preset?.plugin || "").trim();
|
|
1155
|
+
if (!plugin) continue;
|
|
1156
|
+
const expert = byPlugin.get(plugin);
|
|
1157
|
+
items.push({
|
|
1158
|
+
kind: "expert",
|
|
1159
|
+
name: String(expert?.name || plugin),
|
|
1160
|
+
plugin,
|
|
1161
|
+
source: "workbuddy-expert",
|
|
1162
|
+
...(expert?.updatedAt ? { updatedAt: String(expert.updatedAt) } : {}),
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
} catch {
|
|
1166
|
+
// 没有 profile 补丁(或读不到)时跳过专家组。
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// 4) 插件:只有客户端在安装成功后回写的账本知道装了什么。
|
|
1170
|
+
for (const record of records) {
|
|
1171
|
+
if (record?.kind !== "plugin") continue;
|
|
1172
|
+
const key = String(record.key || "").trim();
|
|
1173
|
+
const name = String(record.name || key).trim();
|
|
1174
|
+
if (!name) continue;
|
|
1175
|
+
items.push({
|
|
1176
|
+
kind: "plugin",
|
|
1177
|
+
name,
|
|
1178
|
+
...(key ? { key } : {}),
|
|
1179
|
+
source: String(record.source?.marketplace || ""),
|
|
1180
|
+
...(record.version ? { version: String(record.version) } : {}),
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
return items;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1051
1187
|
let McpManagerGateway = (() => {
|
|
1052
1188
|
let _classSuper = TypertRemoteService;
|
|
1053
1189
|
let _instanceExtraInitializers = [];
|
|
1054
|
-
let _list_decorators, _status_decorators, _enable_decorators, _disable_decorators, _reconnect_decorators, _tools_decorators,
|
|
1190
|
+
let _list_decorators, _status_decorators, _enable_decorators, _disable_decorators, _reconnect_decorators, _tools_decorators, _add_decorators, _update_decorators, _remove_decorators, _reveal_decorators, _previewImport_decorators, _importJson_decorators, _listWorkspaces_decorators, _getWorkspaceView_decorators, _addWorkspaceServer_decorators, _updateWorkspaceServer_decorators, _removeWorkspaceServer_decorators, _setWorkspaceExclude_decorators, _previewWorkspaceImport_decorators, _importWorkspaceJson_decorators, _revealWorkspaceServer_decorators, _projectConnections_decorators, _reconnectWorkspaceServer_decorators, _updateHint_decorators, _whatsNew_decorators, _scanImportSources_decorators, _previewImportSource_decorators, _importSource_decorators, _toolTailor_decorators, _wbConnectorCatalog_decorators, _wbConnectorCachedCount_decorators, _wbConnectorIcon_decorators, _wbConnectorPreview_decorators, _wbConnectorInstall_decorators, _wbOAuthBegin_decorators, _wbOAuthPoll_decorators, _wbOAuthCancel_decorators, _wbOAuthToken_decorators, _wbOAuthOverview_decorators, _wbOAuthOpen_decorators, _wbSkillCatalog_decorators, _wbSkillDetail_decorators, _wbSkillInstall_decorators, _wbSkillUninstall_decorators, _wbSkillInstalled_decorators, _wbMarketRegistry_decorators, _wbMarketRegistryUpsert_decorators, _wbInstallState_decorators, _wbInstallStateRecord_decorators, _wbInstallStateForget_decorators, _wbAwesomeCatalog_decorators, _wbAwesomeDetail_decorators, _wbAwesomeReadme_decorators, _wbAwesomeUpdateCheck_decorators, _wbRepoMeta_decorators, _wbNpmDownloads_decorators, _wbLoadoutExport_decorators, _wbLoadoutDiff_decorators;
|
|
1055
1191
|
return class McpManagerGateway extends _classSuper {
|
|
1056
1192
|
static {
|
|
1057
1193
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
@@ -1067,10 +1203,6 @@ let McpManagerGateway = (() => {
|
|
|
1067
1203
|
__esDecorate(this, null, _reconnect_decorators, { kind: "method", name: "reconnect", static: false, private: false, access: { has: (obj) => "reconnect" in obj, get: (obj) => obj.reconnect }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1068
1204
|
_tools_decorators = [Remote("tools")];
|
|
1069
1205
|
__esDecorate(this, null, _tools_decorators, { kind: "method", name: "tools", static: false, private: false, access: { has: (obj) => "tools" in obj, get: (obj) => obj.tools }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1070
|
-
_builtins_decorators = [Remote("builtins")];
|
|
1071
|
-
__esDecorate(this, null, _builtins_decorators, { kind: "method", name: "builtins", static: false, private: false, access: { has: (obj) => "builtins" in obj, get: (obj) => obj.builtins }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1072
|
-
_installBuiltins_decorators = [Remote("installBuiltins")];
|
|
1073
|
-
__esDecorate(this, null, _installBuiltins_decorators, { kind: "method", name: "installBuiltins", static: false, private: false, access: { has: (obj) => "installBuiltins" in obj, get: (obj) => obj.installBuiltins }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1074
1206
|
_add_decorators = [Remote("add")];
|
|
1075
1207
|
__esDecorate(this, null, _add_decorators, { kind: "method", name: "add", static: false, private: false, access: { has: (obj) => "add" in obj, get: (obj) => obj.add }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1076
1208
|
_update_decorators = [Remote("update")];
|
|
@@ -1095,8 +1227,6 @@ let McpManagerGateway = (() => {
|
|
|
1095
1227
|
__esDecorate(this, null, _removeWorkspaceServer_decorators, { kind: "method", name: "removeWorkspaceServer", static: false, private: false, access: { has: (obj) => "removeWorkspaceServer" in obj, get: (obj) => obj.removeWorkspaceServer }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1096
1228
|
_setWorkspaceExclude_decorators = [Remote("setWorkspaceExclude")];
|
|
1097
1229
|
__esDecorate(this, null, _setWorkspaceExclude_decorators, { kind: "method", name: "setWorkspaceExclude", static: false, private: false, access: { has: (obj) => "setWorkspaceExclude" in obj, get: (obj) => obj.setWorkspaceExclude }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1098
|
-
_installWorkspaceBuiltins_decorators = [Remote("installWorkspaceBuiltins")];
|
|
1099
|
-
__esDecorate(this, null, _installWorkspaceBuiltins_decorators, { kind: "method", name: "installWorkspaceBuiltins", static: false, private: false, access: { has: (obj) => "installWorkspaceBuiltins" in obj, get: (obj) => obj.installWorkspaceBuiltins }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1100
1230
|
_previewWorkspaceImport_decorators = [Remote("previewWorkspaceImport")];
|
|
1101
1231
|
__esDecorate(this, null, _previewWorkspaceImport_decorators, { kind: "method", name: "previewWorkspaceImport", static: false, private: false, access: { has: (obj) => "previewWorkspaceImport" in obj, get: (obj) => obj.previewWorkspaceImport }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1102
1232
|
_importWorkspaceJson_decorators = [Remote("importWorkspaceJson")];
|
|
@@ -1123,6 +1253,8 @@ let McpManagerGateway = (() => {
|
|
|
1123
1253
|
__esDecorate(this, null, _wbConnectorCatalog_decorators, { kind: "method", name: "wbConnectorCatalog", static: false, private: false, access: { has: (obj) => "wbConnectorCatalog" in obj, get: (obj) => obj.wbConnectorCatalog }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1124
1254
|
_wbConnectorCachedCount_decorators = [Remote("wbConnectorCachedCount")];
|
|
1125
1255
|
__esDecorate(this, null, _wbConnectorCachedCount_decorators, { kind: "method", name: "wbConnectorCachedCount", static: false, private: false, access: { has: (obj) => "wbConnectorCachedCount" in obj, get: (obj) => obj.wbConnectorCachedCount }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1256
|
+
_wbConnectorIcon_decorators = [Remote("wbConnectorIcon")];
|
|
1257
|
+
__esDecorate(this, null, _wbConnectorIcon_decorators, { kind: "method", name: "wbConnectorIcon", static: false, private: false, access: { has: (obj) => "wbConnectorIcon" in obj, get: (obj) => obj.wbConnectorIcon }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1126
1258
|
_wbConnectorPreview_decorators = [Remote("wbConnectorPreview")];
|
|
1127
1259
|
__esDecorate(this, null, _wbConnectorPreview_decorators, { kind: "method", name: "wbConnectorPreview", static: false, private: false, access: { has: (obj) => "wbConnectorPreview" in obj, get: (obj) => obj.wbConnectorPreview }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1128
1260
|
_wbConnectorInstall_decorators = [Remote("wbConnectorInstall")];
|
|
@@ -1154,11 +1286,6 @@ let McpManagerGateway = (() => {
|
|
|
1154
1286
|
__esDecorate(this, null, _wbMarketRegistry_decorators, { kind: "method", name: "wbMarketRegistry", static: false, private: false, access: { has: (obj) => "wbMarketRegistry" in obj, get: (obj) => obj.wbMarketRegistry }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1155
1287
|
_wbMarketRegistryUpsert_decorators = [Remote("wbMarketRegistryUpsert")];
|
|
1156
1288
|
__esDecorate(this, null, _wbMarketRegistryUpsert_decorators, { kind: "method", name: "wbMarketRegistryUpsert", static: false, private: false, access: { has: (obj) => "wbMarketRegistryUpsert" in obj, get: (obj) => obj.wbMarketRegistryUpsert }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1157
|
-
// T03/R5:codebuddy 插件市场(列表 / 详情)。
|
|
1158
|
-
_wbPluginCatalog_decorators = [Remote("wbPluginCatalog")];
|
|
1159
|
-
__esDecorate(this, null, _wbPluginCatalog_decorators, { kind: "method", name: "wbPluginCatalog", static: false, private: false, access: { has: (obj) => "wbPluginCatalog" in obj, get: (obj) => obj.wbPluginCatalog }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1160
|
-
_wbPluginDetail_decorators = [Remote("wbPluginDetail")];
|
|
1161
|
-
__esDecorate(this, null, _wbPluginDetail_decorators, { kind: "method", name: "wbPluginDetail", static: false, private: false, access: { has: (obj) => "wbPluginDetail" in obj, get: (obj) => obj.wbPluginDetail }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1162
1289
|
// T04/R8:安装状态登记(connector/skill/plugin 三类安装的统一账本)。
|
|
1163
1290
|
_wbInstallState_decorators = [Remote("wbInstallState")];
|
|
1164
1291
|
__esDecorate(this, null, _wbInstallState_decorators, { kind: "method", name: "wbInstallState", static: false, private: false, access: { has: (obj) => "wbInstallState" in obj, get: (obj) => obj.wbInstallState }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
@@ -1166,8 +1293,6 @@ let McpManagerGateway = (() => {
|
|
|
1166
1293
|
__esDecorate(this, null, _wbInstallStateRecord_decorators, { kind: "method", name: "wbInstallStateRecord", static: false, private: false, access: { has: (obj) => "wbInstallStateRecord" in obj, get: (obj) => obj.wbInstallStateRecord }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1167
1294
|
_wbInstallStateForget_decorators = [Remote("wbInstallStateForget")];
|
|
1168
1295
|
__esDecorate(this, null, _wbInstallStateForget_decorators, { kind: "method", name: "wbInstallStateForget", static: false, private: false, access: { has: (obj) => "wbInstallStateForget" in obj, get: (obj) => obj.wbInstallStateForget }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1169
|
-
_wbPluginUpdateCheck_decorators = [Remote("wbPluginUpdateCheck")];
|
|
1170
|
-
__esDecorate(this, null, _wbPluginUpdateCheck_decorators, { kind: "method", name: "wbPluginUpdateCheck", static: false, private: false, access: { has: (obj) => "wbPluginUpdateCheck" in obj, get: (obj) => obj.wbPluginUpdateCheck }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1171
1296
|
// T06/Awesome:DSH 社区插件市场(awesome-dsh-plugins)。
|
|
1172
1297
|
_wbAwesomeCatalog_decorators = [Remote("wbAwesomeCatalog")];
|
|
1173
1298
|
__esDecorate(this, null, _wbAwesomeCatalog_decorators, { kind: "method", name: "wbAwesomeCatalog", static: false, private: false, access: { has: (obj) => "wbAwesomeCatalog" in obj, get: (obj) => obj.wbAwesomeCatalog }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
@@ -1177,6 +1302,14 @@ let McpManagerGateway = (() => {
|
|
|
1177
1302
|
__esDecorate(this, null, _wbAwesomeReadme_decorators, { kind: "method", name: "wbAwesomeReadme", static: false, private: false, access: { has: (obj) => "wbAwesomeReadme" in obj, get: (obj) => obj.wbAwesomeReadme }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1178
1303
|
_wbAwesomeUpdateCheck_decorators = [Remote("wbAwesomeUpdateCheck")];
|
|
1179
1304
|
__esDecorate(this, null, _wbAwesomeUpdateCheck_decorators, { kind: "method", name: "wbAwesomeUpdateCheck", static: false, private: false, access: { has: (obj) => "wbAwesomeUpdateCheck" in obj, get: (obj) => obj.wbAwesomeUpdateCheck }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1305
|
+
_wbRepoMeta_decorators = [Remote("wbRepoMeta")];
|
|
1306
|
+
__esDecorate(this, null, _wbRepoMeta_decorators, { kind: "method", name: "wbRepoMeta", static: false, private: false, access: { has: (obj) => "wbRepoMeta" in obj, get: (obj) => obj.wbRepoMeta }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1307
|
+
_wbNpmDownloads_decorators = [Remote("wbNpmDownloads")];
|
|
1308
|
+
__esDecorate(this, null, _wbNpmDownloads_decorators, { kind: "method", name: "wbNpmDownloads", static: false, private: false, access: { has: (obj) => "wbNpmDownloads" in obj, get: (obj) => obj.wbNpmDownloads }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1309
|
+
_wbLoadoutExport_decorators = [Remote("wbLoadoutExport")];
|
|
1310
|
+
__esDecorate(this, null, _wbLoadoutExport_decorators, { kind: "method", name: "wbLoadoutExport", static: false, private: false, access: { has: (obj) => "wbLoadoutExport" in obj, get: (obj) => obj.wbLoadoutExport }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1311
|
+
_wbLoadoutDiff_decorators = [Remote("wbLoadoutDiff")];
|
|
1312
|
+
__esDecorate(this, null, _wbLoadoutDiff_decorators, { kind: "method", name: "wbLoadoutDiff", static: false, private: false, access: { has: (obj) => "wbLoadoutDiff" in obj, get: (obj) => obj.wbLoadoutDiff }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1180
1313
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
1181
1314
|
}
|
|
1182
1315
|
static inject = ["loader", "tools"];
|
|
@@ -1253,27 +1386,6 @@ let McpManagerGateway = (() => {
|
|
|
1253
1386
|
const wsPath = typeof rawPath === "string" && rawPath ? canonicalWorkspacePath(rawPath) : undefined;
|
|
1254
1387
|
return { tools: workspaceToolSchemas(this.ctx, value, wsPath).map(projectToolSchema), ambiguous: false };
|
|
1255
1388
|
}
|
|
1256
|
-
async builtins() {
|
|
1257
|
-
const state = await managedState(this.ctx);
|
|
1258
|
-
return { builtins: builtinMcpCatalog(effectiveMcpServers(this.ctx, state)) };
|
|
1259
|
-
}
|
|
1260
|
-
async installBuiltins(payload) {
|
|
1261
|
-
return withPatchWrite(this.ctx, async () => {
|
|
1262
|
-
const state = await managedState(this.ctx);
|
|
1263
|
-
const result = appendSelectedBuiltinMcpServers(
|
|
1264
|
-
state.content,
|
|
1265
|
-
effectiveMcpServers(this.ctx, state),
|
|
1266
|
-
payload?.ids,
|
|
1267
|
-
{ reservedIds: reservedEntryIds(this.ctx) },
|
|
1268
|
-
);
|
|
1269
|
-
if (result.content !== state.content) await writePatch(state, result.content);
|
|
1270
|
-
const notes = [];
|
|
1271
|
-
if (result.added.length) notes.push(`已追加:${result.added.join(", ")}`);
|
|
1272
|
-
if (result.skipped.length) notes.push(`已存在并跳过:${result.skipped.join(", ")}`);
|
|
1273
|
-
const { content: _content, ...summary } = result;
|
|
1274
|
-
return { ...summary, note: notes.join(";") || "未修改配置" };
|
|
1275
|
-
});
|
|
1276
|
-
}
|
|
1277
1389
|
async add(spec) {
|
|
1278
1390
|
if (containsRedactedValue(spec)) throw new Error("新增 MCP 不能包含保留原值标记");
|
|
1279
1391
|
const normalized = normalizeOne(spec);
|
|
@@ -1511,36 +1623,6 @@ let McpManagerGateway = (() => {
|
|
|
1511
1623
|
catch (error) { this.ctx.logger?.error?.(`mcp-manager: 屏蔽已写入,但对齐会话作用域失败(下次挂载会重试): ${error?.message ?? error}`); }
|
|
1512
1624
|
return result;
|
|
1513
1625
|
}
|
|
1514
|
-
async installWorkspaceBuiltins(payload) {
|
|
1515
|
-
const wsPath = canonicalWorkspacePath(String(payload?.wsPath || ""));
|
|
1516
|
-
const ids = payload?.ids;
|
|
1517
|
-
if (!Array.isArray(ids) || !ids.length || !ids.every((id) => typeof id === "string")) throw new Error("内置 MCP ids 必须是非空字符串数组");
|
|
1518
|
-
const result = await withWorkspaceWrite(wsPath, async () => {
|
|
1519
|
-
const config = await readWorkspaceConfigCached(this.ctx, wsPath);
|
|
1520
|
-
if (config.error) throw new Error(config.error);
|
|
1521
|
-
const known = new Map(BUILTIN_MCP_SERVERS.map((builtin) => [builtin.id, builtin]));
|
|
1522
|
-
const unknown = ids.filter((id) => !known.has(id));
|
|
1523
|
-
if (unknown.length) throw new Error(`未知的内置 MCP:${unknown.join(", ")}`);
|
|
1524
|
-
const existing = new Set(config.servers.map((server) => server.name));
|
|
1525
|
-
const added = [];
|
|
1526
|
-
const skipped = [];
|
|
1527
|
-
for (const id of ids) {
|
|
1528
|
-
const builtin = known.get(id);
|
|
1529
|
-
if (existing.has(builtin.name)) { skipped.push(id); continue; }
|
|
1530
|
-
const spec = { name: builtin.name };
|
|
1531
|
-
for (const field of ["transport", "command", "args", "env", "cwd", "url", "headers", "toolCallTimeoutMs", "failOnStartupError", "reconnect"]) {
|
|
1532
|
-
if (builtin[field] !== undefined) spec[field] = structuredClone(builtin[field]);
|
|
1533
|
-
}
|
|
1534
|
-
added.push(spec);
|
|
1535
|
-
}
|
|
1536
|
-
if (!added.length) return { note: skipped.length ? `已存在并跳过:${skipped.join(", ")}` : "未修改配置", unchanged: true };
|
|
1537
|
-
await writeWorkspaceConfigFile(wsPath, [...config.servers, ...added], config.exclude);
|
|
1538
|
-
return { note: `已追加:${added.map((server) => server.name).join(", ")}${skipped.length ? `;已存在并跳过:${skipped.join(", ")}` : ""}` };
|
|
1539
|
-
});
|
|
1540
|
-
// 没写盘就不用对齐(否则每次点"安装内置"都会白跑一遍挂载)。
|
|
1541
|
-
if (!result.unchanged) await alignWorkspaceConnections(this.ctx, wsPath);
|
|
1542
|
-
return { note: result.note };
|
|
1543
|
-
}
|
|
1544
1626
|
async previewWorkspaceImport(payload) {
|
|
1545
1627
|
const wsPath = canonicalWorkspacePath(String(payload?.wsPath || ""));
|
|
1546
1628
|
const json = typeof payload?.json === "string" ? JSON.parse(payload.json) : payload?.json;
|
|
@@ -1731,6 +1813,10 @@ let McpManagerGateway = (() => {
|
|
|
1731
1813
|
async wbConnectorPreview(payload) {
|
|
1732
1814
|
return wbConnectorPreview(payload?.connectorId);
|
|
1733
1815
|
}
|
|
1816
|
+
// 某条连接器的图标(data URL)。市场卡片进视口才调,payload 形状同 wbConnectorPreview。
|
|
1817
|
+
async wbConnectorIcon(payload) {
|
|
1818
|
+
return wbConnectorIcon(payload?.connectorId);
|
|
1819
|
+
}
|
|
1734
1820
|
async wbConnectorInstall(payload) {
|
|
1735
1821
|
const connectorId = String(payload?.connectorId || "").trim();
|
|
1736
1822
|
if (!connectorId) throw new Error("缺少 connectorId");
|
|
@@ -1902,15 +1988,6 @@ let McpManagerGateway = (() => {
|
|
|
1902
1988
|
async wbMarketRegistryUpsert(payload) {
|
|
1903
1989
|
return upsertMarketplace(payload || {});
|
|
1904
1990
|
}
|
|
1905
|
-
// ── T03/R5:codebuddy 插件市场 ──────────────────────────────────────
|
|
1906
|
-
// 数据层在 lib/wb-plugin-market.js;这里只做 RPC 转发(下载/缓存/解析都在那边)。
|
|
1907
|
-
async wbPluginCatalog() {
|
|
1908
|
-
return wbPluginCatalog();
|
|
1909
|
-
}
|
|
1910
|
-
async wbPluginDetail(payload) {
|
|
1911
|
-
return wbPluginDetail(payload?.name);
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
1991
|
// ── T04/R8:安装状态账本(connector / skill / plugin 三类安装的统一登记) ──
|
|
1915
1992
|
async wbInstallState() {
|
|
1916
1993
|
return readInstallState();
|
|
@@ -1930,9 +2007,6 @@ let McpManagerGateway = (() => {
|
|
|
1930
2007
|
return { note: `已移除安装状态:${kind}/${key}` };
|
|
1931
2008
|
}
|
|
1932
2009
|
|
|
1933
|
-
async wbPluginUpdateCheck() {
|
|
1934
|
-
return wbPluginUpdateCheck();
|
|
1935
|
-
}
|
|
1936
2010
|
// ── T06/Awesome:DSH 社区插件市场(awesome-dsh-plugins)──────────────
|
|
1937
2011
|
// 数据层在 lib/wb-awesome-market.js;这里只做 RPC 转发。
|
|
1938
2012
|
async wbAwesomeCatalog(payload) {
|
|
@@ -1947,6 +2021,35 @@ let McpManagerGateway = (() => {
|
|
|
1947
2021
|
async wbAwesomeUpdateCheck() {
|
|
1948
2022
|
return wbAwesomeUpdateCheck();
|
|
1949
2023
|
}
|
|
2024
|
+
// ── 社区元信息:仓库「人味」信号(维护者 / 最近提交 / issue / star)────────
|
|
2025
|
+
// 详情弹层按需调用,只读公开接口,6 小时缓存;不接收也不保存任何凭据。
|
|
2026
|
+
async wbRepoMeta(payload) {
|
|
2027
|
+
return wbRepoMeta(payload || {});
|
|
2028
|
+
}
|
|
2029
|
+
// ── npm 近 30 天下载量(只读)───────────────────────────────────────
|
|
2030
|
+
// 卡片进视口才按需批量取,7 天缓存;只读公开统计接口,不接收也不保存任何凭据。
|
|
2031
|
+
async wbNpmDownloads(payload) {
|
|
2032
|
+
return wbNpmDownloads(payload || {});
|
|
2033
|
+
}
|
|
2034
|
+
// ── 装备清单(只读)────────────────────────────────────────────────
|
|
2035
|
+
// 导出:收集本机全局 MCP / 技能 / 已召唤专家 / 已装插件,只留身份字段。
|
|
2036
|
+
// 对比:校验用户贴入的清单并与本机做差异,返回逐条状态;不安装、不写盘。
|
|
2037
|
+
async wbLoadoutExport(payload) {
|
|
2038
|
+
const items = await collectLoadoutItems(this.ctx);
|
|
2039
|
+
return {
|
|
2040
|
+
ok: true,
|
|
2041
|
+
loadout: buildLoadout({
|
|
2042
|
+
name: payload?.name,
|
|
2043
|
+
items,
|
|
2044
|
+
appVersion: pluginVersion(),
|
|
2045
|
+
}),
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
async wbLoadoutDiff(payload) {
|
|
2049
|
+
const raw = payload?.loadout ?? payload?.text ?? payload;
|
|
2050
|
+
const items = await collectLoadoutItems(this.ctx);
|
|
2051
|
+
return { ok: true, ...diffLoadout(raw, items) };
|
|
2052
|
+
}
|
|
1950
2053
|
// ── WorkBuddy 专家直连(召唤预设 + 捆绑技能)────────────────────────
|
|
1951
2054
|
// 数据与安装逻辑都在 lib/expert-market.js;这里只做 RPC 转发与 patch 写盘收尾。
|
|
1952
2055
|
// 召唤 = 往 profile patch 插入一条独立 @deepseek-ai/dsh-agent-preset 行(persona.prefix
|
|
@@ -2088,7 +2191,31 @@ let McpManagerGateway = (() => {
|
|
|
2088
2191
|
// 不存在的 TeamCreate/Agent(成员开不出来)。标记桥接判定放前端(需要市场目录的
|
|
2089
2192
|
// expertType),这里只把 personaText 原样带出去。
|
|
2090
2193
|
const patch = await readPatch(this.ctx);
|
|
2091
|
-
|
|
2194
|
+
const presets = readExpertPresets(patch.content);
|
|
2195
|
+
// patch 只记 plugin / presetId,中英显示名在专家目录缓存里。这里补上两个字段,
|
|
2196
|
+
// 客户端按宿主语言挑:中文名给中文界面,英文名给英文界面。读缓存失败只回落
|
|
2197
|
+
// plugin 名,不影响「已召唤 / 已装技能」本身的状态判断。
|
|
2198
|
+
// 回落有三层(目录缓存 → 预设行自带的名字 → 英文标识名):召唤那一刻
|
|
2199
|
+
// `WorkBuddy 专家 · <显示名>` 就写进了 patch 行,readExpertPresets 剥前缀即得,
|
|
2200
|
+
// 所以目录没落盘 / 专家已下架时菜单仍能出「吴八哥」,而不是 senior-developer。
|
|
2201
|
+
let displayNames = new Map();
|
|
2202
|
+
try {
|
|
2203
|
+
displayNames = await readExpertDisplayNames();
|
|
2204
|
+
} catch (error) {
|
|
2205
|
+
this.ctx.logger?.warn?.(`wb-expert: 读取专家显示名失败:${error?.message ?? error}`);
|
|
2206
|
+
}
|
|
2207
|
+
return {
|
|
2208
|
+
presets: presets.map((row) => {
|
|
2209
|
+
const info = displayNames.get(row.plugin) || {};
|
|
2210
|
+
return {
|
|
2211
|
+
...row,
|
|
2212
|
+
...info,
|
|
2213
|
+
nameZh: info.nameZh || row.displayName || "",
|
|
2214
|
+
nameEn: info.nameEn || row.displayName || "",
|
|
2215
|
+
};
|
|
2216
|
+
}),
|
|
2217
|
+
skills: await listExpertSkills(),
|
|
2218
|
+
};
|
|
2092
2219
|
}
|
|
2093
2220
|
// 最近使用记录(输入框召唤菜单的数据源):召唤/送还时自动记录专家;技能与 MCP 由
|
|
2094
2221
|
// 客户端在用户从菜单里显式选用时上报(recordUse)。只存展示所需最小字段。
|
|
@@ -2103,4 +2230,4 @@ let McpManagerGateway = (() => {
|
|
|
2103
2230
|
};
|
|
2104
2231
|
})();
|
|
2105
2232
|
|
|
2106
|
-
export {
|
|
2233
|
+
export { McpManagerGateway, McpManagerGateway as default };
|
package/lib/loadout.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// 装备清单(loadout):把「这台机器装了什么」变成一份可分享、可对比的清单。
|
|
2
|
+
//
|
|
3
|
+
// 边界(决定了这里为什么只做投影,不做安装):
|
|
4
|
+
// - 只记录名称 / 类型 / 来源 / 版本这类**身份信息**,绝不写入 env、header、
|
|
5
|
+
// token、authorization 或任何配置值;MCP 最多保留 env/header 的**键名**。
|
|
6
|
+
// - 导入只做解析与差异计算,不写盘、不安装、不调用任何市场写链。
|
|
7
|
+
// - 清单是用户主动导出、主动分享的文件;没有后端、没有账号、没有遥测。
|
|
8
|
+
//
|
|
9
|
+
// 数据层保持纯函数:宿主只负责收集本机条目,校验与 diff 都在这里,可独立测试。
|
|
10
|
+
export const LOADOUT_SCHEMA = "dsh-armory/loadout@1";
|
|
11
|
+
export const LOADOUT_MAX_ITEMS = 200;
|
|
12
|
+
export const LOADOUT_MAX_BYTES = 256 * 1024;
|
|
13
|
+
export const LOADOUT_DEFAULT_NAME = "我的 DSH 装备清单";
|
|
14
|
+
|
|
15
|
+
const KINDS = new Set(["mcp", "skill", "expert", "plugin"]);
|
|
16
|
+
const TRANSPORTS = new Set(["http", "stdio", "local", "?"]);
|
|
17
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f]+/g;
|
|
18
|
+
|
|
19
|
+
function text(value, limit) {
|
|
20
|
+
return String(value ?? "").replace(CONTROL_CHARS, " ").trim().slice(0, limit);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function keyOf(item) {
|
|
24
|
+
return `${item.kind}\u0000${item.name}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 单条投影:只放行白名单字段,其余(含任何看起来像密钥的键)一律丢弃。 */
|
|
28
|
+
function normalizeItem(raw) {
|
|
29
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
30
|
+
const kind = text(raw.kind, 20).toLowerCase();
|
|
31
|
+
if (!KINDS.has(kind)) return null;
|
|
32
|
+
const name = text(raw.name, 120);
|
|
33
|
+
if (!name) return null;
|
|
34
|
+
|
|
35
|
+
const item = { kind, name };
|
|
36
|
+
const source = text(raw.source, 200);
|
|
37
|
+
if (source) item.source = source;
|
|
38
|
+
const version = text(raw.version, 100);
|
|
39
|
+
if (version) item.version = version;
|
|
40
|
+
const updatedAt = text(raw.updatedAt, 60);
|
|
41
|
+
if (updatedAt) item.updatedAt = updatedAt;
|
|
42
|
+
|
|
43
|
+
if (kind === "mcp") {
|
|
44
|
+
const transport = text(raw.transport, 12).toLowerCase();
|
|
45
|
+
if (TRANSPORTS.has(transport)) item.transport = transport;
|
|
46
|
+
const endpointHost = text(raw.endpointHost, 200);
|
|
47
|
+
if (endpointHost) item.endpointHost = endpointHost;
|
|
48
|
+
if (Array.isArray(raw.configKeys)) {
|
|
49
|
+
const keys = [];
|
|
50
|
+
for (const value of raw.configKeys) {
|
|
51
|
+
const key = text(value, 80);
|
|
52
|
+
if (!key || keys.includes(key)) continue;
|
|
53
|
+
keys.push(key);
|
|
54
|
+
if (keys.length >= 20) break;
|
|
55
|
+
}
|
|
56
|
+
if (keys.length) item.configKeys = keys;
|
|
57
|
+
}
|
|
58
|
+
if (typeof raw.enabled === "boolean") item.enabled = raw.enabled;
|
|
59
|
+
} else if (kind === "expert") {
|
|
60
|
+
const plugin = text(raw.plugin, 160);
|
|
61
|
+
if (plugin) item.plugin = plugin;
|
|
62
|
+
} else if (kind === "plugin") {
|
|
63
|
+
const key = text(raw.key, 160);
|
|
64
|
+
if (key) item.key = key;
|
|
65
|
+
}
|
|
66
|
+
return item;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function countItems(items) {
|
|
70
|
+
const counts = { total: items.length, mcp: 0, skill: 0, expert: 0, plugin: 0 };
|
|
71
|
+
for (const item of items) counts[item.kind] += 1;
|
|
72
|
+
return counts;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function countStatuses(rows) {
|
|
76
|
+
const counts = { total: rows.length, installed: 0, update: 0, missing: 0 };
|
|
77
|
+
for (const row of rows) counts[row.status] += 1;
|
|
78
|
+
return counts;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 解析并校验一份清单(对象或 JSON 文本)。
|
|
83
|
+
* 非法结构直接抛错;单条非法 / 重复静默丢弃,避免一个坏行毁掉整份清单。
|
|
84
|
+
*/
|
|
85
|
+
export function normalizeLoadout(input) {
|
|
86
|
+
let raw = input;
|
|
87
|
+
if (typeof input === "string") {
|
|
88
|
+
if (input.length > LOADOUT_MAX_BYTES) throw new Error("装备清单文件过大(上限 256KB)");
|
|
89
|
+
try {
|
|
90
|
+
raw = JSON.parse(input);
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error("不是有效的 JSON");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("装备清单必须是一个对象");
|
|
96
|
+
if (raw.schema !== LOADOUT_SCHEMA) throw new Error("不是 dsh-armory 装备清单(schema 不匹配)");
|
|
97
|
+
if (!Array.isArray(raw.items)) throw new Error("装备清单缺少 items 数组");
|
|
98
|
+
|
|
99
|
+
const items = [];
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
for (const row of raw.items) {
|
|
102
|
+
const item = normalizeItem(row);
|
|
103
|
+
if (!item) continue;
|
|
104
|
+
const key = keyOf(item);
|
|
105
|
+
if (seen.has(key)) continue;
|
|
106
|
+
seen.add(key);
|
|
107
|
+
items.push(item);
|
|
108
|
+
if (items.length >= LOADOUT_MAX_ITEMS) break;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const loadout = {
|
|
112
|
+
schema: LOADOUT_SCHEMA,
|
|
113
|
+
name: text(raw.name, 120) || LOADOUT_DEFAULT_NAME,
|
|
114
|
+
generatedAt: text(raw.generatedAt, 60),
|
|
115
|
+
items,
|
|
116
|
+
counts: countItems(items),
|
|
117
|
+
};
|
|
118
|
+
if (raw.app && typeof raw.app === "object") {
|
|
119
|
+
const name = text(raw.app.name, 80);
|
|
120
|
+
const version = text(raw.app.version, 40);
|
|
121
|
+
if (name) loadout.app = { name, ...(version ? { version } : {}) };
|
|
122
|
+
}
|
|
123
|
+
if (raw.truncated === true) loadout.truncated = true;
|
|
124
|
+
return loadout;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 由本机收集到的条目构建一份可导出的清单。 */
|
|
128
|
+
export function buildLoadout({ name, items, generatedAt, appVersion } = {}) {
|
|
129
|
+
const sourceItems = Array.isArray(items) ? items : [];
|
|
130
|
+
const loadout = normalizeLoadout({
|
|
131
|
+
schema: LOADOUT_SCHEMA,
|
|
132
|
+
name: text(name, 120) || LOADOUT_DEFAULT_NAME,
|
|
133
|
+
generatedAt: text(generatedAt, 60) || new Date().toISOString(),
|
|
134
|
+
app: { name: "dsh-armory", version: text(appVersion, 40) },
|
|
135
|
+
items: sourceItems,
|
|
136
|
+
});
|
|
137
|
+
if (sourceItems.length > LOADOUT_MAX_ITEMS) loadout.truncated = true;
|
|
138
|
+
return loadout;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 与清单做本机差异对比。
|
|
143
|
+
* 只按 kind + name 匹配;双方都有版本且不同才算「可更新」,避免把缺版本的条目误判。
|
|
144
|
+
*/
|
|
145
|
+
export function diffLoadout(remoteInput, localItems) {
|
|
146
|
+
const remote = normalizeLoadout(remoteInput);
|
|
147
|
+
const local = new Map();
|
|
148
|
+
for (const row of Array.isArray(localItems) ? localItems : []) {
|
|
149
|
+
const item = normalizeItem(row);
|
|
150
|
+
if (item && !local.has(keyOf(item))) local.set(keyOf(item), item);
|
|
151
|
+
}
|
|
152
|
+
const rows = remote.items.map((item) => {
|
|
153
|
+
const hit = local.get(keyOf(item));
|
|
154
|
+
const status = !hit ? "missing" : item.version && hit.version && item.version !== hit.version ? "update" : "installed";
|
|
155
|
+
return { ...item, status, localVersion: hit?.version || "" };
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
loadout: {
|
|
159
|
+
schema: remote.schema,
|
|
160
|
+
name: remote.name,
|
|
161
|
+
generatedAt: remote.generatedAt,
|
|
162
|
+
counts: remote.counts,
|
|
163
|
+
},
|
|
164
|
+
rows,
|
|
165
|
+
counts: countStatuses(rows),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 稳定、人类可读的导出文本。 */
|
|
170
|
+
export function stringifyLoadout(input) {
|
|
171
|
+
return JSON.stringify(normalizeLoadout(input), null, 2);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 下载文件名:保留中文,只清理 Windows / URL 不接受的字符。 */
|
|
175
|
+
export function loadoutFileName(name) {
|
|
176
|
+
const base = text(name, 80).replace(/[\\/:*?"<>|\s]+/g, "-").replace(/^-+|-+$/g, "");
|
|
177
|
+
return `${base || "dsh-loadout"}.json`;
|
|
178
|
+
}
|