dsh-skill-mcp 0.2.0 → 0.4.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/lib/client.js +840 -154
- package/lib/index.js +412 -1
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-skill-mcp client half (v0.
|
|
2
|
+
* dsh-skill-mcp client half (v0.3.0).
|
|
3
3
|
*
|
|
4
4
|
* "管理中心"插件:单面板内 tab 切换「技能 / MCP」。
|
|
5
5
|
*
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* /api/cc-skills/* HTTP 路由。信封 {ok, value | error{message}}。
|
|
8
8
|
* - 按项目 MCP:监测当前会话工作区 cwd(sessions.list store),变化即推送
|
|
9
9
|
* mcp/sync;host 热加载/热卸载对应 @deepseek-ai/dsh-mcp-client 条目。
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
10
|
+
* - MCP 管理(v0.3):添加/编辑(表单 + JSON 切换)/删除(二次确认),
|
|
11
|
+
* 从其他 agent(Claude Code / Codex / Cursor / Gemini CLI)扫描导入,
|
|
12
|
+
* 写入作用域显式选择(全局 / 当前项目)。
|
|
13
|
+
* - 挂载点:设置页 settings.section 槽位,渲染 CenterPanel。
|
|
12
14
|
*/
|
|
13
15
|
window.__ModuleLoader__.load({
|
|
14
16
|
id: "dsh-skill-mcp",
|
|
@@ -40,15 +42,9 @@ window.__ModuleLoader__.load({
|
|
|
40
42
|
// ------------------------------------------------------------------
|
|
41
43
|
|
|
42
44
|
const css = `
|
|
43
|
-
.dsh-ccs-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.dsh-ccs-overlay-panel { width: 640px; max-width: 92vw; max-height: 86vh; overflow-y: auto;
|
|
47
|
-
background: var(--dsw-alias-bg-primary, #fff); color: var(--dsw-alias-text-primary, #222);
|
|
48
|
-
border-radius: 12px; padding: 20px 22px;
|
|
49
|
-
box-shadow: 0 8px 32px rgba(0,0,0,0.2); }
|
|
50
|
-
.dsh-ccs-title { margin: 0 0 6px; font-size: 16px; font-weight: 600; }
|
|
51
|
-
.dsh-ccs-tabs { display: flex; gap: 2px; margin: 8px 0 14px;
|
|
45
|
+
.dsh-ccs-page { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
|
|
46
|
+
.dsh-ccs-title { margin: 0 0 6px; font-size: 16px; font-weight: 600; flex-shrink: 0; }
|
|
47
|
+
.dsh-ccs-tabs { display: flex; gap: 2px; margin: 8px 0 14px; flex-shrink: 0;
|
|
52
48
|
border-bottom: 1px solid var(--dsw-alias-border-primary, rgba(127,127,127,0.25)); }
|
|
53
49
|
.dsh-ccs-tab { appearance: none; padding: 7px 16px; background: transparent; border: none;
|
|
54
50
|
border-bottom: 2px solid transparent; margin-bottom: -1px; cursor: pointer; font-size: 13px;
|
|
@@ -62,6 +58,7 @@ window.__ModuleLoader__.load({
|
|
|
62
58
|
.dsh-ccs-row { display: flex; align-items: center; gap: 10px;
|
|
63
59
|
padding: 10px 12px; border-radius: 8px;
|
|
64
60
|
background: var(--dsw-alias-bg-secondary, rgba(127,127,127,0.07)); }
|
|
61
|
+
.dsh-ccs-row-disabled { opacity: 0.55; }
|
|
65
62
|
.dsh-ccs-main { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; }
|
|
66
63
|
.dsh-ccs-text { flex: 1; min-width: 0; }
|
|
67
64
|
.dsh-ccs-name { font-size: 13px; font-weight: 600; white-space: nowrap;
|
|
@@ -83,6 +80,24 @@ window.__ModuleLoader__.load({
|
|
|
83
80
|
.dsh-ccs-empty { padding: 18px; text-align: center; font-size: 12px;
|
|
84
81
|
color: var(--dsw-alias-text-secondary, #666); border: 1px dashed var(--dsw-alias-border-primary, rgba(127,127,127,0.3));
|
|
85
82
|
border-radius: 8px; }
|
|
83
|
+
/* 技能页头部:根目录信息 + 动作按钮(置顶) */
|
|
84
|
+
.dsh-ccs-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
|
85
|
+
margin-bottom: 10px; flex-shrink: 0; }
|
|
86
|
+
.dsh-ccs-head .dsh-ccs-root { flex: 1 1 auto; min-width: 0; margin-bottom: 0; }
|
|
87
|
+
.dsh-ccs-head-actions { display: flex; gap: 8px; flex-shrink: 0; }
|
|
88
|
+
.dsh-ccs-search-row { margin-bottom: 10px; flex-shrink: 0; }
|
|
89
|
+
.dsh-ccs-search { width: 100%; box-sizing: border-box; }
|
|
90
|
+
/* 列表区独立滚动:flex 填充 + 上限兜底(settings 页内无外层高度约束时用) */
|
|
91
|
+
.dsh-ccs-list-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; max-height: 52vh; }
|
|
92
|
+
.dsh-ccs-list-scroll::-webkit-scrollbar { width: 8px; }
|
|
93
|
+
.dsh-ccs-list-scroll::-webkit-scrollbar-thumb { background: var(--dsw-alias-scrollbar-bg-l2, rgba(127,127,127,0.35)); border-radius: 4px; }
|
|
94
|
+
.dsh-ccs-body-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; max-height: 60vh; }
|
|
95
|
+
.dsh-ccs-body-scroll::-webkit-scrollbar { width: 8px; }
|
|
96
|
+
.dsh-ccs-body-scroll::-webkit-scrollbar-thumb { background: var(--dsw-alias-scrollbar-bg-l2, rgba(127,127,127,0.35)); border-radius: 4px; }
|
|
97
|
+
/* 设置页上下文:区块撑满设置内容区剩余高度,消除底部大片留白 */
|
|
98
|
+
.dsh-ccs-page-fill { flex: 1 0 auto; min-height: calc(100vh - 180px); }
|
|
99
|
+
.dsh-ccs-page-fill .dsh-ccs-list-scroll,
|
|
100
|
+
.dsh-ccs-page-fill .dsh-ccs-body-scroll { max-height: none; }
|
|
86
101
|
.dsh-ccs-actions { display: flex; gap: 8px; margin-top: 14px; flex-wrap: wrap; }
|
|
87
102
|
.dsh-ccs-msg { font-size: 12px; margin-top: 10px; white-space: pre-wrap; word-break: break-all; }
|
|
88
103
|
.dsh-ccs-msg-ok { color: var(--dsw-alias-state-success-label, #2e7d32); }
|
|
@@ -112,6 +127,140 @@ window.__ModuleLoader__.load({
|
|
|
112
127
|
border: 1px solid var(--dsw-alias-border-primary, rgba(127,127,127,0.35));
|
|
113
128
|
background: var(--dsw-alias-bg-primary, transparent); color: var(--dsw-alias-text-primary, #222); }
|
|
114
129
|
.dsh-ccs-input-row { display: flex; gap: 8px; align-items: center; margin: 12px 0; }
|
|
130
|
+
/* 弹框:添加/编辑 MCP(遮罩用 bg-mask-1,表面用实底 bg-base,避免透出变暗) */
|
|
131
|
+
.dsh-ccs-modal-mask { position: fixed; inset: 0; z-index: 1100; display: flex;
|
|
132
|
+
align-items: center; justify-content: center; padding: 24px;
|
|
133
|
+
background: var(--dsw-alias-bg-mask-1, rgba(0,0,0,0.45)); }
|
|
134
|
+
.dsh-ccs-modal { display: flex; flex-direction: column; min-width: 0; box-sizing: border-box;
|
|
135
|
+
width: min(600px, 100%); max-height: min(80vh, 680px); overflow: auto; padding: 14px 16px;
|
|
136
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.3)); border-radius: 12px;
|
|
137
|
+
background: var(--dsw-alias-bg-base, #fff); box-shadow: 0 12px 40px rgba(0,0,0,0.22); }
|
|
138
|
+
.dsh-ccs-modal .dsh-ccs-card { border: none; background: none; padding: 0; border-radius: 0; }
|
|
139
|
+
/* ===== MCP 页:对齐 dsh-plugin-capabilities 的 dpc- 设计语言 =====
|
|
140
|
+
走宿主 --dsw-alias-label-*/-bg-layer-*/-border-l2/-state-* tokens,
|
|
141
|
+
深浅主题自适应;共享类用 .dsh-ccs-mcp 作用域覆盖,不影响技能页。 */
|
|
142
|
+
.dsh-ccs-mcp { display: flex; flex-direction: column; gap: 12px; width: 100%;
|
|
143
|
+
max-width: 760px; color: var(--dsw-alias-label-primary, #222); }
|
|
144
|
+
.dsh-ccs-mcp .dsh-ccs-sub { color: var(--dsw-alias-label-tertiary, #8a8a8a); }
|
|
145
|
+
.dsh-ccs-mcp .dsh-ccs-root { margin: 0; color: var(--dsw-alias-label-secondary, #555); }
|
|
146
|
+
.dsh-ccs-mcp .dsh-ccs-fileinfo { margin: 0; font-size: 11px; line-height: 1.8;
|
|
147
|
+
color: var(--dsw-alias-label-tertiary, #8a8a8a); word-break: break-all; }
|
|
148
|
+
/* MCP 工具栏:添加 / 导入 / 重新加载 */
|
|
149
|
+
.dsh-ccs-toolbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
|
|
150
|
+
flex-shrink: 0; }
|
|
151
|
+
/* 列表头:标题 + 计数(同 dpc-listHead) */
|
|
152
|
+
.dsh-ccs-listhead { display: flex; align-items: baseline; gap: 7px; padding: 0 2px; }
|
|
153
|
+
.dsh-ccs-listhead-title { margin: 0; font-size: 13px; line-height: 20px; font-weight: 600; }
|
|
154
|
+
.dsh-ccs-listhead-count { font-size: 12px; line-height: 18px;
|
|
155
|
+
color: var(--dsw-alias-label-tertiary, #8a8a8a); font-variant-numeric: tabular-nums; }
|
|
156
|
+
/* 服务卡片:双列网格(同 dpc-cards/dpc-card) */
|
|
157
|
+
.dsh-ccs-cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
158
|
+
align-items: stretch; gap: 10px; }
|
|
159
|
+
@media (max-width: 680px) { .dsh-ccs-cards { grid-template-columns: minmax(0, 1fr); } }
|
|
160
|
+
.dsh-ccs-mcard { display: flex; flex-direction: column; gap: 6px; min-width: 0;
|
|
161
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.3)); border-radius: 10px;
|
|
162
|
+
background: var(--dsw-alias-bg-layer-3, rgba(127,127,127,0.06)); padding: 10px 12px;
|
|
163
|
+
transition: background 0.15s; }
|
|
164
|
+
.dsh-ccs-mcard:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.1)); }
|
|
165
|
+
.dsh-ccs-mcard-off { opacity: 0.6; }
|
|
166
|
+
.dsh-ccs-mcard-top { display: flex; align-items: center; gap: 8px; }
|
|
167
|
+
.dsh-ccs-mcard-name { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
|
|
168
|
+
white-space: nowrap; font-size: 14px; line-height: 20px; font-weight: 600;
|
|
169
|
+
font-family: var(--ds-font-family-code, ui-monospace, Consolas, monospace); }
|
|
170
|
+
.dsh-ccs-mcard-spacer { flex: 1; }
|
|
171
|
+
.dsh-ccs-mcard-desc { font-size: 12px; line-height: 18px;
|
|
172
|
+
color: var(--dsw-alias-label-secondary, #555);
|
|
173
|
+
font-family: var(--ds-font-family-code, ui-monospace, Consolas, monospace);
|
|
174
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
175
|
+
.dsh-ccs-mcard-acts { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
176
|
+
/* 标签(同 dpc-tag):source 业务色、off 警示色 */
|
|
177
|
+
.dsh-ccs-tag { display: inline-flex; align-items: center; min-height: 18px; flex: none;
|
|
178
|
+
border-radius: 5px; padding: 1px 6px; background: var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.14));
|
|
179
|
+
color: var(--dsw-alias-label-secondary, #555); font-size: 11px; line-height: 16px; white-space: nowrap; }
|
|
180
|
+
.dsh-ccs-tag[data-kind='source'] { background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #4b7bec) 10%, transparent);
|
|
181
|
+
color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
182
|
+
.dsh-ccs-tag[data-kind='off'] { background: color-mix(in srgb, var(--dsw-alias-state-warning-primary, #b7791f) 12%, transparent);
|
|
183
|
+
color: var(--dsw-alias-label-secondary, #555); }
|
|
184
|
+
/* MCP 消息条(同 dpc-banner):ok 绿 / err 红 tint */
|
|
185
|
+
.dsh-ccs-mcp .dsh-ccs-msg { margin: 0; display: flex; align-items: flex-start; gap: 8px;
|
|
186
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.3)); border-radius: 8px;
|
|
187
|
+
padding: 9px 12px; background: var(--dsw-alias-bg-layer-3, rgba(127,127,127,0.06));
|
|
188
|
+
font-size: 12px; line-height: 18px; white-space: pre-wrap; word-break: break-all;
|
|
189
|
+
color: var(--dsw-alias-label-primary, #222); }
|
|
190
|
+
.dsh-ccs-mcp .dsh-ccs-msg-ok { border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary, #2e7d32) 35%, transparent);
|
|
191
|
+
background: color-mix(in srgb, var(--dsw-alias-state-success-primary, #2e7d32) 8%, transparent); }
|
|
192
|
+
.dsh-ccs-mcp .dsh-ccs-msg-err { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary, #e5484d) 35%, transparent);
|
|
193
|
+
background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #e5484d) 8%, transparent);
|
|
194
|
+
color: var(--dsw-alias-state-error-primary, #e5484d); }
|
|
195
|
+
.dsh-ccs-mcp .dsh-ccs-empty { margin: 0; padding: 14px 2px; text-align: left;
|
|
196
|
+
font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-tertiary, #8a8a8a);
|
|
197
|
+
border: none; background: none; white-space: pre-wrap; }
|
|
198
|
+
/* MCP 按钮 / 输入框(作用域覆盖,不动技能页) */
|
|
199
|
+
.dsh-ccs-mcp .dsh-ccs-btn { appearance: none; padding: 5px 12px; border-radius: 8px;
|
|
200
|
+
cursor: pointer; font-size: 12px; line-height: 18px;
|
|
201
|
+
background: var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.12));
|
|
202
|
+
color: var(--dsw-alias-label-primary, #222);
|
|
203
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.3)); }
|
|
204
|
+
.dsh-ccs-mcp .dsh-ccs-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.16)); }
|
|
205
|
+
.dsh-ccs-mcp .dsh-ccs-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
206
|
+
.dsh-ccs-mcp .dsh-ccs-btn-primary { background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #4b7bec) 14%, transparent);
|
|
207
|
+
border-color: color-mix(in srgb, var(--dsw-alias-state-business-primary, #4b7bec) 40%, transparent);
|
|
208
|
+
color: var(--dsw-alias-state-business-primary, #4b7bec); font-weight: 600; }
|
|
209
|
+
.dsh-ccs-mcp .dsh-ccs-btn-primary:hover:not(:disabled) { background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #4b7bec) 22%, transparent); }
|
|
210
|
+
.dsh-ccs-mcp .dsh-ccs-icon-btn { appearance: none; background: none; border: none; cursor: pointer;
|
|
211
|
+
font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary, #8a8a8a);
|
|
212
|
+
padding: 3px 8px; border-radius: 6px; }
|
|
213
|
+
.dsh-ccs-mcp .dsh-ccs-icon-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.12));
|
|
214
|
+
color: var(--dsw-alias-label-primary, #222); }
|
|
215
|
+
.dsh-ccs-mcp .dsh-ccs-icon-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
216
|
+
.dsh-ccs-mcp .dsh-ccs-icon-btn-danger:hover:not(:disabled) { background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #e5484d) 12%, transparent);
|
|
217
|
+
color: var(--dsw-alias-state-error-primary, #e5484d); }
|
|
218
|
+
.dsh-ccs-mcp .dsh-ccs-input, .dsh-ccs-mcp .dsh-ccs-select, .dsh-ccs-mcp .dsh-ccs-textarea {
|
|
219
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.35)); border-radius: 8px;
|
|
220
|
+
background: var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.08));
|
|
221
|
+
color: var(--dsw-alias-label-primary, #222); outline: none; }
|
|
222
|
+
.dsh-ccs-mcp .dsh-ccs-input:focus-visible, .dsh-ccs-mcp .dsh-ccs-select:focus-visible,
|
|
223
|
+
.dsh-ccs-mcp .dsh-ccs-textarea:focus-visible { border-color: var(--dsw-alias-state-business-primary, #4b7bec);
|
|
224
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary, #4b7bec) 18%, transparent); }
|
|
225
|
+
.dsh-ccs-mcp .dsh-ccs-select:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
226
|
+
/* MCP 添加/编辑表单与导入面板:内联展开的卡片 */
|
|
227
|
+
.dsh-ccs-card { display: flex; flex-direction: column; gap: 10px; padding: 12px 14px;
|
|
228
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.3)); border-radius: 10px;
|
|
229
|
+
background: var(--dsw-alias-bg-layer-3, rgba(127,127,127,0.06)); flex-shrink: 0; }
|
|
230
|
+
.dsh-ccs-card-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
231
|
+
.dsh-ccs-card-title { font-size: 13px; line-height: 20px; font-weight: 600; flex: 1; min-width: 0; }
|
|
232
|
+
.dsh-ccs-json-toggle { display: inline-flex; align-items: center; gap: 5px; font-size: 12px;
|
|
233
|
+
color: var(--dsw-alias-label-tertiary, #8a8a8a); cursor: pointer; user-select: none; }
|
|
234
|
+
.dsh-ccs-json-toggle:hover { color: var(--dsw-alias-label-primary, #222); }
|
|
235
|
+
.dsh-ccs-form-row { display: flex; gap: 8px; align-items: center; }
|
|
236
|
+
.dsh-ccs-label { flex: none; width: 64px; font-size: 12px;
|
|
237
|
+
color: var(--dsw-alias-label-tertiary, #8a8a8a); text-align: right; }
|
|
238
|
+
.dsh-ccs-select { appearance: auto; padding: 6px 8px; border-radius: 8px; font-size: 12px;
|
|
239
|
+
cursor: pointer; min-width: 0; }
|
|
240
|
+
.dsh-ccs-textarea { width: 100%; box-sizing: border-box; min-height: 64px; padding: 7px 10px;
|
|
241
|
+
border-radius: 8px; font-size: 12px;
|
|
242
|
+
font-family: var(--ds-font-family-code, ui-monospace, Consolas, 'Courier New', monospace);
|
|
243
|
+
line-height: 1.5; resize: vertical; }
|
|
244
|
+
.dsh-ccs-textarea-tall { min-height: 120px; }
|
|
245
|
+
/* 导入面板:按 agent 分组的复选列表 */
|
|
246
|
+
.dsh-ccs-group { border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.3));
|
|
247
|
+
border-radius: 8px; overflow: hidden; margin-bottom: 8px; }
|
|
248
|
+
.dsh-ccs-group-head { display: flex; align-items: center; gap: 8px; padding: 7px 10px;
|
|
249
|
+
background: var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.08)); font-size: 12px; font-weight: 600;
|
|
250
|
+
color: var(--dsw-alias-label-primary, #222); cursor: pointer; user-select: none; }
|
|
251
|
+
.dsh-ccs-group-head:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.14)); }
|
|
252
|
+
.dsh-ccs-group-count { font-weight: 400; color: var(--dsw-alias-label-tertiary, #8a8a8a);
|
|
253
|
+
font-variant-numeric: tabular-nums; }
|
|
254
|
+
.dsh-ccs-check-row { display: flex; align-items: center; gap: 10px; padding: 7px 10px;
|
|
255
|
+
border-top: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.18));
|
|
256
|
+
font-size: 12px; color: var(--dsw-alias-label-primary, #222); cursor: pointer; }
|
|
257
|
+
.dsh-ccs-check-row:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.1)); }
|
|
258
|
+
.dsh-ccs-check-row-existing { opacity: 0.5; cursor: not-allowed; }
|
|
259
|
+
.dsh-ccs-check-name { font-weight: 600; flex: none;
|
|
260
|
+
font-family: var(--ds-font-family-code, ui-monospace, Consolas, monospace); }
|
|
261
|
+
.dsh-ccs-check-sub { flex: 1; min-width: 0; color: var(--dsw-alias-label-tertiary, #8a8a8a);
|
|
262
|
+
font-family: var(--ds-font-family-code, ui-monospace, Consolas, monospace);
|
|
263
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
115
264
|
`;
|
|
116
265
|
|
|
117
266
|
function ensureStyles() {
|
|
@@ -137,10 +286,10 @@ window.__ModuleLoader__.load({
|
|
|
137
286
|
// 技能与MCP:单面板 + tab(技能 / MCP)
|
|
138
287
|
// ------------------------------------------------------------------
|
|
139
288
|
|
|
140
|
-
function CenterPanel({ pickDirectory }) {
|
|
289
|
+
function CenterPanel({ pickDirectory, fillHeight }) {
|
|
141
290
|
const [tab, setTab] = React.useState('skills');
|
|
142
291
|
return jsx('div', {
|
|
143
|
-
className: 'dsh-ccs-page'
|
|
292
|
+
className: `dsh-ccs-page${fillHeight ? ' dsh-ccs-page-fill' : ''}`,
|
|
144
293
|
children: [
|
|
145
294
|
jsx('h2', { key: 'title', className: 'dsh-ccs-title', children: '技能与MCP' }),
|
|
146
295
|
jsx('div', {
|
|
@@ -159,13 +308,16 @@ window.__ModuleLoader__.load({
|
|
|
159
308
|
}),
|
|
160
309
|
tab === 'skills'
|
|
161
310
|
? jsx(SkillManagerSection, { key: 'body', pickDirectory })
|
|
162
|
-
: jsx(
|
|
311
|
+
: jsx('div', {
|
|
312
|
+
key: 'body', className: 'dsh-ccs-body-scroll',
|
|
313
|
+
children: jsx(McpPanel),
|
|
314
|
+
}),
|
|
163
315
|
],
|
|
164
316
|
});
|
|
165
317
|
}
|
|
166
318
|
|
|
167
319
|
// ------------------------------------------------------------------
|
|
168
|
-
// MCP tab
|
|
320
|
+
// MCP tab:状态 + 添加/编辑/删除 + 从其他 agent 导入
|
|
169
321
|
// ------------------------------------------------------------------
|
|
170
322
|
|
|
171
323
|
const SOURCE_LABEL = {
|
|
@@ -174,6 +326,26 @@ window.__ModuleLoader__.load({
|
|
|
174
326
|
'project-mcp-json': '.mcp.json',
|
|
175
327
|
};
|
|
176
328
|
|
|
329
|
+
/** 编辑模式下 RPC source → 保存作用域(写回原文件)。 */
|
|
330
|
+
const SOURCE_TO_SCOPE = {
|
|
331
|
+
user: 'user',
|
|
332
|
+
'project-dsh-json': 'project-dsh',
|
|
333
|
+
'project-mcp-json': 'project-mcp',
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const SCOPE_LABEL = {
|
|
337
|
+
user: '全局 ~/.dsh/mcp.json',
|
|
338
|
+
'project-dsh': '当前项目 .dsh/mcp.json',
|
|
339
|
+
'project-mcp': '当前项目 .mcp.json',
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const AGENT_LABEL = {
|
|
343
|
+
'claude-code': 'Claude Code',
|
|
344
|
+
codex: 'Codex',
|
|
345
|
+
cursor: 'Cursor',
|
|
346
|
+
gemini: 'Gemini CLI',
|
|
347
|
+
};
|
|
348
|
+
|
|
177
349
|
function mcpSummary(row) {
|
|
178
350
|
if (row.transport === 'streamable-http') return String(row.url || '');
|
|
179
351
|
const args = Array.isArray(row.args) && row.args.length > 0 ? ' ' + row.args.join(' ') : '';
|
|
@@ -188,10 +360,371 @@ window.__ModuleLoader__.load({
|
|
|
188
360
|
return 'dsh-ccs-dot-off';
|
|
189
361
|
}
|
|
190
362
|
|
|
363
|
+
/** 每行 `KEY=VALUE` 或 `KEY: VALUE` 的文本 → 对象(# 开头跳过)。 */
|
|
364
|
+
function parseKvLines(text) {
|
|
365
|
+
const out = {};
|
|
366
|
+
for (const line of String(text || '').split('\n')) {
|
|
367
|
+
const t = line.trim();
|
|
368
|
+
if (t === '' || t.startsWith('#')) continue;
|
|
369
|
+
const m = t.match(/^([^=:]+?)\s*[=:]\s*(.*)$/);
|
|
370
|
+
if (m) out[m[1].trim()] = m[2].trim();
|
|
371
|
+
}
|
|
372
|
+
return out;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function kvToText(record) {
|
|
376
|
+
return Object.entries(record || {}).map(([k, v]) => `${k}=${v}`).join('\n');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function headersToText(record) {
|
|
380
|
+
return Object.entries(record || {}).map(([k, v]) => `${k}: ${v}`).join('\n');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function importSummary(server) {
|
|
384
|
+
if (server.transport === 'http') return String(server.url || '');
|
|
385
|
+
const args = Array.isArray(server.args) && server.args.length > 0 ? ' ' + server.args.join(' ') : '';
|
|
386
|
+
return `${server.command ?? ''}${args}`.trim();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* 编辑器字段 → mcp/save payload。表单模式解析 KV / 参数文本;
|
|
391
|
+
* JSON 模式直接取原始条目字段。非法输入抛 Error(消息展示给用户)。
|
|
392
|
+
*/
|
|
393
|
+
function editorToPayload(fields) {
|
|
394
|
+
if (fields.jsonMode) {
|
|
395
|
+
let raw;
|
|
396
|
+
try { raw = JSON.parse(fields.jsonText); } catch (e) { throw new Error(`JSON 解析失败:${e.message}`); }
|
|
397
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('JSON 条目必须是一个对象。');
|
|
398
|
+
const isHttp = raw.command === undefined || raw.command === null || raw.command === '';
|
|
399
|
+
return {
|
|
400
|
+
name: fields.name, scope: fields.scope,
|
|
401
|
+
transport: isHttp ? 'http' : 'stdio',
|
|
402
|
+
command: raw.command, args: raw.args, env: raw.env, cwd: raw.cwd,
|
|
403
|
+
url: raw.url, headers: raw.headers,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
name: fields.name.trim(), scope: fields.scope,
|
|
408
|
+
transport: fields.transport,
|
|
409
|
+
command: fields.command,
|
|
410
|
+
args: fields.transport === 'stdio' ? fields.args.trim().split(/\s+/).filter(Boolean) : [],
|
|
411
|
+
env: fields.transport === 'stdio' ? parseKvLines(fields.env) : {},
|
|
412
|
+
// cwd 不在表单里提供:host 统一按当前项目根兜底;需要固定目录请走 JSON 编辑(写 cwd 字段)。
|
|
413
|
+
url: fields.url,
|
|
414
|
+
headers: fields.transport === 'http' ? parseKvLines(fields.headers) : {},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** 添加/编辑表单(含 JSON 编辑切换)。 */
|
|
419
|
+
function McpEditor({ editor, setEditor, hasProject, onSaved, setMsg, setErr, busy, setBusy }) {
|
|
420
|
+
const f = editor.fields;
|
|
421
|
+
const setField = (patch) => setEditor({ ...editor, fields: { ...f, ...patch } });
|
|
422
|
+
|
|
423
|
+
const save = async () => {
|
|
424
|
+
setBusy(true); setErr(null);
|
|
425
|
+
try {
|
|
426
|
+
const payload = editorToPayload(f);
|
|
427
|
+
const res = await callApi('mcp/save', payload);
|
|
428
|
+
if (!res.ok) throw new Error(res.error);
|
|
429
|
+
setMsg(res.message);
|
|
430
|
+
try { hostCtx?.emit?.('connection/reset'); } catch { /* 静默降级 */ }
|
|
431
|
+
await onSaved();
|
|
432
|
+
setEditor(null);
|
|
433
|
+
} catch (e) {
|
|
434
|
+
setErr(e?.message || String(e));
|
|
435
|
+
} finally {
|
|
436
|
+
setBusy(false);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const toggleJson = (on) => {
|
|
441
|
+
const jsonText = f.jsonText
|
|
442
|
+
|| JSON.stringify(f.transport === 'http'
|
|
443
|
+
? { type: 'http', url: 'https://example.com/mcp' }
|
|
444
|
+
: { command: 'npx', args: ['-y', 'package'] }, null, 2);
|
|
445
|
+
setField({ jsonMode: on, jsonText });
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const rows = [];
|
|
449
|
+
if (editor.mode === 'add') {
|
|
450
|
+
rows.push(jsx('div', {
|
|
451
|
+
key: 'name', className: 'dsh-ccs-form-row', children: [
|
|
452
|
+
jsx('span', { key: 'l', className: 'dsh-ccs-label', children: '名称' }),
|
|
453
|
+
jsx('input', {
|
|
454
|
+
key: 'v', className: 'dsh-ccs-input', value: f.name,
|
|
455
|
+
onChange: (e) => setField({ name: e.target.value }),
|
|
456
|
+
placeholder: '1-32 位字母数字 _ -(如 codegraph)',
|
|
457
|
+
}),
|
|
458
|
+
],
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
if (!f.jsonMode) {
|
|
462
|
+
rows.push(jsx('div', {
|
|
463
|
+
key: 'transport', className: 'dsh-ccs-form-row', children: [
|
|
464
|
+
jsx('span', { key: 'l', className: 'dsh-ccs-label', children: '类型' }),
|
|
465
|
+
jsx('select', {
|
|
466
|
+
key: 'v', className: 'dsh-ccs-select', value: f.transport,
|
|
467
|
+
onChange: (e) => setField({ transport: e.target.value }),
|
|
468
|
+
children: [
|
|
469
|
+
jsx('option', { key: 'stdio', value: 'stdio', children: 'stdio(本地命令)' }),
|
|
470
|
+
jsx('option', { key: 'http', value: 'http', children: 'HTTP(远程服务)' }),
|
|
471
|
+
],
|
|
472
|
+
}),
|
|
473
|
+
],
|
|
474
|
+
}));
|
|
475
|
+
if (f.transport === 'stdio') {
|
|
476
|
+
rows.push(
|
|
477
|
+
jsx('div', {
|
|
478
|
+
key: 'command', className: 'dsh-ccs-form-row', children: [
|
|
479
|
+
jsx('span', { key: 'l', className: 'dsh-ccs-label', children: '命令' }),
|
|
480
|
+
jsx('input', {
|
|
481
|
+
key: 'v', className: 'dsh-ccs-input', value: f.command,
|
|
482
|
+
onChange: (e) => setField({ command: e.target.value }),
|
|
483
|
+
placeholder: '如 npx / codegraph / node',
|
|
484
|
+
}),
|
|
485
|
+
],
|
|
486
|
+
}),
|
|
487
|
+
jsx('div', {
|
|
488
|
+
key: 'args', className: 'dsh-ccs-form-row', children: [
|
|
489
|
+
jsx('span', { key: 'l', className: 'dsh-ccs-label', children: '参数' }),
|
|
490
|
+
jsx('input', {
|
|
491
|
+
key: 'v', className: 'dsh-ccs-input', value: f.args,
|
|
492
|
+
onChange: (e) => setField({ args: e.target.value }),
|
|
493
|
+
placeholder: '空格分隔,如 serve --mcp',
|
|
494
|
+
}),
|
|
495
|
+
],
|
|
496
|
+
}),
|
|
497
|
+
jsx('div', {
|
|
498
|
+
key: 'env', className: 'dsh-ccs-form-row', children: [
|
|
499
|
+
jsx('span', { key: 'l', className: 'dsh-ccs-label', children: '环境变量' }),
|
|
500
|
+
jsx('textarea', {
|
|
501
|
+
key: 'v', className: 'dsh-ccs-textarea', value: f.env,
|
|
502
|
+
onChange: (e) => setField({ env: e.target.value }),
|
|
503
|
+
placeholder: '每行一条,如 API_KEY=abc',
|
|
504
|
+
}),
|
|
505
|
+
],
|
|
506
|
+
}),
|
|
507
|
+
);
|
|
508
|
+
} else {
|
|
509
|
+
rows.push(
|
|
510
|
+
jsx('div', {
|
|
511
|
+
key: 'url', className: 'dsh-ccs-form-row', children: [
|
|
512
|
+
jsx('span', { key: 'l', className: 'dsh-ccs-label', children: 'URL' }),
|
|
513
|
+
jsx('input', {
|
|
514
|
+
key: 'v', className: 'dsh-ccs-input', value: f.url,
|
|
515
|
+
onChange: (e) => setField({ url: e.target.value }),
|
|
516
|
+
placeholder: 'https://example.com/mcp',
|
|
517
|
+
}),
|
|
518
|
+
],
|
|
519
|
+
}),
|
|
520
|
+
jsx('div', {
|
|
521
|
+
key: 'headers', className: 'dsh-ccs-form-row', children: [
|
|
522
|
+
jsx('span', { key: 'l', className: 'dsh-ccs-label', children: '请求头' }),
|
|
523
|
+
jsx('textarea', {
|
|
524
|
+
key: 'v', className: 'dsh-ccs-textarea', value: f.headers,
|
|
525
|
+
onChange: (e) => setField({ headers: e.target.value }),
|
|
526
|
+
placeholder: '每行一条,如 Authorization: Bearer xxx',
|
|
527
|
+
}),
|
|
528
|
+
],
|
|
529
|
+
}),
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
} else {
|
|
533
|
+
rows.push(jsx('textarea', {
|
|
534
|
+
key: 'json', className: 'dsh-ccs-textarea dsh-ccs-textarea-tall', value: f.jsonText,
|
|
535
|
+
onChange: (e) => setField({ jsonText: e.target.value }),
|
|
536
|
+
spellCheck: false,
|
|
537
|
+
placeholder: '{ "command": "…", "args": […] } 或 { "type": "http", "url": "…" }',
|
|
538
|
+
}));
|
|
539
|
+
}
|
|
540
|
+
// 写入位置:添加时显式选择;编辑时锁定为来源文件。
|
|
541
|
+
rows.push(jsx('div', {
|
|
542
|
+
key: 'scope', className: 'dsh-ccs-form-row', children: [
|
|
543
|
+
jsx('span', { key: 'l', className: 'dsh-ccs-label', children: '写入位置' }),
|
|
544
|
+
editor.mode === 'add'
|
|
545
|
+
? jsx('select', {
|
|
546
|
+
key: 'v', className: 'dsh-ccs-select', value: f.scope,
|
|
547
|
+
onChange: (e) => setField({ scope: e.target.value }),
|
|
548
|
+
children: [
|
|
549
|
+
jsx('option', { key: 'user', value: 'user', children: SCOPE_LABEL.user }),
|
|
550
|
+
jsx('option', {
|
|
551
|
+
key: 'project-dsh', value: 'project-dsh', disabled: !hasProject,
|
|
552
|
+
children: hasProject ? SCOPE_LABEL['project-dsh'] : `${SCOPE_LABEL['project-dsh']}(需先打开项目)`,
|
|
553
|
+
}),
|
|
554
|
+
],
|
|
555
|
+
})
|
|
556
|
+
: jsx('span', { key: 'v', className: 'dsh-ccs-sub', children: SCOPE_LABEL[f.scope] ?? f.scope }),
|
|
557
|
+
],
|
|
558
|
+
}));
|
|
559
|
+
|
|
560
|
+
return jsx('div', {
|
|
561
|
+
className: 'dsh-ccs-card',
|
|
562
|
+
children: [
|
|
563
|
+
jsx('div', {
|
|
564
|
+
key: 'head', className: 'dsh-ccs-card-head', children: [
|
|
565
|
+
jsx('span', {
|
|
566
|
+
key: 'title', className: 'dsh-ccs-card-title',
|
|
567
|
+
children: editor.mode === 'add' ? '添加 MCP 服务' : `编辑 MCP 服务:${editor.name}`,
|
|
568
|
+
}),
|
|
569
|
+
jsx('label', {
|
|
570
|
+
key: 'json', className: 'dsh-ccs-json-toggle', children: [
|
|
571
|
+
jsx('input', {
|
|
572
|
+
key: 'cb', type: 'checkbox', checked: f.jsonMode,
|
|
573
|
+
onChange: (e) => toggleJson(e.target.checked),
|
|
574
|
+
}),
|
|
575
|
+
'JSON 编辑',
|
|
576
|
+
],
|
|
577
|
+
}),
|
|
578
|
+
],
|
|
579
|
+
}),
|
|
580
|
+
...rows,
|
|
581
|
+
jsx('div', {
|
|
582
|
+
key: 'btns', className: 'dsh-ccs-form-row', children: [
|
|
583
|
+
jsx('button', {
|
|
584
|
+
key: 'save', type: 'button', className: 'dsh-ccs-btn dsh-ccs-btn-primary',
|
|
585
|
+
disabled: busy || (editor.mode === 'add' && !f.name.trim()),
|
|
586
|
+
onClick: () => void save(), children: busy ? '保存中…' : '保存',
|
|
587
|
+
}),
|
|
588
|
+
jsx('button', {
|
|
589
|
+
key: 'cancel', type: 'button', className: 'dsh-ccs-btn',
|
|
590
|
+
disabled: busy, onClick: () => setEditor(null), children: '取消',
|
|
591
|
+
}),
|
|
592
|
+
],
|
|
593
|
+
}),
|
|
594
|
+
],
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** 从其他 agent 导入:按 agent 分组的复选列表 + 作用域选择。 */
|
|
599
|
+
function McpImporter({ importer, setImporter, hasProject, onSaved, setMsg, setErr, busy, setBusy }) {
|
|
600
|
+
const toggleKey = (key, on) => {
|
|
601
|
+
const checked = new Set(importer.checked);
|
|
602
|
+
if (on) checked.add(key); else checked.delete(key);
|
|
603
|
+
setImporter({ ...importer, checked });
|
|
604
|
+
};
|
|
605
|
+
// 按来源 agent 分组(保持扫描顺序:claude-code / cursor / gemini / codex)。
|
|
606
|
+
const groups = [];
|
|
607
|
+
for (const s of importer.servers) {
|
|
608
|
+
let g = groups.find((x) => x.agent === s.agent);
|
|
609
|
+
if (!g) { g = { agent: s.agent, items: [] }; groups.push(g); }
|
|
610
|
+
g.items.push(s);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const doImport = async () => {
|
|
614
|
+
const items = importer.servers
|
|
615
|
+
.filter((s) => importer.checked.has(`${s.agent}/${s.name}`))
|
|
616
|
+
.map((s) => ({ agent: s.agent, name: s.name }));
|
|
617
|
+
if (items.length === 0) return;
|
|
618
|
+
setBusy(true); setErr(null);
|
|
619
|
+
try {
|
|
620
|
+
const res = await callApi('mcp/import/apply', { items, scope: importer.scope });
|
|
621
|
+
if (!res.ok) throw new Error(res.error);
|
|
622
|
+
setMsg(res.message);
|
|
623
|
+
setImporter({ ...importer, checked: new Set(), results: res.results });
|
|
624
|
+
try { hostCtx?.emit?.('connection/reset'); } catch { /* 静默降级 */ }
|
|
625
|
+
await onSaved();
|
|
626
|
+
} catch (e) {
|
|
627
|
+
setErr(e?.message || String(e));
|
|
628
|
+
} finally {
|
|
629
|
+
setBusy(false);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
return jsx('div', {
|
|
634
|
+
className: 'dsh-ccs-card',
|
|
635
|
+
children: [
|
|
636
|
+
jsx('div', {
|
|
637
|
+
key: 'head', className: 'dsh-ccs-card-head', children: [
|
|
638
|
+
jsx('span', { key: 'title', className: 'dsh-ccs-card-title', children: '从其他 agent 导入 MCP' }),
|
|
639
|
+
jsx('select', {
|
|
640
|
+
key: 'scope', className: 'dsh-ccs-select', value: importer.scope,
|
|
641
|
+
onChange: (e) => setImporter({ ...importer, scope: e.target.value }),
|
|
642
|
+
children: [
|
|
643
|
+
jsx('option', { key: 'user', value: 'user', children: SCOPE_LABEL.user }),
|
|
644
|
+
jsx('option', {
|
|
645
|
+
key: 'project-dsh', value: 'project-dsh', disabled: !hasProject,
|
|
646
|
+
children: hasProject ? SCOPE_LABEL['project-dsh'] : `${SCOPE_LABEL['project-dsh']}(需先打开项目)`,
|
|
647
|
+
}),
|
|
648
|
+
],
|
|
649
|
+
}),
|
|
650
|
+
jsx('button', {
|
|
651
|
+
key: 'close', type: 'button', className: 'dsh-ccs-btn', disabled: busy,
|
|
652
|
+
onClick: () => setImporter(null), children: '关闭',
|
|
653
|
+
}),
|
|
654
|
+
],
|
|
655
|
+
}),
|
|
656
|
+
importer.loading
|
|
657
|
+
? jsx('div', { key: 'loading', className: 'dsh-ccs-empty', children: '正在扫描其他 agent 的配置…' })
|
|
658
|
+
: null,
|
|
659
|
+
!importer.loading && importer.servers.length === 0
|
|
660
|
+
? jsx('div', {
|
|
661
|
+
key: 'empty', className: 'dsh-ccs-empty',
|
|
662
|
+
children: '未在本机发现其他 agent 的 MCP 配置。\n支持:Claude Code(~/.claude.json)、Cursor(~/.cursor/mcp.json)、Codex(~/.codex/config.toml)、Gemini CLI(~/.gemini/settings.json)。',
|
|
663
|
+
})
|
|
664
|
+
: null,
|
|
665
|
+
...groups.map((g) => {
|
|
666
|
+
const selectable = g.items.filter((s) => !importer.existing.has(s.name));
|
|
667
|
+
const allChecked = selectable.length > 0 && selectable.every((s) => importer.checked.has(`${s.agent}/${s.name}`));
|
|
668
|
+
return jsx('div', { key: g.agent, className: 'dsh-ccs-group', children: [
|
|
669
|
+
jsx('label', {
|
|
670
|
+
key: 'head', className: 'dsh-ccs-group-head', children: [
|
|
671
|
+
jsx('input', {
|
|
672
|
+
key: 'cb', type: 'checkbox', checked: allChecked, disabled: selectable.length === 0,
|
|
673
|
+
onChange: (e) => {
|
|
674
|
+
const checked = new Set(importer.checked);
|
|
675
|
+
for (const s of selectable) {
|
|
676
|
+
const key = `${s.agent}/${s.name}`;
|
|
677
|
+
if (e.target.checked) checked.add(key); else checked.delete(key);
|
|
678
|
+
}
|
|
679
|
+
setImporter({ ...importer, checked });
|
|
680
|
+
},
|
|
681
|
+
}),
|
|
682
|
+
`${AGENT_LABEL[g.agent] ?? g.agent}`,
|
|
683
|
+
jsx('span', { key: 'n', className: 'dsh-ccs-group-count', children: `(${g.items.length})` }),
|
|
684
|
+
],
|
|
685
|
+
}),
|
|
686
|
+
...g.items.map((s) => {
|
|
687
|
+
const key = `${s.agent}/${s.name}`;
|
|
688
|
+
const exists = importer.existing.has(s.name);
|
|
689
|
+
return jsx('label', {
|
|
690
|
+
key, className: `dsh-ccs-check-row${exists ? ' dsh-ccs-check-row-existing' : ''}`, children: [
|
|
691
|
+
jsx('input', {
|
|
692
|
+
key: 'cb', type: 'checkbox', checked: importer.checked.has(key), disabled: exists,
|
|
693
|
+
onChange: (e) => toggleKey(key, e.target.checked),
|
|
694
|
+
}),
|
|
695
|
+
jsx('span', { key: 'name', className: 'dsh-ccs-check-name', children: s.name }),
|
|
696
|
+
jsx('span', { key: 'sub', className: 'dsh-ccs-check-sub', children: importSummary(s) || s.transport }),
|
|
697
|
+
exists ? jsx('span', { key: 'badge', className: 'dsh-ccs-badge', children: '已存在' }) : null,
|
|
698
|
+
],
|
|
699
|
+
});
|
|
700
|
+
}),
|
|
701
|
+
] });
|
|
702
|
+
}),
|
|
703
|
+
Array.isArray(importer.results) && importer.results.length > 0
|
|
704
|
+
? jsx('div', {
|
|
705
|
+
key: 'results', className: 'dsh-ccs-msg',
|
|
706
|
+
children: importer.results.map((r, i) => `${r.ok ? '✓' : '✗'} ${r.name}${r.error ? `:${r.error}` : ''}`).join('\n'),
|
|
707
|
+
})
|
|
708
|
+
: null,
|
|
709
|
+
!importer.loading && importer.servers.length > 0
|
|
710
|
+
? jsx('div', {
|
|
711
|
+
key: 'btns', className: 'dsh-ccs-form-row', children: jsx('button', {
|
|
712
|
+
type: 'button', className: 'dsh-ccs-btn dsh-ccs-btn-primary', disabled: busy || importer.checked.size === 0,
|
|
713
|
+
onClick: () => void doImport(), children: busy ? '导入中…' : `导入所选(${importer.checked.size})`,
|
|
714
|
+
}),
|
|
715
|
+
})
|
|
716
|
+
: null,
|
|
717
|
+
],
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
191
721
|
function McpPanel() {
|
|
192
722
|
const [status, setStatus] = React.useState(null);
|
|
193
723
|
const [error, setError] = React.useState(null);
|
|
724
|
+
const [msg, setMsg] = React.useState(null);
|
|
194
725
|
const [busy, setBusy] = React.useState(false);
|
|
726
|
+
const [editor, setEditor] = React.useState(null);
|
|
727
|
+
const [importer, setImporter] = React.useState(null);
|
|
195
728
|
|
|
196
729
|
const load = React.useCallback(async () => {
|
|
197
730
|
try {
|
|
@@ -206,7 +739,7 @@ window.__ModuleLoader__.load({
|
|
|
206
739
|
|
|
207
740
|
const toggle = async (row) => {
|
|
208
741
|
if (busy) return;
|
|
209
|
-
setBusy(true); setError(null);
|
|
742
|
+
setBusy(true); setError(null); setMsg(null);
|
|
210
743
|
try {
|
|
211
744
|
await callApi('mcp/setEnabled', { name: row.name, enabled: row.disabled });
|
|
212
745
|
// MCP 工具目录变了:让聊天侧的会话缓存失效并重取。
|
|
@@ -221,7 +754,7 @@ window.__ModuleLoader__.load({
|
|
|
221
754
|
|
|
222
755
|
const reload = async () => {
|
|
223
756
|
if (busy) return;
|
|
224
|
-
setBusy(true); setError(null);
|
|
757
|
+
setBusy(true); setError(null); setMsg(null);
|
|
225
758
|
try {
|
|
226
759
|
await callApi('mcp/reload');
|
|
227
760
|
await load();
|
|
@@ -232,16 +765,93 @@ window.__ModuleLoader__.load({
|
|
|
232
765
|
}
|
|
233
766
|
};
|
|
234
767
|
|
|
768
|
+
const openAdd = () => {
|
|
769
|
+
setError(null); setMsg(null);
|
|
770
|
+
setEditor({
|
|
771
|
+
mode: 'add', name: '', scope: 'user',
|
|
772
|
+
// name / scope 必须放 fields 里 —— 输入框、保存校验与 payload 都读 f.*。
|
|
773
|
+
fields: {
|
|
774
|
+
name: '', scope: 'user', transport: 'stdio', command: '', args: '', env: '', url: '', headers: '',
|
|
775
|
+
jsonMode: false, jsonText: '',
|
|
776
|
+
},
|
|
777
|
+
});
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
const openEdit = async (row) => {
|
|
781
|
+
setError(null); setMsg(null);
|
|
782
|
+
try {
|
|
783
|
+
const res = await callApi('mcp/get', { name: row.name });
|
|
784
|
+
if (!res.ok) throw new Error(res.error);
|
|
785
|
+
const raw = res.raw ?? {};
|
|
786
|
+
const isHttp = raw.command === undefined || raw.command === null || raw.command === '';
|
|
787
|
+
setEditor({
|
|
788
|
+
mode: 'edit', name: row.name, scope: SOURCE_TO_SCOPE[res.source] || 'user',
|
|
789
|
+
fields: {
|
|
790
|
+
name: row.name,
|
|
791
|
+
// 编辑模式锁定写回原来源文件(scope 必须在 fields 里才会随 payload 提交)。
|
|
792
|
+
scope: SOURCE_TO_SCOPE[res.source] || 'user',
|
|
793
|
+
transport: isHttp ? 'http' : 'stdio',
|
|
794
|
+
command: typeof raw.command === 'string' ? raw.command : '',
|
|
795
|
+
args: Array.isArray(raw.args) ? raw.args.join(' ') : '',
|
|
796
|
+
env: kvToText(raw.env),
|
|
797
|
+
url: typeof raw.url === 'string' ? raw.url : '',
|
|
798
|
+
headers: headersToText(raw.headers),
|
|
799
|
+
jsonMode: false,
|
|
800
|
+
jsonText: JSON.stringify(raw, null, 2),
|
|
801
|
+
},
|
|
802
|
+
});
|
|
803
|
+
} catch (e) {
|
|
804
|
+
setError(e?.message || String(e));
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
const removeRow = async (row) => {
|
|
809
|
+
const path = status?.files?.[row.source]?.path || SOURCE_LABEL[row.source] || row.source;
|
|
810
|
+
if (!confirm(`确定从 ${path} 中删除 MCP 服务 '${row.name}'?\n该操作会直接修改配置文件并卸载对应工具。`)) return;
|
|
811
|
+
setBusy(true); setError(null); setMsg(null);
|
|
812
|
+
try {
|
|
813
|
+
const res = await callApi('mcp/remove', { name: row.name });
|
|
814
|
+
if (!res.ok) throw new Error(res.error);
|
|
815
|
+
setMsg(res.message);
|
|
816
|
+
try { hostCtx?.emit?.('connection/reset'); } catch { /* 静默降级 */ }
|
|
817
|
+
await load();
|
|
818
|
+
} catch (e) {
|
|
819
|
+
setError(e?.message || String(e));
|
|
820
|
+
} finally {
|
|
821
|
+
setBusy(false);
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
const openImport = async () => {
|
|
826
|
+
setError(null); setMsg(null);
|
|
827
|
+
setImporter({ loading: true, servers: [], existing: new Set(), checked: new Set(), scope: 'user', results: null });
|
|
828
|
+
try {
|
|
829
|
+
const res = await callApi('mcp/import/scan');
|
|
830
|
+
if (!res.ok) throw new Error(res.error);
|
|
831
|
+
setImporter({
|
|
832
|
+
loading: false,
|
|
833
|
+
servers: Array.isArray(res.servers) ? res.servers : [],
|
|
834
|
+
existing: new Set(Array.isArray(res.existing) ? res.existing : []),
|
|
835
|
+
checked: new Set(), scope: 'user', results: null,
|
|
836
|
+
});
|
|
837
|
+
} catch (e) {
|
|
838
|
+
setError(e?.message || String(e));
|
|
839
|
+
setImporter(null);
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
|
|
235
843
|
const servers = Array.isArray(status?.servers) ? status.servers : [];
|
|
236
844
|
const files = status?.files ?? {};
|
|
845
|
+
const hasProject = Boolean(status?.cwd);
|
|
237
846
|
const fileLines = ['user', 'project-dsh-json', 'project-mcp-json']
|
|
238
847
|
.filter((k) => files[k]?.path)
|
|
239
848
|
.map((k) => `${files[k].exists ? '✓' : '—'} ${files[k].path}`);
|
|
240
849
|
|
|
241
850
|
return jsx('div', {
|
|
851
|
+
className: 'dsh-ccs-mcp',
|
|
242
852
|
children: [
|
|
243
853
|
jsx('div', {
|
|
244
|
-
key: 'hint', className: 'dsh-ccs-sub', style: { marginBottom:
|
|
854
|
+
key: 'hint', className: 'dsh-ccs-sub', style: { marginBottom: 0 },
|
|
245
855
|
children: '切换工作区后自动加载该项目声明的 MCP 服务;改动配置文件约 0.5 秒内生效。',
|
|
246
856
|
}),
|
|
247
857
|
status?.cwd
|
|
@@ -253,52 +863,107 @@ window.__ModuleLoader__.load({
|
|
|
253
863
|
children: fileLines.join('\n'),
|
|
254
864
|
})
|
|
255
865
|
: null,
|
|
866
|
+
// 工具栏:添加 / 导入 / 重新加载
|
|
867
|
+
jsx('div', {
|
|
868
|
+
key: 'toolbar', className: 'dsh-ccs-toolbar', children: [
|
|
869
|
+
jsx('button', {
|
|
870
|
+
key: 'add', type: 'button', className: 'dsh-ccs-btn dsh-ccs-btn-primary',
|
|
871
|
+
disabled: busy || editor !== null, onClick: openAdd, children: '添加 MCP 服务…',
|
|
872
|
+
}),
|
|
873
|
+
jsx('button', {
|
|
874
|
+
key: 'import', type: 'button', className: 'dsh-ccs-btn',
|
|
875
|
+
disabled: busy || importer !== null, onClick: () => void openImport(), children: '从其他 agent 导入…',
|
|
876
|
+
}),
|
|
877
|
+
jsx('button', {
|
|
878
|
+
key: 'reload', type: 'button', className: 'dsh-ccs-btn',
|
|
879
|
+
disabled: busy, onClick: () => void reload(), children: busy ? '处理中…' : '重新加载配置',
|
|
880
|
+
}),
|
|
881
|
+
],
|
|
882
|
+
}),
|
|
883
|
+
editor
|
|
884
|
+
? jsx('div', {
|
|
885
|
+
key: 'editor', className: 'dsh-ccs-modal-mask',
|
|
886
|
+
// 点遮罩关闭(点在弹框本体上不关)
|
|
887
|
+
onClick: (e) => { if (e.target === e.currentTarget) setEditor(null); },
|
|
888
|
+
children: jsx('div', {
|
|
889
|
+
className: 'dsh-ccs-modal',
|
|
890
|
+
children: jsx(McpEditor, {
|
|
891
|
+
editor, setEditor, hasProject, onSaved: load,
|
|
892
|
+
setMsg, setErr: setError, busy, setBusy,
|
|
893
|
+
}),
|
|
894
|
+
}),
|
|
895
|
+
})
|
|
896
|
+
: null,
|
|
897
|
+
importer
|
|
898
|
+
? jsx(McpImporter, {
|
|
899
|
+
key: 'importer', importer, setImporter, hasProject, onSaved: load,
|
|
900
|
+
setMsg, setErr: setError, busy, setBusy,
|
|
901
|
+
})
|
|
902
|
+
: null,
|
|
256
903
|
error
|
|
257
904
|
? jsx('div', { key: 'err', className: 'dsh-ccs-msg dsh-ccs-msg-err', children: error })
|
|
258
905
|
: null,
|
|
906
|
+
msg
|
|
907
|
+
? jsx('div', { key: 'msg', className: 'dsh-ccs-msg dsh-ccs-msg-ok', children: msg })
|
|
908
|
+
: null,
|
|
259
909
|
servers.length === 0
|
|
260
910
|
? jsx('div', {
|
|
261
911
|
key: 'empty', className: 'dsh-ccs-empty',
|
|
262
|
-
children: '当前项目没有配置任何 MCP 服务。\n
|
|
912
|
+
children: '当前项目没有配置任何 MCP 服务。\n点击上方「添加 MCP 服务」或「从其他 agent 导入」,也可直接在 ~/.dsh/mcp.json(全局)或项目根 .dsh/mcp.json / .mcp.json(按项目)中声明。',
|
|
263
913
|
})
|
|
264
|
-
:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
key:
|
|
268
|
-
jsx('
|
|
269
|
-
jsx('
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
914
|
+
: [
|
|
915
|
+
// 列表头:标题 + 计数(同 dpc-listHead)
|
|
916
|
+
jsx('div', {
|
|
917
|
+
key: 'listhead', className: 'dsh-ccs-listhead', children: [
|
|
918
|
+
jsx('h3', { key: 't', className: 'dsh-ccs-listhead-title', children: 'MCP 服务' }),
|
|
919
|
+
jsx('span', { key: 'n', className: 'dsh-ccs-listhead-count', children: `${servers.length}` }),
|
|
920
|
+
],
|
|
921
|
+
}),
|
|
922
|
+
// 服务卡片:双列网格(同 dpc-card:top / desc / acts)
|
|
923
|
+
jsx('div', {
|
|
924
|
+
key: 'list', className: 'dsh-ccs-cards',
|
|
925
|
+
children: servers.map((row) => jsx('div', {
|
|
926
|
+
key: row.name, className: `dsh-ccs-mcard${row.disabled ? ' dsh-ccs-mcard-off' : ''}`, children: [
|
|
927
|
+
jsx('div', {
|
|
928
|
+
key: 'top', className: 'dsh-ccs-mcard-top', children: [
|
|
929
|
+
jsx('span', { key: 'dot', className: `dsh-ccs-dot ${mcpDotClass(row)}`, title: row.disabled ? '已停用' : row.fiberPhase ?? '未同步' }),
|
|
930
|
+
jsx('span', { key: 'name', className: 'dsh-ccs-mcard-name', title: row.name, children: row.name }),
|
|
931
|
+
row.disabled ? jsx('span', { key: 'off', className: 'dsh-ccs-tag', 'data-kind': 'off', children: '已停用' }) : null,
|
|
932
|
+
jsx('span', { key: 'sp', className: 'dsh-ccs-mcard-spacer' }),
|
|
933
|
+
jsx(Switch, {
|
|
934
|
+
key: 'sw', checked: !row.disabled, disabled: busy,
|
|
935
|
+
onChange: () => void toggle(row),
|
|
936
|
+
title: row.disabled ? '启用该 MCP 服务' : '停用该 MCP 服务(重启后保持)',
|
|
283
937
|
}),
|
|
284
|
-
jsx('div', { key: 'sub', className: 'dsh-ccs-sub', children: mcpSummary(row) || row.transport }),
|
|
285
938
|
],
|
|
286
939
|
}),
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
940
|
+
jsx('div', {
|
|
941
|
+
key: 'desc', className: 'dsh-ccs-mcard-desc', title: mcpSummary(row) || row.transport,
|
|
942
|
+
children: mcpSummary(row) || row.transport,
|
|
943
|
+
}),
|
|
944
|
+
jsx('div', {
|
|
945
|
+
key: 'acts', className: 'dsh-ccs-mcard-acts', children: [
|
|
946
|
+
jsx('span', { key: 'src', className: 'dsh-ccs-tag', 'data-kind': 'source', children: SOURCE_LABEL[row.source] ?? row.source }),
|
|
947
|
+
row.toolCount > 0
|
|
948
|
+
? jsx('span', { key: 'tools', className: 'dsh-ccs-tag', children: `${row.toolCount} 工具` })
|
|
949
|
+
: null,
|
|
950
|
+
jsx('span', { key: 'sp', className: 'dsh-ccs-mcard-spacer' }),
|
|
951
|
+
jsx('button', {
|
|
952
|
+
key: 'edit', type: 'button', className: 'dsh-ccs-icon-btn', disabled: busy,
|
|
953
|
+
onClick: () => void openEdit(row),
|
|
954
|
+
title: '编辑该条目(表单 / JSON)', children: '编辑',
|
|
955
|
+
}),
|
|
956
|
+
jsx('button', {
|
|
957
|
+
key: 'del', type: 'button', className: 'dsh-ccs-icon-btn dsh-ccs-icon-btn-danger', disabled: busy,
|
|
958
|
+
onClick: () => void removeRow(row),
|
|
959
|
+
title: '从配置文件删除(需确认)', children: '删除',
|
|
960
|
+
}),
|
|
961
|
+
],
|
|
962
|
+
}),
|
|
963
|
+
],
|
|
964
|
+
})),
|
|
965
|
+
}),
|
|
966
|
+
],
|
|
302
967
|
],
|
|
303
968
|
});
|
|
304
969
|
}
|
|
@@ -315,8 +980,10 @@ window.__ModuleLoader__.load({
|
|
|
315
980
|
const [busy, setBusy] = React.useState(false);
|
|
316
981
|
const [linkOpen, setLinkOpen] = React.useState(false);
|
|
317
982
|
const [sourceDir, setSourceDir] = React.useState('');
|
|
983
|
+
const [query, setQuery] = React.useState('');
|
|
318
984
|
|
|
319
985
|
const load = React.useCallback(async () => {
|
|
986
|
+
setBusy(true);
|
|
320
987
|
try {
|
|
321
988
|
setErr(null);
|
|
322
989
|
const data = await callApi('skills/list');
|
|
@@ -324,6 +991,8 @@ window.__ModuleLoader__.load({
|
|
|
324
991
|
setRoot(data.root || null);
|
|
325
992
|
} catch (e) {
|
|
326
993
|
setErr(e?.message || String(e));
|
|
994
|
+
} finally {
|
|
995
|
+
setBusy(false);
|
|
327
996
|
}
|
|
328
997
|
}, []);
|
|
329
998
|
|
|
@@ -348,73 +1017,50 @@ window.__ModuleLoader__.load({
|
|
|
348
1017
|
|
|
349
1018
|
const openPicker = async () => {
|
|
350
1019
|
try {
|
|
1020
|
+
// pickDirectory 依赖 this(内部用 this.api.host),不能以裸函数传递后直接调用。
|
|
1021
|
+
if (typeof pickDirectory !== 'function') throw new Error('宿主未提供目录选择器');
|
|
351
1022
|
const picked = await pickDirectory();
|
|
352
1023
|
if (typeof picked === 'string' && picked !== '') {
|
|
353
1024
|
setSourceDir(picked);
|
|
354
1025
|
setLinkOpen(true);
|
|
355
1026
|
}
|
|
356
|
-
|
|
1027
|
+
// null = 用户取消,静默即可。
|
|
1028
|
+
} catch (e) {
|
|
1029
|
+
setErr(`无法打开目录选择器:${e?.message || e}。可手动粘贴路径后点击「链接」。`);
|
|
357
1030
|
setLinkOpen(true);
|
|
358
1031
|
}
|
|
359
1032
|
};
|
|
360
1033
|
|
|
1034
|
+
// 搜索过滤:按名称 / 描述,大小写不敏感。
|
|
1035
|
+
const q = query.trim().toLowerCase();
|
|
1036
|
+
const filtered = q === ''
|
|
1037
|
+
? skills
|
|
1038
|
+
: skills.filter((s) =>
|
|
1039
|
+
(s.name || '').toLowerCase().includes(q)
|
|
1040
|
+
|| (s.description || '').toLowerCase().includes(q));
|
|
1041
|
+
|
|
361
1042
|
return jsx(React.Fragment, {
|
|
362
1043
|
children: [
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
key: 'list', className: 'dsh-ccs-list',
|
|
379
|
-
children: skills.map((s) => jsx('div', {
|
|
380
|
-
key: s.name, className: 'dsh-ccs-row', children: jsx('div', {
|
|
381
|
-
className: 'dsh-ccs-main', children: [
|
|
382
|
-
jsx('div', {
|
|
383
|
-
key: 'text', className: 'dsh-ccs-text', children: [
|
|
384
|
-
jsx('div', {
|
|
385
|
-
key: 'name', className: 'dsh-ccs-name', children: [
|
|
386
|
-
s.name,
|
|
387
|
-
jsx('span', {
|
|
388
|
-
key: 'badges', className: 'dsh-ccs-badges', children: [
|
|
389
|
-
s.agent_created ? jsx('span', { key: 'a', className: 'dsh-ccs-badge', children: 'agent' }) : null,
|
|
390
|
-
s.pinned ? jsx('span', { key: 'p', className: 'dsh-ccs-badge', children: '置顶' }) : null,
|
|
391
|
-
s.linked ? jsx('span', { key: 'l', className: 'dsh-ccs-badge dsh-ccs-badge-linked', children: '已链接' }) : null,
|
|
392
|
-
],
|
|
393
|
-
}),
|
|
394
|
-
],
|
|
395
|
-
}),
|
|
396
|
-
jsx('div', { key: 'desc', className: 'dsh-ccs-sub', children: s.description || s.target || '' }),
|
|
397
|
-
],
|
|
398
|
-
}),
|
|
399
|
-
jsx(Switch, {
|
|
400
|
-
key: 'sw', checked: !s.disabled, disabled: busy || s.layout === 'file',
|
|
401
|
-
onChange: (on) => void run(on ? 'skills/enable' : 'skills/disable', { name: s.name }),
|
|
402
|
-
title: s.layout === 'file' ? '单文件技能请手动编辑' : on_off_title(s),
|
|
403
|
-
}),
|
|
404
|
-
jsx('button', {
|
|
405
|
-
key: 'unlink', type: 'button', className: 'dsh-ccs-icon-btn', disabled: busy,
|
|
406
|
-
onClick: () => void run('skills/unlink', { name: s.name }),
|
|
407
|
-
title: '移除联接点(保留源目录)', children: '断开',
|
|
408
|
-
}),
|
|
409
|
-
jsx('button', {
|
|
410
|
-
key: 'del', type: 'button', className: 'dsh-ccs-icon-btn dsh-ccs-icon-btn-danger', disabled: busy,
|
|
411
|
-
onClick: () => { if (confirm(`确定删除技能 '${s.name}'?\n仅 agent 创建且非置顶的技能可删除。`)) void run('skills/delete', { name: s.name }); },
|
|
412
|
-
title: '删除技能目录(仅 agent 创建)', children: '删除',
|
|
413
|
-
}),
|
|
414
|
-
],
|
|
1044
|
+
// 头部:根目录信息(左)+ 动作按钮(右),固定在列表上方
|
|
1045
|
+
jsx('div', {
|
|
1046
|
+
key: 'head', className: 'dsh-ccs-head', children: [
|
|
1047
|
+
root
|
|
1048
|
+
? jsx('div', { key: 'root', className: 'dsh-ccs-root', children: `技能根目录:${root}` })
|
|
1049
|
+
: null,
|
|
1050
|
+
jsx('div', {
|
|
1051
|
+
key: 'acts', className: 'dsh-ccs-head-actions', children: [
|
|
1052
|
+
jsx('button', {
|
|
1053
|
+
key: 'pick', type: 'button', className: 'dsh-ccs-btn dsh-ccs-btn-primary', disabled: busy,
|
|
1054
|
+
onClick: () => void openPicker(), children: '选择目录链接…',
|
|
1055
|
+
}),
|
|
1056
|
+
jsx('button', {
|
|
1057
|
+
key: 'refresh', type: 'button', className: 'dsh-ccs-btn', disabled: busy,
|
|
1058
|
+
onClick: () => void load(), children: busy ? '处理中…' : '刷新',
|
|
415
1059
|
}),
|
|
416
|
-
|
|
1060
|
+
],
|
|
417
1061
|
}),
|
|
1062
|
+
],
|
|
1063
|
+
}),
|
|
418
1064
|
linkOpen
|
|
419
1065
|
? jsx('div', {
|
|
420
1066
|
key: 'link', className: 'dsh-ccs-input-row', children: [
|
|
@@ -431,18 +1077,78 @@ window.__ModuleLoader__.load({
|
|
|
431
1077
|
],
|
|
432
1078
|
})
|
|
433
1079
|
: null,
|
|
1080
|
+
err
|
|
1081
|
+
? jsx('div', { key: 'err', className: 'dsh-ccs-msg dsh-ccs-msg-err', children: err })
|
|
1082
|
+
: null,
|
|
1083
|
+
msg
|
|
1084
|
+
? jsx('div', { key: 'msg', className: 'dsh-ccs-msg dsh-ccs-msg-ok', children: msg })
|
|
1085
|
+
: null,
|
|
1086
|
+
// 搜索框
|
|
434
1087
|
jsx('div', {
|
|
435
|
-
key: '
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
jsx('button', {
|
|
441
|
-
key: 'refresh', type: 'button', className: 'dsh-ccs-btn', disabled: busy,
|
|
442
|
-
onClick: () => void load(), children: busy ? '处理中…' : '刷新',
|
|
443
|
-
}),
|
|
444
|
-
],
|
|
1088
|
+
key: 'search', className: 'dsh-ccs-search-row', children: jsx('input', {
|
|
1089
|
+
type: 'search', className: 'dsh-ccs-input dsh-ccs-search', value: query,
|
|
1090
|
+
onChange: (e) => setQuery(e.target.value),
|
|
1091
|
+
placeholder: '搜索技能(名称 / 描述)…',
|
|
1092
|
+
}),
|
|
445
1093
|
}),
|
|
1094
|
+
// 列表区:独立滚动,不带动整页
|
|
1095
|
+
skills.length === 0
|
|
1096
|
+
? jsx('div', {
|
|
1097
|
+
key: 'empty', className: 'dsh-ccs-empty',
|
|
1098
|
+
children: '还没有任何技能。从其它目录链接技能进来,或直接把技能目录放进根目录。',
|
|
1099
|
+
})
|
|
1100
|
+
: filtered.length === 0
|
|
1101
|
+
? jsx('div', { key: 'nomatch', className: 'dsh-ccs-empty', children: '没有匹配的技能。' })
|
|
1102
|
+
: jsx('div', {
|
|
1103
|
+
key: 'scroll', className: 'dsh-ccs-list-scroll', children: jsx('div', {
|
|
1104
|
+
key: 'list', className: 'dsh-ccs-list',
|
|
1105
|
+
children: filtered.map((s) => jsx('div', {
|
|
1106
|
+
key: s.name, className: `dsh-ccs-row${s.disabled ? ' dsh-ccs-row-disabled' : ''}`, children: jsx('div', {
|
|
1107
|
+
className: 'dsh-ccs-main', children: [
|
|
1108
|
+
jsx('div', {
|
|
1109
|
+
key: 'text', className: 'dsh-ccs-text', children: [
|
|
1110
|
+
jsx('div', {
|
|
1111
|
+
key: 'name', className: 'dsh-ccs-name', children: [
|
|
1112
|
+
s.name,
|
|
1113
|
+
jsx('span', {
|
|
1114
|
+
key: 'badges', className: 'dsh-ccs-badges', children: [
|
|
1115
|
+
s.agent_created ? jsx('span', { key: 'a', className: 'dsh-ccs-badge', children: 'agent' }) : null,
|
|
1116
|
+
s.pinned ? jsx('span', { key: 'p', className: 'dsh-ccs-badge', children: '置顶' }) : null,
|
|
1117
|
+
s.linked ? jsx('span', { key: 'l', className: 'dsh-ccs-badge dsh-ccs-badge-linked', children: '已链接' }) : null,
|
|
1118
|
+
],
|
|
1119
|
+
}),
|
|
1120
|
+
],
|
|
1121
|
+
}),
|
|
1122
|
+
jsx('div', { key: 'desc', className: 'dsh-ccs-sub', children: s.description || s.target || '' }),
|
|
1123
|
+
],
|
|
1124
|
+
}),
|
|
1125
|
+
jsx(Switch, {
|
|
1126
|
+
key: 'sw', checked: !s.disabled, disabled: busy || s.layout === 'file',
|
|
1127
|
+
onChange: (on) => void run(on ? 'skills/enable' : 'skills/disable', { name: s.name }),
|
|
1128
|
+
title: s.layout === 'file' ? '单文件技能请手动编辑' : on_off_title(s),
|
|
1129
|
+
}),
|
|
1130
|
+
// 断开与删除职责不同、守卫互补,都只在必然成功时显示:
|
|
1131
|
+
// 断开 = 仅移除联接点、保留源目录(junction 技能才有);
|
|
1132
|
+
// 删除 = 连源目录一起删(仅 agent 创建、非置顶、非联接)。
|
|
1133
|
+
s.linked
|
|
1134
|
+
? jsx('button', {
|
|
1135
|
+
key: 'unlink', type: 'button', className: 'dsh-ccs-icon-btn', disabled: busy,
|
|
1136
|
+
onClick: () => void run('skills/unlink', { name: s.name }),
|
|
1137
|
+
title: '移除联接点(保留源目录)', children: '断开',
|
|
1138
|
+
})
|
|
1139
|
+
: null,
|
|
1140
|
+
s.agent_created && !s.pinned && !s.linked
|
|
1141
|
+
? jsx('button', {
|
|
1142
|
+
key: 'del', type: 'button', className: 'dsh-ccs-icon-btn dsh-ccs-icon-btn-danger', disabled: busy,
|
|
1143
|
+
onClick: () => { if (confirm(`确定删除技能 '${s.name}'?\n仅 agent 创建且非置顶的技能可删除。`)) void run('skills/delete', { name: s.name }); },
|
|
1144
|
+
title: '删除技能目录(仅 agent 创建)', children: '删除',
|
|
1145
|
+
})
|
|
1146
|
+
: null,
|
|
1147
|
+
],
|
|
1148
|
+
}),
|
|
1149
|
+
})),
|
|
1150
|
+
}),
|
|
1151
|
+
}),
|
|
446
1152
|
],
|
|
447
1153
|
});
|
|
448
1154
|
}
|
|
@@ -455,14 +1161,20 @@ window.__ModuleLoader__.load({
|
|
|
455
1161
|
|
|
456
1162
|
/**
|
|
457
1163
|
* 订阅当前会话列表:cwd 变化即推送 mcp/sync。
|
|
458
|
-
*
|
|
1164
|
+
* sessions 服务通过 ctx.get('sessions') 获取(服务不在注入列表里,
|
|
1165
|
+
* ctx.sessions 永远是 undefined —— v0.2/v0.3.0 的静默失效根因)。
|
|
1166
|
+
* 快照形状见 SessionsPort:list.getSnapshot() → { ids, byId, current, phase },
|
|
1167
|
+
* 当前会话 cwd = byId[current].cwd。subscribe 不可用时退化为 2 秒轮询兜底。
|
|
459
1168
|
*/
|
|
460
1169
|
function setupProjectMcpSync(ctx) {
|
|
461
1170
|
let lastCwd = null;
|
|
1171
|
+
const sessionsOf = () => {
|
|
1172
|
+
try { return typeof ctx.get === 'function' ? ctx.get('sessions') : undefined; } catch { return undefined; }
|
|
1173
|
+
};
|
|
462
1174
|
const tick = () => {
|
|
463
1175
|
let cwd;
|
|
464
1176
|
try {
|
|
465
|
-
const snap =
|
|
1177
|
+
const snap = sessionsOf()?.list?.getSnapshot?.();
|
|
466
1178
|
const id = snap?.current;
|
|
467
1179
|
cwd = id !== undefined ? snap?.byId?.[id]?.cwd : undefined;
|
|
468
1180
|
} catch { return; }
|
|
@@ -473,7 +1185,7 @@ window.__ModuleLoader__.load({
|
|
|
473
1185
|
};
|
|
474
1186
|
tick();
|
|
475
1187
|
let disposeSub = null;
|
|
476
|
-
try { disposeSub =
|
|
1188
|
+
try { disposeSub = sessionsOf()?.list?.subscribe?.(tick) ?? null; } catch { /* store 不存在 */ }
|
|
477
1189
|
const timer = setInterval(tick, 2000);
|
|
478
1190
|
ctx.effect(() => () => {
|
|
479
1191
|
clearInterval(timer);
|
|
@@ -481,29 +1193,6 @@ window.__ModuleLoader__.load({
|
|
|
481
1193
|
});
|
|
482
1194
|
}
|
|
483
1195
|
|
|
484
|
-
/** 侧边栏 footer ⚡ 快捷入口:点击打开技能与MCP全屏浮层。 */
|
|
485
|
-
function SidebarLauncher() {
|
|
486
|
-
const [open, setOpen] = React.useState(false);
|
|
487
|
-
return jsx(React.Fragment, {
|
|
488
|
-
children: [
|
|
489
|
-
jsx('button', {
|
|
490
|
-
key: 'btn', type: 'button', className: 'dsh-ccs-icon-btn',
|
|
491
|
-
title: '技能与MCP', onClick: () => setOpen(true), children: '⚡',
|
|
492
|
-
}),
|
|
493
|
-
open
|
|
494
|
-
? jsx('div', {
|
|
495
|
-
key: 'overlay', className: 'dsh-ccs-overlay',
|
|
496
|
-
onClick: (e) => { if (e.target === e.currentTarget) setOpen(false); },
|
|
497
|
-
children: jsx('div', {
|
|
498
|
-
className: 'dsh-ccs-overlay-panel',
|
|
499
|
-
children: jsx(CenterPanel, { pickDirectory: hostCtx?.workspaces?.pickDirectory }),
|
|
500
|
-
}),
|
|
501
|
-
})
|
|
502
|
-
: null,
|
|
503
|
-
],
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
|
|
507
1196
|
// ------------------------------------------------------------------
|
|
508
1197
|
// apply
|
|
509
1198
|
// ------------------------------------------------------------------
|
|
@@ -521,15 +1210,12 @@ window.__ModuleLoader__.load({
|
|
|
521
1210
|
id: 'dsh-skill-mcp',
|
|
522
1211
|
order: 99,
|
|
523
1212
|
label: () => '技能与MCP',
|
|
524
|
-
}, (props) => jsx(CenterPanel, {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
order: 30,
|
|
531
|
-
label: () => '技能与MCP',
|
|
532
|
-
}, SidebarLauncher));
|
|
1213
|
+
}, (props) => jsx(CenterPanel, {
|
|
1214
|
+
...props,
|
|
1215
|
+
fillHeight: true,
|
|
1216
|
+
// 注意要包一层箭头函数:方法内部依赖 this,摘下来传会丢绑定。
|
|
1217
|
+
pickDirectory: ctx.workspaces ? () => ctx.workspaces.pickDirectory() : undefined,
|
|
1218
|
+
})));
|
|
533
1219
|
};
|
|
534
1220
|
|
|
535
1221
|
return { apply, inject: INJECT };
|