dsh-armory 1.10.0 → 2.1.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/lib/typert.js CHANGED
@@ -94,6 +94,24 @@ export const TYPERT = {
94
94
  mv('wbSkillInstall', [payloadParam], 'NoteResult'),
95
95
  mv('wbSkillUninstall', [payloadParam], 'NoteResult'),
96
96
  mv('wbSkillInstalled', [], 'WbInstalledResult'),
97
+ // T03/R4:多市场注册表(读 / 增改)。payload $any,形状由 lib/wb-registry.js 校验。
98
+ mv('wbMarketRegistry', [], 'WbMarketRegistryResult'),
99
+ mv('wbMarketRegistryUpsert', [payloadParam], 'WbMarketRegistryResult'),
100
+ // T03/R5:codebuddy 插件市场(列表 / 详情)。payload $any,形状由 lib/wb-plugin-market.js 校验。
101
+ mv('wbPluginCatalog', [], 'WbPluginCatalogResult'),
102
+ mv('wbPluginDetail', [payloadParam], 'WbPluginDetailResult'),
103
+ // T04/R8:安装状态登记与查询(宿主侧落盘,客户端「已安装」筛选的数据源)。
104
+ mv('wbInstallState', [], 'InstallStateResult'),
105
+ mv('wbInstallStateRecord', [payloadParam], 'NoteResult'),
106
+ mv('wbInstallStateForget', [payloadParam], 'NoteResult'),
107
+ // T05/R9:插件市场更新探测(只做条件请求,不落新包)。
108
+ mv('wbPluginUpdateCheck', [], 'WbUpdateHintResult'),
109
+ // T06/Awesome:DSH 社区插件市场(awesome-dsh-plugins,数据层 lib/wb-awesome-market.js)。
110
+ mv('wbAwesomeCatalog', [payloadParam], 'AwesomeCatalogResult'),
111
+ mv('wbAwesomeDetail', [payloadParam], 'AwesomeDetailResult'),
112
+ // README 预览:点击卡片按需取一次(npm registry / GitHub raw),落盘缓存 7 天。
113
+ mv('wbAwesomeReadme', [payloadParam], 'AwesomeReadmeResult'),
114
+ mv('wbAwesomeUpdateCheck', [], 'WbUpdateHintResult'),
97
115
  // WorkBuddy 专家直连(召唤预设 + 捆绑技能)。payload 全部 $any:数据形状由
98
116
  // lib/expert-market.js 校验,与 wb* 系列同一宽容度。
99
117
  mv('wbExpertCatalog', [], 'WbExpertCatalogResult'),
@@ -108,6 +126,14 @@ export const TYPERT = {
108
126
  mv('expertAttach', [payloadParam], 'NoteResult'),
109
127
  mv('expertDetach', [payloadParam], 'NoteResult'),
110
128
  mv('expertActive', [], 'ExpertActiveResult'),
129
+ // R8:连接器 MCP 的浏览器授权(RFC 9728/8414/7591/7636)。副作用是起本地回调
130
+ // 监听 + 拉系统浏览器,所以 begin/poll 都是独立 RPC,面板按需轮询。
131
+ mv('wbOAuthBegin', [payloadParam], 'NoteResult'),
132
+ mv('wbOAuthPoll', [payloadParam], 'NoteResult'),
133
+ mv('wbOAuthCancel', [payloadParam], 'NoteResult'),
134
+ mv('wbOAuthToken', [payloadParam], 'NoteResult'),
135
+ mv('wbOAuthOverview', [payloadParam], 'NoteResult'),
136
+ mv('wbOAuthOpen', [payloadParam], 'NoteResult'),
111
137
  ],
112
138
  model: { services: [], events: [], objects: [] },
113
139
  }
@@ -0,0 +1,128 @@
1
+ // auth_injection_rules 解析 + mcpServers token 占位符工具(R1 / R7 的纯函数部分)。
2
+ //
3
+ // 设计边界(X9):WB 的 auth_injection_rules 是 WB 账号体系的**代注入**机制,DSH 不做
4
+ // 注入——本模块只解析、只关联、只展示(R1);R7 的令牌引导安装用的是**用户自填 token**
5
+ // 对 `${...}` 占位符的显式替换,与 WB 代注入是两回事。
6
+ //
7
+ // 纯函数库:零 IO、零网络、零 Remote/UI 依赖。所有函数对畸形输入一律防御性返回
8
+ // 空集合/原样副本,绝不抛错——上游 JSON 是远端 zip 里的内容,任何形状都可能出现。
9
+ //
10
+ // 实测参考结构(仅防御解析参考,不硬编码依赖):
11
+ // 规则 = { id, when:{active_in:[]}, applies_to_connectors:[], requires_connected_connectors:[],
12
+ // timing:"request", inject:[{from_connector, token_type, header, value_template}] }
13
+
14
+ /**
15
+ * 解析 connectors.json 顶层的 auth_injection_rules 数组。
16
+ * 非数组 / 字段缺失 / 元素不是对象 → 返回 [](绝不抛错)。
17
+ * 元素原样保留(下游只读 id / applies_to_connectors / inject 等字段做展示)。
18
+ */
19
+ export function parseInjectionRules(indexJson) {
20
+ const rules = indexJson?.auth_injection_rules;
21
+ if (!Array.isArray(rules)) return [];
22
+ return rules.filter((rule) => rule && typeof rule === "object" && !Array.isArray(rule));
23
+ }
24
+
25
+ /**
26
+ * 按 connectorId 命中规则:applies_to_connectors 数组包含该 id 即命中。
27
+ * 返回命中规则数组(空输入 / 非数组 applies_to_connectors 一律不命中)。
28
+ */
29
+ export function rulesFor(rules, connectorId) {
30
+ if (!Array.isArray(rules)) return [];
31
+ const id = String(connectorId ?? "").trim();
32
+ if (!id) return [];
33
+ return rules.filter((rule) => {
34
+ const applies = rule?.applies_to_connectors;
35
+ return Array.isArray(applies) && applies.map((entry) => String(entry ?? "").trim()).includes(id);
36
+ });
37
+ }
38
+
39
+ /** 一个待填 token 槽位:占位符在哪个 server 的哪个容器字段里。 */
40
+ // 形状:{ server, container: "env"|"header", key, template, placeholder, value }
41
+
42
+ const PLACEHOLDER_PATTERN = /\$\{([^}]+)\}/g;
43
+ // 非全局版:.test() 不会推进 lastIndex(全局正则连续 test 会漏判残留占位符)。
44
+ const HAS_PLACEHOLDER = /\$\{[^}]+\}/;
45
+
46
+ /**
47
+ * 扫描 mcpServers 的 env 与 headers 值里的 `${...}` 模板占位符。
48
+ * 返回槽位清单:[{ server, container: "env"|"header", key, template, placeholder, value }]
49
+ * - container 字段容器:env → env 对象;header → headers 对象(对外统一命名)
50
+ * - template 原始模板串(如 "Bearer ${token}")
51
+ * - placeholder 第一个占位符(如 "token");模板无占位符的条目不进清单
52
+ * - value 原值(供 applyTokens 做精确字符串替换与回显)
53
+ * mcpServers / env / headers 畸形(null、数组、值非字符串)一律跳过,不抛错。
54
+ */
55
+ export function scanTokenSlots(mcpServers) {
56
+ if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) return [];
57
+ const slots = [];
58
+ for (const [server, rawServer] of Object.entries(mcpServers)) {
59
+ // transport 可能嵌套(wb-market.serverInstallability 的 nested 形态):env/headers
60
+ // 在 server 本体或 server.transport 上都认。
61
+ const nested = rawServer && typeof rawServer === "object" && typeof rawServer.transport === "object" && rawServer.transport !== null
62
+ ? rawServer.transport
63
+ : (typeof rawServer === "object" && rawServer !== null ? rawServer : null);
64
+ if (!nested) continue;
65
+ const bags = [["env", nested.env], ["header", nested.headers]];
66
+ for (const [container, bag] of bags) {
67
+ if (!bag || typeof bag !== "object" || Array.isArray(bag)) continue;
68
+ for (const [key, value] of Object.entries(bag)) {
69
+ if (typeof value !== "string") continue;
70
+ const matches = [...value.matchAll(PLACEHOLDER_PATTERN)];
71
+ if (!matches.length) continue;
72
+ slots.push({
73
+ server,
74
+ container,
75
+ key,
76
+ template: value,
77
+ placeholder: matches[0][1].trim(),
78
+ value,
79
+ });
80
+ }
81
+ }
82
+ }
83
+ return slots;
84
+ }
85
+
86
+ /**
87
+ * 按槽位把占位符替换为用户 token,产出「可安全写盘」的 mcpServers 副本。
88
+ * - tokens: Record<placeholder, value>,键为占位符名(如 { access_token: "xxx" })。
89
+ * - 未提供 token 的槽位**保留原样**并在 missingSlots 里标记(供 UI 提示缺槽)。
90
+ * 返回 { mcpServers, missingSlots }:
91
+ * mcpServers 深拷贝后的替换结果(原对象永不改动)
92
+ * missingSlots [{ server, container, key, placeholder, template }] 未填槽位清单
93
+ * 输入畸形时返回 { mcpServers: {}, missingSlots: [] };对每个槽位同时应用模板内**所有**
94
+ * 占位符("Bearer ${a}/${b}" 两处都能换),全替换后仍含 `${` 的值按未填槽位上报。
95
+ */
96
+ export function applyTokens(mcpServers, tokens) {
97
+ const source = mcpServers && typeof mcpServers === "object" && !Array.isArray(mcpServers) ? mcpServers : {};
98
+ const tokenMap = tokens && typeof tokens === "object" && !Array.isArray(tokens) ? tokens : {};
99
+ const missingSlots = [];
100
+ const result = structuredClone(source);
101
+ const slots = scanTokenSlots(result);
102
+ for (const slot of slots) {
103
+ const target = slot.container === "header"
104
+ ? result[slot.server]?.transport?.headers ?? result[slot.server]?.headers
105
+ : result[slot.server]?.transport?.env ?? result[slot.server]?.env;
106
+ if (!target || typeof target !== "object") continue;
107
+ let filled = String(target[slot.key]);
108
+ for (const match of filled.matchAll(PLACEHOLDER_PATTERN)) {
109
+ const token = tokenMap[match[1].trim()];
110
+ if (typeof token === "string" && token.length) {
111
+ filled = filled.split(match[0]).join(token);
112
+ }
113
+ }
114
+ if (HAS_PLACEHOLDER.test(filled)) {
115
+ // 仍有残留占位符 → 该槽位缺 token:保留原样并上报。
116
+ missingSlots.push({
117
+ server: slot.server,
118
+ container: slot.container,
119
+ key: slot.key,
120
+ placeholder: slot.placeholder,
121
+ template: slot.template,
122
+ });
123
+ } else {
124
+ target[slot.key] = filled;
125
+ }
126
+ }
127
+ return { mcpServers: result, missingSlots };
128
+ }