dsh-armory 1.9.4 → 2.0.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 +26 -8
- package/README.md +39 -16
- package/docs/chunk-split-feasibility.md +153 -0
- package/docs/host-proposal-mcp-teardown.md +184 -0
- package/docs/host-proposal-skill-toggle.md +166 -0
- package/docs/mcp-market-design.md +62 -0
- package/docs/prd-awesome-dsh-plugins.md +105 -0
- package/docs/prd-workbuddy-parity.md +128 -0
- package/docs/system-design-awesome.md +269 -0
- package/docs/system_design.md +530 -0
- package/docs/workbuddy-market-gap-analysis.md +197 -0
- package/lib/client.js +4507 -271
- package/lib/expert-market.js +13 -2
- package/lib/index.js +395 -9
- package/lib/mcp-catalog.js +104 -0
- package/lib/mcp-config.js +10 -1
- package/lib/mcp-import-sources.js +154 -14
- package/lib/mcp-oauth.js +806 -0
- package/lib/tool-ledger.js +119 -0
- package/lib/typert.js +26 -0
- package/lib/wb-auth-rules.js +128 -0
- package/lib/wb-awesome-market.js +453 -0
- package/lib/wb-install-state.js +151 -0
- package/lib/wb-market.js +232 -8
- package/lib/wb-plugin-market.js +291 -0
- package/lib/wb-registry.js +224 -0
- package/lib/whats-new.js +82 -0
- package/lib/workspace-runtime.js +133 -4
- package/package.json +10 -9
|
@@ -527,6 +527,127 @@ function parseFailure(error, text) {
|
|
|
527
527
|
return 0
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
// VS Code `${input:...}`:逐条降级,不整份拒绝
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* VS Code 的 `${input:名称}`:值由人在**导入时**填写,DSH 读配置的时候无从知道。
|
|
536
|
+
* 它是 VS Code 专属写法,出现在别的客户端配置里也代表同一件事。
|
|
537
|
+
*/
|
|
538
|
+
const VSCODE_INPUT = /\$\{input:([^}]*)\}/g
|
|
539
|
+
|
|
540
|
+
/** 一个字符串里出现的 input 名,按出现顺序去重。 */
|
|
541
|
+
function vscodeInputNames(value) {
|
|
542
|
+
if (typeof value !== 'string' || !value.includes('${input:')) return []
|
|
543
|
+
const names = []
|
|
544
|
+
for (const match of value.matchAll(VSCODE_INPUT)) {
|
|
545
|
+
const name = match[1].trim()
|
|
546
|
+
if (name && !names.includes(name)) names.push(name)
|
|
547
|
+
}
|
|
548
|
+
return names
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** 报错/提示里统一用 `${input:x}` 原样回显名字(名字本身不是密钥)。 */
|
|
552
|
+
const showInputs = (names) => names.map((name) => `\${input:${name}}`).join('、')
|
|
553
|
+
|
|
554
|
+
/** 条目会走哪条传输:只用于把不支持项说清楚,不参与归一化判定。 */
|
|
555
|
+
function guessTransport(raw) {
|
|
556
|
+
if (typeof raw.type === 'string' && raw.type.trim()) return raw.type.trim()
|
|
557
|
+
if (typeof raw.transport === 'string' && raw.transport.trim()) return raw.transport.trim()
|
|
558
|
+
if (typeof raw.command === 'string' && raw.command.trim()) return 'stdio'
|
|
559
|
+
const url = raw.url ?? raw.serverUrl ?? raw.server_url
|
|
560
|
+
return typeof url === 'string' && url.trim() ? 'streamable-http' : 'unknown'
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* 把一条 VS Code 条目里的 `${input:...}` 按位置分三类处理:
|
|
565
|
+
*
|
|
566
|
+
* - **url / command / cwd**:地址或可执行文件本身就是交互值,剔掉就没得导了,
|
|
567
|
+
* 所以整条标记为不支持、不进 `entries`。
|
|
568
|
+
* - **env / headers 的某一项**:先剔掉那一项,剩下的照常导入——少一个环境变量
|
|
569
|
+
* 会变成一个看得见的启动/鉴权失败,比整条消失更容易被用户理解和补救。
|
|
570
|
+
* - **args 的某一个元素**:同上,剔掉那个参数。
|
|
571
|
+
*
|
|
572
|
+
* 返回 `blocked` 非空表示整条不可用;否则 `raw` 是剔干净后的副本。
|
|
573
|
+
*/
|
|
574
|
+
function splitVscodeInputs(name, raw) {
|
|
575
|
+
// 顺序固定成 url → command → cwd → args → env → headers:前面出现就整条判死,
|
|
576
|
+
// 后面即使也用到 input 也一并记进 names,方便用户一次看全。
|
|
577
|
+
const hard = []
|
|
578
|
+
for (const field of ['url', 'serverUrl', 'server_url', 'command', 'cwd']) {
|
|
579
|
+
for (const found of vscodeInputNames(raw[field])) if (!hard.includes(found)) hard.push(found)
|
|
580
|
+
}
|
|
581
|
+
if (hard.length) {
|
|
582
|
+
for (const field of ['args', 'env', 'headers']) {
|
|
583
|
+
for (const value of field === 'args' && Array.isArray(raw.args) ? raw.args : Object.values(isObject(raw[field]) ? raw[field] : {})) {
|
|
584
|
+
for (const found of vscodeInputNames(value)) if (!hard.includes(found)) hard.push(found)
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return { blocked: hard, raw: null, warnings: [] }
|
|
588
|
+
}
|
|
589
|
+
const warnings = []
|
|
590
|
+
const soft = []
|
|
591
|
+
const clean = { ...raw }
|
|
592
|
+
const args = stripFromArgs(name, raw.args, soft, warnings)
|
|
593
|
+
if (args === undefined) delete clean.args
|
|
594
|
+
else clean.args = args
|
|
595
|
+
for (const field of ['env', 'headers']) {
|
|
596
|
+
// 不是对象就原样留给下游报错:这里静默删掉会把"env 写错了"变成"没有 env"。
|
|
597
|
+
if (!isObject(raw[field])) continue
|
|
598
|
+
const table = stripFromMap(name, field, raw[field], soft, warnings)
|
|
599
|
+
// 整个字段被剔空时删掉它,别让下游把 `env: {}` 当成"用户写了空表"。
|
|
600
|
+
if (table === undefined) delete clean[field]
|
|
601
|
+
else setOwn(clean, field, table)
|
|
602
|
+
}
|
|
603
|
+
return { blocked: [], raw: clean, warnings, names: soft }
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* args 里带 `${input:...}` 的元素剔除。剔除后可能剩一个空数组——那就把字段删掉,
|
|
608
|
+
* 别让下游把 `args: []` 当成"用户显式写了空参数表"。
|
|
609
|
+
*/
|
|
610
|
+
function stripFromArgs(name, args, soft, warnings) {
|
|
611
|
+
// `args` 写成单个字符串时下游会原样当成一个参数:它带 input 就同样不可用。
|
|
612
|
+
if (typeof args === 'string') {
|
|
613
|
+
const found = vscodeInputNames(args)
|
|
614
|
+
if (!found.length) return args
|
|
615
|
+
for (const inputName of found) if (!soft.includes(inputName)) soft.push(inputName)
|
|
616
|
+
warnings.push(`${name}.args 里的 ${showInputs(found)} 需要交互取值,已跳过这个参数`)
|
|
617
|
+
return undefined
|
|
618
|
+
}
|
|
619
|
+
if (!Array.isArray(args)) return args
|
|
620
|
+
const kept = []
|
|
621
|
+
for (const item of args) {
|
|
622
|
+
const found = vscodeInputNames(item)
|
|
623
|
+
if (!found.length) {
|
|
624
|
+
kept.push(item)
|
|
625
|
+
continue
|
|
626
|
+
}
|
|
627
|
+
for (const inputName of found) if (!soft.includes(inputName)) soft.push(inputName)
|
|
628
|
+
warnings.push(`${name}.args 里的 ${showInputs(found)} 需要交互取值,已跳过这个参数`)
|
|
629
|
+
}
|
|
630
|
+
return kept.length || args.length === 0 ? kept : undefined
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** env / headers 里值带 `${input:...}` 的项剔除。整表剔空时删掉字段。 */
|
|
634
|
+
function stripFromMap(name, field, table, soft, warnings) {
|
|
635
|
+
if (!isObject(table)) return undefined
|
|
636
|
+
const kept = {}
|
|
637
|
+
let dropped = 0
|
|
638
|
+
for (const [key, value] of Object.entries(table)) {
|
|
639
|
+
const found = vscodeInputNames(value)
|
|
640
|
+
if (!found.length) {
|
|
641
|
+
setOwn(kept, key, value)
|
|
642
|
+
continue
|
|
643
|
+
}
|
|
644
|
+
dropped += 1
|
|
645
|
+
for (const inputName of found) if (!soft.includes(inputName)) soft.push(inputName)
|
|
646
|
+
warnings.push(`${name}.${field}.${key} 需要交互取值(${showInputs(found)}),已跳过`)
|
|
647
|
+
}
|
|
648
|
+
return dropped && !Object.keys(kept).length ? undefined : kept
|
|
649
|
+
}
|
|
650
|
+
|
|
530
651
|
/**
|
|
531
652
|
* 按来源解析文本并归一化成 DSH spec。逐条目容错:一条不支持不影响其余条目。
|
|
532
653
|
* 返回的 `spec` 是 Host 侧内部值(含 `!!js` 表达式与字面密钥),**不得**直接发给浏览器。
|
|
@@ -541,33 +662,50 @@ export function normalizeSourceText(source, text, format = 'json') {
|
|
|
541
662
|
parsed = parseSourceText(format, content)
|
|
542
663
|
} catch (error) {
|
|
543
664
|
const line = parseFailure(error, content)
|
|
544
|
-
return { entries: [], warnings: [], error: `不是合法 ${PARSE_LABEL[format] || format}${line ? `(第 ${line} 行附近)` : ''}` }
|
|
665
|
+
return { entries: [], warnings: [], unsupportedEntries: [], error: `不是合法 ${PARSE_LABEL[format] || format}${line ? `(第 ${line} 行附近)` : ''}` }
|
|
545
666
|
}
|
|
546
|
-
if (!isObject(parsed)) return { entries: [], warnings: [], error: '配置根节点必须是对象' }
|
|
547
|
-
// VS Code 的
|
|
548
|
-
//
|
|
667
|
+
if (!isObject(parsed)) return { entries: [], warnings: [], unsupportedEntries: [], error: '配置根节点必须是对象' }
|
|
668
|
+
// OPT-17:VS Code 的 `${input:...}` 原本在这里整份拒绝,结果是一条交互变量吞掉
|
|
669
|
+
// 文件里其余 19 条能用的服务。改成逐条降级:位置可剔的(env/headers/args)剔掉后
|
|
670
|
+
// 照常导入并在 warnings 里说清楚,位置不可剔的(url/command/cwd)只把那一条记进
|
|
671
|
+
// unsupportedEntries,`error` 只留给"整份文件读不懂"。
|
|
549
672
|
const warnings = []
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
673
|
+
const unsupportedEntries = []
|
|
674
|
+
// 声明了 `inputs` 只说明这份文件"可能"用到交互变量,不再是拒绝理由;真正的判定
|
|
675
|
+
// 落在每个条目上——没被引用的 inputs 声明不该影响任何导入,也不该冒出一条噪音提示。
|
|
553
676
|
const selected = source.select(parsed, warnings)
|
|
554
|
-
if (selected === null) return { entries: [], warnings, error: '没有找到该客户端使用的 MCP 段落' }
|
|
677
|
+
if (selected === null) return { entries: [], warnings, unsupportedEntries, error: '没有找到该客户端使用的 MCP 段落' }
|
|
555
678
|
const entries = []
|
|
556
679
|
for (const item of selected) {
|
|
557
680
|
const carried = item.warnings || []
|
|
558
681
|
if (item.error) {
|
|
559
|
-
entries.push({ name: item.name, spec: null, error: item.error, warnings: carried })
|
|
682
|
+
entries.push({ name: item.name, spec: null, error: item.error, warnings: carried, unsupported: null })
|
|
683
|
+
continue
|
|
684
|
+
}
|
|
685
|
+
const split = splitVscodeInputs(item.name, item.raw)
|
|
686
|
+
const merged = [...carried, ...split.warnings]
|
|
687
|
+
// url / command / cwd 本身是交互值:这条没法安全转换。不进 `entries`(下游只认
|
|
688
|
+
// 可导入的 spec),改在 `unsupportedEntries` 里留一份可展示信息,别让它凭空消失。
|
|
689
|
+
if (split.blocked.length) {
|
|
690
|
+
unsupportedEntries.push({
|
|
691
|
+
name: item.name,
|
|
692
|
+
transport: guessTransport(item.raw),
|
|
693
|
+
reason: `地址/命令里用到 ${showInputs(split.blocked)},需要交互取值,DSH 无法安全转换`,
|
|
694
|
+
names: split.blocked,
|
|
695
|
+
})
|
|
560
696
|
continue
|
|
561
697
|
}
|
|
562
698
|
try {
|
|
563
|
-
const normalized = normalizeMcpImport({ mcpServers: { [item.name]:
|
|
564
|
-
entries.push({ name: item.name, spec: normalized.servers[0], error: null, warnings: [...
|
|
699
|
+
const normalized = normalizeMcpImport({ mcpServers: { [item.name]: split.raw } })
|
|
700
|
+
entries.push({ name: item.name, spec: normalized.servers[0], error: null, warnings: [...merged, ...normalized.warnings], unsupported: null })
|
|
565
701
|
} catch (error) {
|
|
566
|
-
entries.push({ name: item.name, spec: null, error: String(error?.message ?? error), warnings:
|
|
702
|
+
entries.push({ name: item.name, spec: null, error: String(error?.message ?? error), warnings: merged, unsupported: null })
|
|
567
703
|
}
|
|
568
704
|
}
|
|
569
|
-
|
|
570
|
-
|
|
705
|
+
// 全部条目都不可用时 `error` 仍然是 null:上层要能说"3 条不支持 / 0 条可导入",
|
|
706
|
+
// 而不是一句"失败"。只有真的一条 MCP 都没找到才落到下面的文案。
|
|
707
|
+
if (!entries.length && !unsupportedEntries.length) return { entries, warnings, unsupportedEntries, error: '这条配置里没有 MCP' }
|
|
708
|
+
return { entries, warnings, unsupportedEntries, error: '' }
|
|
571
709
|
}
|
|
572
710
|
|
|
573
711
|
export function hashSourceText(text) {
|
|
@@ -640,6 +778,7 @@ async function inspectImportLocation(source, location, ctx, readText) {
|
|
|
640
778
|
error: '',
|
|
641
779
|
contentHash: '',
|
|
642
780
|
entries: [],
|
|
781
|
+
unsupportedEntries: [],
|
|
643
782
|
warnings: [],
|
|
644
783
|
}
|
|
645
784
|
const picked = await pickCandidate(location, readText)
|
|
@@ -655,6 +794,7 @@ async function inspectImportLocation(source, location, ctx, readText) {
|
|
|
655
794
|
row.contentHash = hashSourceText(picked.text)
|
|
656
795
|
const normalized = normalizeSourceText(source, picked.text, picked.candidate.format)
|
|
657
796
|
row.entries = normalized.entries
|
|
797
|
+
row.unsupportedEntries = normalized.unsupportedEntries
|
|
658
798
|
row.warnings = normalized.warnings
|
|
659
799
|
row.error = normalized.error
|
|
660
800
|
return row
|