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