dsh-my-config 1.7.1 → 1.8.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.md +12 -3
- package/client.js +300 -43
- package/index.js +476 -18
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -4,12 +4,21 @@ DeepSeek Harness 插件:在 Web 侧边栏提供「我的技能 / 我的连接
|
|
|
4
4
|
|
|
5
5
|
| 模块 | 能力 |
|
|
6
6
|
|---|---|
|
|
7
|
-
| 我的技能 | 创建 / 编辑 / 删除 / 导入技能;**ZIP 技能包导入**(多文件包);**外部技能市场**(内置精选市场 + 任意 `github://owner/repo` 仓库 + JSON
|
|
8
|
-
| 我的连接器 | 创建 / 编辑(配置)/ 启停 / 删除 MCP 连接器(streamable-http 与 stdio),通过 `ctx.loader` 动态下发 `@deepseek-ai/dsh-mcp-client`
|
|
9
|
-
| 我的智能体 | 创建 / 编辑 / 删除自定义 Agent
|
|
7
|
+
| 我的技能 | 创建 / 编辑 / 删除 / 导入技能;**ZIP 技能包导入**(多文件包);**外部技能市场**(内置精选市场 + 任意 `github://owner/repo` 仓库 + JSON 索引市场);**全字段编辑**(license / compatibility / metadata / allowed-tools,对齐 Agent Skills 开放规范);**规范校验**(保存/导入时逐字段拦截并定位错误);分组管理 |
|
|
8
|
+
| 我的连接器 | 创建 / 编辑(配置)/ 启停 / 删除 MCP 连接器(streamable-http 与 stdio),通过 `ctx.loader` 动态下发 `@deepseek-ai/dsh-mcp-client` 条目;**连接测试**(MCP 握手 + 工具清单预览);**工具级授权**(白名单拦截 + 自动执行放行,经审批 answerer 实现);**凭据脱敏**(env / headers 掩码显示,防误覆盖);分组管理 |
|
|
9
|
+
| 我的智能体 | 创建 / 编辑 / 删除自定义 Agent 预设;**关联技能**(独立技能目录,精确限定该智能体可见的技能)与**关联连接器**(工具清单注入系统提示词);**模型绑定**(provider/model/reasoningEffort 记录与展示);**工具组白名单**(文件 / 联网 / 终端 / 技能 四组粒度过滤预设组合);分组管理 |
|
|
10
10
|
|
|
11
11
|
三个页面统一布局:搜索框 + 分组筛选 + 刷新 + 新建。
|
|
12
12
|
|
|
13
|
+
## 更新日志
|
|
14
|
+
|
|
15
|
+
### v1.8.0
|
|
16
|
+
|
|
17
|
+
- **技能**:编辑器支持 license / compatibility / metadata / allowed-tools 全字段(对齐 agentskills.io 规范);保存与导入时执行规范校验,错误逐字段定位展示。
|
|
18
|
+
- **连接器**:新增「测试连接」(stdio / streamable-http 真实 MCP 握手,返回工具清单与延迟);工具级授权(允许使用白名单 + 自动执行列表,经 `approval/request` answerer 拦截/放行);env / headers 密钥掩码显示与防误覆盖。
|
|
19
|
+
- **智能体**:预设新增模型绑定(provider / model / reasoningEffort)与工具组白名单(文件 / 联网 / 终端 / 技能)。
|
|
20
|
+
- 已知限制:审批拒绝原因经审计日志留痕(内核 outcome 词表无原因通道);连接器密钥存储仍为本地明文(data.json),请妥善保管该文件。
|
|
21
|
+
|
|
13
22
|
## 安装
|
|
14
23
|
|
|
15
24
|
```bash
|
package/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* 我的配置 —— Web 客户端半区。
|
|
3
|
+
* 注册侧边栏面板「我的配置」与 main 面板,包含技能 / 连接器 / 智能体三个管理页。
|
|
4
4
|
* Host 操作走同源 POST /manager-center/api;插件管理走 ctx.get('remote.pluginManager')。
|
|
5
5
|
*/
|
|
6
6
|
window.__ModuleLoader__.load({
|
|
@@ -16,8 +16,8 @@ window.__ModuleLoader__.load({
|
|
|
16
16
|
/* ───────────────────────── 文案 ───────────────────────── */
|
|
17
17
|
|
|
18
18
|
const zh = {
|
|
19
|
-
panel: '
|
|
20
|
-
title: '
|
|
19
|
+
panel: '我的配置',
|
|
20
|
+
title: '我的配置',
|
|
21
21
|
intro: '集中管理技能、连接器、智能体与插件',
|
|
22
22
|
tabSkills: '技能',
|
|
23
23
|
tabConnectors: '连接器',
|
|
@@ -102,6 +102,18 @@ window.__ModuleLoader__.load({
|
|
|
102
102
|
skillReadonly: '只读',
|
|
103
103
|
skillInvalid: 'frontmatter 无效',
|
|
104
104
|
skillRoot: '存储目录',
|
|
105
|
+
skillLicense: '许可证(可选)',
|
|
106
|
+
skillCompatibility: '兼容性(可选)',
|
|
107
|
+
skillMetadata: '元数据(键值对,可选)',
|
|
108
|
+
skillMetadataKey: '键',
|
|
109
|
+
skillMetadataValue: '值',
|
|
110
|
+
skillMetadataAdd: '添加一行',
|
|
111
|
+
skillAllowedTools: '允许的工具(可选)',
|
|
112
|
+
skillAllowedToolsHint: '实验性功能:写入 frontmatter 的 allowed-tools,空格分隔的工具名',
|
|
113
|
+
skillErrName: '名称须为小写 kebab-case(字母/数字/连字符),不超过 64 字符',
|
|
114
|
+
skillErrDescEmpty: '描述不能为空',
|
|
115
|
+
skillErrDescLong: '描述不能超过 1024 字符',
|
|
116
|
+
skillErrCompatLong: '兼容性说明不能超过 500 字符',
|
|
105
117
|
marketAdd: '添加市场',
|
|
106
118
|
marketName: '市场名称',
|
|
107
119
|
marketUrl: '市场索引地址(JSON)',
|
|
@@ -127,6 +139,16 @@ window.__ModuleLoader__.load({
|
|
|
127
139
|
connectorLive: '运行中',
|
|
128
140
|
connectorStopped: '未加载',
|
|
129
141
|
connectorToolsHint: '连接后工具以 mcp__{serverName}__ 前缀注册',
|
|
142
|
+
connectorTest: '测试连接',
|
|
143
|
+
connectorUntested: '未测试',
|
|
144
|
+
connectorConnected: '已连接 {n} 个工具',
|
|
145
|
+
connectorTestFailed: '失败:{reason}',
|
|
146
|
+
connectorTools: '工具',
|
|
147
|
+
connectorToolsEmpty: '暂无工具清单,请先点击「测试连接」获取。',
|
|
148
|
+
connectorToolAllowed: '允许使用',
|
|
149
|
+
connectorToolAuto: '自动执行',
|
|
150
|
+
connectorApprovalNever: '当前审批策略为 never,自动执行不生效。',
|
|
151
|
+
connectorSecretHidden: '敏感值不可查看,如需修改请重新填写。',
|
|
130
152
|
// 智能体
|
|
131
153
|
expertCreate: '创建智能体',
|
|
132
154
|
expertEdit: '编辑智能体',
|
|
@@ -139,6 +161,19 @@ window.__ModuleLoader__.load({
|
|
|
139
161
|
expertDefault: '默认',
|
|
140
162
|
expertBroken: '加载失败',
|
|
141
163
|
expertHint: '自定义智能体注册为 Agent 预设,新会话可在预设选择器中选用。',
|
|
164
|
+
expertModel: '模型绑定(可选)',
|
|
165
|
+
expertModelProvider: '提供商(如 anthropic)',
|
|
166
|
+
expertModelName: '模型名(如 claude-sonnet-4)',
|
|
167
|
+
expertModelDefault: '跟随默认',
|
|
168
|
+
expertModelHint: '提供商与模型名均填写才生效;留空 = 跟随默认模型。',
|
|
169
|
+
expertModelPartial: '模型绑定须同时填写提供商与模型名,或留空。',
|
|
170
|
+
expertTools: '工具组(可选)',
|
|
171
|
+
expertToolFs: '文件读写检索',
|
|
172
|
+
expertToolWeb: '联网',
|
|
173
|
+
expertToolShell: '终端',
|
|
174
|
+
expertToolSkills: '技能',
|
|
175
|
+
expertToolsHint: '留空 = 全部可用。',
|
|
176
|
+
expertToolsSummary: '工具组',
|
|
142
177
|
// 插件
|
|
143
178
|
pluginInstall: '安装插件',
|
|
144
179
|
pluginSpec: '包名 / Git 地址 / 本地目录',
|
|
@@ -157,8 +192,8 @@ window.__ModuleLoader__.load({
|
|
|
157
192
|
};
|
|
158
193
|
|
|
159
194
|
const en = {
|
|
160
|
-
panel: '
|
|
161
|
-
title: '
|
|
195
|
+
panel: 'My Config',
|
|
196
|
+
title: 'My Config',
|
|
162
197
|
intro: 'Manage skills, connectors, agents and plugins in one place',
|
|
163
198
|
tabSkills: 'Skills',
|
|
164
199
|
tabConnectors: 'Connectors',
|
|
@@ -242,6 +277,18 @@ window.__ModuleLoader__.load({
|
|
|
242
277
|
skillReadonly: 'Read-only',
|
|
243
278
|
skillInvalid: 'Invalid frontmatter',
|
|
244
279
|
skillRoot: 'Storage directory',
|
|
280
|
+
skillLicense: 'License (optional)',
|
|
281
|
+
skillCompatibility: 'Compatibility (optional)',
|
|
282
|
+
skillMetadata: 'Metadata (key-value pairs, optional)',
|
|
283
|
+
skillMetadataKey: 'Key',
|
|
284
|
+
skillMetadataValue: 'Value',
|
|
285
|
+
skillMetadataAdd: 'Add row',
|
|
286
|
+
skillAllowedTools: 'Allowed tools (optional)',
|
|
287
|
+
skillAllowedToolsHint: 'Experimental: written to frontmatter as allowed-tools, space-separated tool names',
|
|
288
|
+
skillErrName: 'Name must be lowercase kebab-case (letters/digits/hyphens), max 64 characters',
|
|
289
|
+
skillErrDescEmpty: 'Description is required',
|
|
290
|
+
skillErrDescLong: 'Description must be at most 1024 characters',
|
|
291
|
+
skillErrCompatLong: 'Compatibility must be at most 500 characters',
|
|
245
292
|
marketAdd: 'Add market',
|
|
246
293
|
marketName: 'Market name',
|
|
247
294
|
marketUrl: 'Market index URL (JSON)',
|
|
@@ -266,6 +313,16 @@ window.__ModuleLoader__.load({
|
|
|
266
313
|
connectorLive: 'Live',
|
|
267
314
|
connectorStopped: 'Not loaded',
|
|
268
315
|
connectorToolsHint: 'Tools register with the mcp__{serverName}__ prefix once connected',
|
|
316
|
+
connectorTest: 'Test connection',
|
|
317
|
+
connectorUntested: 'Not tested',
|
|
318
|
+
connectorConnected: 'Connected: {n} tools',
|
|
319
|
+
connectorTestFailed: 'Failed: {reason}',
|
|
320
|
+
connectorTools: 'Tools',
|
|
321
|
+
connectorToolsEmpty: 'No tool list yet. Click "Test connection" to fetch it.',
|
|
322
|
+
connectorToolAllowed: 'Allow',
|
|
323
|
+
connectorToolAuto: 'Auto-run',
|
|
324
|
+
connectorApprovalNever: 'Current approval policy is "never"; auto-run has no effect.',
|
|
325
|
+
connectorSecretHidden: 'Secret values cannot be viewed. Re-enter to change them.',
|
|
269
326
|
expertCreate: 'New agent',
|
|
270
327
|
expertEdit: 'Edit agent',
|
|
271
328
|
expertId: 'ID (lowercase letters/digits/hyphens)',
|
|
@@ -277,6 +334,19 @@ window.__ModuleLoader__.load({
|
|
|
277
334
|
expertDefault: 'Default',
|
|
278
335
|
expertBroken: 'Broken',
|
|
279
336
|
expertHint: 'Custom agents register as agent presets and can be selected for new sessions.',
|
|
337
|
+
expertModel: 'Model binding (optional)',
|
|
338
|
+
expertModelProvider: 'Provider (e.g. anthropic)',
|
|
339
|
+
expertModelName: 'Model (e.g. claude-sonnet-4)',
|
|
340
|
+
expertModelDefault: 'Follow default',
|
|
341
|
+
expertModelHint: 'Both provider and model are required to take effect; empty = follow the default model.',
|
|
342
|
+
expertModelPartial: 'Model binding requires both provider and model, or leave empty.',
|
|
343
|
+
expertTools: 'Tool groups (optional)',
|
|
344
|
+
expertToolFs: 'File read/write & search',
|
|
345
|
+
expertToolWeb: 'Web access',
|
|
346
|
+
expertToolShell: 'Terminal',
|
|
347
|
+
expertToolSkills: 'Skills',
|
|
348
|
+
expertToolsHint: 'Empty = all available.',
|
|
349
|
+
expertToolsSummary: 'Tool groups',
|
|
280
350
|
pluginInstall: 'Install plugin',
|
|
281
351
|
pluginSpec: 'Package name / Git URL / local directory',
|
|
282
352
|
pluginUninstall: 'Uninstall',
|
|
@@ -350,6 +420,7 @@ window.__ModuleLoader__.load({
|
|
|
350
420
|
.mc-empty { color: var(--dsw-alias-label-secondary); padding: 48px 0; text-align: center; }
|
|
351
421
|
.mc-error { color: var(--dsw-alias-state-error-primary); padding: 24px 0; text-align: center; display: flex; flex-direction: column; gap: 8px; align-items: center; }
|
|
352
422
|
.mc-hint { font-size: 12px; color: var(--dsw-alias-label-secondary); line-height: 1.6; }
|
|
423
|
+
.mc-field-error { color: var(--dsw-alias-state-error-primary); font-size: 12px; }
|
|
353
424
|
.mc-section { margin-bottom: 20px; }
|
|
354
425
|
.mc-section-title { font-size: 13px; font-weight: 600; margin: 0 0 10px; color: var(--dsw-alias-label-secondary); }
|
|
355
426
|
.mc-row { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; margin-bottom: 8px; }
|
|
@@ -450,7 +521,11 @@ window.__ModuleLoader__.load({
|
|
|
450
521
|
clearTimeout(timer);
|
|
451
522
|
}
|
|
452
523
|
const data = await res.json().catch(() => ({ ok: false, message: `HTTP ${res.status} [${url}]` }));
|
|
453
|
-
if (!data.ok)
|
|
524
|
+
if (!data.ok) {
|
|
525
|
+
const err = new Error(data.message || `HTTP ${res.status}`);
|
|
526
|
+
if (Array.isArray(data.errors)) err.fields = data.errors;
|
|
527
|
+
throw err;
|
|
528
|
+
}
|
|
454
529
|
return data;
|
|
455
530
|
}
|
|
456
531
|
|
|
@@ -509,17 +584,51 @@ window.__ModuleLoader__.load({
|
|
|
509
584
|
group: skill?.group ?? '',
|
|
510
585
|
userInvocable: skill?.userInvocable !== false,
|
|
511
586
|
disableModelInvocation: skill?.disableModelInvocation === true,
|
|
587
|
+
license: skill?.license ?? '',
|
|
588
|
+
compatibility: skill?.compatibility ?? '',
|
|
589
|
+
metadata: Object.entries(skill?.metadata ?? {}).map(([key, value]) => ({ key, value: String(value ?? '') })),
|
|
590
|
+
allowedTools: skill?.allowedTools ?? '',
|
|
512
591
|
});
|
|
513
592
|
const [busy, setBusy] = useState(false);
|
|
514
|
-
const [error, setError] = useState(
|
|
515
|
-
const
|
|
593
|
+
const [error, setError] = useState(undefined); // { message, fields? }
|
|
594
|
+
const [errors, setErrors] = useState({}); // 本地校验:字段 → 错误文案
|
|
595
|
+
const validateForm = (f) => {
|
|
596
|
+
const errs = {};
|
|
597
|
+
if (f.name.length > 64 || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(f.name)) errs.name = t('skillErrName');
|
|
598
|
+
if (!f.description.trim()) errs.description = t('skillErrDescEmpty');
|
|
599
|
+
else if (f.description.length > 1024) errs.description = t('skillErrDescLong');
|
|
600
|
+
if (f.compatibility.length > 500) errs.compatibility = t('skillErrCompatLong');
|
|
601
|
+
return errs;
|
|
602
|
+
};
|
|
603
|
+
const set = (key) => (e) => {
|
|
604
|
+
const next = { ...form, [key]: e.target.value };
|
|
605
|
+
setForm(next);
|
|
606
|
+
if (Object.keys(errors).length > 0) setErrors(validateForm(next));
|
|
607
|
+
};
|
|
608
|
+
const setMetadata = (i, key) => (e) => {
|
|
609
|
+
const metadata = form.metadata.map((row, idx) => (idx === i ? { ...row, [key]: e.target.value } : row));
|
|
610
|
+
setForm({ ...form, metadata });
|
|
611
|
+
};
|
|
516
612
|
const submit = async () => {
|
|
517
|
-
|
|
613
|
+
const errs = validateForm(form);
|
|
614
|
+
setErrors(errs);
|
|
615
|
+
if (Object.keys(errs).length > 0) return;
|
|
616
|
+
setBusy(true); setError(undefined);
|
|
518
617
|
try {
|
|
519
|
-
|
|
618
|
+
const metadata = {};
|
|
619
|
+
for (const row of form.metadata) {
|
|
620
|
+
const k = row.key.trim();
|
|
621
|
+
if (k) metadata[k] = row.value;
|
|
622
|
+
}
|
|
623
|
+
await api(isEdit ? 'skills.update' : 'skills.create', { ...form, metadata });
|
|
520
624
|
onSaved();
|
|
521
|
-
} catch (err) { setError(err.message); } finally { setBusy(false); }
|
|
625
|
+
} catch (err) { setError({ message: err.message, fields: err.fields }); } finally { setBusy(false); }
|
|
522
626
|
};
|
|
627
|
+
const renderServerError = (err) => err && h('div', { className: 'mc-diag' },
|
|
628
|
+
err.message,
|
|
629
|
+
(err.fields ?? []).length > 0 && h('ul', { style: { margin: '6px 0 0', paddingLeft: 18 } },
|
|
630
|
+
err.fields.map((f, i) => h('li', { key: i }, (f.field ? f.field + ': ' : '') + (f.message ?? '')))),
|
|
631
|
+
);
|
|
523
632
|
return h(Modal, {
|
|
524
633
|
title: isEdit ? t('skillEdit') : t('skillCreate'), onClose,
|
|
525
634
|
footer: [
|
|
@@ -527,10 +636,28 @@ window.__ModuleLoader__.load({
|
|
|
527
636
|
h(Button, { key: 's', kind: 'primary', disabled: busy || !form.name || !form.description, onClick: submit }, busy ? '…' : t('save')),
|
|
528
637
|
],
|
|
529
638
|
},
|
|
530
|
-
|
|
531
|
-
h(Field, { label: t('skillName') },
|
|
532
|
-
|
|
639
|
+
renderServerError(error),
|
|
640
|
+
h(Field, { label: t('skillName') },
|
|
641
|
+
h('input', { className: 'mc-input', value: form.name, disabled: isEdit, onChange: set('name'), placeholder: 'my-skill', autoFocus: !isEdit }),
|
|
642
|
+
errors.name && h('span', { className: 'mc-field-error' }, errors.name)),
|
|
643
|
+
h(Field, { label: t('skillDescription') },
|
|
644
|
+
h('input', { className: 'mc-input', value: form.description, onChange: set('description') }),
|
|
645
|
+
errors.description && h('span', { className: 'mc-field-error' }, errors.description)),
|
|
533
646
|
h(Field, { label: t('skillWhenToUse') }, h('input', { className: 'mc-input', value: form.whenToUse, onChange: set('whenToUse') })),
|
|
647
|
+
h(Field, { label: t('skillCompatibility') },
|
|
648
|
+
h('input', { className: 'mc-input', value: form.compatibility, onChange: set('compatibility'), placeholder: 'node >= 18, macos' }),
|
|
649
|
+
errors.compatibility && h('span', { className: 'mc-field-error' }, errors.compatibility)),
|
|
650
|
+
h(Field, { label: t('skillLicense') }, h('input', { className: 'mc-input', value: form.license, onChange: set('license'), placeholder: 'MIT' })),
|
|
651
|
+
h(Field, { label: t('skillMetadata') },
|
|
652
|
+
form.metadata.map((row, i) => h('div', { key: i, style: { display: 'flex', gap: 6, alignItems: 'center' } },
|
|
653
|
+
h('input', { className: 'mc-input', style: { flex: 1 }, placeholder: t('skillMetadataKey'), value: row.key, onChange: setMetadata(i, 'key') }),
|
|
654
|
+
h('input', { className: 'mc-input', style: { flex: 2 }, placeholder: t('skillMetadataValue'), value: row.value, onChange: setMetadata(i, 'value') }),
|
|
655
|
+
h(Button, { className: 'link danger', kind: undefined, 'aria-label': t('delete'), onClick: () => setForm({ ...form, metadata: form.metadata.filter((_, idx) => idx !== i) }) }, '✕'),
|
|
656
|
+
)),
|
|
657
|
+
h(Button, { className: 'link', kind: undefined, onClick: () => setForm({ ...form, metadata: [...form.metadata, { key: '', value: '' }] }) }, '+ ' + t('skillMetadataAdd'))),
|
|
658
|
+
h(Field, { label: t('skillAllowedTools') },
|
|
659
|
+
h('input', { className: 'mc-input', value: form.allowedTools, onChange: set('allowedTools'), placeholder: 'tool-a tool-b' }),
|
|
660
|
+
h('span', { className: 'mc-hint' }, t('skillAllowedToolsHint'))),
|
|
534
661
|
h(Field, { label: t('group') }, h('input', { className: 'mc-input', value: form.group, onChange: set('group'), placeholder: t('groupPlaceholder') })),
|
|
535
662
|
h(Field, { label: t('skillContent') }, h('textarea', { className: 'mc-textarea', value: form.content, onChange: set('content'), placeholder: '# ...' })),
|
|
536
663
|
h('label', { className: 'mc-checkbox' }, h('input', { type: 'checkbox', checked: form.userInvocable, onChange: (e) => setForm({ ...form, userInvocable: e.target.checked }) }), t('skillUserInvocable')),
|
|
@@ -636,7 +763,7 @@ window.__ModuleLoader__.load({
|
|
|
636
763
|
setError(undefined);
|
|
637
764
|
if (file.size > 30 * 1024 * 1024) {
|
|
638
765
|
setFileName(''); setContent(''); setZipBase64('');
|
|
639
|
-
setError(t('importTooLarge'));
|
|
766
|
+
setError({ message: t('importTooLarge') });
|
|
640
767
|
return;
|
|
641
768
|
}
|
|
642
769
|
setFileName(file.name);
|
|
@@ -647,12 +774,12 @@ window.__ModuleLoader__.load({
|
|
|
647
774
|
let bin = '';
|
|
648
775
|
for (let i = 0; i < buf.length; i += 0x8000) bin += String.fromCharCode.apply(null, buf.subarray(i, i + 0x8000));
|
|
649
776
|
setZipBase64(btoa(bin));
|
|
650
|
-
} catch { setZipBase64(''); setError(t('importReadFailed')); }
|
|
777
|
+
} catch { setZipBase64(''); setError({ message: t('importReadFailed') }); }
|
|
651
778
|
} else {
|
|
652
779
|
setZipBase64('');
|
|
653
780
|
const reader = new FileReader();
|
|
654
781
|
reader.onload = () => setContent(String(reader.result ?? ''));
|
|
655
|
-
reader.onerror = () => setError(t('importReadFailed'));
|
|
782
|
+
reader.onerror = () => setError({ message: t('importReadFailed') });
|
|
656
783
|
reader.readAsText(file);
|
|
657
784
|
}
|
|
658
785
|
};
|
|
@@ -682,7 +809,7 @@ window.__ModuleLoader__.load({
|
|
|
682
809
|
await api('skills.import', params);
|
|
683
810
|
onImported();
|
|
684
811
|
} catch (err) {
|
|
685
|
-
setError(err.message);
|
|
812
|
+
setError({ message: err.message, fields: err.fields });
|
|
686
813
|
} finally {
|
|
687
814
|
setBusy(false);
|
|
688
815
|
}
|
|
@@ -713,7 +840,10 @@ window.__ModuleLoader__.load({
|
|
|
713
840
|
h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' } },
|
|
714
841
|
h('input', { type: 'checkbox', checked: overwrite, onChange: (e) => setOverwrite(e.target.checked) }),
|
|
715
842
|
t('importOverwrite')),
|
|
716
|
-
error && h('div', { className: 'mc-error' },
|
|
843
|
+
error && h('div', { className: 'mc-error' },
|
|
844
|
+
error.message,
|
|
845
|
+
(error.fields ?? []).length > 0 && h('ul', { style: { margin: '6px 0 0', paddingLeft: 18, textAlign: 'left' } },
|
|
846
|
+
error.fields.map((f, i) => h('li', { key: i }, (f.field ? f.field + ': ' : '') + (f.message ?? ''))))),
|
|
717
847
|
);
|
|
718
848
|
}
|
|
719
849
|
|
|
@@ -803,6 +933,8 @@ window.__ModuleLoader__.load({
|
|
|
803
933
|
});
|
|
804
934
|
const [busy, setBusy] = useState(false);
|
|
805
935
|
const [error, setError] = useState('');
|
|
936
|
+
// 掩码值('••••••')不可查看明文;点击眼睛图标仅提示重新填写
|
|
937
|
+
const [secretHint, setSecretHint] = useState('');
|
|
806
938
|
const set = (key) => (e) => setForm({ ...form, [key]: e.target.value });
|
|
807
939
|
const submit = async () => {
|
|
808
940
|
setBusy(true); setError('');
|
|
@@ -811,6 +943,14 @@ window.__ModuleLoader__.load({
|
|
|
811
943
|
onSaved();
|
|
812
944
|
} catch (err) { setError(err.message); } finally { setBusy(false); }
|
|
813
945
|
};
|
|
946
|
+
const secretField = (label, value, key, placeholder) => h(Field, { label },
|
|
947
|
+
h('textarea', { className: 'mc-textarea', style: { minHeight: 72 }, value, onChange: set(key), placeholder }),
|
|
948
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
949
|
+
h(Button, {
|
|
950
|
+
className: 'link', kind: undefined, 'aria-label': label,
|
|
951
|
+
onClick: () => setSecretHint(secretHint === key ? '' : key),
|
|
952
|
+
}, '👁'),
|
|
953
|
+
secretHint === key && h('span', { className: 'mc-hint' }, t('connectorSecretHidden'))));
|
|
814
954
|
return h(Modal, {
|
|
815
955
|
title: isEdit ? t('connectorEdit') : t('connectorCreate'), onClose,
|
|
816
956
|
footer: [
|
|
@@ -829,10 +969,10 @@ window.__ModuleLoader__.load({
|
|
|
829
969
|
),
|
|
830
970
|
),
|
|
831
971
|
form.transport === 'streamable-http' && h(Field, { label: t('connectorUrl') }, h('input', { className: 'mc-input', value: form.url, onChange: set('url'), placeholder: 'https://example.com/mcp' })),
|
|
832
|
-
form.transport === 'streamable-http' &&
|
|
972
|
+
form.transport === 'streamable-http' && secretField(t('connectorHeaders'), form.headers, 'headers', 'Authorization=Bearer …'),
|
|
833
973
|
form.transport === 'stdio' && h(Field, { label: t('connectorCommand') }, h('input', { className: 'mc-input', value: form.command, onChange: set('command'), placeholder: 'npx' })),
|
|
834
974
|
form.transport === 'stdio' && h(Field, { label: t('connectorArgs') }, h('textarea', { className: 'mc-textarea', style: { minHeight: 72 }, value: form.args, onChange: set('args'), placeholder: '-y\n@modelcontextprotocol/server-github' })),
|
|
835
|
-
form.transport === 'stdio' &&
|
|
975
|
+
form.transport === 'stdio' && secretField(t('connectorEnv'), form.env, 'env', 'GITHUB_TOKEN=…'),
|
|
836
976
|
form.transport === 'stdio' && h(Field, { label: t('connectorCwd') }, h('input', { className: 'mc-input', value: form.cwd, onChange: set('cwd') })),
|
|
837
977
|
h(Field, { label: t('group') }, h('input', { className: 'mc-input', value: form.group, onChange: set('group'), placeholder: t('groupPlaceholder') })),
|
|
838
978
|
);
|
|
@@ -846,11 +986,33 @@ window.__ModuleLoader__.load({
|
|
|
846
986
|
const [busyDelete, setBusyDelete] = useState(false);
|
|
847
987
|
const [groupFilter, setGroupFilter] = useState('');
|
|
848
988
|
const [q, setQ] = useState('');
|
|
989
|
+
const [expanded, setExpanded] = useState({});
|
|
990
|
+
// 测试连接结果(易失,不持久):key → { status:'testing' } | { ok:true, latencyMs, tools } | { ok:false, error }
|
|
991
|
+
const [testResults, setTestResults] = useState({});
|
|
849
992
|
|
|
850
993
|
const toggle = async (connector, enabled) => {
|
|
851
994
|
try { await api('connectors.setEnabled', { key: connector.key, enabled }); reload(); }
|
|
852
995
|
catch (err) { toast(err.message, true); }
|
|
853
996
|
};
|
|
997
|
+
const test = async (connector) => {
|
|
998
|
+
const key = connector.key;
|
|
999
|
+
setTestResults((prev) => ({ ...prev, [key]: { status: 'testing' } }));
|
|
1000
|
+
try {
|
|
1001
|
+
const result = await api('connectors.test', { key });
|
|
1002
|
+
setTestResults((prev) => ({ ...prev, [key]: { ok: true, latencyMs: result.latencyMs, tools: result.tools ?? [] } }));
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
setTestResults((prev) => ({ ...prev, [key]: { ok: false, error: err.message } }));
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
const setTools = async (connector, allowed, autoApprove) => {
|
|
1008
|
+
const known = testResults[connector.key]?.tools ?? [];
|
|
1009
|
+
const names = known.map((tool) => tool.name);
|
|
1010
|
+
const mode = allowed.length >= names.length && names.length > 0 ? 'all' : 'whitelist';
|
|
1011
|
+
try {
|
|
1012
|
+
await api('connectors.setTools', { key: connector.key, mode, tools: allowed, autoApprove });
|
|
1013
|
+
reload();
|
|
1014
|
+
} catch (err) { toast(err.message, true); }
|
|
1015
|
+
};
|
|
854
1016
|
const doDelete = async () => {
|
|
855
1017
|
setBusyDelete(true);
|
|
856
1018
|
try { await api('connectors.delete', { key: deleting.key }); toast(t('deleted')); setDeleting(undefined); reload(); }
|
|
@@ -863,6 +1025,8 @@ window.__ModuleLoader__.load({
|
|
|
863
1025
|
const connectors = allConnectors.filter((c) =>
|
|
864
1026
|
(!groupFilter || c.group === groupFilter) &&
|
|
865
1027
|
(!needle || (c.label + ' ' + c.serverName + ' ' + c.key + ' ' + (c.transport ?? '') + ' ' + (c.url || '') + ' ' + (c.command || '')).toLowerCase().includes(needle)));
|
|
1028
|
+
// 审批策略:Host 若暴露则读取(列表级或行级),缺省视为非 never
|
|
1029
|
+
const approvalNever = (state.data?.approvalPolicy ?? '') === 'never';
|
|
866
1030
|
return h('div', null,
|
|
867
1031
|
h('div', { className: 'mc-toolbar' },
|
|
868
1032
|
h('span', { className: 'mc-spacer' }),
|
|
@@ -877,26 +1041,80 @@ window.__ModuleLoader__.load({
|
|
|
877
1041
|
state.status === 'error' && h('div', { className: 'mc-error' }, t('loadFailed') + ': ' + state.error.message, h(Button, { onClick: reload }, t('retry'))),
|
|
878
1042
|
state.status !== 'error' && h('div', { className: 'mc-grid' },
|
|
879
1043
|
connectors.length === 0 && state.status === 'done' && h('div', { className: 'mc-empty' }, t('empty')),
|
|
880
|
-
connectors.map((connector) =>
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
h('div', { className: 'mc-card
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1044
|
+
connectors.map((connector) => {
|
|
1045
|
+
const result = testResults[connector.key];
|
|
1046
|
+
const testing = result?.status === 'testing';
|
|
1047
|
+
const tools = result?.ok ? (result.tools ?? []) : [];
|
|
1048
|
+
const policy = connector.toolPolicy ?? { mode: 'all', tools: [], autoApprove: [] };
|
|
1049
|
+
const allowedSet = new Set(policy.mode === 'all' ? tools.map((tool) => tool.name) : (policy.tools ?? []));
|
|
1050
|
+
const autoSet = new Set(policy.autoApprove ?? []);
|
|
1051
|
+
const open = expanded[connector.key] === true;
|
|
1052
|
+
const saveTools = (nextAllowed, nextAuto) =>
|
|
1053
|
+
setTools(connector, tools.map((tool) => tool.name).filter((name) => nextAllowed.has(name)), tools.map((tool) => tool.name).filter((name) => nextAuto.has(name)));
|
|
1054
|
+
return h('div', { key: connector.key, className: 'mc-card' },
|
|
1055
|
+
h('div', { className: 'mc-card-title' }, connector.label || connector.serverName,
|
|
1056
|
+
connector.group && h(Badge, { tone: 'info' }, connector.group),
|
|
1057
|
+
h(Badge, { tone: 'brand' }, connector.transport),
|
|
1058
|
+
connector.external
|
|
1059
|
+
? h(Badge, null, t('connectorExternal'))
|
|
1060
|
+
: connector.live
|
|
1061
|
+
? h(Badge, { tone: connector.enabled === false ? undefined : 'success' }, connector.enabled === false ? t('disabled') : t('connectorLive'))
|
|
1062
|
+
: h(Badge, { tone: 'warn' }, t('connectorStopped')),
|
|
1063
|
+
),
|
|
1064
|
+
h('div', { className: 'mc-card-meta' }, 'mcp__' + connector.serverName + '__*'),
|
|
1065
|
+
h('div', { className: 'mc-card-desc' }, connector.transport === 'stdio'
|
|
1066
|
+
? [connector.command, ...(connector.args ?? [])].join(' ')
|
|
1067
|
+
: connector.url),
|
|
1068
|
+
!connector.external && h('div', { className: 'mc-card-actions' },
|
|
1069
|
+
h(Switch, { checked: connector.enabled !== false, onChange: (v) => toggle(connector, v), title: connector.enabled !== false ? t('disable') : t('enable') }),
|
|
1070
|
+
h(Button, { className: 'link', disabled: testing, onClick: () => test(connector) }, testing ? '…' : t('connectorTest')),
|
|
1071
|
+
h(Badge, {
|
|
1072
|
+
tone: result?.ok ? 'success' : result?.ok === false ? 'error' : undefined,
|
|
1073
|
+
title: result?.ok ? result.latencyMs + ' ms' : (result?.error ?? ''),
|
|
1074
|
+
}, testing
|
|
1075
|
+
? '…'
|
|
1076
|
+
: result?.ok
|
|
1077
|
+
? t('connectorConnected').replace('{n}', String(result.tools?.length ?? 0))
|
|
1078
|
+
: result?.ok === false
|
|
1079
|
+
? t('connectorTestFailed').replace('{reason}', (result.error || '').slice(0, 60))
|
|
1080
|
+
: t('connectorUntested')),
|
|
1081
|
+
h('span', { className: 'mc-spacer' }),
|
|
1082
|
+
h(Button, { className: 'link', onClick: () => setExpanded({ ...expanded, [connector.key]: !open }) }, (open ? '▾ ' : '▸ ') + t('connectorTools')),
|
|
1083
|
+
h(Button, { className: 'link', onClick: () => setEditing(connector) }, t('edit')),
|
|
1084
|
+
h(Button, { className: 'link danger', onClick: () => setDeleting(connector) }, t('delete')),
|
|
1085
|
+
),
|
|
1086
|
+
!connector.external && open && h('div', null,
|
|
1087
|
+
tools.length === 0 && h('div', { className: 'mc-row-sub', style: { marginBottom: 6 } }, t('connectorToolsEmpty')),
|
|
1088
|
+
tools.map((tool) => h('div', { key: tool.name, className: 'mc-row', style: { padding: '6px 10px', marginBottom: 6 } },
|
|
1089
|
+
h('div', { className: 'mc-row-main' },
|
|
1090
|
+
h('div', { className: 'mc-row-title' }, tool.name),
|
|
1091
|
+
tool.description && h('div', { className: 'mc-row-sub' }, tool.description),
|
|
1092
|
+
),
|
|
1093
|
+
h('label', { className: 'mc-checkbox', style: { fontSize: 12 } },
|
|
1094
|
+
h('input', {
|
|
1095
|
+
type: 'checkbox', checked: allowedSet.has(tool.name),
|
|
1096
|
+
onChange: (e) => {
|
|
1097
|
+
const nextAllowed = new Set(allowedSet);
|
|
1098
|
+
const nextAuto = new Set(autoSet);
|
|
1099
|
+
if (e.target.checked) nextAllowed.add(tool.name);
|
|
1100
|
+
else { nextAllowed.delete(tool.name); nextAuto.delete(tool.name); }
|
|
1101
|
+
saveTools(nextAllowed, nextAuto);
|
|
1102
|
+
},
|
|
1103
|
+
}), t('connectorToolAllowed')),
|
|
1104
|
+
h('label', { className: 'mc-checkbox', style: { fontSize: 12 } },
|
|
1105
|
+
h('input', {
|
|
1106
|
+
type: 'checkbox', checked: autoSet.has(tool.name), disabled: !allowedSet.has(tool.name),
|
|
1107
|
+
onChange: (e) => {
|
|
1108
|
+
const nextAuto = new Set(autoSet);
|
|
1109
|
+
if (e.target.checked) nextAuto.add(tool.name); else nextAuto.delete(tool.name);
|
|
1110
|
+
saveTools(allowedSet, nextAuto);
|
|
1111
|
+
},
|
|
1112
|
+
}), t('connectorToolAuto')),
|
|
1113
|
+
)),
|
|
1114
|
+
approvalNever && h('div', { className: 'mc-hint' }, t('connectorApprovalNever')),
|
|
1115
|
+
),
|
|
1116
|
+
);
|
|
1117
|
+
}),
|
|
900
1118
|
),
|
|
901
1119
|
editing && h(ConnectorEditModal, { t, connector: editing.key ? editing : undefined, onClose: () => setEditing(undefined), onSaved: () => { setEditing(undefined); toast(t('saved')); reload(); } }),
|
|
902
1120
|
deleting && h(ConfirmModal, { t, title: t('confirmDelete'), message: t('confirmDeleteBody').replace('{name}', deleting.label || deleting.serverName), busy: busyDelete, onClose: () => setDeleting(undefined), onConfirm: doDelete }),
|
|
@@ -957,14 +1175,29 @@ window.__ModuleLoader__.load({
|
|
|
957
1175
|
// undefined(旧数据)= 全部技能;编辑时显式化为全选列表
|
|
958
1176
|
skills: expert?.skills ?? (expert ? allSkills.map((s) => s.name) : []),
|
|
959
1177
|
connectors: expert?.connectors ?? [],
|
|
1178
|
+
// 模型绑定:provider+model 均填写才提交;否则缺省不传(跟随默认)
|
|
1179
|
+
modelProvider: expert?.model?.provider ?? '',
|
|
1180
|
+
modelModel: expert?.model?.model ?? '',
|
|
1181
|
+
modelReasoning: expert?.model?.reasoningEffort ?? '',
|
|
1182
|
+
// 工具组白名单:空数组/缺省 = 全部可用
|
|
1183
|
+
tools: expert?.tools ?? [],
|
|
960
1184
|
});
|
|
961
1185
|
const [busy, setBusy] = useState(false);
|
|
962
1186
|
const [error, setError] = useState('');
|
|
1187
|
+
const modelError = (form.modelProvider && !form.modelModel) || (!form.modelProvider && form.modelModel)
|
|
1188
|
+
? t('expertModelPartial') : '';
|
|
963
1189
|
const set = (key) => (e) => setForm({ ...form, [key]: e.target.value });
|
|
964
1190
|
const submit = async () => {
|
|
1191
|
+
if (modelError) return;
|
|
965
1192
|
setBusy(true); setError('');
|
|
966
1193
|
try {
|
|
967
|
-
|
|
1194
|
+
const { modelProvider, modelModel, modelReasoning, ...rest } = form;
|
|
1195
|
+
const payload = { ...rest };
|
|
1196
|
+
if (modelProvider && modelModel) {
|
|
1197
|
+
payload.model = { provider: modelProvider, model: modelModel };
|
|
1198
|
+
if (modelReasoning) payload.model.reasoningEffort = modelReasoning;
|
|
1199
|
+
}
|
|
1200
|
+
await api(isEdit ? 'experts.update' : 'experts.create', payload);
|
|
968
1201
|
onSaved();
|
|
969
1202
|
} catch (err) { setError(err.message); } finally { setBusy(false); }
|
|
970
1203
|
};
|
|
@@ -981,6 +1214,25 @@ window.__ModuleLoader__.load({
|
|
|
981
1214
|
h(Field, { label: t('expertDescription') }, h('input', { className: 'mc-input', value: form.description, onChange: set('description') })),
|
|
982
1215
|
h(Field, { label: t('group') }, h('input', { className: 'mc-input', value: form.group, onChange: set('group'), placeholder: t('groupPlaceholder') })),
|
|
983
1216
|
h(Field, { label: t('expertPrefix') }, h('textarea', { className: 'mc-textarea', value: form.prefix, onChange: set('prefix'), placeholder: 'You are …' })),
|
|
1217
|
+
h(Field, { label: t('expertModel') },
|
|
1218
|
+
h('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
1219
|
+
h('input', { className: 'mc-input', style: { flex: 1, minWidth: 140 }, placeholder: t('expertModelProvider'), value: form.modelProvider, onChange: set('modelProvider') }),
|
|
1220
|
+
h('input', { className: 'mc-input', style: { flex: 1, minWidth: 140 }, placeholder: t('expertModelName'), value: form.modelModel, onChange: set('modelModel') }),
|
|
1221
|
+
h('select', { className: 'mc-select', value: form.modelReasoning, onChange: set('modelReasoning'), title: 'reasoningEffort' },
|
|
1222
|
+
h('option', { value: '' }, t('expertModelDefault')),
|
|
1223
|
+
['low', 'medium', 'high'].map((v) => h('option', { key: v, value: v }, v)))),
|
|
1224
|
+
h('span', { className: 'mc-hint' }, t('expertModelHint')),
|
|
1225
|
+
modelError && h('span', { className: 'mc-field-error' }, modelError)),
|
|
1226
|
+
h(Field, { label: t('expertTools') },
|
|
1227
|
+
h('div', { style: { display: 'flex', gap: 12, flexWrap: 'wrap' } },
|
|
1228
|
+
[['fs', t('expertToolFs')], ['web', t('expertToolWeb')], ['shell', t('expertToolShell')], ['skills', t('expertToolSkills')]].map(([value, labelText]) =>
|
|
1229
|
+
h('label', { key: value, className: 'mc-checkbox', style: { fontSize: 12 } },
|
|
1230
|
+
h('input', {
|
|
1231
|
+
type: 'checkbox', checked: form.tools.includes(value),
|
|
1232
|
+
onChange: (e) => setForm({ ...form, tools: e.target.checked ? [...form.tools, value] : form.tools.filter((v) => v !== value) }),
|
|
1233
|
+
}),
|
|
1234
|
+
labelText))),
|
|
1235
|
+
h('span', { className: 'mc-hint' }, t('expertToolsHint'))),
|
|
984
1236
|
h(LinkPicker, {
|
|
985
1237
|
t, title: t('expertSkills'), items: allSkills,
|
|
986
1238
|
selected: form.skills.filter((n) => allSkills.some((s) => s.name === n)),
|
|
@@ -1040,9 +1292,14 @@ window.__ModuleLoader__.load({
|
|
|
1040
1292
|
expert.custom ? h(Badge, { tone: 'brand' }, t('expertCustom')) : h(Badge, null, t('expertBuiltin')),
|
|
1041
1293
|
expert.isDefault && h(Badge, { tone: 'success' }, t('expertDefault')),
|
|
1042
1294
|
expert.broken && h(Badge, { tone: 'error' }, t('expertBroken')),
|
|
1295
|
+
expert.model && h(Badge, { tone: 'brand' }, expert.model.provider + '/' + expert.model.model),
|
|
1043
1296
|
),
|
|
1044
1297
|
h('div', { className: 'mc-card-desc' }, expert.description || '—'),
|
|
1045
1298
|
h('div', { className: 'mc-card-meta' }, expert.id + (expert.broken ? ' · ' + expert.broken : '')),
|
|
1299
|
+
expert.custom && (expert.tools ?? []).length > 0 && h('div', { className: 'mc-card-meta' },
|
|
1300
|
+
t('expertToolsSummary') + ': ' + expert.tools
|
|
1301
|
+
.map((v) => ({ fs: t('expertToolFs'), web: t('expertToolWeb'), shell: t('expertToolShell'), skills: t('expertToolSkills') }[v] ?? v))
|
|
1302
|
+
.join(', ')),
|
|
1046
1303
|
expert.custom && (expert.skills !== undefined || expert.connectors !== undefined) && h('div', { className: 'mc-card-meta' },
|
|
1047
1304
|
t('expertLinkedMeta')
|
|
1048
1305
|
.replace('{skills}', expert.skills === undefined ? t('expertSkillsAll') : expert.skills.length)
|