dsh-armory 2.3.1 → 2.4.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 +1 -1
- package/README.md +1 -1
- package/lib/client.js +194 -28
- package/lib/index.js +51 -4
- package/lib/mcp-oauth.js +130 -23
- package/lib/skill-dir-times.js +111 -0
- package/lib/typert.js +5 -0
- package/lib/wb-market.js +85 -16
- package/lib/whats-new-en.js +24 -0
- package/lib/whats-new.js +30 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
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
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.
|
|
7
7
|
>
|
|
8
|
-
> **New in v2.3.
|
|
8
|
+
> **New in v2.3.2**: fixed a whole-panel crash when installing an OAuth connector (a string style in the authorization dialog triggered React error #62); a failed browser open is now retried automatically. See the [changelog](.github/release-notes/v2.3.2.md).
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
11
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>
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> **DeepSeek Harness 的一体化管理看板**:MCP · 技能 · 专家 · 插件 · 工具账本 · 四大市场,浏览、搜索、一键安装、一键召唤。
|
|
6
6
|
> fork 自 [Imzl-zl/dsh-mcp-manager-ui](https://github.com/Imzl-zl/dsh-mcp-manager-ui)(MIT),现已扩展为完整军械库。
|
|
7
7
|
>
|
|
8
|
-
> **最新 v2.3.
|
|
8
|
+
> **最新 v2.3.2**:修复安装需 OAuth 连接器时的整面板崩溃(授权弹窗 style 字符串触发 React #62),浏览器打开失败会自动重试唤起。详见 [更新日志](.github/release-notes/v2.3.2.md)。
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
11
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>
|
package/lib/client.js
CHANGED
|
@@ -372,8 +372,14 @@ const EN_US_MESSAGES = {
|
|
|
372
372
|
"安装": "Install",
|
|
373
373
|
"安装 = 逐文件下载并 sha256 校验后写入 ~/.agents/skills/,即刻被 DSH 发现(无需重启)。已装版本重装即更新。": "Install = download file by file with sha256 verification into ~/.agents/skills/; DSH picks it up immediately (no restart). Reinstalling an installed version updates it.",
|
|
374
374
|
"安装中…": "Installing…",
|
|
375
|
+
"该技能正在安装中,请等当前任务结束": "This skill is already installing; wait for the current task to finish.",
|
|
375
376
|
"安装到全局": "Install globally",
|
|
376
377
|
"安装失败": "Install failed",
|
|
378
|
+
"安装于 {0}": "Installed {0}",
|
|
379
|
+
"安装时间": "Install time",
|
|
380
|
+
"安装时间与市场原名不可用:{0}。宿主侧接口在重启 DeepSeek Harness 后才会注册,重启后即可显示。": "Install times and market names unavailable: {0}. The host-side API registers only after restarting DeepSeek Harness; they will appear after a restart.",
|
|
381
|
+
"按安装时间倒序平铺;随 DSH 分发、运行时注册的技能没有安装时间,不在此列": "Flat list sorted by install time, newest first; skills shipped with DSH or registered at runtime have no install time and are not listed",
|
|
382
|
+
"它在市场里叫「{0}」,安装后看板显示的是 SKILL.md 的 name 字段。": "It is called \"{0}\" in the market; the board shows the name field from SKILL.md after install.",
|
|
377
383
|
"安装任务已结束": "Install finished",
|
|
378
384
|
"大肥鱼军械库": "Big Fish Armory",
|
|
379
385
|
"宿主正在停止,未升级;请重启后再试。": "Host is shutting down; not upgraded. Restart and try again.",
|
|
@@ -444,6 +450,7 @@ const EN_US_MESSAGES = {
|
|
|
444
450
|
"已重连 ": "Reconnected ",
|
|
445
451
|
"已隐藏 {0} 个不兼容条目": "Hidden {0} incompatible entries",
|
|
446
452
|
"市场": "Market",
|
|
453
|
+
"市场原名": "Market name",
|
|
447
454
|
"市场数据加载失败;其余分类仍可正常使用。": "Failed to load market data; other categories keep working.",
|
|
448
455
|
"市场:连接器、SkillHub 技能与专家召唤。": "Market: connectors, SkillHub skills and expert summoning.",
|
|
449
456
|
"归属存疑": "Ownership uncertain",
|
|
@@ -555,6 +562,7 @@ const EN_US_MESSAGES = {
|
|
|
555
562
|
"最新版本说明:": "Latest release notes: ",
|
|
556
563
|
"最近": "Recent",
|
|
557
564
|
"最近使用": "Recently used",
|
|
565
|
+
"最近安装": "Recently installed",
|
|
558
566
|
"服务器": "Servers",
|
|
559
567
|
"未知来源": "Unknown source",
|
|
560
568
|
"未知错误": "Unknown error",
|
|
@@ -568,6 +576,7 @@ const EN_US_MESSAGES = {
|
|
|
568
576
|
"本页没有已安装的技能": "No installed skills on this page",
|
|
569
577
|
"来自 mcp-client 最近日志": "From recent mcp-client logs",
|
|
570
578
|
"来自其他配置层,只读": "From another config layer, read-only",
|
|
579
|
+
"来自市场 {0}": "From market: {0}",
|
|
571
580
|
"构建授权已过期,未升级;请重试。": "Build authorization expired; not upgraded. Please retry.",
|
|
572
581
|
"查看": "View",
|
|
573
582
|
"查看原始 JSON": "View raw JSON",
|
|
@@ -656,6 +665,7 @@ const EN_US_MESSAGES = {
|
|
|
656
665
|
"自定义目录": "Custom directory",
|
|
657
666
|
"裁缝建议:": "Tailor suggestions: ",
|
|
658
667
|
"装 {0}": "Install {0}",
|
|
668
|
+
"装于 {0}": "Added {0}",
|
|
659
669
|
"解析中…": "Parsing…",
|
|
660
670
|
"解析并预览": "Parse and preview",
|
|
661
671
|
"认证": "Authentication",
|
|
@@ -674,6 +684,7 @@ const EN_US_MESSAGES = {
|
|
|
674
684
|
"读取 skill 列表失败": "Failed to read the skill list",
|
|
675
685
|
"读取专家目录失败": "Failed to read the expert catalog",
|
|
676
686
|
"读取中…": "Reading…",
|
|
687
|
+
"读取失败": "Read failed",
|
|
677
688
|
"读取已召唤专家失败": "Failed to read summoned experts",
|
|
678
689
|
"读取技能目录失败": "Failed to read the skill catalog",
|
|
679
690
|
"读取本机客户端配置失败": "Failed to read local client configs",
|
|
@@ -935,6 +946,10 @@ const EN_US_MESSAGES = {
|
|
|
935
946
|
"正在启动授权…": "Starting authorization…",
|
|
936
947
|
"无法启动授权流程": "Unable to start the authorization flow",
|
|
937
948
|
"没能自动唤起浏览器,请复制下方链接手动打开。": "The browser could not be opened automatically — copy the link below and open it manually.",
|
|
949
|
+
// 服务器不做 OAuth 发现(如腾讯 ima)时授权页根本开不出来,这里把手填令牌那条
|
|
950
|
+
// 兜底路的弹层文案补齐——只报一句「启动失败」等于把用户堵死在门外。
|
|
951
|
+
"该服务器无法自动打开授权页。": "This server can't have an authorization page opened automatically.",
|
|
952
|
+
"该服务器没有提供标准 OAuth 授权发现,无法自动打开授权页。如果你已经从服务方拿到访问令牌,粘贴在下面即可安装。": "This server publishes no standard OAuth authorization-server metadata, so an authorization page can't be opened automatically. If you already have an access token from the provider, paste it below to install.",
|
|
938
953
|
"授权链接": "Authorization link",
|
|
939
954
|
"重新打开浏览器": "Reopen browser",
|
|
940
955
|
"该服务器只接受自定义协议回调,浏览器跳转后页面会打不开。请把地址栏里的完整地址复制到下面。": "This server only accepts a custom-scheme callback, so the redirect page will not load. Copy the full address from the browser's address bar into the field below.",
|
|
@@ -1924,6 +1939,10 @@ body[data-ds-dark-theme]{--mcp-cat-dev:var(--dsw-static-deepseek-400);--mcp-cat-
|
|
|
1924
1939
|
.dsh-mcp-wb-search-input:hover:not(:disabled){border-color:var(--dsw-alias-border-l3)}
|
|
1925
1940
|
.dsh-mcp-wb-search-input:focus{border-color:var(--mcp-accent);background:var(--mcp-bg-2);box-shadow:0 0 0 3px color-mix(in srgb,var(--mcp-accent) 16%,transparent)}
|
|
1926
1941
|
.dsh-mcp-wb-search-input:disabled{opacity:.5;cursor:not-allowed}
|
|
1942
|
+
/* busy=服务端搜索在飞:输入框保持可用(disabled 会收走焦点,见 WbSearchBox 注释),
|
|
1943
|
+
只让放大镜呼吸提示「在加载」,光标与选区都不受影响。 */
|
|
1944
|
+
.dsh-mcp-wb-search.busy .dsh-mcp-wb-search-ico{color:var(--mcp-accent);animation:dsh-wb-search-busy 1.1s ease-in-out infinite}
|
|
1945
|
+
@keyframes dsh-wb-search-busy{0%,100%{opacity:1}50%{opacity:.3}}
|
|
1927
1946
|
.dsh-mcp-wb-search-input::-webkit-search-cancel-button{display:none}
|
|
1928
1947
|
.dsh-mcp-wb-search-clear{position:absolute;right:7px;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border:0;border-radius:999px;background:var(--dsw-alias-interactive-bg-hover);color:var(--mcp-mut);cursor:pointer;padding:0;transition:color .12s,background .12s}
|
|
1929
1948
|
.dsh-mcp-wb-search-clear:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-active)}
|
|
@@ -5393,6 +5412,9 @@ const SKILL_CATS = [
|
|
|
5393
5412
|
weak: ['debug', 'windows', '安装', '升级', '更新'] },
|
|
5394
5413
|
];
|
|
5395
5414
|
const SKILL_UNCAT = { key: 'uncat', get name() { return t('未分类'); }, color: 'var(--mcp-cat-uncat)' };
|
|
5415
|
+
// 「最近安装」视图的簇头:不是真分类——不进 SKILL_CATS 轨道、不参与 skillcat hash 校验,
|
|
5416
|
+
// 只在 recentOnly 开启时当单簇标题(时间线)用。
|
|
5417
|
+
const SKILL_RECENT_CAT = { key: 'recent', get name() { return t('最近安装'); }, color: 'var(--mcp-cat-all)' };
|
|
5396
5418
|
// skillcat hash 值合法性校验用。
|
|
5397
5419
|
const SKILL_CAT_KEYS = new Set(SKILL_CATS.map((c) => c.key));
|
|
5398
5420
|
const skillCatByKey = (key) => SKILL_CATS.find((c) => c.key === key) || null;
|
|
@@ -6662,6 +6684,9 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
6662
6684
|
const [onlyModel, setOnlyModel] = useState(false);
|
|
6663
6685
|
const [onlyUser, setOnlyUser] = useState(false);
|
|
6664
6686
|
const [sourceFilter, setSourceFilter] = useState('all');
|
|
6687
|
+
// 「最近安装」:按安装时间倒序平铺。分类是关键词猜的不一定准,时间线正好补导航缺口——
|
|
6688
|
+
// 刚装的永远排最前,用户一眼认出哪个是自己刚装的。
|
|
6689
|
+
const [recentOnly, setRecentOnly] = useState(false);
|
|
6665
6690
|
const [selected, setSelected] = useState('');
|
|
6666
6691
|
const [detail, setDetail] = useState(null);
|
|
6667
6692
|
const [detailLoading, setDetailLoading] = useState(false);
|
|
@@ -6765,6 +6790,36 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
6765
6790
|
return () => { alive = false; };
|
|
6766
6791
|
}, [call]);
|
|
6767
6792
|
|
|
6793
|
+
// 安装信息(skill 名字 → { installedAt, marketName?, marketplace? }):宿主自扫标准
|
|
6794
|
+
// 技能根目录、读 SKILL.md 头部 name 建映射后按名字返回——**不依赖 listSkills 行的
|
|
6795
|
+
// path**(注册表行可能整列缺 path,按 path 查会静默落空,实测踩过)。账本命中的
|
|
6796
|
+
// 附带市场原名:SKILL.md 的 name 可以和市场名不同,供卡片对应显示。
|
|
6797
|
+
// 失败不静默:新接口在旧宿主进程上不存在(404 / 未连接 ≈ 没重启),提示用户重启。
|
|
6798
|
+
const [installInfo, setInstallInfo] = useState(() => new Map());
|
|
6799
|
+
const [installInfoNote, setInstallInfoNote] = useState('');
|
|
6800
|
+
useEffect(() => {
|
|
6801
|
+
let alive = true;
|
|
6802
|
+
call('skillInstallInfo', skillScopeArg(scope)).then((res) => {
|
|
6803
|
+
if (!alive) return;
|
|
6804
|
+
if (res?.ok && res.info) {
|
|
6805
|
+
const map = new Map();
|
|
6806
|
+
for (const [name, row] of Object.entries(res.info)) {
|
|
6807
|
+
if (row && Number(row.installedAt) > 0) map.set(name, row);
|
|
6808
|
+
}
|
|
6809
|
+
setInstallInfo(map);
|
|
6810
|
+
setInstallInfoNote('');
|
|
6811
|
+
return;
|
|
6812
|
+
}
|
|
6813
|
+
setInstallInfo(new Map());
|
|
6814
|
+
setInstallInfoNote(t('安装时间与市场原名不可用:{0}。宿主侧接口在重启 DeepSeek Harness 后才会注册,重启后即可显示。', [String((res && res.error) || t('读取失败'))]));
|
|
6815
|
+
}).catch((e) => {
|
|
6816
|
+
if (!alive) return;
|
|
6817
|
+
setInstallInfo(new Map());
|
|
6818
|
+
setInstallInfoNote(t('安装时间与市场原名不可用:{0}。宿主侧接口在重启 DeepSeek Harness 后才会注册,重启后即可显示。', [String((e && e.message) || e)]));
|
|
6819
|
+
});
|
|
6820
|
+
return () => { alive = false; };
|
|
6821
|
+
}, [skills, scope, call]);
|
|
6822
|
+
|
|
6768
6823
|
const queryKey = (query || '').toLowerCase();
|
|
6769
6824
|
// 分类判定只依赖 name+description、与筛选无关 → 一次 memo 全量映射;
|
|
6770
6825
|
// 分类轨道计数固定全量(决策 9),不随来源/权限/搜索联动。
|
|
@@ -6794,15 +6849,45 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
6794
6849
|
return true;
|
|
6795
6850
|
});
|
|
6796
6851
|
const activeCat = cat ? skillCatByKey(cat) : null;
|
|
6852
|
+
// 卡片的安装时间徽标:30 天内相对时间(「3 天前装」,7 天内高亮),更早用绝对日期;
|
|
6853
|
+
// title 悬浮给完整日期。拿不到时间(bundled / runtime 等非磁盘来源)不渲染。
|
|
6854
|
+
const skillTimeOf = (s) => (installInfo.get(s.name)?.installedAt || 0);
|
|
6855
|
+
const skillTimeBadge = (s) => {
|
|
6856
|
+
const at = skillTimeOf(s);
|
|
6857
|
+
if (!at) return null;
|
|
6858
|
+
const days = Math.floor((Date.now() - at) / 86_400_000);
|
|
6859
|
+
const label = days <= 30 ? t('装于 {0}', [fmtLast(at)]) : t('装于 {0}', [wbFormatDate(at)]);
|
|
6860
|
+
return h(Badge, { cls: days <= 7 ? 's-on' : '', title: t('安装于 {0}', [wbFormatDate(at)]) }, label);
|
|
6861
|
+
};
|
|
6862
|
+
// 卡片上的市场原名徽标:只在「账本登记的市场名 ≠ SKILL.md 显示名」时出现——
|
|
6863
|
+
// 名字一致的没有歧义,不值得占一格。
|
|
6864
|
+
const skillMarketBadge = (s) => {
|
|
6865
|
+
const info = installInfo.get(s.name);
|
|
6866
|
+
if (!info?.marketName || info.marketName === s.name) return null;
|
|
6867
|
+
const label = info.marketplace === 'workbuddy-skills' ? t('SkillHub') : (info.marketplace || '');
|
|
6868
|
+
return h(Badge, {
|
|
6869
|
+
title: t('它在市场里叫「{0}」,安装后看板显示的是 SKILL.md 的 name 字段。', [info.marketName])
|
|
6870
|
+
+ (label ? ' ' + t('来源:{0}', [label]) : ''),
|
|
6871
|
+
}, t('来自市场 {0}', [info.marketName]));
|
|
6872
|
+
};
|
|
6873
|
+
// 「最近安装」视图行:只留拿得到安装时间的(bundled / runtime 没有落盘时间,不在此列),
|
|
6874
|
+
// 按时间倒序。其余筛选(搜索 / 来源 / 权限 / 分类)照常叠加。
|
|
6875
|
+
const visibleRows = useMemo(() => {
|
|
6876
|
+
if (!recentOnly) return filtered;
|
|
6877
|
+
return filtered.filter((s) => skillTimeOf(s) > 0).sort((a, b) => skillTimeOf(b) - skillTimeOf(a));
|
|
6878
|
+
// eslint 不识别 skillTimeOf 的闭包稳定性;installInfo 变化即重算。
|
|
6879
|
+
}, [filtered, installInfo, recentOnly]);
|
|
6797
6880
|
// 分类聚簇:网格与列表共用同一套簇序(决策 7:不按来源分组,改按分类聚簇,
|
|
6798
6881
|
// 簇序 = SKILL_CATS 声明序,未分类垫底,簇内保持注册表顺序)。网格渲染成卡片簇,
|
|
6799
6882
|
// 列表渲染成一个列表面板 + 分类分割线。选中具体分类时退化为单簇(无聚簇意义)。
|
|
6800
6883
|
const clustered = useMemo(() => {
|
|
6884
|
+
// 「最近安装」:单簇平铺,簇头当时间线标题用。
|
|
6885
|
+
if (recentOnly) return [{ key: 'recent', cat: SKILL_RECENT_CAT, rows: visibleRows }];
|
|
6801
6886
|
// 选中具体分类时退化为单簇(网格无聚簇意义,列表也无需一条分割线)。
|
|
6802
|
-
if (activeCat) return [{ key: activeCat.key, cat: activeCat, rows:
|
|
6887
|
+
if (activeCat) return [{ key: activeCat.key, cat: activeCat, rows: visibleRows }];
|
|
6803
6888
|
const order = [...SKILL_CATS.filter((c) => c.key !== 'all').map((c) => c.key), SKILL_UNCAT.key];
|
|
6804
6889
|
const buckets = new Map(order.map((k) => [k, []]));
|
|
6805
|
-
for (const s of
|
|
6890
|
+
for (const s of visibleRows) {
|
|
6806
6891
|
const key = (catOf.get(s.name) || SKILL_UNCAT).key;
|
|
6807
6892
|
if (!buckets.has(key)) buckets.set(key, []);
|
|
6808
6893
|
buckets.get(key).push(s);
|
|
@@ -6810,7 +6895,7 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
6810
6895
|
return order
|
|
6811
6896
|
.filter((k) => buckets.get(k).length)
|
|
6812
6897
|
.map((k) => ({ key: k, cat: k === SKILL_UNCAT.key ? SKILL_UNCAT : skillCatByKey(k), rows: buckets.get(k) }));
|
|
6813
|
-
}, [
|
|
6898
|
+
}, [visibleRows, catOf, view, activeCat, recentOnly]);
|
|
6814
6899
|
// 来源统计一次算完(不要在每个 chip 里各 filter 一遍)。
|
|
6815
6900
|
const sourceCounts = useMemo(() => {
|
|
6816
6901
|
const map = new Map();
|
|
@@ -6915,6 +7000,9 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
6915
7000
|
|
|
6916
7001
|
const summary = selected ? skills.find((s) => s.name === selected) : null;
|
|
6917
7002
|
const summaryCat = summary ? (catOf.get(summary.name) || SKILL_UNCAT) : null;
|
|
7003
|
+
// 详情的安装时间 + 市场原名:按 skill 名字从 skillInstallInfo 映射取。
|
|
7004
|
+
const summaryInstall = summary ? installInfo.get(summary.name) : null;
|
|
7005
|
+
const installTime = summaryInstall?.installedAt || 0;
|
|
6918
7006
|
|
|
6919
7007
|
// 网格视图不再占右侧详情栏:有选中就浮成居中弹窗;列表视图仍走右侧栏(决策见 §12)。
|
|
6920
7008
|
// 注意 view 的取值是 'list' | ''(网格是空串,见 setView(readHashParam('skillview') === 'list' ...)),
|
|
@@ -7067,9 +7155,11 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
7067
7155
|
))),
|
|
7068
7156
|
// 结果行 + 权限/来源 chips + 只读提示:位置保持原样(决策 8)。
|
|
7069
7157
|
h('div', { className: 'dsh-mcp-skill-result dsh-mcp-item-sub' },
|
|
7070
|
-
h('span', null, (loading && !refreshing) ? t('读取中…') : t('显示 {0} / {1} 个 skill', [
|
|
7071
|
-
+ (
|
|
7158
|
+
h('span', null, (loading && !refreshing) ? t('读取中…') : t('显示 {0} / {1} 个 skill', [visibleRows.length, skills.length])
|
|
7159
|
+
+ (visibleRows.length !== skills.length ? t('(共 {0},已筛选)', [skills.length]) : '')),
|
|
7072
7160
|
refreshing ? h('span', null, t(' · 刷新中…')) : null,
|
|
7161
|
+
// 安装时间数据源不可用(旧宿主 / 服务未连接):不静默,一行说明 + 悬浮全文。
|
|
7162
|
+
installInfoNote ? h('span', { title: installInfoNote }, ' · ' + installInfoNote) : null,
|
|
7073
7163
|
// OPT-31 · 5.1:把「能做什么、不能做什么」从代码注释提到界面上。
|
|
7074
7164
|
// 宿主注册表本身只有 listSkills / skillDetail 两个读接口;卸载是本插件补的
|
|
7075
7165
|
// removeSkill(仅限用户级/项目级目录)。这层边界不写明,用户会把「随包分发 /
|
|
@@ -7091,6 +7181,11 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
7091
7181
|
title: t('只显示用户可以用 /名字 触发的 skill'),
|
|
7092
7182
|
onClick: () => setOnlyUser((v) => !v),
|
|
7093
7183
|
}, t('用户可调用')),
|
|
7184
|
+
h('button', {
|
|
7185
|
+
type: 'button', className: 'dsh-mcp-chip-s' + (recentOnly ? ' on' : ''), 'aria-pressed': recentOnly,
|
|
7186
|
+
title: t('按安装时间倒序平铺;随 DSH 分发、运行时注册的技能没有安装时间,不在此列'),
|
|
7187
|
+
onClick: () => setRecentOnly((v) => !v),
|
|
7188
|
+
}, t('最近安装')),
|
|
7094
7189
|
[...sourceCounts.entries()].map(([source, count]) => h('button', {
|
|
7095
7190
|
key: source,
|
|
7096
7191
|
type: 'button',
|
|
@@ -7108,7 +7203,7 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
7108
7203
|
),
|
|
7109
7204
|
// 主区画布:网格 = 分类聚簇双壳卡片;列表 = 一个列表面板 + 分类分割线。
|
|
7110
7205
|
h('div', { className: 'dsh-mcp-skill-canvas' },
|
|
7111
|
-
|
|
7206
|
+
visibleRows.length === 0 && !loading
|
|
7112
7207
|
? emptyBoard
|
|
7113
7208
|
: (view === 'list'
|
|
7114
7209
|
? h('div', { className: 'dsh-mcp-skill-list', role: 'listbox', 'aria-label': t('skill 列表') },
|
|
@@ -7141,6 +7236,7 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
7141
7236
|
// §3.1 来源固定在行内:不再有来源分组头,滚动到中途也要知道这条来自哪里。
|
|
7142
7237
|
s.source ? h('span', { className: 'dsh-mcp-skill-src-badge', title: skillSourceLabel(s.source) }, skillSourceLabel(s.source)) : null,
|
|
7143
7238
|
s.provider ? h('span', { className: 'dsh-mcp-skill-src' }, s.provider) : null,
|
|
7239
|
+
skillTimeBadge(s),
|
|
7144
7240
|
),
|
|
7145
7241
|
s.description ? h('div', { className: 'dsh-mcp-skill-desc' }, s.description) : null,
|
|
7146
7242
|
),
|
|
@@ -7178,7 +7274,9 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
7178
7274
|
: h('div', { className: 'dsh-mcp-skill-desc dsh-mcp-skill-desc-none' }, t('(无描述)')),
|
|
7179
7275
|
h('div', { className: 'dsh-mcp-skill-card-foot' },
|
|
7180
7276
|
h('span', { className: 'dsh-mcp-skill-cat-tag' }, sCat.name),
|
|
7181
|
-
|
|
7277
|
+
skillTimeBadge(s),
|
|
7278
|
+
skillMarketBadge(s),
|
|
7279
|
+
h('div', { className: 'dsh-mcp-skill-sub' },
|
|
7182
7280
|
s.modelInvocable ? h(Badge, { cls: 't-http' }, t('模型可调用')) : null,
|
|
7183
7281
|
s.userInvocable ? h(Badge, { cls: 't-stdio' }, t('用户可调用')) : null,
|
|
7184
7282
|
),
|
|
@@ -7265,6 +7363,13 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
7265
7363
|
h('span', null, t('来源目录')),
|
|
7266
7364
|
h('b', null, skillSourceLabel(summary.source) + (summary.provider ? ' · ' + summary.provider : '')),
|
|
7267
7365
|
summary.path ? [h('span', { key: 'pl' }, t('文件位置')), h('b', { key: 'pv' }, summary.path)] : null,
|
|
7366
|
+
installTime ? [h('span', { key: 'itl' }, t('安装时间')), h('b', { key: 'itv' }, wbFormatDate(installTime))] : null,
|
|
7367
|
+
summaryInstall?.marketName
|
|
7368
|
+
? [h('span', { key: 'ml' }, t('市场原名')),
|
|
7369
|
+
h('b', { key: 'mnv' }, summaryInstall.marketName
|
|
7370
|
+
+ (summaryInstall.marketplace
|
|
7371
|
+
? ' · ' + (summaryInstall.marketplace === 'workbuddy-skills' ? t('SkillHub') : summaryInstall.marketplace)
|
|
7372
|
+
: ''))] : null,
|
|
7268
7373
|
),
|
|
7269
7374
|
),
|
|
7270
7375
|
h('div', { className: 'dsh-mcp-skill-d-actions' },
|
|
@@ -7902,7 +8007,7 @@ const ExpertMarket = ({ call }) => {
|
|
|
7902
8007
|
return h('div', null,
|
|
7903
8008
|
h('div', { className: 'dsh-mcp-wb-toolbar' },
|
|
7904
8009
|
h(WbSearchBox, {
|
|
7905
|
-
value: query, placeholder: t('搜索专家(花名 / 职业 / 描述 / 标签)'),
|
|
8010
|
+
value: query, placeholder: t('搜索专家(花名 / 职业 / 描述 / 标签)'), busy: loading,
|
|
7906
8011
|
onChange: (value) => { setQuery(value); setPage(1); },
|
|
7907
8012
|
}),
|
|
7908
8013
|
h('span', { className: 'dsh-mcp-builtin-count' },
|
|
@@ -8098,7 +8203,7 @@ const ExpertMarket = ({ call }) => {
|
|
|
8098
8203
|
// stale 团队行:详情弹层里给出直白解释 + 更新入口(同一条召唤链,upsert 整行重写)。
|
|
8099
8204
|
// 判定与卡片按钮同源(detailActions 在上方按 expertCardActions 算出)。
|
|
8100
8205
|
detailActions.update
|
|
8101
|
-
? h('p', { className: 'dsh-mcp-wb-modal-note', style:
|
|
8206
|
+
? h('p', { className: 'dsh-mcp-wb-modal-note', style: { color: 'var(--dsw-alias-state-warn-primary)' } },
|
|
8102
8207
|
t('⚠ 这条预设是旧版本召唤写入的:人设缺「DSH 运行时适配」桥接段,用它的新会话主理人只会调不存在的 TeamCreate/Agent,成员子智能体开不出来。点下方「更新预设」重跑召唤即可刷新。'))
|
|
8103
8208
|
: null,
|
|
8104
8209
|
),
|
|
@@ -9850,18 +9955,21 @@ const MarketPulse = ({ state, onJump, onClear }) => {
|
|
|
9850
9955
|
// 市场搜索框:左内置放大镜、圆角胶囊、聚焦高亮、右一键清空。
|
|
9851
9956
|
// 两处复用(连接器 / 插件):两者都是纯本地过滤,输入即过滤;
|
|
9852
9957
|
// 技能走服务端搜索,onCommit 由防抖或回车触发(见 WorkBuddyMarket 内注释)。
|
|
9853
|
-
|
|
9854
|
-
|
|
9958
|
+
// busy 只做视觉提示,**绝不 disabled 输入框**:disabled 会让浏览器立刻收走焦点,
|
|
9959
|
+
// 技能搜索防抖生效那一刻(含按 Delete 删字后的停顿)输入框被禁用 → 光标丢失,
|
|
9960
|
+
// 用户得重新点一次才能继续输入。加载态改由放大镜呼吸动画表达(见 .busy CSS)。
|
|
9961
|
+
const WbSearchBox = ({ value, onChange, placeholder, busy, onCommit }) =>
|
|
9962
|
+
h('div', { className: 'dsh-mcp-wb-search' + (busy ? ' busy' : '') },
|
|
9855
9963
|
h('span', { className: 'dsh-mcp-wb-search-ico', 'aria-hidden': true }, h(Icon, { d: ICONS.search, size: 14 })),
|
|
9856
9964
|
h('input', {
|
|
9857
9965
|
type: 'search', className: 'dsh-mcp-wb-search-input', placeholder,
|
|
9858
|
-
value,
|
|
9966
|
+
value, spellCheck: false, 'aria-busy': busy || undefined,
|
|
9859
9967
|
onChange: (e) => onChange(e.target.value),
|
|
9860
9968
|
onKeyDown: onCommit
|
|
9861
9969
|
? (e) => { if (e.key === 'Enter') { e.preventDefault(); onCommit(); } }
|
|
9862
9970
|
: undefined,
|
|
9863
9971
|
}),
|
|
9864
|
-
value
|
|
9972
|
+
value
|
|
9865
9973
|
? h('button', {
|
|
9866
9974
|
type: 'button', className: 'dsh-mcp-wb-search-clear', title: t('清空'),
|
|
9867
9975
|
'aria-label': t('清空搜索'),
|
|
@@ -9918,7 +10026,9 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
9918
10026
|
const [skillLoading, setSkillLoading] = useState(true);
|
|
9919
10027
|
const [skillError, setSkillError] = useState('');
|
|
9920
10028
|
const [skillDetail, setSkillDetail] = useState(null);
|
|
9921
|
-
|
|
10029
|
+
// 已安装集合:键是 namespace/slug 双键 → 值是安装时间(毫秒,0 = 取不到)。Map 上的
|
|
10030
|
+
// has() 与 Set 同用法,所有既有判定点不变;get() 供「安装于 …」徽标取时间。
|
|
10031
|
+
const [installedSkills, setInstalledSkills] = useState(new Map());
|
|
9922
10032
|
// §6.3 已安装筛选:SkillHub 每页 20 条远程分页,而「已装」是本地集合,
|
|
9923
10033
|
// 所以这层过滤只能作用于**当前页**——必须在标签上写清,否则会被误读成全库筛选。
|
|
9924
10034
|
// T04/R7:令牌弹窗。连接器 mcp.json 里带 ${...} 占位符时要先让用户填值——
|
|
@@ -9935,6 +10045,12 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
9935
10045
|
const authDialogRefNode = useRef(null);
|
|
9936
10046
|
useFocusTrap(authDialogRefNode, !!authDialog);
|
|
9937
10047
|
const [skillOnlyInstalled, setSkillOnlyInstalled] = useState(false);
|
|
10048
|
+
// 「已安装」集合的键是 namespace/slug 双键,不是 slug 单键:SkillHub 上同 slug 不同
|
|
10049
|
+
// namespace 的技能一大把(find-skills 实测三家在架)。单键会让没装过的同名卡片假显示
|
|
10050
|
+
// 「已安装」,在假卡片上点卸载又必撞「本机没有这个市场技能的安装记录」——用户报的
|
|
10051
|
+
// 「假安装」截图正是这一对症状。wbSkillInstalled 行的 namespace/slug 从目录名解析
|
|
10052
|
+
// (与安装时同一套 sanitize 口径),与卡片两字段天然一一对应。
|
|
10053
|
+
const skillInstalledKey = (row) => row.namespace + '/' + row.slug;
|
|
9938
10054
|
// OPT-21:两个详情弹层此前都没有 ref(焦点陷阱需要容器节点)。
|
|
9939
10055
|
const connectorDetailRef = useRef(null);
|
|
9940
10056
|
const skillDetailRef = useRef(null);
|
|
@@ -9944,7 +10060,7 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
9944
10060
|
|
|
9945
10061
|
const loadInstalled = useCallback(async () => {
|
|
9946
10062
|
const res = await call('wbSkillInstalled');
|
|
9947
|
-
if (res?.ok) setInstalledSkills(new
|
|
10063
|
+
if (res?.ok) setInstalledSkills(new Map((res.installed || []).map((row) => [skillInstalledKey(row), Math.max(0, Number(row.installedAt) || 0)])));
|
|
9948
10064
|
}, [call]);
|
|
9949
10065
|
|
|
9950
10066
|
// T04/R8:安装状态账本(三类安装的统一登记,宿主 ~/.dsh/armory/install-state.json)。
|
|
@@ -10130,7 +10246,27 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
10130
10246
|
setBusy(true); setBusyLabel(t('正在启动授权…'));
|
|
10131
10247
|
try {
|
|
10132
10248
|
const res = await call('wbOAuthBegin', { connectorId: item.id, serverName });
|
|
10133
|
-
if (!res?.ok)
|
|
10249
|
+
if (!res?.ok) {
|
|
10250
|
+
// 授权页这条路走不通(服务器不做 OAuth 发现、动态注册被拒、要求客户端认证)时,
|
|
10251
|
+
// 不能只甩一行红字就把用户关在门外:手填令牌那条兜底路还在,把弹层开出来摊给他。
|
|
10252
|
+
if (res?.manualTokenOnly) {
|
|
10253
|
+
setAuthDialog({
|
|
10254
|
+
item,
|
|
10255
|
+
serverName: res.serverName || serverName || item.name,
|
|
10256
|
+
sessionId: '',
|
|
10257
|
+
authorizeUrl: '',
|
|
10258
|
+
mode: 'manual-token',
|
|
10259
|
+
redirectUri: '',
|
|
10260
|
+
hint: '',
|
|
10261
|
+
status: 'manual',
|
|
10262
|
+
error: res.error || t('该服务器无法自动打开授权页。'),
|
|
10263
|
+
callbackUrl: '',
|
|
10264
|
+
manualToken: '',
|
|
10265
|
+
});
|
|
10266
|
+
return;
|
|
10267
|
+
}
|
|
10268
|
+
throw new Error(res?.error || t('无法启动授权流程'));
|
|
10269
|
+
}
|
|
10134
10270
|
setAuthDialog({
|
|
10135
10271
|
item,
|
|
10136
10272
|
serverName: res.serverName || serverName || item.name,
|
|
@@ -10144,6 +10280,16 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
10144
10280
|
callbackUrl: '',
|
|
10145
10281
|
manualToken: '',
|
|
10146
10282
|
});
|
|
10283
|
+
// 宿主第一条打开通路(shell / rundll32)失败时立即补一发 wbOAuthOpen 再试一次:
|
|
10284
|
+
// 多数失败是瞬时抖动,同一 URL 重开往往就成功了;补开成功就撤掉"没能唤起浏览器"
|
|
10285
|
+
// 的提示回到纯等待态,仍失败则弹层里的链接与「重新打开浏览器」按钮还在。
|
|
10286
|
+
if (!res.opened && res.authorizeUrl) {
|
|
10287
|
+
call('wbOAuthOpen', { url: res.authorizeUrl }).then((openRes) => {
|
|
10288
|
+
if (openRes?.opened && authDialogRef.current?.sessionId === res.sessionId) {
|
|
10289
|
+
setAuthDialog((cur) => (cur && cur.sessionId === res.sessionId ? { ...cur, error: '' } : cur));
|
|
10290
|
+
}
|
|
10291
|
+
}).catch(() => {});
|
|
10292
|
+
}
|
|
10147
10293
|
if (res.sessionId) {
|
|
10148
10294
|
stopAuthPoll();
|
|
10149
10295
|
authPollRef.current = setTimeout(() => { pollAuthOnce(res.sessionId); }, 1200);
|
|
@@ -10238,7 +10384,16 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
10238
10384
|
}
|
|
10239
10385
|
installConnector(item);
|
|
10240
10386
|
};
|
|
10387
|
+
const skillInstallInFlight = useRef(new Set());
|
|
10241
10388
|
const installSkill = async (skill) => {
|
|
10389
|
+
// 同一技能的上一次安装还在飞时直接挡回:宿主侧已有按目录的互斥锁兜底串行,
|
|
10390
|
+
// 这里再挡一层是为了不给用户制造「点了两次、两次都报成功」的错觉。
|
|
10391
|
+
const flightKey = skillInstalledKey(skill);
|
|
10392
|
+
if (skillInstallInFlight.current.has(flightKey)) {
|
|
10393
|
+
setNote(t('该技能正在安装中,请等当前任务结束'));
|
|
10394
|
+
return;
|
|
10395
|
+
}
|
|
10396
|
+
skillInstallInFlight.current.add(flightKey);
|
|
10242
10397
|
setBusy(true); setBusyLabel(t('正在下载并校验 {0}…', [skill.name])); setError('');
|
|
10243
10398
|
try {
|
|
10244
10399
|
const res = await call('wbSkillInstall', { slug: skill.slug, namespace: skill.namespace, name: skill.name });
|
|
@@ -10253,6 +10408,7 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
10253
10408
|
} catch (e) {
|
|
10254
10409
|
setError(String(e?.message || e));
|
|
10255
10410
|
} finally {
|
|
10411
|
+
skillInstallInFlight.current.delete(flightKey);
|
|
10256
10412
|
setBusy(false); setBusyLabel('');
|
|
10257
10413
|
}
|
|
10258
10414
|
};
|
|
@@ -10543,11 +10699,11 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
10543
10699
|
};
|
|
10544
10700
|
|
|
10545
10701
|
const connectorSearch = h(WbSearchBox, {
|
|
10546
|
-
value: connectorQuery, placeholder: t('搜索连接器(名称 / 说明 / ID)'),
|
|
10702
|
+
value: connectorQuery, placeholder: t('搜索连接器(名称 / 说明 / ID)'), busy: loading,
|
|
10547
10703
|
onChange: (value) => { setConnectorQuery(value); setConnectorPage(1); },
|
|
10548
10704
|
});
|
|
10549
10705
|
const skillSearch = h(WbSearchBox, {
|
|
10550
|
-
value: skillKeywordInput, placeholder: t('搜索 SkillHub 技能(19 万+)'),
|
|
10706
|
+
value: skillKeywordInput, placeholder: t('搜索 SkillHub 技能(19 万+)'), busy: skillLoading,
|
|
10551
10707
|
onChange: (value) => setSkillKeywordInput(value),
|
|
10552
10708
|
// 回车跳过防抖立即生效(防抖本身也会触发,双保险但幂等)。
|
|
10553
10709
|
onCommit: () => { setSkillPage(1); setSkillKeyword(skillKeywordInput); },
|
|
@@ -10561,9 +10717,9 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
10561
10717
|
// 已安装状态只在本地知道,所以这层过滤只作用于本页——计数文案里写清楚,
|
|
10562
10718
|
// 免得用户以为「只看已安装」能翻遍全库。
|
|
10563
10719
|
const shownSkills = skillOnlyInstalled
|
|
10564
|
-
? skills.filter((skill) => installedSkills.has(skill
|
|
10720
|
+
? skills.filter((skill) => installedSkills.has(skillInstalledKey(skill)))
|
|
10565
10721
|
: skills;
|
|
10566
|
-
const installedOnPage = skills.filter((skill) => installedSkills.has(skill
|
|
10722
|
+
const installedOnPage = skills.filter((skill) => installedSkills.has(skillInstalledKey(skill))).length;
|
|
10567
10723
|
|
|
10568
10724
|
// §6.1 子页签:原来用与顶栏 `.dsh-mcp-cat` 同款的胶囊按钮,两排长得一样,
|
|
10569
10725
|
// 用户分不清「换整页」还是「换市场内部子页」。改成下划线式页签 + 面包屑,
|
|
@@ -10619,7 +10775,7 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
10619
10775
|
? h('div', { className: 'dsh-mcp-wb-toolbar' },
|
|
10620
10776
|
connectorSearch,
|
|
10621
10777
|
h('label', { className: 'dsh-mcp-check', title: t('默认只显示可直接安装的条目;取消勾选可看到「仅展示」的不兼容条目') },
|
|
10622
|
-
h('input', { type: 'checkbox', checked: connectorOnlyReady,
|
|
10778
|
+
h('input', { type: 'checkbox', checked: connectorOnlyReady, onChange: (e) => { setConnectorOnlyReady(e.target.checked); setConnectorPage(1); } }),
|
|
10623
10779
|
t('只看可直接安装')),
|
|
10624
10780
|
// T04/R8:已安装筛选是全量本地筛选(跨页生效),不随分页变化。
|
|
10625
10781
|
h('label', { className: 'dsh-mcp-check', title: t('在全量 {0} 条目录里筛出已安装的连接器(跨页生效,与技能侧「仅本页」不同)', [connectors.length]) },
|
|
@@ -10681,12 +10837,12 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
10681
10837
|
? h('div', { className: 'dsh-mcp-wb-toolbar' },
|
|
10682
10838
|
skillSearch,
|
|
10683
10839
|
h('select', {
|
|
10684
|
-
className: 'dsh-mcp-input dsh-mcp-wb-sort', value: skillSort,
|
|
10840
|
+
className: 'dsh-mcp-input dsh-mcp-wb-sort', value: skillSort,
|
|
10685
10841
|
title: t('SkillHub 已不再提供全站下载量排序;这一项只在当前页内按下载量排序。'),
|
|
10686
10842
|
onChange: (e) => { setSkillSort(e.target.value); setSkillPage(1); },
|
|
10687
10843
|
}, WB_SKILLHUB_SORTS.map((option) => h('option', { key: option.key, value: option.key }, option.label))),
|
|
10688
10844
|
h('label', { className: 'dsh-mcp-check', title: t('仅在本页 {0} 条里筛出已安装的(SkillHub 是远程分页,无法跨页筛选)', [skills.length]) },
|
|
10689
|
-
h('input', { type: 'checkbox', checked: skillOnlyInstalled,
|
|
10845
|
+
h('input', { type: 'checkbox', checked: skillOnlyInstalled, onChange: (e) => setSkillOnlyInstalled(e.target.checked) }),
|
|
10690
10846
|
t('只看已安装')),
|
|
10691
10847
|
skillOnlyInstalled
|
|
10692
10848
|
? h('span', { className: 'dsh-mcp-builtin-count' }, t('本页已安装 {0} / {1}', [installedOnPage, skills.length]))
|
|
@@ -10808,7 +10964,11 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
10808
10964
|
tab === 'skill' && shownSkills.length
|
|
10809
10965
|
? h('div', { className: 'dsh-mcp-wb-grid' },
|
|
10810
10966
|
shownSkills.map((skill) => {
|
|
10811
|
-
const
|
|
10967
|
+
const installedKey = skillInstalledKey(skill);
|
|
10968
|
+
const installed = installedSkills.has(installedKey);
|
|
10969
|
+
// 安装时间:wbSkillInstalled 行自带(账本首次安装时间,无账本回落目录创建时间)。
|
|
10970
|
+
const installedAt = installedSkills.get(installedKey) || 0;
|
|
10971
|
+
const installedLabel = installedAt ? t('安装于 {0}', [wbFormatDate(installedAt)]) : '';
|
|
10812
10972
|
return h('div', { key: skill.namespace + '/' + skill.slug, className: 'dsh-mcp-card wb' },
|
|
10813
10973
|
h('div', { className: 'dsh-mcp-card-top' },
|
|
10814
10974
|
h('div', { className: 'dsh-mcp-card-person' },
|
|
@@ -10822,7 +10982,7 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
10822
10982
|
h('div', { className: 'dsh-mcp-card-vendor' }, skill.canonical || skill.namespace),
|
|
10823
10983
|
),
|
|
10824
10984
|
),
|
|
10825
|
-
installed ? h(Badge, { cls: 's-on' }, t('已安装')) : null,
|
|
10985
|
+
installed ? h(Badge, { cls: 's-on', title: installedLabel || undefined }, t('已安装')) : null,
|
|
10826
10986
|
),
|
|
10827
10987
|
h('div', { className: 'dsh-mcp-card-summary' }, skill.description || skill.name),
|
|
10828
10988
|
h(PresenceLine, {
|
|
@@ -10839,6 +10999,7 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
10839
10999
|
skill.verified ? h(Badge, { cls: 's-on' }, t('认证')) : null,
|
|
10840
11000
|
skill.version ? h(Badge, null, 'v' + skill.version) : null,
|
|
10841
11001
|
wbFormatDate(skill.updatedAt) ? h(Badge, null, wbFormatDate(skill.updatedAt)) : null,
|
|
11002
|
+
installed && installedAt ? h(Badge, null, installedLabel) : null,
|
|
10842
11003
|
),
|
|
10843
11004
|
h('div', { className: 'dsh-mcp-wb-card-actions' },
|
|
10844
11005
|
h('button', { type: 'button', className: 'dsh-mcp-btn', onClick: () => openSkillDetail(skill) }, t('详情')),
|
|
@@ -11042,9 +11203,11 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
11042
11203
|
h('p', { className: 'dsh-mcp-wb-modal-note', role: 'note' },
|
|
11043
11204
|
authDialog.status === 'authorized'
|
|
11044
11205
|
? t('{0}:授权成功,正在完成安装…', [authDialog.item?.name || authDialog.serverName])
|
|
11045
|
-
:
|
|
11206
|
+
: authDialog.mode === 'manual-token'
|
|
11207
|
+
? t('该服务器没有提供标准 OAuth 授权发现,无法自动打开授权页。如果你已经从服务方拿到访问令牌,粘贴在下面即可安装。')
|
|
11208
|
+
: t('该连接器要求在浏览器里完成一次登录授权。授权页会尝试自动打开;授权完成后这里会自动继续,无需手动刷新。')),
|
|
11046
11209
|
authDialog.error
|
|
11047
|
-
? h('p', { className: 'dsh-mcp-wb-modal-note', role: 'alert', style:
|
|
11210
|
+
? h('p', { className: 'dsh-mcp-wb-modal-note', role: 'alert', style: { color: 'var(--dsw-alias-state-warn-primary)' } }, authDialog.error)
|
|
11048
11211
|
: null,
|
|
11049
11212
|
authDialog.authorizeUrl
|
|
11050
11213
|
? h('label', { className: 'dsh-mcp-wb-tokenfield' },
|
|
@@ -11187,6 +11350,8 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
11187
11350
|
skillDetail.version ? h(Badge, null, 'v' + skillDetail.version) : null,
|
|
11188
11351
|
skillDetail.publisher ? h(Badge, null, skillDetail.publisher + (skillDetail.verifiedPublisher ? t('(已认证)') : '')) : null,
|
|
11189
11352
|
skillDetail.certifiedName ? h(Badge, { cls: 's-on' }, skillDetail.certifiedName) : null,
|
|
11353
|
+
(installedSkills.get(skillInstalledKey(skillDetail)) || 0)
|
|
11354
|
+
? h(Badge, { cls: 's-on' }, t('安装于 {0}', [wbFormatDate(installedSkills.get(skillInstalledKey(skillDetail)))])) : null,
|
|
11190
11355
|
skillDetail.createdAt ? h(Badge, null, t('加入时间 {0}', [wbFormatDate(skillDetail.createdAt)])) : null,
|
|
11191
11356
|
skillDetail.updatedAt ? h(Badge, null, t('更新于 {0}', [relativeLabel(skillDetail.updatedAt)])) : null,
|
|
11192
11357
|
Array.isArray(skillDetail.security) ? skillDetail.security.map((status, index) => h(Badge, { key: index, cls: status === 'benign' ? 's-on' : '' }, t('安全: ') + status)) : null,
|
|
@@ -11197,7 +11362,7 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
11197
11362
|
),
|
|
11198
11363
|
h('div', { className: 'dsh-mcp-modal-actions' },
|
|
11199
11364
|
h(Btn, { onClick: () => setSkillDetail(null), disabled: busy, children: t('关闭') }),
|
|
11200
|
-
installedSkills.has(skillDetail
|
|
11365
|
+
installedSkills.has(skillInstalledKey(skillDetail))
|
|
11201
11366
|
? h(Btn, { danger: true, icon: 'trash', disabled: busy, onClick: () => uninstallSkill(skillDetail), children: t('卸载') })
|
|
11202
11367
|
: h(Btn, { primary: true, icon: 'plus', disabled: busy || skillDetail.loading, onClick: () => installSkill(skillDetail), children: busy ? t('安装中…') : t('安装') }),
|
|
11203
11368
|
),
|
|
@@ -12300,6 +12465,7 @@ const MCP_CONTRIBUTION = {
|
|
|
12300
12465
|
{ id: 'dsh-armory#mcpManager/skillDetail', service: 'mcpManager', namespace: 'mcpManager', method: 'skillDetail', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('SkillDetailResult') },
|
|
12301
12466
|
{ id: 'dsh-armory#mcpManager/revealSkill', service: 'mcpManager', namespace: 'mcpManager', method: 'revealSkill', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('NoteResult') },
|
|
12302
12467
|
{ id: 'dsh-armory#mcpManager/removeSkill', service: 'mcpManager', namespace: 'mcpManager', method: 'removeSkill', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('NoteResult') },
|
|
12468
|
+
{ id: 'dsh-armory#mcpManager/skillInstallInfo', service: 'mcpManager', namespace: 'mcpManager', method: 'skillInstallInfo', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('SkillInstallInfoResult') },
|
|
12303
12469
|
// 自定义套件:读全量集合 / 整集合覆盖保存(与 typert.js、lib/index.js 三处同步,
|
|
12304
12470
|
// 漏一处这条通道恒 404 —— toolTailor、installInfo 各踩过一次)。
|
|
12305
12471
|
{ id: 'dsh-armory#mcpManager/listSkillSuites', service: 'mcpManager', namespace: 'mcpManager', method: 'listSkillSuites', invocation: { kind: 'direct' }, parameters: [], result: mcpCodec('SkillSuiteListResult') },
|