dsh-armory 2.3.2 → 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/lib/client.js +117 -11
- package/lib/index.js +8 -0
- package/lib/skill-dir-times.js +111 -0
- package/lib/typert.js +5 -0
- package/lib/wb-market.js +20 -2
- package/lib/whats-new-en.js +18 -0
- package/lib/whats-new.js +21 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -375,6 +375,11 @@ const EN_US_MESSAGES = {
|
|
|
375
375
|
"该技能正在安装中,请等当前任务结束": "This skill is already installing; wait for the current task to finish.",
|
|
376
376
|
"安装到全局": "Install globally",
|
|
377
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.",
|
|
378
383
|
"安装任务已结束": "Install finished",
|
|
379
384
|
"大肥鱼军械库": "Big Fish Armory",
|
|
380
385
|
"宿主正在停止,未升级;请重启后再试。": "Host is shutting down; not upgraded. Restart and try again.",
|
|
@@ -445,6 +450,7 @@ const EN_US_MESSAGES = {
|
|
|
445
450
|
"已重连 ": "Reconnected ",
|
|
446
451
|
"已隐藏 {0} 个不兼容条目": "Hidden {0} incompatible entries",
|
|
447
452
|
"市场": "Market",
|
|
453
|
+
"市场原名": "Market name",
|
|
448
454
|
"市场数据加载失败;其余分类仍可正常使用。": "Failed to load market data; other categories keep working.",
|
|
449
455
|
"市场:连接器、SkillHub 技能与专家召唤。": "Market: connectors, SkillHub skills and expert summoning.",
|
|
450
456
|
"归属存疑": "Ownership uncertain",
|
|
@@ -556,6 +562,7 @@ const EN_US_MESSAGES = {
|
|
|
556
562
|
"最新版本说明:": "Latest release notes: ",
|
|
557
563
|
"最近": "Recent",
|
|
558
564
|
"最近使用": "Recently used",
|
|
565
|
+
"最近安装": "Recently installed",
|
|
559
566
|
"服务器": "Servers",
|
|
560
567
|
"未知来源": "Unknown source",
|
|
561
568
|
"未知错误": "Unknown error",
|
|
@@ -569,6 +576,7 @@ const EN_US_MESSAGES = {
|
|
|
569
576
|
"本页没有已安装的技能": "No installed skills on this page",
|
|
570
577
|
"来自 mcp-client 最近日志": "From recent mcp-client logs",
|
|
571
578
|
"来自其他配置层,只读": "From another config layer, read-only",
|
|
579
|
+
"来自市场 {0}": "From market: {0}",
|
|
572
580
|
"构建授权已过期,未升级;请重试。": "Build authorization expired; not upgraded. Please retry.",
|
|
573
581
|
"查看": "View",
|
|
574
582
|
"查看原始 JSON": "View raw JSON",
|
|
@@ -657,6 +665,7 @@ const EN_US_MESSAGES = {
|
|
|
657
665
|
"自定义目录": "Custom directory",
|
|
658
666
|
"裁缝建议:": "Tailor suggestions: ",
|
|
659
667
|
"装 {0}": "Install {0}",
|
|
668
|
+
"装于 {0}": "Added {0}",
|
|
660
669
|
"解析中…": "Parsing…",
|
|
661
670
|
"解析并预览": "Parse and preview",
|
|
662
671
|
"认证": "Authentication",
|
|
@@ -675,6 +684,7 @@ const EN_US_MESSAGES = {
|
|
|
675
684
|
"读取 skill 列表失败": "Failed to read the skill list",
|
|
676
685
|
"读取专家目录失败": "Failed to read the expert catalog",
|
|
677
686
|
"读取中…": "Reading…",
|
|
687
|
+
"读取失败": "Read failed",
|
|
678
688
|
"读取已召唤专家失败": "Failed to read summoned experts",
|
|
679
689
|
"读取技能目录失败": "Failed to read the skill catalog",
|
|
680
690
|
"读取本机客户端配置失败": "Failed to read local client configs",
|
|
@@ -5402,6 +5412,9 @@ const SKILL_CATS = [
|
|
|
5402
5412
|
weak: ['debug', 'windows', '安装', '升级', '更新'] },
|
|
5403
5413
|
];
|
|
5404
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)' };
|
|
5405
5418
|
// skillcat hash 值合法性校验用。
|
|
5406
5419
|
const SKILL_CAT_KEYS = new Set(SKILL_CATS.map((c) => c.key));
|
|
5407
5420
|
const skillCatByKey = (key) => SKILL_CATS.find((c) => c.key === key) || null;
|
|
@@ -6671,6 +6684,9 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
6671
6684
|
const [onlyModel, setOnlyModel] = useState(false);
|
|
6672
6685
|
const [onlyUser, setOnlyUser] = useState(false);
|
|
6673
6686
|
const [sourceFilter, setSourceFilter] = useState('all');
|
|
6687
|
+
// 「最近安装」:按安装时间倒序平铺。分类是关键词猜的不一定准,时间线正好补导航缺口——
|
|
6688
|
+
// 刚装的永远排最前,用户一眼认出哪个是自己刚装的。
|
|
6689
|
+
const [recentOnly, setRecentOnly] = useState(false);
|
|
6674
6690
|
const [selected, setSelected] = useState('');
|
|
6675
6691
|
const [detail, setDetail] = useState(null);
|
|
6676
6692
|
const [detailLoading, setDetailLoading] = useState(false);
|
|
@@ -6774,6 +6790,36 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
6774
6790
|
return () => { alive = false; };
|
|
6775
6791
|
}, [call]);
|
|
6776
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
|
+
|
|
6777
6823
|
const queryKey = (query || '').toLowerCase();
|
|
6778
6824
|
// 分类判定只依赖 name+description、与筛选无关 → 一次 memo 全量映射;
|
|
6779
6825
|
// 分类轨道计数固定全量(决策 9),不随来源/权限/搜索联动。
|
|
@@ -6803,15 +6849,45 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
6803
6849
|
return true;
|
|
6804
6850
|
});
|
|
6805
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]);
|
|
6806
6880
|
// 分类聚簇:网格与列表共用同一套簇序(决策 7:不按来源分组,改按分类聚簇,
|
|
6807
6881
|
// 簇序 = SKILL_CATS 声明序,未分类垫底,簇内保持注册表顺序)。网格渲染成卡片簇,
|
|
6808
6882
|
// 列表渲染成一个列表面板 + 分类分割线。选中具体分类时退化为单簇(无聚簇意义)。
|
|
6809
6883
|
const clustered = useMemo(() => {
|
|
6884
|
+
// 「最近安装」:单簇平铺,簇头当时间线标题用。
|
|
6885
|
+
if (recentOnly) return [{ key: 'recent', cat: SKILL_RECENT_CAT, rows: visibleRows }];
|
|
6810
6886
|
// 选中具体分类时退化为单簇(网格无聚簇意义,列表也无需一条分割线)。
|
|
6811
|
-
if (activeCat) return [{ key: activeCat.key, cat: activeCat, rows:
|
|
6887
|
+
if (activeCat) return [{ key: activeCat.key, cat: activeCat, rows: visibleRows }];
|
|
6812
6888
|
const order = [...SKILL_CATS.filter((c) => c.key !== 'all').map((c) => c.key), SKILL_UNCAT.key];
|
|
6813
6889
|
const buckets = new Map(order.map((k) => [k, []]));
|
|
6814
|
-
for (const s of
|
|
6890
|
+
for (const s of visibleRows) {
|
|
6815
6891
|
const key = (catOf.get(s.name) || SKILL_UNCAT).key;
|
|
6816
6892
|
if (!buckets.has(key)) buckets.set(key, []);
|
|
6817
6893
|
buckets.get(key).push(s);
|
|
@@ -6819,7 +6895,7 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
6819
6895
|
return order
|
|
6820
6896
|
.filter((k) => buckets.get(k).length)
|
|
6821
6897
|
.map((k) => ({ key: k, cat: k === SKILL_UNCAT.key ? SKILL_UNCAT : skillCatByKey(k), rows: buckets.get(k) }));
|
|
6822
|
-
}, [
|
|
6898
|
+
}, [visibleRows, catOf, view, activeCat, recentOnly]);
|
|
6823
6899
|
// 来源统计一次算完(不要在每个 chip 里各 filter 一遍)。
|
|
6824
6900
|
const sourceCounts = useMemo(() => {
|
|
6825
6901
|
const map = new Map();
|
|
@@ -6924,6 +7000,9 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
6924
7000
|
|
|
6925
7001
|
const summary = selected ? skills.find((s) => s.name === selected) : null;
|
|
6926
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;
|
|
6927
7006
|
|
|
6928
7007
|
// 网格视图不再占右侧详情栏:有选中就浮成居中弹窗;列表视图仍走右侧栏(决策见 §12)。
|
|
6929
7008
|
// 注意 view 的取值是 'list' | ''(网格是空串,见 setView(readHashParam('skillview') === 'list' ...)),
|
|
@@ -7076,9 +7155,11 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
7076
7155
|
))),
|
|
7077
7156
|
// 结果行 + 权限/来源 chips + 只读提示:位置保持原样(决策 8)。
|
|
7078
7157
|
h('div', { className: 'dsh-mcp-skill-result dsh-mcp-item-sub' },
|
|
7079
|
-
h('span', null, (loading && !refreshing) ? t('读取中…') : t('显示 {0} / {1} 个 skill', [
|
|
7080
|
-
+ (
|
|
7158
|
+
h('span', null, (loading && !refreshing) ? t('读取中…') : t('显示 {0} / {1} 个 skill', [visibleRows.length, skills.length])
|
|
7159
|
+
+ (visibleRows.length !== skills.length ? t('(共 {0},已筛选)', [skills.length]) : '')),
|
|
7081
7160
|
refreshing ? h('span', null, t(' · 刷新中…')) : null,
|
|
7161
|
+
// 安装时间数据源不可用(旧宿主 / 服务未连接):不静默,一行说明 + 悬浮全文。
|
|
7162
|
+
installInfoNote ? h('span', { title: installInfoNote }, ' · ' + installInfoNote) : null,
|
|
7082
7163
|
// OPT-31 · 5.1:把「能做什么、不能做什么」从代码注释提到界面上。
|
|
7083
7164
|
// 宿主注册表本身只有 listSkills / skillDetail 两个读接口;卸载是本插件补的
|
|
7084
7165
|
// removeSkill(仅限用户级/项目级目录)。这层边界不写明,用户会把「随包分发 /
|
|
@@ -7100,6 +7181,11 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
7100
7181
|
title: t('只显示用户可以用 /名字 触发的 skill'),
|
|
7101
7182
|
onClick: () => setOnlyUser((v) => !v),
|
|
7102
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('最近安装')),
|
|
7103
7189
|
[...sourceCounts.entries()].map(([source, count]) => h('button', {
|
|
7104
7190
|
key: source,
|
|
7105
7191
|
type: 'button',
|
|
@@ -7117,7 +7203,7 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
7117
7203
|
),
|
|
7118
7204
|
// 主区画布:网格 = 分类聚簇双壳卡片;列表 = 一个列表面板 + 分类分割线。
|
|
7119
7205
|
h('div', { className: 'dsh-mcp-skill-canvas' },
|
|
7120
|
-
|
|
7206
|
+
visibleRows.length === 0 && !loading
|
|
7121
7207
|
? emptyBoard
|
|
7122
7208
|
: (view === 'list'
|
|
7123
7209
|
? h('div', { className: 'dsh-mcp-skill-list', role: 'listbox', 'aria-label': t('skill 列表') },
|
|
@@ -7150,6 +7236,7 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
7150
7236
|
// §3.1 来源固定在行内:不再有来源分组头,滚动到中途也要知道这条来自哪里。
|
|
7151
7237
|
s.source ? h('span', { className: 'dsh-mcp-skill-src-badge', title: skillSourceLabel(s.source) }, skillSourceLabel(s.source)) : null,
|
|
7152
7238
|
s.provider ? h('span', { className: 'dsh-mcp-skill-src' }, s.provider) : null,
|
|
7239
|
+
skillTimeBadge(s),
|
|
7153
7240
|
),
|
|
7154
7241
|
s.description ? h('div', { className: 'dsh-mcp-skill-desc' }, s.description) : null,
|
|
7155
7242
|
),
|
|
@@ -7187,7 +7274,9 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
7187
7274
|
: h('div', { className: 'dsh-mcp-skill-desc dsh-mcp-skill-desc-none' }, t('(无描述)')),
|
|
7188
7275
|
h('div', { className: 'dsh-mcp-skill-card-foot' },
|
|
7189
7276
|
h('span', { className: 'dsh-mcp-skill-cat-tag' }, sCat.name),
|
|
7190
|
-
|
|
7277
|
+
skillTimeBadge(s),
|
|
7278
|
+
skillMarketBadge(s),
|
|
7279
|
+
h('div', { className: 'dsh-mcp-skill-sub' },
|
|
7191
7280
|
s.modelInvocable ? h(Badge, { cls: 't-http' }, t('模型可调用')) : null,
|
|
7192
7281
|
s.userInvocable ? h(Badge, { cls: 't-stdio' }, t('用户可调用')) : null,
|
|
7193
7282
|
),
|
|
@@ -7274,6 +7363,13 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
7274
7363
|
h('span', null, t('来源目录')),
|
|
7275
7364
|
h('b', null, skillSourceLabel(summary.source) + (summary.provider ? ' · ' + summary.provider : '')),
|
|
7276
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,
|
|
7277
7373
|
),
|
|
7278
7374
|
),
|
|
7279
7375
|
h('div', { className: 'dsh-mcp-skill-d-actions' },
|
|
@@ -9930,7 +10026,9 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
9930
10026
|
const [skillLoading, setSkillLoading] = useState(true);
|
|
9931
10027
|
const [skillError, setSkillError] = useState('');
|
|
9932
10028
|
const [skillDetail, setSkillDetail] = useState(null);
|
|
9933
|
-
|
|
10029
|
+
// 已安装集合:键是 namespace/slug 双键 → 值是安装时间(毫秒,0 = 取不到)。Map 上的
|
|
10030
|
+
// has() 与 Set 同用法,所有既有判定点不变;get() 供「安装于 …」徽标取时间。
|
|
10031
|
+
const [installedSkills, setInstalledSkills] = useState(new Map());
|
|
9934
10032
|
// §6.3 已安装筛选:SkillHub 每页 20 条远程分页,而「已装」是本地集合,
|
|
9935
10033
|
// 所以这层过滤只能作用于**当前页**——必须在标签上写清,否则会被误读成全库筛选。
|
|
9936
10034
|
// T04/R7:令牌弹窗。连接器 mcp.json 里带 ${...} 占位符时要先让用户填值——
|
|
@@ -9962,7 +10060,7 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
9962
10060
|
|
|
9963
10061
|
const loadInstalled = useCallback(async () => {
|
|
9964
10062
|
const res = await call('wbSkillInstalled');
|
|
9965
|
-
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)])));
|
|
9966
10064
|
}, [call]);
|
|
9967
10065
|
|
|
9968
10066
|
// T04/R8:安装状态账本(三类安装的统一登记,宿主 ~/.dsh/armory/install-state.json)。
|
|
@@ -10866,7 +10964,11 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
10866
10964
|
tab === 'skill' && shownSkills.length
|
|
10867
10965
|
? h('div', { className: 'dsh-mcp-wb-grid' },
|
|
10868
10966
|
shownSkills.map((skill) => {
|
|
10869
|
-
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)]) : '';
|
|
10870
10972
|
return h('div', { key: skill.namespace + '/' + skill.slug, className: 'dsh-mcp-card wb' },
|
|
10871
10973
|
h('div', { className: 'dsh-mcp-card-top' },
|
|
10872
10974
|
h('div', { className: 'dsh-mcp-card-person' },
|
|
@@ -10880,7 +10982,7 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
10880
10982
|
h('div', { className: 'dsh-mcp-card-vendor' }, skill.canonical || skill.namespace),
|
|
10881
10983
|
),
|
|
10882
10984
|
),
|
|
10883
|
-
installed ? h(Badge, { cls: 's-on' }, t('已安装')) : null,
|
|
10985
|
+
installed ? h(Badge, { cls: 's-on', title: installedLabel || undefined }, t('已安装')) : null,
|
|
10884
10986
|
),
|
|
10885
10987
|
h('div', { className: 'dsh-mcp-card-summary' }, skill.description || skill.name),
|
|
10886
10988
|
h(PresenceLine, {
|
|
@@ -10897,6 +10999,7 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
10897
10999
|
skill.verified ? h(Badge, { cls: 's-on' }, t('认证')) : null,
|
|
10898
11000
|
skill.version ? h(Badge, null, 'v' + skill.version) : null,
|
|
10899
11001
|
wbFormatDate(skill.updatedAt) ? h(Badge, null, wbFormatDate(skill.updatedAt)) : null,
|
|
11002
|
+
installed && installedAt ? h(Badge, null, installedLabel) : null,
|
|
10900
11003
|
),
|
|
10901
11004
|
h('div', { className: 'dsh-mcp-wb-card-actions' },
|
|
10902
11005
|
h('button', { type: 'button', className: 'dsh-mcp-btn', onClick: () => openSkillDetail(skill) }, t('详情')),
|
|
@@ -11247,6 +11350,8 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
11247
11350
|
skillDetail.version ? h(Badge, null, 'v' + skillDetail.version) : null,
|
|
11248
11351
|
skillDetail.publisher ? h(Badge, null, skillDetail.publisher + (skillDetail.verifiedPublisher ? t('(已认证)') : '')) : null,
|
|
11249
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,
|
|
11250
11355
|
skillDetail.createdAt ? h(Badge, null, t('加入时间 {0}', [wbFormatDate(skillDetail.createdAt)])) : null,
|
|
11251
11356
|
skillDetail.updatedAt ? h(Badge, null, t('更新于 {0}', [relativeLabel(skillDetail.updatedAt)])) : null,
|
|
11252
11357
|
Array.isArray(skillDetail.security) ? skillDetail.security.map((status, index) => h(Badge, { key: index, cls: status === 'benign' ? 's-on' : '' }, t('安全: ') + status)) : null,
|
|
@@ -12360,6 +12465,7 @@ const MCP_CONTRIBUTION = {
|
|
|
12360
12465
|
{ id: 'dsh-armory#mcpManager/skillDetail', service: 'mcpManager', namespace: 'mcpManager', method: 'skillDetail', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('SkillDetailResult') },
|
|
12361
12466
|
{ id: 'dsh-armory#mcpManager/revealSkill', service: 'mcpManager', namespace: 'mcpManager', method: 'revealSkill', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('NoteResult') },
|
|
12362
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') },
|
|
12363
12469
|
// 自定义套件:读全量集合 / 整集合覆盖保存(与 typert.js、lib/index.js 三处同步,
|
|
12364
12470
|
// 漏一处这条通道恒 404 —— toolTailor、installInfo 各踩过一次)。
|
|
12365
12471
|
{ id: 'dsh-armory#mcpManager/listSkillSuites', service: 'mcpManager', namespace: 'mcpManager', method: 'listSkillSuites', invocation: { kind: 'direct' }, parameters: [], result: mcpCodec('SkillSuiteListResult') },
|
package/lib/index.js
CHANGED
|
@@ -14,6 +14,8 @@ import { deriveMcpPhase, formatMcpLog, sanitizeMcpLog } from "./mcp-observabilit
|
|
|
14
14
|
import { allMcpEntries, entriesForName, projectToolSchema, revisionOf, toolInventory } from "./mcp-registry.js";
|
|
15
15
|
// Skill 看板的卸载落盘点:来源白名单 + 根目录包含性 + 符号链接/根目录校验(lib/skill-remove.js)。
|
|
16
16
|
import { removeSkillFromDisk } from "./skill-remove.js";
|
|
17
|
+
// Skill 看板的安装时间 + 市场原名投影(名字自建映射,lib/skill-dir-times.js)。
|
|
18
|
+
import { skillInstallInfo } from "./skill-dir-times.js";
|
|
17
19
|
import { WHATS_NEW } from "./whats-new.js";
|
|
18
20
|
import { attachWhatsNewEnglish } from "./whats-new-en.js";
|
|
19
21
|
import { probeMcpServersAuthorization, wbConnectorCatalog, wbConnectorCachedCount, wbConnectorIcon, wbConnectorPreview, wbSkillCatalog, wbSkillDetail, wbSkillInstall, wbSkillInstalled, wbSkillUninstall } from "./wb-market.js";
|
|
@@ -1815,6 +1817,12 @@ let McpManagerGateway = (() => {
|
|
|
1815
1817
|
if (result === undefined) throw new Error("宿主没有挂载 skill 注册表(@deepseek-ai/dsh-skill),无法卸载");
|
|
1816
1818
|
return { note: `已卸载 ${name}(${result.kind === "symlink" ? "符号链接" : "目录"}:${result.path})` };
|
|
1817
1819
|
}
|
|
1820
|
+
// 安装时间 + 市场原名(按 skill 名字索引):宿主自扫标准技能根目录、读 SKILL.md
|
|
1821
|
+
// 头部 name 建立映射——不依赖 listSkills 行里的 path(注册表可能整列不给)。
|
|
1822
|
+
// 永不抛错——时间与原名是辅助信息,单个目录读失败只是没有该条目,不该拖垮看板。
|
|
1823
|
+
async skillInstallInfo(payload) {
|
|
1824
|
+
return skillInstallInfo(payload || {});
|
|
1825
|
+
}
|
|
1818
1826
|
// ── 自定义套件(Skill 看板)─────────────────────────────────────────
|
|
1819
1827
|
// 数据层在 lib/suites-registry.js(~/.dsh/armory/suites.json);这里只做 RPC 转发。
|
|
1820
1828
|
// listSkillSuites —— 永不抛错:文件缺失 / JSON 损坏 / 结构不对都回落空集合,
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Skill 看板的「安装时间 + 市场原名」投影(按 skill 名字索引)。
|
|
2
|
+
//
|
|
3
|
+
// 为什么不按 listSkills 行里的 path 查:宿主注册表的行**不一定**带 path(字段是条件
|
|
4
|
+
// 展开,lib/index.js 只透传注册表给的),实测有的版本整列都缺——按 path 查会整表静默
|
|
5
|
+
// 落空。这里改为宿主自建映射:扫描标准技能根目录,读每个目录 SKILL.md 头部的
|
|
6
|
+
// `name:` 字段得到「看板显示名」,建立 名字 → 目录 → 时间/原名 的映射,按名字返回。
|
|
7
|
+
// 面板的每一行必有 name(看板主键),join 永不落空。
|
|
8
|
+
//
|
|
9
|
+
// installedAt 取值优先级(与 wb-market.wbSkillInstalled 同一口径):
|
|
10
|
+
// ① 安装状态账本(kind=skill,按 installPath 归一对齐)登记的首次安装时间;
|
|
11
|
+
// ② 目录创建时间——重装 = rm 后 rename 换入新目录,创建时间即最近一次落盘;
|
|
12
|
+
// ③ 目录修改时间兜底(个别文件系统不吐 birthtime)。
|
|
13
|
+
// 没有 SKILL.md 的目录不会被注册表发现,直接跳过;同名目录按扫描序首个优先
|
|
14
|
+
// (agents 根在 dsh 根之前,与注册表的来源优先级同向)。
|
|
15
|
+
// 账本命中的条目附带 marketName / marketplace / marketVersion:账本登记的是市场里的
|
|
16
|
+
// 名字,看板显示的是 SKILL.md 的 name 字段——两者可以不同,投影出去供面板对应。
|
|
17
|
+
//
|
|
18
|
+
// 纯只读(readdir / stat / 只读 SKILL.md 头部 4KB);账本读失败只降级到 ②③。
|
|
19
|
+
// 存储根目录可注入(options.rootDir / DSH_ARMORY_HOME),home 可注入(options.home,测试用)。
|
|
20
|
+
import { open, readdir, stat } from "node:fs/promises";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { listInstalled } from "./wb-install-state.js";
|
|
24
|
+
|
|
25
|
+
/** join 键归一:分隔符统一 /、去尾斜杠。账本 installPath 与实际扫描目录是两套来源,
|
|
26
|
+
* Windows 下分隔符与结尾斜杠可能有差异,对齐前先拉齐形态。 */
|
|
27
|
+
const normPath = (value) => String(value || "").trim().replace(/[\\/]+/g, "/").replace(/\/+$/, "");
|
|
28
|
+
|
|
29
|
+
/** 只读文件头部 4KB——frontmatter 永远在最前面,整读大 SKILL.md 不值得。 */
|
|
30
|
+
async function head(path, bytes = 4096) {
|
|
31
|
+
const handle = await open(path, "r");
|
|
32
|
+
try {
|
|
33
|
+
const buffer = Buffer.alloc(bytes);
|
|
34
|
+
const { bytesRead } = await handle.read(buffer, 0, bytes, 0);
|
|
35
|
+
return buffer.toString("utf8", 0, bytesRead);
|
|
36
|
+
} finally {
|
|
37
|
+
await handle.close().catch(() => {});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** SKILL.md 头部 frontmatter 的 name 字段;没有 frontmatter / 没写 name 返回空串。 */
|
|
42
|
+
export function frontmatterName(text) {
|
|
43
|
+
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(String(text || ""));
|
|
44
|
+
if (!block) return "";
|
|
45
|
+
const line = /^name:[ \t]*(.+?)[ \t]*$/m.exec(block[1]);
|
|
46
|
+
return line ? line[1].replace(/^["']|["']$/g, "") : "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 扫描技能根目录,返回「看板名字 → 安装信息」映射。
|
|
51
|
+
* @param {object} payload 客户端信封:{ cwd? }(项目作用域时带项目根)
|
|
52
|
+
* @returns {Promise<{info: Record<string, {installedAt: number, dir: string,
|
|
53
|
+
* marketName?: string, marketplace?: string, marketVersion?: string}>}>}
|
|
54
|
+
*/
|
|
55
|
+
export async function skillInstallInfo(payload = {}, options = {}) {
|
|
56
|
+
const home = options.home || homedir();
|
|
57
|
+
const roots = [
|
|
58
|
+
join(home, ".agents", "skills"),
|
|
59
|
+
join(home, ".dsh", "skills"),
|
|
60
|
+
];
|
|
61
|
+
const cwd = String(payload?.cwd || "").trim();
|
|
62
|
+
if (cwd) {
|
|
63
|
+
roots.push(join(cwd, ".agents", "skills"));
|
|
64
|
+
roots.push(join(cwd, ".dsh", "skills"));
|
|
65
|
+
}
|
|
66
|
+
const ledger = await listInstalled("skill", options).catch(() => []);
|
|
67
|
+
const byPath = new Map(
|
|
68
|
+
ledger.filter((row) => row && row.installPath).map((row) => [normPath(row.installPath), row]),
|
|
69
|
+
);
|
|
70
|
+
const info = {};
|
|
71
|
+
for (const root of roots) {
|
|
72
|
+
let entries = [];
|
|
73
|
+
try {
|
|
74
|
+
entries = await readdir(root);
|
|
75
|
+
} catch {
|
|
76
|
+
continue; // 根目录不存在(没装过任何市场技能 / 没有 ~/.dsh/skills):跳过
|
|
77
|
+
}
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (entry.startsWith(".")) continue;
|
|
80
|
+
const dir = join(root, entry);
|
|
81
|
+
let dirStat;
|
|
82
|
+
try {
|
|
83
|
+
dirStat = await stat(dir);
|
|
84
|
+
} catch {
|
|
85
|
+
continue; // 竞态删除等:跳过
|
|
86
|
+
}
|
|
87
|
+
if (!dirStat.isDirectory()) continue;
|
|
88
|
+
let name = "";
|
|
89
|
+
try {
|
|
90
|
+
name = frontmatterName(await head(join(dir, "SKILL.md")));
|
|
91
|
+
} catch {
|
|
92
|
+
continue; // 没有 SKILL.md:注册表发现不了,不进映射
|
|
93
|
+
}
|
|
94
|
+
if (!name) name = entry;
|
|
95
|
+
if (info[name]) continue; // 同名:首个根/首个目录优先
|
|
96
|
+
const hit = byPath.get(normPath(dir));
|
|
97
|
+
const bornAt = Math.round(Number(dirStat.birthtimeMs) || 0);
|
|
98
|
+
const row = {
|
|
99
|
+
installedAt: (hit?.installedAt || 0) || bornAt || Math.round(Number(dirStat.mtimeMs) || 0),
|
|
100
|
+
dir,
|
|
101
|
+
};
|
|
102
|
+
if (hit?.name) {
|
|
103
|
+
row.marketName = hit.name;
|
|
104
|
+
row.marketplace = hit.source?.marketplace || "";
|
|
105
|
+
row.marketVersion = hit.version || "";
|
|
106
|
+
}
|
|
107
|
+
info[name] = row;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { info };
|
|
111
|
+
}
|
package/lib/typert.js
CHANGED
|
@@ -77,6 +77,11 @@ export const TYPERT = {
|
|
|
77
77
|
// 卸载已安装 skill:永久删除用户级/项目级 skills 根目录内的条目。三处契约缺一即 404,
|
|
78
78
|
// 删除规则全在 lib/skill-remove.js(来源白名单 / 根目录包含性 / 符号链接只删链接)。
|
|
79
79
|
mv('removeSkill', [payloadParam], 'NoteResult'),
|
|
80
|
+
// 安装时间 + 市场原名(Skill 看板卡片徽标 /「最近安装」排序的数据源):宿主自扫
|
|
81
|
+
// 标准根目录按 SKILL.md name 建映射,出 { ok, info: { [name]: { installedAt, ... } } }。
|
|
82
|
+
// 不按 listSkills 行的 path 查——注册表行可能整列缺 path(实测)。形状由
|
|
83
|
+
// lib/skill-dir-times.js 校验,与 wb* 系列同一宽容度(payload $any)。
|
|
84
|
+
mv('skillInstallInfo', [payloadParam], 'SkillInstallInfoResult'),
|
|
80
85
|
// 自定义套件(Skill 看板的「一组相关 skill 打包引用」):读 = 全量集合,
|
|
81
86
|
// 存 = 整集合覆盖(UI 一次提交右侧全部套件,不做增量 upsert)。数据层在
|
|
82
87
|
// lib/suites-registry.js,id 分配 / 成员去重保序 / 脏数据拒绝都在那边,
|
package/lib/wb-market.js
CHANGED
|
@@ -32,6 +32,8 @@ import { sourceFor } from "./wb-registry.js";
|
|
|
32
32
|
// R8:鉴权失败的判定与 OAuth 客户端共用同一份实现(见 mcp-oauth.js 的 mcpAuthFailure)。
|
|
33
33
|
// 两家各写一套「什么算 401」迟早分叉,而分叉的代价是用户看不到授权入口。
|
|
34
34
|
import { mcpAuthFailure } from "./mcp-oauth.js";
|
|
35
|
+
// R8:安装状态账本读侧——wbSkillInstalled 用它给每条已装技能补 installedAt。
|
|
36
|
+
import { listInstalled } from "./wb-install-state.js";
|
|
35
37
|
|
|
36
38
|
const CONNECTOR_ZIP_URL = "https://static.workbuddy.cn/connectors-config-v2/connectors-config.zip";
|
|
37
39
|
|
|
@@ -723,7 +725,12 @@ export async function wbSkillUninstall(payload = {}) {
|
|
|
723
725
|
await rm(finalDir, { recursive: true, force: true });
|
|
724
726
|
return { note: `已卸载 ${targetName}` };
|
|
725
727
|
}
|
|
726
|
-
/** 已装的市场技能(安装记录即目录本身),用于卡片上的「已安装」标记与卸载。
|
|
728
|
+
/** 已装的市场技能(安装记录即目录本身),用于卡片上的「已安装」标记与卸载。
|
|
729
|
+
* installedAt:安装时间(毫秒时间戳,0 = 完全取不到)。取值优先级:
|
|
730
|
+
* ① 安装状态账本(kind=skill 按 installPath 对齐)登记的首次安装时间;
|
|
731
|
+
* ② 目录创建时间——重装是 rm 后 rename 换入新目录,创建时间即最近一次安装;
|
|
732
|
+
* ③ 目录修改时间兜底(个别文件系统不吐 birthtime)。
|
|
733
|
+
* 账本读失败只降级到 ②③,不影响已装列表本身。 */
|
|
727
734
|
export async function wbSkillInstalled() {
|
|
728
735
|
const root = join(homedir(), ".agents", "skills");
|
|
729
736
|
const rows = [];
|
|
@@ -733,6 +740,10 @@ export async function wbSkillInstalled() {
|
|
|
733
740
|
} catch {
|
|
734
741
|
return { installed: [] };
|
|
735
742
|
}
|
|
743
|
+
const ledger = await listInstalled("skill").catch(() => []);
|
|
744
|
+
const ledgerAt = new Map(
|
|
745
|
+
ledger.filter((row) => row && row.installPath).map((row) => [row.installPath, row.installedAt || 0]),
|
|
746
|
+
);
|
|
736
747
|
for (const name of names) {
|
|
737
748
|
if (!name.includes("__")) continue;
|
|
738
749
|
const [namespace, ...slugParts] = name.split("__");
|
|
@@ -740,7 +751,14 @@ export async function wbSkillInstalled() {
|
|
|
740
751
|
try {
|
|
741
752
|
const info = await stat(dir);
|
|
742
753
|
if (!info.isDirectory()) continue;
|
|
743
|
-
|
|
754
|
+
const bornAt = Math.round(Number(info.birthtimeMs) || 0);
|
|
755
|
+
rows.push({
|
|
756
|
+
dirName: name,
|
|
757
|
+
namespace,
|
|
758
|
+
slug: slugParts.join("__"),
|
|
759
|
+
dir,
|
|
760
|
+
installedAt: ledgerAt.get(dir) || bornAt || Math.round(Number(info.mtimeMs) || 0),
|
|
761
|
+
});
|
|
744
762
|
} catch {
|
|
745
763
|
// 竞态删除:跳过
|
|
746
764
|
}
|
package/lib/whats-new-en.js
CHANGED
|
@@ -416,6 +416,24 @@ export const WHATS_NEW_EN = {
|
|
|
416
416
|
where: 'Market -> install a connector that needs authorization',
|
|
417
417
|
},
|
|
418
418
|
],
|
|
419
|
+
'2.4.0': [
|
|
420
|
+
{
|
|
421
|
+
text: '**Every Skill card now shows its install time**: "Added 3 h ago / Added 2026-09-01" sits right on the card (green highlight within 7 days). The install ledger\'s first-install time takes priority, falling back to the directory\'s creation time — the card you just installed is obvious at a glance; the detail panel gains an "Install time" row too',
|
|
422
|
+
where: 'Skill board / Market -> skills',
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
text: '**New "Recently installed" filter**: a flat timeline sorted by install time, newest first, with a green highlight within 7 days. Categories are keyword guesses and not always right — the timeline fills that navigation gap; search, source and category filters keep working on top',
|
|
426
|
+
where: 'Skill board -> filter bar',
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
text: '**Market-name correspondence**: when a skill\'s board name (the name field in SKILL.md) differs from its market name, the card shows a "From market: …" badge and the detail panel gains a "Market name" row — no more "I installed X from the market but can\'t find X on the board"; matching names stay badge-free to avoid noise',
|
|
430
|
+
where: 'Skill board / Market -> skills',
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
text: '**Install-time lookup now uses a host-built name map (skillInstallInfo)**: the host scans the standard skill roots, reads the name field from each SKILL.md header and returns the map keyed by name — no longer depending on the path column that the registry rows may omit entirely (hit in practice: path-based lookups silently returned nothing); when the API is unavailable the board says so and asks for a restart instead of staying silent',
|
|
434
|
+
where: 'Skill board',
|
|
435
|
+
},
|
|
436
|
+
],
|
|
419
437
|
};
|
|
420
438
|
|
|
421
439
|
/**
|
package/lib/whats-new.js
CHANGED
|
@@ -478,4 +478,25 @@ export const WHATS_NEW = [
|
|
|
478
478
|
},
|
|
479
479
|
],
|
|
480
480
|
},
|
|
481
|
+
{
|
|
482
|
+
version: '2.4.0',
|
|
483
|
+
items: [
|
|
484
|
+
{
|
|
485
|
+
text: '**每张 Skill 卡片显示安装时间**:「装于 3 小时前 / 装于 2026-09-01」直接标在卡片上(7 天内绿色高亮),安装账本登记的首次安装时间优先、无账本用目录创建时间兜底——刚装的是哪张一眼可见;详情面板同步补「安装时间」行',
|
|
486
|
+
where: 'Skill 看板 / 市场 → 技能',
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
text: '**新增「最近安装」筛选**:按安装时间倒序平铺成时间线,刚装的永远排最前,7 天内高亮——分类是关键词猜的、不一定准,时间线正好补上导航缺口;搜索、来源、分类筛选照常叠加',
|
|
490
|
+
where: 'Skill 看板 → 筛选条',
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
text: '**市场原名对应**:看板显示名(SKILL.md 的 name)与市场名不一致的技能,卡片出现「来自市场 原名」徽标、详情面板新增「市场原名」行——解决「在市场装了 X,看板里找不到 X」;名字一致的不显示,避免噪音',
|
|
494
|
+
where: 'Skill 看板 / 市场 → 技能',
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
text: '**安装时间查询改为宿主自建名字映射(skillInstallInfo)**:宿主扫描标准技能根目录、读 SKILL.md 头部 name 建立映射后按名字返回——不再依赖注册表行可能整列缺失的 path 字段(实测踩过:按 path 查静默落空);接口不可用时看板明确提示重启,不再无声无息',
|
|
498
|
+
where: 'Skill 看板',
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
},
|
|
481
502
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-armory",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Unified Skill + MCP + Expert + Plugin board for DeepSeek Harness (desktop app and Web GUI) — custom skill suites, skill install/uninstall with live refresh, global panel page, JSON import, WorkBuddy connector & SkillHub & Expert & DSH community plugin market installs, MCP OAuth authorization, tool-usage tailor with wire-tax trends, and profile-backed Host Remote management.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|