dsh-armory 2.2.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +11 -0
- package/README.md +14 -3
- package/lib/client.js +254 -89
- package/lib/index.js +4 -1
- package/lib/mcp-catalog-en.js +244 -0
- package/lib/mcp-config.js +15 -0
- package/lib/mcp-oauth.js +1 -1
- package/lib/whats-new-en.js +406 -0
- package/lib/whats-new.js +16 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -46,15 +46,54 @@ const { useState, useEffect, useCallback, useMemo, useRef } = React2;
|
|
|
46
46
|
// 将来加新语言可改走 require.async 外部字典文件。
|
|
47
47
|
const i18nMessages = {};
|
|
48
48
|
const setI18nMessages = (table) => { Object.assign(i18nMessages, table || {}); };
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
|
|
49
|
+
const resetI18nMessages = () => { for (const key of Object.keys(i18nMessages)) delete i18nMessages[key]; };
|
|
50
|
+
// OPT-14 · 1.1 / OPT-25 locale 判定(首个命中即返回):
|
|
51
|
+
// ① localStorage['dsh-armory.locale'] —— 显式覆盖(测试与高级用户)
|
|
52
|
+
// ② DSH 宿主 locale.active —— 设置 → 通用里的语言,用户真正看到的 UI 语言
|
|
53
|
+
// ③ navigator.language —— 宿主 locale 不可用时的浏览器回落
|
|
54
|
+
// ④ zh-CN —— 源语言兜底
|
|
55
|
+
// 只读 navigator 是 2.2.0 及以前的做法:中文系统 + DSH 设英文时 navigator 仍是
|
|
56
|
+
// zh-CN,英文表永远不装载(issue #1 的根因)。宿主服务用 ctx.get('locale') 取,
|
|
57
|
+
// **不写进 inject**——老版本 DSH 没有该服务时仍能照常加载。
|
|
58
|
+
// 所有失败路径(存储不可用、无 navigator、宿主快照抛错)都静默回落,绝不抛错。
|
|
59
|
+
const readStoredI18nLocale = () => {
|
|
52
60
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
return localStorage['dsh-armory.locale'] || null;
|
|
62
|
+
} catch { return null; }
|
|
63
|
+
};
|
|
64
|
+
const hostActiveLocale = (localeService) => {
|
|
65
|
+
try {
|
|
66
|
+
const snapshot = localeService && typeof localeService.getSnapshot === 'function' ? localeService.getSnapshot() : null;
|
|
67
|
+
return snapshot && typeof snapshot.active === 'string' && snapshot.active ? snapshot.active : null;
|
|
68
|
+
} catch { return null; }
|
|
69
|
+
};
|
|
70
|
+
const initialI18nLocale = readStoredI18nLocale()
|
|
71
|
+
|| (typeof navigator !== 'undefined' && navigator.language ? navigator.language : 'zh-CN');
|
|
72
|
+
let I18N_LOCALE = initialI18nLocale;
|
|
73
|
+
const resolveI18nLocale = (localeService) => readStoredI18nLocale()
|
|
74
|
+
|| hostActiveLocale(localeService)
|
|
75
|
+
|| initialI18nLocale
|
|
76
|
+
|| 'zh-CN';
|
|
77
|
+
// 切换语言时必须先清空旧字典:中文(源语言)要求空表才能恒等直出,
|
|
78
|
+
// 否则从 en 切回 zh 会残留英文译文。
|
|
79
|
+
// P1:源语言是中文、唯一译文是英文,所以按「zh* → 中文,其余 → 英文」判定。
|
|
80
|
+
// 非 zh 的非英文语言(ja / de / fr…)宁可给英文,也不把中文丢给看不懂的用户;
|
|
81
|
+
// en* 之外的显式覆盖同理。新增第三种语言时才需要把这里改成精确查表。
|
|
82
|
+
const isChineseLocale = (locale = I18N_LOCALE) => String(locale || '').toLowerCase().startsWith('zh');
|
|
83
|
+
const usesEnglish = (locale = I18N_LOCALE) => !isChineseLocale(locale);
|
|
84
|
+
const applyI18nLocale = (locale) => {
|
|
85
|
+
I18N_LOCALE = locale || initialI18nLocale || 'zh-CN';
|
|
86
|
+
resetI18nMessages();
|
|
87
|
+
if (usesEnglish()) setI18nMessages(EN_US_MESSAGES);
|
|
88
|
+
return I18N_LOCALE;
|
|
89
|
+
};
|
|
90
|
+
// 宿主 locale 订阅:包一层 try/catch,服务没实现 subscribe 或抛错都不影响面板。
|
|
91
|
+
const subscribeI18nLocale = (localeService, listener) => {
|
|
92
|
+
try {
|
|
93
|
+
if (localeService && typeof localeService.subscribe === 'function') return localeService.subscribe(listener);
|
|
94
|
+
} catch { /* 订阅失败:保留启动时判定的语言 */ }
|
|
95
|
+
return null;
|
|
96
|
+
};
|
|
58
97
|
const EN_US_MESSAGES = {
|
|
59
98
|
" + 捆绑技能": " + bundled skills",
|
|
60
99
|
" · {0} 个失败": " · {0} failed",
|
|
@@ -967,9 +1006,10 @@ const EN_US_MESSAGES = {
|
|
|
967
1006
|
|
|
968
1007
|
":": ": ",
|
|
969
1008
|
};
|
|
970
|
-
//
|
|
971
|
-
//
|
|
972
|
-
|
|
1009
|
+
// 模块加载时先用 ①/③ 判定的 locale 装载一次;apply() 拿到宿主 locale 后会再按
|
|
1010
|
+
// ② 重算(见 applyI18nLocale)。zh-CN(源语言)保持空字典恒等直出,其他语言
|
|
1011
|
+
// 缺键也原样回落,不出现 key 名。
|
|
1012
|
+
applyI18nLocale(I18N_LOCALE);
|
|
973
1013
|
const t = (s, vars) => {
|
|
974
1014
|
let text = i18nMessages[s] || s;
|
|
975
1015
|
if (vars && vars.length) {
|
|
@@ -977,6 +1017,59 @@ const t = (s, vars) => {
|
|
|
977
1017
|
}
|
|
978
1018
|
return text;
|
|
979
1019
|
};
|
|
1020
|
+
// OPT-25:宿主切换语言后通知已挂载的界面重渲染。模块级文案表已改为 getter /
|
|
1021
|
+
// 函数(见 ENTRY_LABELS / SKILL_CATS 等),重渲染时就会读到新字典,不需要重载页面。
|
|
1022
|
+
// 订阅者失败隔离:一个组件抛错不影响其它组件更新。
|
|
1023
|
+
const i18nChangeListeners = new Set();
|
|
1024
|
+
const emitI18nChange = () => {
|
|
1025
|
+
for (const listener of [...i18nChangeListeners]) {
|
|
1026
|
+
try { listener(); } catch { /* 单个订阅者失败不影响其它订阅者 */ }
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
const onI18nChange = (listener) => {
|
|
1030
|
+
i18nChangeListeners.add(listener);
|
|
1031
|
+
return () => { i18nChangeListeners.delete(listener); };
|
|
1032
|
+
};
|
|
1033
|
+
// 顶层槽位组件调用一次即可:locale 变化时强制自身重渲染。
|
|
1034
|
+
const useI18nRevision = () => {
|
|
1035
|
+
const [, setRevision] = useState(0);
|
|
1036
|
+
useEffect(() => onI18nChange(() => setRevision((value) => value + 1)), []);
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
// ── P1:数据型英文覆盖(更新日志 / 内置 MCP 目录)───────────────────────────
|
|
1040
|
+
// 这两类文案不是 UI 字面量,而是宿主随 RPC 送来的数据:更新日志在 whats-new.js,
|
|
1041
|
+
// 目录在生成的 mcp-catalog.js。它们不能走 t() 字典(键集合由那两个文件决定,硬抄
|
|
1042
|
+
// 进字典会让 i18n-coverage 的「死键」断言永久变红),所以由宿主同时送中文与英文
|
|
1043
|
+
// 字段,客户端按当前 locale 选择。英文缺失时逐条回落中文,绝不渲染 undefined。
|
|
1044
|
+
//
|
|
1045
|
+
// 两个 helper 的 `english` 参数只用于测试时显式指定;运行时用当前 locale 判定。
|
|
1046
|
+
const localizeWhatsNewEntries = (entries, english = usesEnglish()) => {
|
|
1047
|
+
if (!english || !Array.isArray(entries)) return entries;
|
|
1048
|
+
return entries.map((entry) => ({
|
|
1049
|
+
...entry,
|
|
1050
|
+
items: Array.isArray(entry.items)
|
|
1051
|
+
? entry.items.map((item) => ({
|
|
1052
|
+
...item,
|
|
1053
|
+
text: item.textEn || item.text,
|
|
1054
|
+
...(item.whereEn ? { where: item.whereEn } : {}),
|
|
1055
|
+
}))
|
|
1056
|
+
: [],
|
|
1057
|
+
}));
|
|
1058
|
+
};
|
|
1059
|
+
const localizeBuiltinItem = (item, english = usesEnglish()) => {
|
|
1060
|
+
if (!english || !item) return item;
|
|
1061
|
+
const authNote = item.authNoteEn || item.authNote;
|
|
1062
|
+
// 只在英文字段真的存在时才覆盖:缺译文时保留原键(连 undefined 键都不新增),
|
|
1063
|
+
// 面板据此区分「没有说明」与「说明是空的」。
|
|
1064
|
+
return {
|
|
1065
|
+
...item,
|
|
1066
|
+
...(item.labelEn ? { label: item.labelEn } : {}),
|
|
1067
|
+
...(item.summaryEn ? { summary: item.summaryEn } : {}),
|
|
1068
|
+
...(item.accessEn ? { access: item.accessEn } : {}),
|
|
1069
|
+
...(authNote ? { authNote } : {}),
|
|
1070
|
+
...(item.categoryLabelEn ? { categoryLabel: item.categoryLabelEn } : {}),
|
|
1071
|
+
};
|
|
1072
|
+
};
|
|
980
1073
|
|
|
981
1074
|
// ── 旧插件名 → 新插件名的 localStorage 键迁移 ──────────────────────────────
|
|
982
1075
|
// 1.9.0 起本插件由 dsh-skill-mcp-board 更名为 dsh-armory(package.json 与
|
|
@@ -2661,10 +2754,16 @@ const useFocusTrap = (ref, active) => {
|
|
|
2661
2754
|
|
|
2662
2755
|
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
2663
2756
|
const REDACTED_VALUE = '__DSH_MCP_REDACTED__';
|
|
2664
|
-
|
|
2757
|
+
// 屏蔽值在表单里需要**稳定哨兵**:locale 切换后译文会变,但已填进 state 的旧
|
|
2758
|
+
// 字符串不能随译文一起变,否则 toFormValue/fromFormValue 的相等判定会失配。
|
|
2759
|
+
// 因此内部只存 ASCII 哨兵,展示层再映射成当前语言的文案。
|
|
2760
|
+
const PRESERVED_VALUE_TOKEN = '__DSH_MCP_PRESERVED__';
|
|
2761
|
+
const preservedValueLabel = () => t('••••••(保留原值)');
|
|
2665
2762
|
const MASKED_VALUE = '••••••';
|
|
2666
|
-
const toFormValue = (value) => value === REDACTED_VALUE ?
|
|
2667
|
-
const fromFormValue = (value) => value ===
|
|
2763
|
+
const toFormValue = (value) => value === REDACTED_VALUE ? PRESERVED_VALUE_TOKEN : String(value || '');
|
|
2764
|
+
const fromFormValue = (value) => value === PRESERVED_VALUE_TOKEN ? REDACTED_VALUE : value;
|
|
2765
|
+
// args 是「每行一个」的多行串,哨兵可能夹在中间,所以按 token 全量替换而不是只比整串。
|
|
2766
|
+
const displayFormValue = (value) => String(value ?? '').split(PRESERVED_VALUE_TOKEN).join(preservedValueLabel());
|
|
2668
2767
|
const displayValue = (value) => value === REDACTED_VALUE ? MASKED_VALUE : value;
|
|
2669
2768
|
function setOwn(target, key, value) {
|
|
2670
2769
|
Object.defineProperty(target, key, { value, enumerable: true, configurable: true, writable: true });
|
|
@@ -3242,8 +3341,9 @@ const Pager = ({ page, pageCount, total, onPage, disabled }) => {
|
|
|
3242
3341
|
// oauth-or-pat)+ 有则挂 authNote。**这与市场连接器 tab 的 authMode 不是一回事**:
|
|
3243
3342
|
// authMode 是上游 connectors 清单里的自由文本,只能原样显示;auth 是枚举,可以映射
|
|
3244
3343
|
// 成人能读的标签。未知枚举值原样显示,绝不猜。
|
|
3245
|
-
const AUTH_KIND_LABELS = {
|
|
3246
|
-
|
|
3344
|
+
const AUTH_KIND_LABELS = { apiKey: 'API Key', oauth: 'OAuth', 'oauth-or-pat': 'OAuth / PAT' };
|
|
3345
|
+
// 惰性求值:模块加载时宿主 locale 还没接进来,只有 none 需要翻译。
|
|
3346
|
+
const authKindLabel = (kind) => (kind === 'none' ? t('免认证') : AUTH_KIND_LABELS[kind] || kind);
|
|
3247
3347
|
// 色语沿用 §1.3:oauth 系装完还得去浏览器授权,属「还需注意」→ 橙;apiKey 有既有
|
|
3248
3348
|
// 「需密钥」徽章兜住,这里只作信息性标注,用默认灰。
|
|
3249
3349
|
const authBadgeCls = (kind) => (kind === 'oauth' || kind === 'oauth-or-pat' ? 's-warn' : '');
|
|
@@ -3778,7 +3878,7 @@ const AddEditModal = ({ open, onClose, onSubmit, busy, initial, onRevealValue, r
|
|
|
3778
3878
|
const kv = (rows, setRows, i, field, v) => {
|
|
3779
3879
|
const next = rows.slice();
|
|
3780
3880
|
const row = { ...next[i], [field]: v };
|
|
3781
|
-
if (field === 'k' && row.v ===
|
|
3881
|
+
if (field === 'k' && row.v === PRESERVED_VALUE_TOKEN && v !== row.originalKey) row.v = '';
|
|
3782
3882
|
next[i] = row;
|
|
3783
3883
|
setRows(next);
|
|
3784
3884
|
};
|
|
@@ -3790,7 +3890,7 @@ const AddEditModal = ({ open, onClose, onSubmit, busy, initial, onRevealValue, r
|
|
|
3790
3890
|
: value && typeof value === 'object'
|
|
3791
3891
|
? Object.entries(value).map(([key, item]) => `${key}=${typeof item === 'object' ? JSON.stringify(item) : String(item)}`).join('\n')
|
|
3792
3892
|
: String(value ?? '');
|
|
3793
|
-
const visibleEditValue = (field, key, fallback) => Object.hasOwn(editRevealed, editRevealId(field, key)) ? editRevealed[editRevealId(field, key)] : fallback;
|
|
3893
|
+
const visibleEditValue = (field, key, fallback) => Object.hasOwn(editRevealed, editRevealId(field, key)) ? editRevealed[editRevealId(field, key)] : displayFormValue(fallback);
|
|
3794
3894
|
const hideEditReveal = (field, key) => setEditRevealed((prev) => {
|
|
3795
3895
|
const next = { ...prev };
|
|
3796
3896
|
delete next[editRevealId(field, key)];
|
|
@@ -3882,7 +3982,7 @@ const AddEditModal = ({ open, onClose, onSubmit, busy, initial, onRevealValue, r
|
|
|
3882
3982
|
h('label', null, 'URL'),
|
|
3883
3983
|
h('div', { className: 'dsh-mcp-input-action' },
|
|
3884
3984
|
h('input', { value: visibleEditValue('url', undefined, url), onChange: (e) => { hideEditReveal('url'); setUrl(e.target.value); }, placeholder: 'https://example.com/mcp' }),
|
|
3885
|
-
editing && url ===
|
|
3985
|
+
editing && url === PRESERVED_VALUE_TOKEN ? revealIcon('url', 'URL', () => toggleEditReveal('url')) : null,
|
|
3886
3986
|
),
|
|
3887
3987
|
)
|
|
3888
3988
|
: h('div', null,
|
|
@@ -3891,7 +3991,7 @@ const AddEditModal = ({ open, onClose, onSubmit, busy, initial, onRevealValue, r
|
|
|
3891
3991
|
h('label', null, t('参数(每行一个,原样传给进程)')),
|
|
3892
3992
|
h('div', { className: 'dsh-mcp-input-action' },
|
|
3893
3993
|
h('textarea', { rows: 4, value: visibleEditValue('args', undefined, args), onChange: (e) => { hideEditReveal('args'); setArgs(e.target.value); }, placeholder: '-y\nmcp-server@latest' }),
|
|
3894
|
-
editing && args ===
|
|
3994
|
+
editing && args === PRESERVED_VALUE_TOKEN ? revealIcon('args', t('参数'), () => toggleEditReveal('args')) : null,
|
|
3895
3995
|
),
|
|
3896
3996
|
),
|
|
3897
3997
|
h('div', { className: 'dsh-mcp-field' }, h('label', null, t('工作目录 cwd(可选)')), h('input', { value: cwd, onChange: (e) => setCwd(e.target.value), placeholder: 'C:\\path\\to\\workspace' })),
|
|
@@ -3906,7 +4006,7 @@ const AddEditModal = ({ open, onClose, onSubmit, busy, initial, onRevealValue, r
|
|
|
3906
4006
|
h('input', { placeholder: transport === 'http' ? 'Authorization' : 'API_KEY', value: row.k, onChange: (e) => { hideEditReveal(field, originalKey); kv(kvRows, setKvRows, i, 'k', e.target.value); } }),
|
|
3907
4007
|
h('input', { placeholder: transport === 'http' ? t('Bearer … 或 !!js process.env.KEY') : 'value', value: visibleEditValue(field, originalKey, row.v), onChange: (e) => { hideEditReveal(field, originalKey); kv(kvRows, setKvRows, i, 'v', e.target.value); } }),
|
|
3908
4008
|
h('div', { className: 'dsh-mcp-kv-row-actions' },
|
|
3909
|
-
editing && row.v ===
|
|
4009
|
+
editing && row.v === PRESERVED_VALUE_TOKEN ? revealIcon(id, row.k, () => toggleEditReveal(field, originalKey)) : null,
|
|
3910
4010
|
h('button', { type: 'button', className: 'dsh-mcp-kv-del', onClick: () => delRow(kvRows, setKvRows, i), title: t('删除此项'), 'aria-label': t('删除 ') + (row.k || t('配置项')) }, '✕'),
|
|
3911
4011
|
),
|
|
3912
4012
|
);
|
|
@@ -4869,12 +4969,14 @@ const ENTRY_VISIBILITY = {
|
|
|
4869
4969
|
sidebar: { panel: false, fab: false, sidebar: true },
|
|
4870
4970
|
all: { panel: true, fab: true, sidebar: true },
|
|
4871
4971
|
};
|
|
4972
|
+
// getter 而不是模块加载时求值:apply() 拿到宿主 locale 后文案才正确,
|
|
4973
|
+
// 且切语言后重渲染能直接读到新译文。
|
|
4872
4974
|
const ENTRY_LABELS = {
|
|
4873
|
-
both
|
|
4874
|
-
panel
|
|
4875
|
-
fab
|
|
4876
|
-
sidebar
|
|
4877
|
-
all
|
|
4975
|
+
get both() { return t('全局面板 + 悬浮按钮(推荐)'); },
|
|
4976
|
+
get panel() { return t('仅全局面板(左上角,"插件"旁)'); },
|
|
4977
|
+
get fab() { return t('仅悬浮按钮'); },
|
|
4978
|
+
get sidebar() { return t('仅侧栏底部入口'); },
|
|
4979
|
+
get all() { return t('三个入口全开'); },
|
|
4878
4980
|
};
|
|
4879
4981
|
const ENTRY_DEFAULT = 'both';
|
|
4880
4982
|
const ENTRY_MODES = Object.keys(ENTRY_VISIBILITY);
|
|
@@ -4946,22 +5048,24 @@ const PANEL_ID = 'mcp-manager';
|
|
|
4946
5048
|
// 入口排序:插件管理器用 0,定时任务用 10;取 5 让本入口正好落在「插件」旁边。
|
|
4947
5049
|
const PANEL_ORDER = 5;
|
|
4948
5050
|
// 看板五分类(MCP / Skill / 专家 / 工具裁缝 / 市场)随名称一并示人。
|
|
4949
|
-
|
|
4950
|
-
|
|
5051
|
+
// 函数而非常量:槽位注册发生在 apply() 里,那时才拿到宿主 locale。
|
|
5052
|
+
const panelLabel = () => t('技能 · MCP · 专家');
|
|
5053
|
+
// 左上角侧栏入口(sidebar.panellist)单独命名,与 panelLabel() 解耦:
|
|
4951
5054
|
// 该行的 label 同时是可见文字、无障碍名称和折叠提示,改动不牵动输入框快捷菜单、
|
|
4952
5055
|
// 会话头专家徽章、悬浮按钮和侧栏底部入口的文案。
|
|
4953
|
-
const
|
|
5056
|
+
const sidebarEntryLabel = () => t('大肥鱼军械库');
|
|
4954
5057
|
|
|
4955
5058
|
// skill 的 source 取值域来自官方 dsh-skill-filesystem 的 roots()(rank 顺序即优先级),
|
|
4956
5059
|
// 与 skill-registry 的 runtime。这里只做展示文案,不参与判定。
|
|
5060
|
+
// getter 保证每次读取都按当前 locale 翻译(切语言后重渲染即生效)。
|
|
4957
5061
|
const SKILL_SOURCE_LABELS = {
|
|
4958
|
-
'project-dsh'
|
|
4959
|
-
'project-agents'
|
|
4960
|
-
custom
|
|
4961
|
-
'user-dsh'
|
|
4962
|
-
'user-agents'
|
|
4963
|
-
bundled
|
|
4964
|
-
runtime
|
|
5062
|
+
get 'project-dsh'() { return t('项目 .dsh/skills'); },
|
|
5063
|
+
get 'project-agents'() { return t('项目 .agents/skills'); },
|
|
5064
|
+
get custom() { return t('自定义目录'); },
|
|
5065
|
+
get 'user-dsh'() { return t('用户 ~/.dsh/skills'); },
|
|
5066
|
+
get 'user-agents'() { return t('用户 ~/.agents/skills'); },
|
|
5067
|
+
get bundled() { return t('随 DSH 分发'); },
|
|
5068
|
+
get runtime() { return t('运行时注册'); },
|
|
4965
5069
|
};
|
|
4966
5070
|
const skillSourceLabel = (source) => SKILL_SOURCE_LABELS[source] || (source || t('未知来源'));
|
|
4967
5071
|
|
|
@@ -4977,24 +5081,24 @@ const skillRemovable = (source) => SKILL_REMOVABLE_SOURCES.has(String(source ||
|
|
|
4977
5081
|
// (跨类污染最小者优先),未命中再扫弱特征词,仍未命中落「未分类」——保证不丢条目。
|
|
4978
5082
|
// 匹配对象 = name + ' ' + description 的小写串;首个命中即定类。关键词表允许实现方微调。
|
|
4979
5083
|
const SKILL_CATS = [
|
|
4980
|
-
{ key: 'all', name
|
|
4981
|
-
{ key: 'dev', name
|
|
5084
|
+
{ key: 'all', get name() { return t('全部'); }, color: 'var(--mcp-cat-all)' },
|
|
5085
|
+
{ key: 'dev', get name() { return t('编程开发'); }, color: 'var(--mcp-cat-dev)',
|
|
4982
5086
|
strong: ['skill p8', 'arkts', 'arkui', '微信小程序', '小程序', '后端', '数据库', 'mysql', 'mariadb', 'api 设计', 'api设计', 'bug 诊断', 'bug诊断', '代码审查', '代码生成', '代码可读', '重构', '测试用例', '全栈', 'karpathy', 'spec 驱动', 'image-to-code', 'mcp server', 'sdk', 'monorepo', '源码'],
|
|
4983
5087
|
weak: ['code', 'coding', 'developer', 'typescript', 'javascript', 'python', 'rust', 'sql', '编程', '开发'] },
|
|
4984
|
-
{ key: 'design', name
|
|
5088
|
+
{ key: 'design', get name() { return t('设计与创作'); }, color: 'var(--mcp-cat-design)',
|
|
4985
5089
|
strong: ['imagegen', '品牌', 'brandkit', 'logo', '海报', 'stitch', 'mockup', '图标', '插画', '视频生成', '视频编辑', '视频翻译', '素材分析', '内容生产', '内容合成', '设计稿', '设计系统', 'landing page', '配图', '设计'],
|
|
4986
5090
|
weak: ['design', '创作', 'image', 'video', '视觉', '前端', 'frontend', '页面'] },
|
|
4987
|
-
{ key: 'office', name
|
|
5091
|
+
{ key: 'office', get name() { return t('文档办公'); }, color: 'var(--mcp-cat-office)',
|
|
4988
5092
|
strong: ['docx', 'xlsx', 'pptx', 'pdf', 'word 文档', 'excel', 'powerpoint', '文档', '公文', '报告', '表格', '幻灯', '公式缓存', 'libreoffice'],
|
|
4989
5093
|
weak: ['office', '写作', 'markdown', '笔记'] },
|
|
4990
|
-
{ key: 'auto', name
|
|
5094
|
+
{ key: 'auto', get name() { return t('效率与自动化'); }, color: 'var(--mcp-cat-auto)',
|
|
4991
5095
|
strong: ['browser', '浏览器', '定时', 'schedule', 'workflow', '工作流', '自动化', '批量', '表单填写', '数据提取', '提醒'],
|
|
4992
5096
|
weak: ['自动', '效率', '抓取', '搜索'] },
|
|
4993
|
-
{ key: 'ops', name
|
|
5097
|
+
{ key: 'ops', get name() { return t('开发运维'); }, color: 'var(--mcp-cat-ops)',
|
|
4994
5098
|
strong: ['sandbox', '沙箱', 'acl', '运维', '部署', 'deploy', 'release', '发布', '排查', '诊断', '环境', '崩溃', '代理下载', '签名', '供应链'],
|
|
4995
5099
|
weak: ['debug', 'windows', '安装', '升级', '更新'] },
|
|
4996
5100
|
];
|
|
4997
|
-
const SKILL_UNCAT = { key: 'uncat', name
|
|
5101
|
+
const SKILL_UNCAT = { key: 'uncat', get name() { return t('未分类'); }, color: 'var(--mcp-cat-uncat)' };
|
|
4998
5102
|
// skillcat hash 值合法性校验用。
|
|
4999
5103
|
const SKILL_CAT_KEYS = new Set(SKILL_CATS.map((c) => c.key));
|
|
5000
5104
|
const skillCatByKey = (key) => SKILL_CATS.find((c) => c.key === key) || null;
|
|
@@ -5026,10 +5130,10 @@ const skillScopeArg = (scope) => (scope === 'global' ? {} : { cwd: scope });
|
|
|
5026
5130
|
// OPT-16 · 3.2:专家详情弹层 persona 预览截断阈值(展开全文后渲染完整原文)。
|
|
5027
5131
|
const PERSONA_PREVIEW_MAX = 4000;
|
|
5028
5132
|
const TAILOR_WINDOWS = [
|
|
5029
|
-
{ key: '1', label
|
|
5030
|
-
{ key: '7', label
|
|
5031
|
-
{ key: '30', label
|
|
5032
|
-
{ key: '90', label
|
|
5133
|
+
{ key: '1', get label() { return t('今日'); }, days: 1 }, // OPT-24b · 4.1:今日档,回答「刚配置完它到底被调了几次」
|
|
5134
|
+
{ key: '7', get label() { return t('近 7 天'); }, days: 7 },
|
|
5135
|
+
{ key: '30', get label() { return t('近 30 天'); }, days: 30 },
|
|
5136
|
+
{ key: '90', get label() { return t('近 90 天'); }, days: 90 },
|
|
5033
5137
|
];
|
|
5034
5138
|
const fmtInt = (n) => (typeof n === 'number' ? n.toLocaleString(I18N_LOCALE || 'zh-CN') : '—'); // OPT-14 · 1.2:跟随 locale 回落
|
|
5035
5139
|
// OPT-18 · 4.2 零值显性化:zstd 不可用时所有调用计数恒为 0(解不出帧),这个 0 是「测不到」
|
|
@@ -5380,19 +5484,19 @@ const ToolTailor = ({ call, timer, report: reportProp, onReport, seedPending })
|
|
|
5380
5484
|
const BOARD_CATEGORIES = [
|
|
5381
5485
|
{ key: 'mcp', label: 'MCP', icon: 'plug' },
|
|
5382
5486
|
{ key: 'skill', label: 'Skill', icon: 'skill' },
|
|
5383
|
-
{ key: 'expert', label
|
|
5384
|
-
{ key: 'tailor', label
|
|
5385
|
-
{ key: 'wb', label
|
|
5487
|
+
{ key: 'expert', get label() { return t('专家'); }, icon: 'expert' },
|
|
5488
|
+
{ key: 'tailor', get label() { return t('工具裁缝'); }, icon: 'scissors' },
|
|
5489
|
+
{ key: 'wb', get label() { return t('市场'); }, icon: 'store' },
|
|
5386
5490
|
];
|
|
5387
5491
|
// 分类摘要文案表:把「这一页到底有什么」压成一句话 + 两三行事实。
|
|
5388
5492
|
// 拆成纯数据而不是写在渲染里,是因为它同时被 hover 浮层和键盘焦点复用,
|
|
5389
5493
|
// 且后面加分类时只要补一条,不必改渲染逻辑。
|
|
5390
5494
|
const CATEGORY_BLURB = {
|
|
5391
|
-
mcp
|
|
5392
|
-
skill
|
|
5393
|
-
expert
|
|
5394
|
-
tailor
|
|
5395
|
-
wb
|
|
5495
|
+
get mcp() { return t('按服务器看工具清单、连接状态与参数;可增删、重连、导入。'); },
|
|
5496
|
+
get skill() { return t('本地 Skill 清单:来源、是否启用、是否可被模型调用。'); },
|
|
5497
|
+
get expert() { return t('已召唤的专家:人设、捆绑技能、当前在场状态,可送还。'); },
|
|
5498
|
+
get tailor() { return t('工具使用账本:谁在被调用、谁在白缴上下文税。'); },
|
|
5499
|
+
get wb() { return t('市场:连接器、内置 MCP、SkillHub 技能与专家召唤。'); },
|
|
5396
5500
|
};
|
|
5397
5501
|
// §1.1 分类 hover 摘要。定位由父层算好(left),这里只负责内容与外观。
|
|
5398
5502
|
// 只用 pointer-events:none 的只读浮层:不承载交互,因此不需要焦点陷阱,
|
|
@@ -6587,32 +6691,32 @@ const connectorMatchesVisibleIn = (rowVisibleTags, selectedTags) => {
|
|
|
6587
6691
|
return rowVisibleTags.some((tag) => selectedTags.includes(tag));
|
|
6588
6692
|
};
|
|
6589
6693
|
const WB_SKILLHUB_SORTS = [
|
|
6590
|
-
{ key: 'downloads', label
|
|
6591
|
-
{ key: 'installs', label
|
|
6592
|
-
{ key: 'stars', label
|
|
6593
|
-
{ key: 'score', label
|
|
6694
|
+
{ key: 'downloads', get label() { return t('下载量(本页排序)'); } },
|
|
6695
|
+
{ key: 'installs', get label() { return t('最多安装'); } },
|
|
6696
|
+
{ key: 'stars', get label() { return t('最多星标'); } },
|
|
6697
|
+
{ key: 'score', get label() { return t('综合评分'); } },
|
|
6594
6698
|
];
|
|
6595
6699
|
// T05/R10:连接器排序(全量本地排序,不受分页影响)。
|
|
6596
6700
|
// 目录自带 skillCount(捆绑技能数)与 visibleIn(适用环境标签数),够做有意义的排序;
|
|
6597
6701
|
// upstream 无下载量/评分字段,不臆造。
|
|
6598
6702
|
const WB_CONNECTOR_SORTS = [
|
|
6599
|
-
{ key: 'default', label
|
|
6600
|
-
{ key: 'skills', label
|
|
6601
|
-
{ key: 'name', label
|
|
6703
|
+
{ key: 'default', get label() { return t('目录默认顺序'); } },
|
|
6704
|
+
{ key: 'skills', get label() { return t('技能数由多到少'); } },
|
|
6705
|
+
{ key: 'name', get label() { return t('名称 A→Z'); } },
|
|
6602
6706
|
];
|
|
6603
6707
|
// T06/Awesome:DSH 社区目录排序(全量本地排序,不受分页影响)。
|
|
6604
6708
|
// upstream 自带 stars 与 lastVerified,无下载量/评分字段,不臆造。
|
|
6605
6709
|
const AWESOME_SORTS = [
|
|
6606
|
-
{ key: 'stars', label
|
|
6607
|
-
{ key: 'recent', label
|
|
6608
|
-
{ key: 'name', label
|
|
6710
|
+
{ key: 'stars', get label() { return t('最多星标'); } },
|
|
6711
|
+
{ key: 'recent', get label() { return t('最新收录'); } },
|
|
6712
|
+
{ key: 'name', get label() { return t('名称 A→Z'); } },
|
|
6609
6713
|
];
|
|
6610
6714
|
const AWESOME_CATEGORIES = [
|
|
6611
|
-
{ key: 'all', label
|
|
6612
|
-
{ key: 'plugin', label
|
|
6613
|
-
{ key: 'skill', label
|
|
6614
|
-
{ key: 'bundle', label
|
|
6615
|
-
{ key: 'tool', label
|
|
6715
|
+
{ key: 'all', get label() { return t('全部'); } },
|
|
6716
|
+
{ key: 'plugin', get label() { return t('插件'); } },
|
|
6717
|
+
{ key: 'skill', get label() { return t('技能'); } },
|
|
6718
|
+
{ key: 'bundle', get label() { return t('套件'); } },
|
|
6719
|
+
{ key: 'tool', get label() { return t('工具'); } },
|
|
6616
6720
|
];
|
|
6617
6721
|
const AWESOME_TAGS = ['ui', 'terminal', 'agents', 'memory', 'models', 'vision', 'capabilities', 'channels', 'notifications', 'usage', 'observability', 'marketplace', 'devtools', 'safety', 'knowledge', 'interop', 'fun'];
|
|
6618
6722
|
const AWESOME_PAGE_SIZE = 50;
|
|
@@ -7295,15 +7399,18 @@ const expertCardActions = ({ summoned, expertType, personaText }) => ({
|
|
|
7295
7399
|
// 也不要让一条读不出来的旧行悄悄用坏人设开新会话)。
|
|
7296
7400
|
const personaTextOf = (rows, plugin) => (Array.isArray(rows) ? rows.find((row) => row?.plugin === plugin)?.personaText : '') || '';
|
|
7297
7401
|
const QUICK_MENU_KINDS = [
|
|
7298
|
-
{ key: 'skill', label
|
|
7299
|
-
{ key: 'mcp', label: 'MCP', icon: 'plug', hint
|
|
7300
|
-
{ key: 'expert', label
|
|
7402
|
+
{ key: 'skill', get label() { return t('技能'); }, icon: 'skill', get hint() { return t('技能按需触发:选择后以 /技能名 插入输入框'); } },
|
|
7403
|
+
{ key: 'mcp', label: 'MCP', icon: 'plug', get hint() { return t('MCP 工具按 server 生效:选择后点名 mcp__服务__* 工具'); } },
|
|
7404
|
+
{ key: 'expert', get label() { return t('专家'); }, icon: 'expert', get hint() { return t('专家随时挂/换/摘:点一个即接管本会话,再点别的直接换,点 chip 的 × 取消'); } },
|
|
7301
7405
|
];
|
|
7302
7406
|
// 子菜单列表每屏固定 6 行(6 × 28px),更多条目在 .dsh-mcp-quick-list 里纵向滚动;
|
|
7303
7407
|
// 「最近」徽章只标最近池里的前 6 名(首屏可见的那批),避免整列都是徽章。
|
|
7304
7408
|
// 专家预设显示名前缀:召唤时写死的 `WorkBuddy 专家 · <显示名>`(见 expert-market.js
|
|
7305
7409
|
// buildExpertPresetRow),宿主的会话头预设标签原样显示它——用来识别「那条会过期的标签」。
|
|
7306
|
-
|
|
7410
|
+
// 这是**匹配用**前缀,不是展示文案:宿主 expert-market.js 写入预设定名时永远用
|
|
7411
|
+
// 中文 `WorkBuddy 专家 · `(见 buildExpertPresetRow),这里必须逐字相同。
|
|
7412
|
+
// 不能包 t()——英文环境下翻译成 "WorkBuddy Expert · " 会永远匹配不上宿主标签。
|
|
7413
|
+
const EXPERT_PRESET_LABEL_PREFIX = 'WorkBuddy 专家 · ';
|
|
7307
7414
|
// 专家挂载状态的最小事件总线:快速菜单挂/换/摘专家后立刻通知会话头徽章与菜单 chip,
|
|
7308
7415
|
// 三处显示同一帧内对齐(不再各自等重挂载)。低频轮询只作兜底,覆盖「别的入口挂的专家」。
|
|
7309
7416
|
const expertChangeListeners = new Set();
|
|
@@ -7333,6 +7440,7 @@ const quickMentionText = (kind, item) => {
|
|
|
7333
7440
|
return /^[\w-]+$/.test(token) ? `/${token} ` : t('{0}:', [token]);
|
|
7334
7441
|
};
|
|
7335
7442
|
const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
7443
|
+
useI18nRevision(); // 宿主切语言后重渲染,模块级 getter 文案即时更新
|
|
7336
7444
|
const [open, setOpen] = useState(false);
|
|
7337
7445
|
const [hoverKind, setHoverKind] = useState(null);
|
|
7338
7446
|
const [recent, setRecent] = useState(null); // { skill: [], mcp: [], expert: [] }
|
|
@@ -7784,6 +7892,7 @@ const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
|
7784
7892
|
// 宿主标签在 540px 以下隐藏,本徽章跟随同一断点(container query 无法跨包复用,
|
|
7785
7893
|
// 用媒体查询近似窄窗行为)。
|
|
7786
7894
|
const SessionExpertBadge = ({ call, sessionId }) => {
|
|
7895
|
+
useI18nRevision(); // 宿主切语言后重渲染,模块级 getter 文案即时更新
|
|
7787
7896
|
const [attached, setAttached] = useState(null);
|
|
7788
7897
|
const [ready, setReady] = useState(false);
|
|
7789
7898
|
const badgeRef = useRef(null);
|
|
@@ -8325,16 +8434,19 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8325
8434
|
const builtinNeedsConfig = (item) => Array.isArray(item.configKeys) && item.configKeys.length > 0;
|
|
8326
8435
|
const builtinNeedsArgs = (item) => Array.isArray(item.placeholders) && item.placeholders.length > 0;
|
|
8327
8436
|
const builtinReady = (item) => !builtinNeedsKey(item) && !builtinNeedsConfig(item) && !builtinNeedsArgs(item);
|
|
8328
|
-
|
|
8437
|
+
// P1:目录随 RPC 同时送中英字段,这里按当前 locale 投影一次。只在 47 条上跑,
|
|
8438
|
+
// 不做 useMemo——locale 变化时 useI18nRevision 已强制重渲染,缓存反而会留下旧语言。
|
|
8439
|
+
const localizedBuiltins = builtins.map((item) => localizeBuiltinItem(item));
|
|
8440
|
+
const builtinCategories = (() => {
|
|
8329
8441
|
const counts = new Map();
|
|
8330
|
-
for (const item of
|
|
8442
|
+
for (const item of localizedBuiltins) {
|
|
8331
8443
|
const key = item.category || 'other';
|
|
8332
8444
|
const entry = counts.get(key) || { label: item.categoryLabel || key, count: 0 };
|
|
8333
8445
|
entry.count += 1;
|
|
8334
8446
|
counts.set(key, entry);
|
|
8335
8447
|
}
|
|
8336
8448
|
return [...counts.entries()].map(([key, entry]) => ({ key, ...entry }));
|
|
8337
|
-
}
|
|
8449
|
+
})();
|
|
8338
8450
|
const builtinQueryKey = builtinQuery.trim().toLowerCase();
|
|
8339
8451
|
// ── T03/R5:codebuddy 插件市场 ──
|
|
8340
8452
|
// 目录一次全量(官方 zip 里的 external_plugins 清单,无分页概念),本地过滤。
|
|
@@ -8588,7 +8700,7 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8588
8700
|
const awesomePageCount = Math.max(1, Math.ceil(sortedAwesome.length / AWESOME_PAGE_SIZE));
|
|
8589
8701
|
const safeAwesomePage = Math.min(awesomePage, awesomePageCount);
|
|
8590
8702
|
const visibleAwesome = sortedAwesome.slice((safeAwesomePage - 1) * AWESOME_PAGE_SIZE, safeAwesomePage * AWESOME_PAGE_SIZE);
|
|
8591
|
-
const filteredBuiltins =
|
|
8703
|
+
const filteredBuiltins = localizedBuiltins.filter((item) => {
|
|
8592
8704
|
if (builtinCategory !== 'all' && (item.category || 'other') !== builtinCategory) return false;
|
|
8593
8705
|
if (builtinOnlyReady && !builtinReady(item)) return false;
|
|
8594
8706
|
if (!builtinQueryKey) return true;
|
|
@@ -9403,6 +9515,7 @@ const EntryMenu = () => {
|
|
|
9403
9515
|
// 左侧栏底部入口(sidebar.footer.action,DSH 0.1.5 起提供):官方标准的发现入口。
|
|
9404
9516
|
// 点击打开的是同一个浮层面板——不切换主面板、不打断正在进行的会话。
|
|
9405
9517
|
const McpFooterAction = ({ wide }) => {
|
|
9518
|
+
useI18nRevision(); // 宿主切语言后重渲染,模块级 getter 文案即时更新
|
|
9406
9519
|
const mode = useEntryMode();
|
|
9407
9520
|
// 「仅悬浮按钮」模式:返回 null 就够了,不需要注销注册。list 型 slot 的每个条目直接渲染
|
|
9408
9521
|
// 进侧栏的 .footerActions(display:flex、无 gap/padding),外层 outlet 锚点又是
|
|
@@ -9820,6 +9933,7 @@ function openBoardCategory(key) {
|
|
|
9820
9933
|
}
|
|
9821
9934
|
const McpBoardPage = (props) => {
|
|
9822
9935
|
const { call, timer, pluginManager } = props;
|
|
9936
|
+
useI18nRevision(); // 宿主切语言后整块看板重渲染,模块级 getter 文案即时更新
|
|
9823
9937
|
// §1.2 URL 记忆:分类写进 hash,刷新 / 从别处退回 / 分享链接都能回到同一页。
|
|
9824
9938
|
// 初值只在首帧读一次(useState 惰性初始化),之后由 effect 单向写出,
|
|
9825
9939
|
// 不做 hash→state 的反向监听:宿主可能在任意时刻改 hash,双向同步必然互相打架。
|
|
@@ -10151,6 +10265,7 @@ function whatsNewFor(current, lastSeen, entries) {
|
|
|
10151
10265
|
// 自带 RPC 与状态:面板只管「在这儿渲染一个」。它挂在面板里,就自然随面板挂载/卸载——
|
|
10152
10266
|
// 这正是「升级后第一次打开面板时弹一次」的触发点,不需要另外判开合。
|
|
10153
10267
|
const WhatsNewModal = ({ call }) => {
|
|
10268
|
+
useI18nRevision(); // P1:宿主切语言后条目按新 locale 重新选择
|
|
10154
10269
|
const [notice, setNotice] = useState(null);
|
|
10155
10270
|
const boxRef = useRef(null);
|
|
10156
10271
|
const confirmRef = useRef(null);
|
|
@@ -10214,6 +10329,8 @@ const WhatsNewModal = ({ call }) => {
|
|
|
10214
10329
|
useFocusTrap(boxRef, !!notice);
|
|
10215
10330
|
|
|
10216
10331
|
if (!notice) return null;
|
|
10332
|
+
// 在渲染期按当前 locale 选择,而不是在 RPC 回包时固化:弹窗开着时切语言也能即时变。
|
|
10333
|
+
const localizedEntries = localizeWhatsNewEntries(notice.entries);
|
|
10217
10334
|
return h(
|
|
10218
10335
|
'div',
|
|
10219
10336
|
{ className: 'dsh-mcp-overlay', onClick: (e) => e.target === e.currentTarget && dismiss() },
|
|
@@ -10227,10 +10344,10 @@ const WhatsNewModal = ({ call }) => {
|
|
|
10227
10344
|
h('span', null, t('这些变化你可能想知道')),
|
|
10228
10345
|
),
|
|
10229
10346
|
h('div', { className: 'dsh-mcp-whatsnew-body' },
|
|
10230
|
-
|
|
10347
|
+
localizedEntries.map((entry) =>
|
|
10231
10348
|
h('div', { key: entry.version, className: 'dsh-mcp-whatsnew-entry' },
|
|
10232
10349
|
// 一次跳多个版本时每条自带版本号;只报一条时标题里的版本号就够了。
|
|
10233
|
-
|
|
10350
|
+
localizedEntries.length > 1 ? h('div', { className: 'dsh-mcp-whatsnew-ver' }, 'v' + entry.version) : null,
|
|
10234
10351
|
entry.items.map((item, position) =>
|
|
10235
10352
|
h('div', { key: position, className: 'dsh-mcp-whatsnew-item' },
|
|
10236
10353
|
h('span', { className: 'dsh-mcp-whatsnew-dot', 'aria-hidden': true }, '·'),
|
|
@@ -10252,6 +10369,7 @@ const WhatsNewModal = ({ call }) => {
|
|
|
10252
10369
|
|
|
10253
10370
|
// ── 悬浮按钮宿主(shell.overlay):只剩这一个席位 ────────────────────────────────
|
|
10254
10371
|
const McpOverlay = () => {
|
|
10372
|
+
useI18nRevision(); // 宿主切语言后重渲染,模块级 getter 文案即时更新
|
|
10255
10373
|
const mode = useEntryMode();
|
|
10256
10374
|
const seats = overlaySeats(mode);
|
|
10257
10375
|
const [pos, setPos] = useState(() => readFabPos() || defaultFabPos());
|
|
@@ -10492,19 +10610,34 @@ const MCP_CONTRIBUTION = {
|
|
|
10492
10610
|
};
|
|
10493
10611
|
// ── 插件入口:挂 RPC、注入样式、注册六个界面槽位 ──────────────────────────────
|
|
10494
10612
|
async function apply(ctx) {
|
|
10613
|
+
// OPT-25:先接宿主 locale(设置 → 通用里的语言),再注册槽位——槽位 label 是
|
|
10614
|
+
// 注册时求值的字符串,必须等这里算完。ctx.get 是可选读取,不写进 inject:
|
|
10615
|
+
// 老版本 DSH 没有 locale 服务时返回 undefined,自动回落 navigator/zh-CN。
|
|
10616
|
+
const readHostLocale = () => {
|
|
10617
|
+
try { return typeof ctx.get === 'function' ? ctx.get('locale') : null; } catch { return null; }
|
|
10618
|
+
};
|
|
10619
|
+
let hostLocaleService = readHostLocale();
|
|
10620
|
+
applyI18nLocale(resolveI18nLocale(hostLocaleService));
|
|
10495
10621
|
const disposeMount = await ctx.remote.$mount(MCP_CONTRIBUTION);
|
|
10496
10622
|
let disposeCss;
|
|
10497
10623
|
let timer;
|
|
10498
10624
|
let cleanupPromise;
|
|
10625
|
+
let disposeLocaleSub = null;
|
|
10626
|
+
let disposeServiceWatch = null;
|
|
10499
10627
|
const cleanup = () => {
|
|
10500
10628
|
cleanupPromise ??= (async () => {
|
|
10501
10629
|
try {
|
|
10502
|
-
|
|
10630
|
+
try { disposeLocaleSub?.(); } catch { /* 宿主订阅已失效 */ }
|
|
10631
|
+
try { disposeServiceWatch?.(); } catch { /* 宿主事件监听已失效 */ }
|
|
10503
10632
|
} finally {
|
|
10504
10633
|
try {
|
|
10505
|
-
|
|
10634
|
+
timer?.dispose();
|
|
10506
10635
|
} finally {
|
|
10507
|
-
|
|
10636
|
+
try {
|
|
10637
|
+
disposeCss?.();
|
|
10638
|
+
} finally {
|
|
10639
|
+
await disposeMount();
|
|
10640
|
+
}
|
|
10508
10641
|
}
|
|
10509
10642
|
}
|
|
10510
10643
|
})();
|
|
@@ -10533,7 +10666,7 @@ async function apply(ctx) {
|
|
|
10533
10666
|
// 直接 register:它声明感知,会等 ui-layout 把 main slot 声明出来(插件加载顺序不保证
|
|
10534
10667
|
// 晚于布局)。
|
|
10535
10668
|
ctx.slots.inject('main', () =>
|
|
10536
|
-
ctx.slots.register({ name: 'main', key: PANEL_ID, label:
|
|
10669
|
+
ctx.slots.register({ name: 'main', key: PANEL_ID, label: panelLabel(), inject: () => ({ call, timer, pluginManager }) }, McpBoardPage),
|
|
10537
10670
|
);
|
|
10538
10671
|
// 输入框召唤菜单(conversation.input.right,list 型 slot):权限/模型座位右侧的小图标,
|
|
10539
10672
|
// 点开 = 技能/MCP/专家三分类 + 悬停最近使用子菜单。声明感知注入(会话 slot 由 conversation
|
|
@@ -10541,13 +10674,13 @@ async function apply(ctx) {
|
|
|
10541
10674
|
// sessionId 由 slot 渲染作为 prop 显式传入(session 座位约定:id 只走 props,不走 summary 投影),
|
|
10542
10675
|
// 「挂专家」的人设通道 RPC 用它寻址当前会话。
|
|
10543
10676
|
ctx.slots.inject('conversation.input.right', () =>
|
|
10544
|
-
ctx.slots.register({ name: 'conversation.input.right', id: PANEL_ID + '-quick', order: 40, label:
|
|
10677
|
+
ctx.slots.register({ name: 'conversation.input.right', id: PANEL_ID + '-quick', order: 40, label: panelLabel(), inject: (sessionId) => ({ call, sessionId }) }, SummonQuickMenu),
|
|
10545
10678
|
);
|
|
10546
10679
|
// 会话头「当前生效专家」徽章(conversation.session.header.actions,list 型 slot):
|
|
10547
10680
|
// 与宿主 AgentPresetLabel 同列——但显示的是人设通道真正生效的专家(expertActive),
|
|
10548
10681
|
// 修复「换专家后顶栏仍显示第一次预设名」的显示/实效分叉。
|
|
10549
10682
|
ctx.slots.inject('conversation.session.header.actions', () =>
|
|
10550
|
-
ctx.slots.register({ name: 'conversation.session.header.actions', id: PANEL_ID + '-head-expert', order: -5, label:
|
|
10683
|
+
ctx.slots.register({ name: 'conversation.session.header.actions', id: PANEL_ID + '-head-expert', order: -5, label: panelLabel(), inject: (sessionId) => ({ call, sessionId }) }, SessionExpertBadge),
|
|
10551
10684
|
);
|
|
10552
10685
|
// 左上角全局面板行(sidebar.panellist):「插件」旁边,主入口。行点击由宿主承担——
|
|
10553
10686
|
// layout.selectPanel(PANEL_ID) 要求 main slot 里存在同 key 条目,上面的注册保证了这点。
|
|
@@ -10557,20 +10690,52 @@ async function apply(ctx) {
|
|
|
10557
10690
|
const syncPanelEntry = () => {
|
|
10558
10691
|
const want = ENTRY_VISIBILITY[entryMode.get()].panel;
|
|
10559
10692
|
if (want && !disposePanelEntry) disposePanelEntry = ctx.slots.register(
|
|
10560
|
-
{ name: 'sidebar.panellist', id: PANEL_ID, order: PANEL_ORDER, label:
|
|
10693
|
+
{ name: 'sidebar.panellist', id: PANEL_ID, order: PANEL_ORDER, label: sidebarEntryLabel() }, McpPanelIcon,
|
|
10561
10694
|
);
|
|
10562
10695
|
if (!want && disposePanelEntry) { disposePanelEntry(); disposePanelEntry = null; }
|
|
10563
10696
|
};
|
|
10564
10697
|
ctx.slots.inject('sidebar.panellist', () => { syncPanelEntry(); });
|
|
10565
10698
|
entryMode.subscribe(syncPanelEntry);
|
|
10699
|
+
// OPT-25:宿主切换语言后重算字典并刷新槽位文案。
|
|
10700
|
+
// sidebar.panellist 的 label 来自注册参数而不是组件,只能 dispose + 重新注册;
|
|
10701
|
+
// 面板/菜单等内容组件通过 useI18nRevision 强制重渲染,getter 文案即时生效。
|
|
10702
|
+
const applyHostLocale = () => {
|
|
10703
|
+
const next = resolveI18nLocale(hostLocaleService);
|
|
10704
|
+
if (next === I18N_LOCALE) return;
|
|
10705
|
+
applyI18nLocale(next);
|
|
10706
|
+
// 只在入口已经注册过时重注册:否则可能在 sidebar.panellist 声明前直接
|
|
10707
|
+
// 调用 register(inject 的声明感知就白用了)。没注册过就没有旧文案可刷新,
|
|
10708
|
+
// 之后的 syncPanelEntry 会用新 locale 取 label。
|
|
10709
|
+
if (disposePanelEntry) {
|
|
10710
|
+
disposePanelEntry();
|
|
10711
|
+
disposePanelEntry = null;
|
|
10712
|
+
syncPanelEntry();
|
|
10713
|
+
}
|
|
10714
|
+
emitI18nChange();
|
|
10715
|
+
};
|
|
10716
|
+
const bindHostLocale = () => {
|
|
10717
|
+
const service = readHostLocale();
|
|
10718
|
+
if (!service) return;
|
|
10719
|
+
hostLocaleService = service;
|
|
10720
|
+
applyHostLocale();
|
|
10721
|
+
if (!disposeLocaleSub) disposeLocaleSub = subscribeI18nLocale(service, applyHostLocale);
|
|
10722
|
+
};
|
|
10723
|
+
bindHostLocale();
|
|
10724
|
+
// locale 插件可能晚于本插件完成 apply:服务注册时再绑一次(Cordis 内置事件)。
|
|
10725
|
+
// 老宿主没有该事件也无妨,启动时的 ctx.get 已经覆盖绝大多数加载顺序。
|
|
10726
|
+
if (typeof ctx.on === 'function') {
|
|
10727
|
+
disposeServiceWatch = ctx.on('internal/service', (name) => {
|
|
10728
|
+
if (name === 'locale') bindHostLocale();
|
|
10729
|
+
});
|
|
10730
|
+
}
|
|
10566
10731
|
// 右下角悬浮按钮:保留既有能力,点击同样切到看板页。
|
|
10567
10732
|
ctx.slots.inject('shell.overlay', () =>
|
|
10568
|
-
ctx.slots.register({ name: 'shell.overlay', id: 'mcp-fab', order: 1000, label:
|
|
10733
|
+
ctx.slots.register({ name: 'shell.overlay', id: 'mcp-fab', order: 1000, label: panelLabel(), inject: () => ({ call, timer, pluginManager }) }, McpOverlay),
|
|
10569
10734
|
);
|
|
10570
10735
|
// 左侧栏底部入口(sidebar.footer.action,0.1.5 起声明):兼容入口,行为与面板行一致。
|
|
10571
10736
|
// 用 inject 而不是直接 register:它声明感知,会等侧边栏把 slot 声明出来。
|
|
10572
10737
|
ctx.slots.inject('sidebar.footer.action', () =>
|
|
10573
|
-
ctx.slots.register({ name: 'sidebar.footer.action', id: 'mcp-manager', order: 100, label:
|
|
10738
|
+
ctx.slots.register({ name: 'sidebar.footer.action', id: 'mcp-manager', order: 100, label: panelLabel() }, McpFooterAction),
|
|
10574
10739
|
);
|
|
10575
10740
|
return cleanup;
|
|
10576
10741
|
} catch (error) {
|