march-control-cli 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +220 -0
- package/core/apply.js +152 -0
- package/core/backup.js +53 -0
- package/core/constants.js +55 -0
- package/core/desktop-service.js +219 -0
- package/core/desktop-state.js +511 -0
- package/core/index.js +1293 -0
- package/core/paths.js +71 -0
- package/core/presets.js +171 -0
- package/core/probe.js +70 -0
- package/core/store.js +218 -0
- package/core/utils.js +178 -0
- package/core/writers/codex.js +102 -0
- package/core/writers/index.js +16 -0
- package/core/writers/openclaw.js +93 -0
- package/core/writers/opencode.js +91 -0
- package/desktop/assets/march-mark.svg +21 -0
- package/desktop/main.js +192 -0
- package/desktop/preload.js +49 -0
- package/desktop/renderer/app.js +327 -0
- package/desktop/renderer/index.html +130 -0
- package/desktop/renderer/styles.css +413 -0
- package/package.json +106 -0
- package/scripts/desktop-dev.mjs +90 -0
- package/scripts/postinstall.mjs +28 -0
- package/scripts/serve-site.mjs +51 -0
- package/site/app.js +10 -0
- package/site/assets/march-mark.svg +22 -0
- package/site/index.html +286 -0
- package/site/styles.css +566 -0
- package/src/App.tsx +1186 -0
- package/src/components/layout/app-sidebar.tsx +103 -0
- package/src/components/layout/top-toolbar.tsx +44 -0
- package/src/components/layout/workspace-tabs.tsx +32 -0
- package/src/components/providers/inspector-panel.tsx +84 -0
- package/src/components/providers/metric-strip.tsx +26 -0
- package/src/components/providers/provider-editor.tsx +87 -0
- package/src/components/providers/provider-table.tsx +85 -0
- package/src/components/ui/logo-mark.tsx +16 -0
- package/src/features/mcp/mcp-view.tsx +45 -0
- package/src/features/prompts/prompts-view.tsx +40 -0
- package/src/features/providers/providers-view.tsx +40 -0
- package/src/features/providers/types.ts +8 -0
- package/src/features/skills/skills-view.tsx +44 -0
- package/src/hooks/use-control-workspace.ts +184 -0
- package/src/index.css +22 -0
- package/src/lib/client.ts +944 -0
- package/src/lib/query-client.ts +3 -0
- package/src/lib/workspace-sections.ts +34 -0
- package/src/main.tsx +14 -0
- package/src/types.ts +76 -0
- package/src/vite-env.d.ts +56 -0
- package/src-tauri/README.md +11 -0
package/src/App.tsx
ADDED
|
@@ -0,0 +1,1186 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
ArrowDownToLine,
|
|
4
|
+
Bot,
|
|
5
|
+
Check,
|
|
6
|
+
CircleUserRound,
|
|
7
|
+
FileStack,
|
|
8
|
+
Plus,
|
|
9
|
+
RefreshCw,
|
|
10
|
+
Sparkles,
|
|
11
|
+
SquarePen,
|
|
12
|
+
Trash2,
|
|
13
|
+
X
|
|
14
|
+
} from "lucide-react";
|
|
15
|
+
import { useControlWorkspace } from "./hooks/use-control-workspace";
|
|
16
|
+
import { workspaceSections } from "./lib/workspace-sections";
|
|
17
|
+
import type { PlatformId, Provider } from "./types";
|
|
18
|
+
|
|
19
|
+
const emptyForm = { name: "", baseUrl: "", apiKey: "", model: "gpt-5.4" };
|
|
20
|
+
const allPlatforms: PlatformId[] = ["codex", "opencode", "openclaw"];
|
|
21
|
+
|
|
22
|
+
type McpEditorForm = {
|
|
23
|
+
kind: "mcp";
|
|
24
|
+
id?: string;
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
tags: string;
|
|
28
|
+
homepage: string;
|
|
29
|
+
enabledPlatforms: PlatformId[];
|
|
30
|
+
error: string | null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type PromptEditorForm = {
|
|
34
|
+
kind: "prompt";
|
|
35
|
+
id?: string;
|
|
36
|
+
name: string;
|
|
37
|
+
description: string;
|
|
38
|
+
content: string;
|
|
39
|
+
appType: "global" | PlatformId;
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
error: string | null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type SkillEditorForm = {
|
|
45
|
+
kind: "skill";
|
|
46
|
+
id?: string;
|
|
47
|
+
name: string;
|
|
48
|
+
description: string;
|
|
49
|
+
directory: string;
|
|
50
|
+
enabledPlatforms: PlatformId[];
|
|
51
|
+
error: string | null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type ResourceEditorForm = McpEditorForm | PromptEditorForm | SkillEditorForm;
|
|
55
|
+
|
|
56
|
+
type DeleteConfirmState =
|
|
57
|
+
| { kind: "mcp"; id: string; name: string }
|
|
58
|
+
| { kind: "prompt"; id: string; name: string }
|
|
59
|
+
| { kind: "skill"; id: string; name: string };
|
|
60
|
+
|
|
61
|
+
function cx(...parts: Array<string | false | null | undefined>) {
|
|
62
|
+
return parts.filter(Boolean).join(" ");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function tone(id: PlatformId) {
|
|
66
|
+
if (id === "openclaw") return { soft: "bg-[#f97316]/10 text-[#f97316]", line: "border-[#f97316]/20" };
|
|
67
|
+
if (id === "opencode") return { soft: "bg-[#14b8a6]/10 text-[#0f766e]", line: "border-[#14b8a6]/20" };
|
|
68
|
+
return { soft: "bg-[#1677ff]/10 text-[#1677ff]", line: "border-[#1677ff]/20" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function timeText(value: string) {
|
|
72
|
+
return new Date(value).toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function shortUrl(value: string) {
|
|
76
|
+
try {
|
|
77
|
+
const u = new URL(value);
|
|
78
|
+
return `${u.origin}${u.pathname === "/" ? "" : u.pathname}`;
|
|
79
|
+
} catch {
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function providerArt(provider: Provider, platformId: PlatformId) {
|
|
85
|
+
const name = provider.name.toLowerCase();
|
|
86
|
+
const url = provider.baseUrl.toLowerCase();
|
|
87
|
+
if (name.includes("openai") || url.includes("openai.com")) return { src: "/assets/openai-small.svg", alt: "OpenAI" };
|
|
88
|
+
if (platformId === "openclaw" || name.includes("openclaw") || name.includes("lobster")) {
|
|
89
|
+
return { src: "/assets/pixel-lobster.svg", alt: "OpenClaw" };
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function platformArt(platformId: PlatformId) {
|
|
95
|
+
if (platformId === "codex") return <img src="/assets/openai-small.svg" alt="Codex" className="h-5 w-5 object-contain" />;
|
|
96
|
+
if (platformId === "openclaw") return <img src="/assets/pixel-lobster.svg" alt="OpenClaw" className="h-5 w-5 object-contain" />;
|
|
97
|
+
return <Bot className="h-4 w-4 text-slate-600" />;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function promptScopeLabel(scope: "global" | PlatformId) {
|
|
101
|
+
if (scope === "global") return "全局";
|
|
102
|
+
if (scope === "codex") return "Codex";
|
|
103
|
+
if (scope === "opencode") return "OpenCode";
|
|
104
|
+
return "OpenClaw";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function platformLabel(id: PlatformId) {
|
|
108
|
+
if (id === "codex") return "Codex";
|
|
109
|
+
if (id === "opencode") return "OpenCode";
|
|
110
|
+
return "OpenClaw";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function BrandMark() {
|
|
114
|
+
return (
|
|
115
|
+
<div className="relative grid h-14 w-14 place-items-center overflow-hidden rounded-[20px] bg-[linear-gradient(160deg,#ff8a1f_0%,#ff6b1a_52%,#ffb347_100%)] shadow-[0_20px_48px_rgba(249,115,22,0.28)] ring-1 ring-[#fdba74]">
|
|
116
|
+
<div className="absolute inset-[6px] rounded-[16px] bg-[linear-gradient(180deg,rgba(255,255,255,0.24),rgba(255,255,255,0.04))]" />
|
|
117
|
+
<svg viewBox="0 0 64 64" className="relative h-8 w-8" fill="none" aria-hidden="true">
|
|
118
|
+
<path d="M14 47V20.5c0-1.38 1.12-2.5 2.5-2.5h1.9c1.03 0 1.99.51 2.58 1.35L32 35.1l11.02-15.75A3.13 3.13 0 0 1 45.58 18h1.92c1.38 0 2.5 1.12 2.5 2.5V47" stroke="white" strokeWidth="5.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
119
|
+
<circle cx="49.5" cy="16.5" r="4.5" fill="#fff1e6" />
|
|
120
|
+
</svg>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default function App() {
|
|
126
|
+
const workspace = useControlWorkspace();
|
|
127
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
128
|
+
const [candidateProbeText, setCandidateProbeText] = useState<string | null>(null);
|
|
129
|
+
const [targetPathMessage, setTargetPathMessage] = useState<string | null>(null);
|
|
130
|
+
const [resourceEditor, setResourceEditor] = useState<ResourceEditorForm | null>(null);
|
|
131
|
+
const [deleteConfirm, setDeleteConfirm] = useState<DeleteConfirmState | null>(null);
|
|
132
|
+
|
|
133
|
+
if (!workspace.snapshot || !workspace.platform) {
|
|
134
|
+
return <div className="flex min-h-screen items-center justify-center text-slate-400">正在加载工作区...</div>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const platform = workspace.platform;
|
|
138
|
+
const styles = tone(platform.id);
|
|
139
|
+
const providers = platform.providers;
|
|
140
|
+
const active = providers.find((item) => item.isActive) ?? providers[0] ?? null;
|
|
141
|
+
const probes = workspace.probeQuery.data ?? [];
|
|
142
|
+
const editing = providers.some((item) => item.name.trim().toLowerCase() === workspace.editorForm.name.trim().toLowerCase());
|
|
143
|
+
const activeSection = workspace.activeSection;
|
|
144
|
+
const mcpServers = workspace.snapshot.mcpServers;
|
|
145
|
+
const prompts = workspace.snapshot.prompts.filter((item) => item.appType === "global" || item.appType === platform.id);
|
|
146
|
+
const skills = workspace.snapshot.skills.filter((item) => item.enabledPlatforms.includes(platform.id));
|
|
147
|
+
const skillRepos = workspace.snapshot.skillRepos;
|
|
148
|
+
const models = workspace.snapshot.models.length > 0 ? workspace.snapshot.models : [emptyForm.model];
|
|
149
|
+
|
|
150
|
+
function openCreate() {
|
|
151
|
+
setResourceEditor(null);
|
|
152
|
+
workspace.setEditorForm({ ...emptyForm, model: models[0] ?? emptyForm.model });
|
|
153
|
+
setCandidateProbeText(null);
|
|
154
|
+
setDialogOpen(true);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function openImport() {
|
|
158
|
+
setResourceEditor(null);
|
|
159
|
+
workspace.setEditorForm({
|
|
160
|
+
name: active?.name || "default",
|
|
161
|
+
baseUrl: active?.baseUrl || "",
|
|
162
|
+
apiKey: "",
|
|
163
|
+
model: active?.model || models[0] || emptyForm.model
|
|
164
|
+
});
|
|
165
|
+
setCandidateProbeText(null);
|
|
166
|
+
setDialogOpen(true);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function openEdit(provider: Provider) {
|
|
170
|
+
setResourceEditor(null);
|
|
171
|
+
workspace.openProvider(provider);
|
|
172
|
+
setCandidateProbeText(null);
|
|
173
|
+
setDialogOpen(true);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function closeDialog() {
|
|
177
|
+
setDialogOpen(false);
|
|
178
|
+
setCandidateProbeText(null);
|
|
179
|
+
workspace.setEditorForm({ ...emptyForm, model: models[0] ?? emptyForm.model });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function submit() {
|
|
183
|
+
workspace.saveMutation.mutate(
|
|
184
|
+
{ platform: platform.id, ...workspace.editorForm },
|
|
185
|
+
{
|
|
186
|
+
onSuccess: () => closeDialog()
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function probeCandidateBaseUrl() {
|
|
192
|
+
if (!workspace.editorForm.baseUrl.trim()) {
|
|
193
|
+
setCandidateProbeText("请先输入基础地址");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
setCandidateProbeText("正在测试...");
|
|
197
|
+
try {
|
|
198
|
+
const response = await workspace.probeCandidateMutation.mutateAsync({ platform: platform.id, baseUrl: workspace.editorForm.baseUrl });
|
|
199
|
+
if (!response.result) {
|
|
200
|
+
setCandidateProbeText("未返回测速结果");
|
|
201
|
+
} else if (response.result.ok) {
|
|
202
|
+
setCandidateProbeText(`可连通,延迟 ${response.result.latency} ms`);
|
|
203
|
+
} else {
|
|
204
|
+
setCandidateProbeText(`不可用:${response.result.error || "未知错误"}`);
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
setCandidateProbeText(error instanceof Error ? error.message : "测试失败");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function openTargetPath(targetPath: string) {
|
|
212
|
+
try {
|
|
213
|
+
const response = await workspace.openPathMutation.mutateAsync({ targetPath });
|
|
214
|
+
setTargetPathMessage(`已打开:${response.targetPath}`);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const message = error instanceof Error ? error.message : "打开失败";
|
|
217
|
+
setTargetPathMessage(`打开失败:${message}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function closeResourceEditor() {
|
|
222
|
+
setResourceEditor(null);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function toggleResourcePlatform(target: PlatformId) {
|
|
226
|
+
setResourceEditor((current) => {
|
|
227
|
+
if (!current || (current.kind !== "mcp" && current.kind !== "skill")) {
|
|
228
|
+
return current;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const enabledPlatforms = current.enabledPlatforms.includes(target)
|
|
232
|
+
? current.enabledPlatforms.filter((item) => item !== target)
|
|
233
|
+
: [...current.enabledPlatforms, target];
|
|
234
|
+
|
|
235
|
+
return { ...current, enabledPlatforms, error: null };
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function openMcpEditor(server?: (typeof mcpServers)[number]) {
|
|
240
|
+
setDialogOpen(false);
|
|
241
|
+
setResourceEditor({
|
|
242
|
+
kind: "mcp",
|
|
243
|
+
id: server?.id,
|
|
244
|
+
name: server?.name || "",
|
|
245
|
+
description: server?.description || "",
|
|
246
|
+
tags: (server?.tags || []).join(", "),
|
|
247
|
+
homepage: server?.homepage || "",
|
|
248
|
+
enabledPlatforms: server?.enabledPlatforms?.length ? [...server.enabledPlatforms] : [...allPlatforms],
|
|
249
|
+
error: null
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function openDeleteConfirm(next: DeleteConfirmState) {
|
|
254
|
+
setDialogOpen(false);
|
|
255
|
+
setResourceEditor(null);
|
|
256
|
+
setDeleteConfirm(next);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function closeDeleteConfirm() {
|
|
260
|
+
setDeleteConfirm(null);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function removeMcp(serverId: string, name: string) {
|
|
264
|
+
openDeleteConfirm({ kind: "mcp", id: serverId, name });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function openPromptEditor(prompt?: (typeof prompts)[number]) {
|
|
268
|
+
setDialogOpen(false);
|
|
269
|
+
setResourceEditor({
|
|
270
|
+
kind: "prompt",
|
|
271
|
+
id: prompt?.id,
|
|
272
|
+
name: prompt?.name || "",
|
|
273
|
+
description: prompt?.description || "",
|
|
274
|
+
content: prompt?.content || "",
|
|
275
|
+
appType: prompt?.appType || "global",
|
|
276
|
+
enabled: prompt?.enabled ?? true,
|
|
277
|
+
error: null
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function removePrompt(promptId: string, name: string) {
|
|
282
|
+
openDeleteConfirm({ kind: "prompt", id: promptId, name });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function openSkillEditor(skill?: (typeof skills)[number]) {
|
|
286
|
+
setDialogOpen(false);
|
|
287
|
+
setResourceEditor({
|
|
288
|
+
kind: "skill",
|
|
289
|
+
id: skill?.id,
|
|
290
|
+
name: skill?.name || "",
|
|
291
|
+
description: skill?.description || "",
|
|
292
|
+
directory: skill?.directory || "",
|
|
293
|
+
enabledPlatforms: skill?.enabledPlatforms?.length ? [...skill.enabledPlatforms] : [...allPlatforms],
|
|
294
|
+
error: null
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function removeSkill(skillId: string, name: string) {
|
|
299
|
+
openDeleteConfirm({ kind: "skill", id: skillId, name });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function confirmDelete() {
|
|
303
|
+
if (!deleteConfirm) return;
|
|
304
|
+
if (deleteConfirm.kind === "mcp") {
|
|
305
|
+
workspace.deleteMcpMutation.mutate({ serverId: deleteConfirm.id }, { onSuccess: () => closeDeleteConfirm() });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (deleteConfirm.kind === "prompt") {
|
|
309
|
+
workspace.deletePromptMutation.mutate({ promptId: deleteConfirm.id }, { onSuccess: () => closeDeleteConfirm() });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
workspace.deleteSkillMutation.mutate({ skillId: deleteConfirm.id }, { onSuccess: () => closeDeleteConfirm() });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function submitResourceEditor() {
|
|
316
|
+
if (!resourceEditor) return;
|
|
317
|
+
|
|
318
|
+
if (resourceEditor.kind === "mcp") {
|
|
319
|
+
if (!resourceEditor.name.trim()) {
|
|
320
|
+
setResourceEditor({ ...resourceEditor, error: "请填写 MCP 名称" });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (!resourceEditor.description.trim()) {
|
|
324
|
+
setResourceEditor({ ...resourceEditor, error: "请填写 MCP 描述" });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (resourceEditor.enabledPlatforms.length === 0) {
|
|
328
|
+
setResourceEditor({ ...resourceEditor, error: "请至少选择一个启用平台" });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const tags = resourceEditor.tags
|
|
333
|
+
.split(",")
|
|
334
|
+
.map((item) => item.trim())
|
|
335
|
+
.filter(Boolean);
|
|
336
|
+
workspace.upsertMcpMutation.mutate(
|
|
337
|
+
{
|
|
338
|
+
id: resourceEditor.id,
|
|
339
|
+
name: resourceEditor.name.trim(),
|
|
340
|
+
description: resourceEditor.description.trim(),
|
|
341
|
+
tags,
|
|
342
|
+
enabledPlatforms: resourceEditor.enabledPlatforms,
|
|
343
|
+
homepage: resourceEditor.homepage.trim() || undefined
|
|
344
|
+
},
|
|
345
|
+
{ onSuccess: () => closeResourceEditor() }
|
|
346
|
+
);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (resourceEditor.kind === "prompt") {
|
|
351
|
+
if (!resourceEditor.name.trim()) {
|
|
352
|
+
setResourceEditor({ ...resourceEditor, error: "请填写提示词名称" });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (!resourceEditor.content.trim()) {
|
|
356
|
+
setResourceEditor({ ...resourceEditor, error: "请填写提示词内容" });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
workspace.upsertPromptMutation.mutate(
|
|
361
|
+
{
|
|
362
|
+
id: resourceEditor.id,
|
|
363
|
+
appType: resourceEditor.appType,
|
|
364
|
+
name: resourceEditor.name.trim(),
|
|
365
|
+
description: resourceEditor.description.trim(),
|
|
366
|
+
content: resourceEditor.content.trim(),
|
|
367
|
+
enabled: resourceEditor.enabled
|
|
368
|
+
},
|
|
369
|
+
{ onSuccess: () => closeResourceEditor() }
|
|
370
|
+
);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (!resourceEditor.name.trim()) {
|
|
375
|
+
setResourceEditor({ ...resourceEditor, error: "请填写技能名称" });
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (!resourceEditor.directory.trim()) {
|
|
379
|
+
setResourceEditor({ ...resourceEditor, error: "请填写技能目录" });
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (resourceEditor.enabledPlatforms.length === 0) {
|
|
383
|
+
setResourceEditor({ ...resourceEditor, error: "请至少选择一个启用平台" });
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
workspace.upsertSkillMutation.mutate(
|
|
388
|
+
{
|
|
389
|
+
id: resourceEditor.id,
|
|
390
|
+
name: resourceEditor.name.trim(),
|
|
391
|
+
description: resourceEditor.description.trim(),
|
|
392
|
+
directory: resourceEditor.directory.trim(),
|
|
393
|
+
enabledPlatforms: resourceEditor.enabledPlatforms
|
|
394
|
+
},
|
|
395
|
+
{ onSuccess: () => closeResourceEditor() }
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const quickAction =
|
|
400
|
+
activeSection === "providers"
|
|
401
|
+
? { label: "新增服务商", run: openCreate }
|
|
402
|
+
: activeSection === "mcp"
|
|
403
|
+
? { label: "新增 MCP", run: () => openMcpEditor() }
|
|
404
|
+
: activeSection === "prompts"
|
|
405
|
+
? { label: "新增提示词", run: () => openPromptEditor() }
|
|
406
|
+
: { label: "新增技能", run: () => openSkillEditor() };
|
|
407
|
+
|
|
408
|
+
const resourceSubmitPending =
|
|
409
|
+
resourceEditor?.kind === "mcp"
|
|
410
|
+
? workspace.upsertMcpMutation.isPending
|
|
411
|
+
: resourceEditor?.kind === "prompt"
|
|
412
|
+
? workspace.upsertPromptMutation.isPending
|
|
413
|
+
: resourceEditor?.kind === "skill"
|
|
414
|
+
? workspace.upsertSkillMutation.isPending
|
|
415
|
+
: false;
|
|
416
|
+
|
|
417
|
+
const deleteSubmitPending =
|
|
418
|
+
deleteConfirm?.kind === "mcp"
|
|
419
|
+
? workspace.deleteMcpMutation.isPending
|
|
420
|
+
: deleteConfirm?.kind === "prompt"
|
|
421
|
+
? workspace.deletePromptMutation.isPending
|
|
422
|
+
: deleteConfirm?.kind === "skill"
|
|
423
|
+
? workspace.deleteSkillMutation.isPending
|
|
424
|
+
: false;
|
|
425
|
+
|
|
426
|
+
return (
|
|
427
|
+
<div className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(59,130,246,0.06),_transparent_22%),linear-gradient(180deg,#ffffff_0%,#f7f9fc_100%)] text-slate-900">
|
|
428
|
+
<div className="mx-auto flex min-h-screen max-w-[1440px] flex-col px-8 py-8">
|
|
429
|
+
<header className="flex flex-wrap items-start justify-between gap-6">
|
|
430
|
+
<div className="flex items-center gap-4">
|
|
431
|
+
<BrandMark />
|
|
432
|
+
<div>
|
|
433
|
+
<div className="inline-flex items-center gap-2 rounded-full bg-white px-3 py-1 text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400 ring-1 ring-slate-200/80">
|
|
434
|
+
<Sparkles className="h-3.5 w-3.5 text-[#f97316]" />
|
|
435
|
+
桌面控制台
|
|
436
|
+
</div>
|
|
437
|
+
<div className="mt-3 flex items-center gap-3">
|
|
438
|
+
<h1 className="text-[28px] font-semibold tracking-[-0.05em] text-slate-950">{workspace.snapshot.appName}</h1>
|
|
439
|
+
<span className={cx("rounded-full px-3 py-1 text-xs font-medium", styles.soft)}>{platform.label}</span>
|
|
440
|
+
</div>
|
|
441
|
+
<p className="mt-2 max-w-2xl text-sm leading-6 text-slate-500">在一个轻量控制台中完成路由切换、目标文件管理与连通性检测。</p>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
<div className="flex items-center gap-3">
|
|
446
|
+
<button
|
|
447
|
+
type="button"
|
|
448
|
+
onClick={quickAction.run}
|
|
449
|
+
className="inline-flex h-11 items-center gap-2 rounded-2xl bg-[#ff7a1a] px-4 text-sm font-medium text-white shadow-[0_16px_30px_rgba(255,122,26,0.28)] transition hover:brightness-105"
|
|
450
|
+
>
|
|
451
|
+
<Plus className="h-4 w-4" />
|
|
452
|
+
{quickAction.label}
|
|
453
|
+
</button>
|
|
454
|
+
</div>
|
|
455
|
+
</header>
|
|
456
|
+
|
|
457
|
+
<section className="mt-7 flex flex-wrap items-center gap-3">
|
|
458
|
+
{workspace.snapshot.platforms.map((item) => (
|
|
459
|
+
<button
|
|
460
|
+
key={item.id}
|
|
461
|
+
type="button"
|
|
462
|
+
onClick={() => workspace.setActivePlatform(item.id)}
|
|
463
|
+
className={cx(
|
|
464
|
+
"inline-flex items-center gap-3 rounded-[20px] border px-4 py-3 text-left transition",
|
|
465
|
+
item.id === platform.id ? cx("bg-white shadow-[0_18px_38px_rgba(15,23,42,0.08)]", tone(item.id).line) : "border-slate-200/80 bg-white/70 hover:bg-white"
|
|
466
|
+
)}
|
|
467
|
+
>
|
|
468
|
+
<div className="grid h-10 w-10 place-items-center rounded-2xl bg-slate-50 ring-1 ring-slate-200/70">{platformArt(item.id)}</div>
|
|
469
|
+
<div>
|
|
470
|
+
<div className="text-sm font-medium text-slate-900">{item.label}</div>
|
|
471
|
+
<div className="text-xs text-slate-400">已保存 {item.providerCount} 条路由</div>
|
|
472
|
+
</div>
|
|
473
|
+
</button>
|
|
474
|
+
))}
|
|
475
|
+
</section>
|
|
476
|
+
|
|
477
|
+
<section className="mt-5 flex flex-wrap items-center gap-2">
|
|
478
|
+
{workspaceSections.map((section) => (
|
|
479
|
+
<button
|
|
480
|
+
key={section.id}
|
|
481
|
+
type="button"
|
|
482
|
+
onClick={() => workspace.setActiveSection(section.id)}
|
|
483
|
+
className={cx(
|
|
484
|
+
"inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium transition",
|
|
485
|
+
activeSection === section.id ? "bg-slate-950 text-white shadow-[0_10px_20px_rgba(15,23,42,0.12)]" : "bg-white text-slate-600 ring-1 ring-slate-200/80 hover:text-slate-900"
|
|
486
|
+
)}
|
|
487
|
+
>
|
|
488
|
+
<section.icon className="h-4 w-4" />
|
|
489
|
+
{section.label}
|
|
490
|
+
</button>
|
|
491
|
+
))}
|
|
492
|
+
</section>
|
|
493
|
+
|
|
494
|
+
<main className="mt-7 grid flex-1 grid-cols-[minmax(0,1.45fr)_380px] gap-6">
|
|
495
|
+
<section className="min-w-0 rounded-[30px] border border-slate-200/80 bg-white p-6 shadow-[0_24px_80px_rgba(15,23,42,0.06)]">
|
|
496
|
+
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
497
|
+
<div>
|
|
498
|
+
<div className="text-[12px] uppercase tracking-[0.24em] text-slate-400">{workspace.activeSectionMeta.eyebrow}</div>
|
|
499
|
+
<h2 className="mt-2 text-[30px] font-semibold tracking-[-0.05em] text-slate-950">
|
|
500
|
+
{platform.label} {workspace.activeSectionMeta.title}
|
|
501
|
+
</h2>
|
|
502
|
+
<p className="mt-2 max-w-2xl text-sm leading-6 text-slate-500">
|
|
503
|
+
{activeSection === "providers" ? "这里支持新增、编辑、切换、刷新和连通性检测,配置会立即回写工作区。" : "布局参考 CC Switch 的信息结构,同时保留 March 的品牌风格。"}
|
|
504
|
+
</p>
|
|
505
|
+
</div>
|
|
506
|
+
|
|
507
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
508
|
+
<button
|
|
509
|
+
type="button"
|
|
510
|
+
onClick={() => workspace.snapshotQuery.refetch()}
|
|
511
|
+
className="inline-flex h-11 items-center gap-2 rounded-2xl border border-slate-200 px-4 text-sm font-medium text-slate-700 transition hover:border-slate-300 hover:bg-slate-50"
|
|
512
|
+
>
|
|
513
|
+
<RefreshCw className={cx("h-4 w-4", workspace.snapshotQuery.isFetching && "animate-spin")} />
|
|
514
|
+
刷新
|
|
515
|
+
</button>
|
|
516
|
+
{activeSection === "providers" ? (
|
|
517
|
+
<>
|
|
518
|
+
<button
|
|
519
|
+
type="button"
|
|
520
|
+
onClick={openImport}
|
|
521
|
+
className="inline-flex h-11 items-center gap-2 rounded-2xl border border-slate-200 px-4 text-sm font-medium text-slate-700 transition hover:border-slate-300 hover:bg-slate-50"
|
|
522
|
+
>
|
|
523
|
+
<ArrowDownToLine className="h-4 w-4" />
|
|
524
|
+
从当前导入
|
|
525
|
+
</button>
|
|
526
|
+
<button
|
|
527
|
+
type="button"
|
|
528
|
+
onClick={openCreate}
|
|
529
|
+
className="inline-flex h-11 items-center gap-2 rounded-2xl bg-[#1677ff] px-4 text-sm font-medium text-white shadow-[0_12px_24px_rgba(22,119,255,0.22)] transition hover:brightness-105"
|
|
530
|
+
>
|
|
531
|
+
<Plus className="h-4 w-4" />
|
|
532
|
+
新增服务商
|
|
533
|
+
</button>
|
|
534
|
+
</>
|
|
535
|
+
) : activeSection === "mcp" ? (
|
|
536
|
+
<button type="button" onClick={() => openMcpEditor()} className="inline-flex h-11 items-center gap-2 rounded-2xl bg-[#1677ff] px-4 text-sm font-medium text-white shadow-[0_12px_24px_rgba(22,119,255,0.22)] transition hover:brightness-105">
|
|
537
|
+
<Plus className="h-4 w-4" />新增 MCP
|
|
538
|
+
</button>
|
|
539
|
+
) : activeSection === "prompts" ? (
|
|
540
|
+
<button type="button" onClick={() => openPromptEditor()} className="inline-flex h-11 items-center gap-2 rounded-2xl bg-[#1677ff] px-4 text-sm font-medium text-white shadow-[0_12px_24px_rgba(22,119,255,0.22)] transition hover:brightness-105">
|
|
541
|
+
<Plus className="h-4 w-4" />新增提示词
|
|
542
|
+
</button>
|
|
543
|
+
) : (
|
|
544
|
+
<button type="button" onClick={() => openSkillEditor()} className="inline-flex h-11 items-center gap-2 rounded-2xl bg-[#1677ff] px-4 text-sm font-medium text-white shadow-[0_12px_24px_rgba(22,119,255,0.22)] transition hover:brightness-105">
|
|
545
|
+
<Plus className="h-4 w-4" />新增技能
|
|
546
|
+
</button>
|
|
547
|
+
)}
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
|
|
551
|
+
{activeSection === "providers" ? (
|
|
552
|
+
<div className="mt-6 grid gap-3">
|
|
553
|
+
{providers.map((provider) => {
|
|
554
|
+
const art = providerArt(provider, platform.id);
|
|
555
|
+
return (
|
|
556
|
+
<article key={provider.id} className="rounded-[24px] border border-slate-200/80 bg-slate-50 px-5 py-4">
|
|
557
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
558
|
+
<div className="min-w-0">
|
|
559
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
560
|
+
<div className="grid h-8 w-8 place-items-center rounded-xl bg-white ring-1 ring-slate-200/80">
|
|
561
|
+
{art ? <img src={art.src} alt={art.alt} className="h-4 w-4 object-contain" /> : <CircleUserRound className="h-4 w-4 text-slate-500" />}
|
|
562
|
+
</div>
|
|
563
|
+
<div className="text-base font-semibold text-slate-950">{provider.name}</div>
|
|
564
|
+
{provider.isActive ? <span className={cx("rounded-full px-2.5 py-1 text-[11px] font-medium", styles.soft)}>当前路由</span> : null}
|
|
565
|
+
</div>
|
|
566
|
+
<div className="mt-2 text-sm text-slate-500">{shortUrl(provider.baseUrl)}</div>
|
|
567
|
+
<div className="mt-1 text-xs text-slate-400">模型:{provider.model} / 密钥:{provider.maskedApiKey}</div>
|
|
568
|
+
<div className="mt-1 text-xs text-slate-400">更新时间:{timeText(provider.updatedAt)}</div>
|
|
569
|
+
</div>
|
|
570
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
571
|
+
{!provider.isActive ? (
|
|
572
|
+
<button type="button" onClick={() => workspace.activateMutation.mutate({ platform: platform.id, providerId: provider.id })} className="inline-flex h-10 items-center rounded-2xl bg-slate-950 px-4 text-sm font-medium text-white transition hover:opacity-90">设为当前</button>
|
|
573
|
+
) : (
|
|
574
|
+
<button type="button" className="inline-flex h-10 items-center gap-1 rounded-2xl border border-slate-200 bg-white px-4 text-sm font-medium text-slate-700" disabled>
|
|
575
|
+
<Check className="h-4 w-4" />生效中
|
|
576
|
+
</button>
|
|
577
|
+
)}
|
|
578
|
+
<button type="button" onClick={() => openEdit(provider)} className="inline-flex h-10 items-center gap-1 rounded-2xl border border-slate-200 bg-white px-3 text-sm font-medium text-slate-700 transition hover:bg-slate-100">
|
|
579
|
+
<SquarePen className="h-4 w-4" />编辑
|
|
580
|
+
</button>
|
|
581
|
+
</div>
|
|
582
|
+
</div>
|
|
583
|
+
</article>
|
|
584
|
+
);
|
|
585
|
+
})}
|
|
586
|
+
</div>
|
|
587
|
+
) : activeSection === "mcp" ? (
|
|
588
|
+
<div className="mt-6 grid gap-3">
|
|
589
|
+
{mcpServers.map((server) => {
|
|
590
|
+
const enabled = server.enabledPlatforms.includes(platform.id);
|
|
591
|
+
return (
|
|
592
|
+
<article key={server.id} className="rounded-[24px] border border-slate-200/80 bg-slate-50 px-5 py-4">
|
|
593
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
594
|
+
<div className="min-w-0">
|
|
595
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
596
|
+
<div className="text-base font-semibold text-slate-950">{server.name}</div>
|
|
597
|
+
<span className={cx("rounded-full px-2.5 py-1 text-[11px] font-medium", enabled ? styles.soft : "bg-slate-200 text-slate-600")}>{enabled ? "已启用" : "未启用"}</span>
|
|
598
|
+
</div>
|
|
599
|
+
<p className="mt-2 text-sm leading-6 text-slate-500">{server.description}</p>
|
|
600
|
+
<div className="mt-3 flex flex-wrap gap-2">
|
|
601
|
+
{server.tags.map((tag) => (
|
|
602
|
+
<span key={tag} className="rounded-full bg-white px-2.5 py-1 text-[11px] text-slate-500 ring-1 ring-slate-200/80">{tag}</span>
|
|
603
|
+
))}
|
|
604
|
+
</div>
|
|
605
|
+
{server.homepage ? <div className="mt-2 text-xs text-slate-400">主页:{server.homepage}</div> : null}
|
|
606
|
+
</div>
|
|
607
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
608
|
+
<button
|
|
609
|
+
type="button"
|
|
610
|
+
onClick={() => workspace.toggleMcpMutation.mutate({ serverId: server.id, platform: platform.id })}
|
|
611
|
+
className={cx("inline-flex h-10 items-center rounded-2xl px-4 text-sm font-medium transition", enabled ? "border border-slate-200 bg-white text-slate-700 hover:bg-slate-100" : "bg-slate-950 text-white hover:opacity-90")}
|
|
612
|
+
>
|
|
613
|
+
{enabled ? "禁用" : "启用"}
|
|
614
|
+
</button>
|
|
615
|
+
<button type="button" onClick={() => openMcpEditor(server)} className="inline-flex h-10 items-center gap-1 rounded-2xl border border-slate-200 bg-white px-3 text-sm font-medium text-slate-700 transition hover:bg-slate-100">
|
|
616
|
+
<SquarePen className="h-4 w-4" />编辑
|
|
617
|
+
</button>
|
|
618
|
+
<button type="button" onClick={() => removeMcp(server.id, server.name)} className="inline-flex h-10 items-center gap-1 rounded-2xl border border-rose-200 bg-white px-3 text-sm font-medium text-rose-600 transition hover:bg-rose-50">
|
|
619
|
+
<Trash2 className="h-4 w-4" />删除
|
|
620
|
+
</button>
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
623
|
+
</article>
|
|
624
|
+
);
|
|
625
|
+
})}
|
|
626
|
+
</div>
|
|
627
|
+
) : activeSection === "prompts" ? (
|
|
628
|
+
<div className="mt-6 grid gap-3">
|
|
629
|
+
{prompts.map((prompt) => (
|
|
630
|
+
<article key={prompt.id} className="rounded-[24px] border border-slate-200/80 bg-slate-50 px-5 py-4">
|
|
631
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
632
|
+
<div className="min-w-0">
|
|
633
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
634
|
+
<div className="text-base font-semibold text-slate-950">{prompt.name}</div>
|
|
635
|
+
<span className={cx("rounded-full px-2.5 py-1 text-[11px] font-medium", prompt.enabled ? styles.soft : "bg-slate-200 text-slate-600")}>{promptScopeLabel(prompt.appType)}</span>
|
|
636
|
+
</div>
|
|
637
|
+
<p className="mt-2 text-sm leading-6 text-slate-500">{prompt.description}</p>
|
|
638
|
+
<div className="mt-3 rounded-2xl bg-white px-4 py-4 text-sm leading-7 text-slate-600 ring-1 ring-slate-200/80">{prompt.content}</div>
|
|
639
|
+
<div className="mt-2 text-xs text-slate-400">更新时间:{timeText(prompt.updatedAt)}</div>
|
|
640
|
+
</div>
|
|
641
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
642
|
+
<button
|
|
643
|
+
type="button"
|
|
644
|
+
onClick={() => workspace.togglePromptMutation.mutate({ promptId: prompt.id })}
|
|
645
|
+
className={cx("inline-flex h-10 items-center rounded-2xl px-4 text-sm font-medium transition", prompt.enabled ? "border border-slate-200 bg-white text-slate-700 hover:bg-slate-100" : "bg-slate-950 text-white hover:opacity-90")}
|
|
646
|
+
>
|
|
647
|
+
{prompt.enabled ? "禁用" : "启用"}
|
|
648
|
+
</button>
|
|
649
|
+
<button type="button" onClick={() => openPromptEditor(prompt)} className="inline-flex h-10 items-center gap-1 rounded-2xl border border-slate-200 bg-white px-3 text-sm font-medium text-slate-700 transition hover:bg-slate-100">
|
|
650
|
+
<SquarePen className="h-4 w-4" />编辑
|
|
651
|
+
</button>
|
|
652
|
+
<button type="button" onClick={() => removePrompt(prompt.id, prompt.name)} className="inline-flex h-10 items-center gap-1 rounded-2xl border border-rose-200 bg-white px-3 text-sm font-medium text-rose-600 transition hover:bg-rose-50">
|
|
653
|
+
<Trash2 className="h-4 w-4" />删除
|
|
654
|
+
</button>
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
</article>
|
|
658
|
+
))}
|
|
659
|
+
</div>
|
|
660
|
+
) : (
|
|
661
|
+
<div className="mt-6 grid gap-4 xl:grid-cols-[1.05fr_0.95fr]">
|
|
662
|
+
<div className="space-y-3">
|
|
663
|
+
{skills.map((skill) => (
|
|
664
|
+
<article key={skill.id} className="rounded-[24px] border border-slate-200/80 bg-slate-50 px-5 py-4">
|
|
665
|
+
<div className="flex items-start justify-between gap-3">
|
|
666
|
+
<div>
|
|
667
|
+
<div className="text-base font-semibold text-slate-950">{skill.name}</div>
|
|
668
|
+
<p className="mt-2 text-sm leading-6 text-slate-500">{skill.description}</p>
|
|
669
|
+
<div className="mt-3 text-xs text-slate-400">{skill.directory}</div>
|
|
670
|
+
</div>
|
|
671
|
+
<div className="flex items-center gap-2">
|
|
672
|
+
<span className={cx("rounded-full px-2.5 py-1 text-[11px] font-medium", styles.soft)}>已安装</span>
|
|
673
|
+
<button type="button" onClick={() => openSkillEditor(skill)} className="inline-flex h-9 items-center gap-1 rounded-xl border border-slate-200 bg-white px-2.5 text-xs font-medium text-slate-700 transition hover:bg-slate-100">
|
|
674
|
+
<SquarePen className="h-3.5 w-3.5" />编辑
|
|
675
|
+
</button>
|
|
676
|
+
<button type="button" onClick={() => removeSkill(skill.id, skill.name)} className="inline-flex h-9 items-center gap-1 rounded-xl border border-rose-200 bg-white px-2.5 text-xs font-medium text-rose-600 transition hover:bg-rose-50">
|
|
677
|
+
<Trash2 className="h-3.5 w-3.5" />删除
|
|
678
|
+
</button>
|
|
679
|
+
</div>
|
|
680
|
+
</div>
|
|
681
|
+
</article>
|
|
682
|
+
))}
|
|
683
|
+
</div>
|
|
684
|
+
<div className="space-y-3">
|
|
685
|
+
{skillRepos.map((repo) => (
|
|
686
|
+
<article key={repo.id} className="rounded-[24px] border border-slate-200/80 bg-slate-50 px-5 py-4">
|
|
687
|
+
<div className="flex items-start justify-between gap-3">
|
|
688
|
+
<div>
|
|
689
|
+
<div className="text-base font-semibold text-slate-950">{repo.owner}/{repo.name}</div>
|
|
690
|
+
<div className="mt-2 text-sm text-slate-500">分支:{repo.branch}</div>
|
|
691
|
+
</div>
|
|
692
|
+
<button
|
|
693
|
+
type="button"
|
|
694
|
+
onClick={() => workspace.toggleSkillRepoMutation.mutate({ repoId: repo.id })}
|
|
695
|
+
className={cx("inline-flex h-10 items-center rounded-2xl px-4 text-sm font-medium transition", repo.enabled ? "border border-slate-200 bg-white text-slate-700 hover:bg-slate-100" : "bg-slate-950 text-white hover:opacity-90")}
|
|
696
|
+
>
|
|
697
|
+
{repo.enabled ? "禁用" : "启用"}
|
|
698
|
+
</button>
|
|
699
|
+
</div>
|
|
700
|
+
</article>
|
|
701
|
+
))}
|
|
702
|
+
</div>
|
|
703
|
+
</div>
|
|
704
|
+
)}
|
|
705
|
+
</section>
|
|
706
|
+
|
|
707
|
+
<aside className="space-y-4">
|
|
708
|
+
<section className="rounded-[28px] border border-slate-200/80 bg-white p-5 shadow-[0_24px_70px_rgba(15,23,42,0.06)]">
|
|
709
|
+
<div className="text-[12px] uppercase tracking-[0.24em] text-slate-400">总览</div>
|
|
710
|
+
<div className="mt-4 grid grid-cols-2 gap-3">
|
|
711
|
+
<div className="rounded-2xl bg-slate-50 px-4 py-4 ring-1 ring-slate-200/70">
|
|
712
|
+
<div className="text-[11px] uppercase tracking-[0.22em] text-slate-400">{activeSection === "providers" ? "服务商" : activeSection === "mcp" ? "MCP" : activeSection === "prompts" ? "提示词" : "技能"}</div>
|
|
713
|
+
<div className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-slate-950">{activeSection === "providers" ? providers.length : activeSection === "mcp" ? mcpServers.length : activeSection === "prompts" ? prompts.length : skills.length}</div>
|
|
714
|
+
</div>
|
|
715
|
+
<div className="rounded-2xl bg-slate-50 px-4 py-4 ring-1 ring-slate-200/70">
|
|
716
|
+
<div className="text-[11px] uppercase tracking-[0.22em] text-slate-400">{activeSection === "skills" ? "仓库" : "目标文件"}</div>
|
|
717
|
+
<div className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-slate-950">{activeSection === "skills" ? skillRepos.length : platform.targetFiles.length}</div>
|
|
718
|
+
</div>
|
|
719
|
+
<div className="col-span-2 rounded-2xl bg-slate-50 px-4 py-4 ring-1 ring-slate-200/70">
|
|
720
|
+
<div className="text-[11px] uppercase tracking-[0.22em] text-slate-400">{activeSection === "providers" ? "当前路由" : "当前工作区"}</div>
|
|
721
|
+
<div className="mt-2 text-sm font-medium text-slate-950">{activeSection === "providers" ? platform.currentProviderName || "无" : `${platform.label} / ${workspace.activeSectionMeta.label}`}</div>
|
|
722
|
+
<div className="mt-1 text-xs text-slate-400">快照更新时间:{timeText(workspace.snapshot.generatedAt)}</div>
|
|
723
|
+
</div>
|
|
724
|
+
</div>
|
|
725
|
+
</section>
|
|
726
|
+
|
|
727
|
+
<section className="rounded-[28px] border border-slate-200/80 bg-white p-5 shadow-[0_24px_70px_rgba(15,23,42,0.06)]">
|
|
728
|
+
<div className="flex items-center justify-between gap-3">
|
|
729
|
+
<div>
|
|
730
|
+
<div className="text-[12px] uppercase tracking-[0.24em] text-slate-400">链路健康度</div>
|
|
731
|
+
<div className="mt-2 text-[22px] font-semibold tracking-[-0.04em] text-slate-950">延迟检测</div>
|
|
732
|
+
</div>
|
|
733
|
+
<button
|
|
734
|
+
type="button"
|
|
735
|
+
onClick={() => workspace.probeQuery.refetch()}
|
|
736
|
+
className="inline-flex h-10 items-center gap-2 rounded-2xl border border-slate-200 px-4 text-sm font-medium text-slate-700 transition hover:border-slate-300 hover:bg-slate-50"
|
|
737
|
+
>
|
|
738
|
+
<RefreshCw className={cx("h-4 w-4", workspace.probeQuery.isFetching && "animate-spin")} />测试
|
|
739
|
+
</button>
|
|
740
|
+
</div>
|
|
741
|
+
<div className="mt-4 space-y-3">
|
|
742
|
+
{probes.length > 0 ? (
|
|
743
|
+
probes.map((item) => (
|
|
744
|
+
<div key={item.baseUrl} className="rounded-2xl bg-slate-50 px-4 py-4 ring-1 ring-slate-200/70">
|
|
745
|
+
<div className="flex items-center justify-between gap-3">
|
|
746
|
+
<div className="min-w-0">
|
|
747
|
+
<div className="truncate text-sm font-medium text-slate-900">{shortUrl(item.baseUrl)}</div>
|
|
748
|
+
<div className="mt-1 text-xs text-slate-400">{item.ok ? "可连通" : item.error ? `不可用:${item.error}` : "不可用"}</div>
|
|
749
|
+
</div>
|
|
750
|
+
<div className="text-sm font-semibold text-[#1677ff]">{item.latency} ms</div>
|
|
751
|
+
</div>
|
|
752
|
+
</div>
|
|
753
|
+
))
|
|
754
|
+
) : (
|
|
755
|
+
<div className="rounded-2xl border border-dashed border-slate-200 px-4 py-6 text-sm leading-6 text-slate-500">执行一次测速后,这里会展示当前工作区的链路延迟结果。</div>
|
|
756
|
+
)}
|
|
757
|
+
</div>
|
|
758
|
+
</section>
|
|
759
|
+
|
|
760
|
+
<section className="rounded-[28px] border border-slate-200/80 bg-white p-5 shadow-[0_24px_70px_rgba(15,23,42,0.06)]">
|
|
761
|
+
<div className="text-[12px] uppercase tracking-[0.24em] text-slate-400">目标文件</div>
|
|
762
|
+
<div className="mt-4 space-y-3">
|
|
763
|
+
{platform.targetFiles.map((target) => (
|
|
764
|
+
<div key={target} className="rounded-2xl bg-slate-50 px-4 py-4 ring-1 ring-slate-200/70">
|
|
765
|
+
<div className="flex items-start justify-between gap-3">
|
|
766
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
767
|
+
<FileStack className="mt-0.5 h-4 w-4 shrink-0 text-slate-400" />
|
|
768
|
+
<div className="min-w-0 break-all text-sm leading-6 text-slate-600">{target}</div>
|
|
769
|
+
</div>
|
|
770
|
+
<button type="button" onClick={() => openTargetPath(target)} className="inline-flex h-9 shrink-0 items-center rounded-xl border border-slate-200 bg-white px-3 text-xs font-medium text-slate-600 transition hover:border-slate-300 hover:text-slate-900">
|
|
771
|
+
打开
|
|
772
|
+
</button>
|
|
773
|
+
</div>
|
|
774
|
+
</div>
|
|
775
|
+
))}
|
|
776
|
+
{targetPathMessage ? <div className="text-xs text-slate-500">{targetPathMessage}</div> : null}
|
|
777
|
+
</div>
|
|
778
|
+
</section>
|
|
779
|
+
</aside>
|
|
780
|
+
</main>
|
|
781
|
+
</div>
|
|
782
|
+
|
|
783
|
+
{dialogOpen ? (
|
|
784
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/20 px-4 backdrop-blur-[3px]">
|
|
785
|
+
<div className="w-full max-w-[560px] rounded-[30px] bg-white p-7 shadow-[0_44px_120px_rgba(15,23,42,0.18)] ring-1 ring-slate-200/80">
|
|
786
|
+
<div className="flex items-start justify-between gap-4">
|
|
787
|
+
<div>
|
|
788
|
+
<div className={cx("inline-flex rounded-full px-3 py-1 text-xs font-medium", styles.soft)}>{editing ? "编辑服务商" : "新增服务商"}</div>
|
|
789
|
+
<h2 className="mt-3 text-[28px] font-semibold tracking-[-0.05em] text-slate-950">{platform.label} 路由编辑器</h2>
|
|
790
|
+
<p className="mt-2 text-sm leading-6 text-slate-500">这里的改动会写回当前工作区配置,并立即刷新列表。</p>
|
|
791
|
+
</div>
|
|
792
|
+
<button type="button" onClick={closeDialog} className="rounded-full p-2 text-slate-400 transition hover:bg-slate-100 hover:text-slate-700">
|
|
793
|
+
<X className="h-4 w-4" />
|
|
794
|
+
</button>
|
|
795
|
+
</div>
|
|
796
|
+
|
|
797
|
+
<form
|
|
798
|
+
className="mt-6 space-y-4"
|
|
799
|
+
onSubmit={(event) => {
|
|
800
|
+
event.preventDefault();
|
|
801
|
+
submit();
|
|
802
|
+
}}
|
|
803
|
+
>
|
|
804
|
+
<label className="block">
|
|
805
|
+
<div className="mb-2 text-sm font-medium text-slate-600">服务商名称</div>
|
|
806
|
+
<input
|
|
807
|
+
value={workspace.editorForm.name}
|
|
808
|
+
onChange={(event) => workspace.setEditorForm({ ...workspace.editorForm, name: event.target.value })}
|
|
809
|
+
placeholder="default"
|
|
810
|
+
className="h-12 w-full rounded-2xl border border-slate-200 bg-[#fbfbfc] px-4 text-sm text-slate-900 outline-none transition focus:border-[#1677ff] focus:bg-white"
|
|
811
|
+
/>
|
|
812
|
+
</label>
|
|
813
|
+
<label className="block">
|
|
814
|
+
<div className="mb-2 text-sm font-medium text-slate-600">基础地址</div>
|
|
815
|
+
<input
|
|
816
|
+
value={workspace.editorForm.baseUrl}
|
|
817
|
+
onChange={(event) => workspace.setEditorForm({ ...workspace.editorForm, baseUrl: event.target.value })}
|
|
818
|
+
placeholder="https://api.openai.com"
|
|
819
|
+
className="h-12 w-full rounded-2xl border border-slate-200 bg-[#fbfbfc] px-4 text-sm text-slate-900 outline-none transition focus:border-[#1677ff] focus:bg-white"
|
|
820
|
+
/>
|
|
821
|
+
<div className="mt-3 flex items-center justify-between gap-3">
|
|
822
|
+
<div className="text-xs text-slate-500">{candidateProbeText || "可先测试地址可用性,再保存。"}</div>
|
|
823
|
+
<button
|
|
824
|
+
type="button"
|
|
825
|
+
onClick={probeCandidateBaseUrl}
|
|
826
|
+
disabled={workspace.probeCandidateMutation.isPending}
|
|
827
|
+
className="inline-flex h-9 items-center rounded-xl border border-slate-200 bg-white px-3 text-xs font-medium text-slate-600 transition hover:border-slate-300 hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-60"
|
|
828
|
+
>
|
|
829
|
+
{workspace.probeCandidateMutation.isPending ? "测试中..." : "测试地址"}
|
|
830
|
+
</button>
|
|
831
|
+
</div>
|
|
832
|
+
</label>
|
|
833
|
+
<label className="block">
|
|
834
|
+
<div className="mb-2 text-sm font-medium text-slate-600">API Key</div>
|
|
835
|
+
<input
|
|
836
|
+
type="password"
|
|
837
|
+
value={workspace.editorForm.apiKey}
|
|
838
|
+
onChange={(event) => workspace.setEditorForm({ ...workspace.editorForm, apiKey: event.target.value })}
|
|
839
|
+
placeholder="sk-..."
|
|
840
|
+
className="h-12 w-full rounded-2xl border border-slate-200 bg-[#fbfbfc] px-4 text-sm text-slate-900 outline-none transition focus:border-[#1677ff] focus:bg-white"
|
|
841
|
+
/>
|
|
842
|
+
</label>
|
|
843
|
+
<label className="block">
|
|
844
|
+
<div className="mb-2 text-sm font-medium text-slate-600">模型</div>
|
|
845
|
+
<select
|
|
846
|
+
value={workspace.editorForm.model}
|
|
847
|
+
onChange={(event) => workspace.setEditorForm({ ...workspace.editorForm, model: event.target.value })}
|
|
848
|
+
className="h-12 w-full rounded-2xl border border-slate-200 bg-[#fbfbfc] px-4 text-sm text-slate-900 outline-none transition focus:border-[#1677ff] focus:bg-white"
|
|
849
|
+
>
|
|
850
|
+
{models.map((model) => (
|
|
851
|
+
<option key={model} value={model}>
|
|
852
|
+
{model}
|
|
853
|
+
</option>
|
|
854
|
+
))}
|
|
855
|
+
</select>
|
|
856
|
+
</label>
|
|
857
|
+
<div className="flex items-center justify-end gap-3 pt-3">
|
|
858
|
+
<button type="button" onClick={closeDialog} className="inline-flex h-11 items-center justify-center rounded-2xl border border-slate-200 px-5 text-sm font-medium text-slate-600 transition hover:border-slate-300 hover:text-slate-900">
|
|
859
|
+
取消
|
|
860
|
+
</button>
|
|
861
|
+
<button
|
|
862
|
+
type="submit"
|
|
863
|
+
disabled={workspace.saveMutation.isPending}
|
|
864
|
+
className="inline-flex h-11 items-center justify-center rounded-2xl bg-[#1677ff] px-5 text-sm font-medium text-white shadow-[0_12px_24px_rgba(22,119,255,0.22)] transition hover:brightness-105 disabled:cursor-not-allowed disabled:opacity-60"
|
|
865
|
+
>
|
|
866
|
+
{workspace.saveMutation.isPending ? "保存中..." : "保存并启用"}
|
|
867
|
+
</button>
|
|
868
|
+
</div>
|
|
869
|
+
</form>
|
|
870
|
+
</div>
|
|
871
|
+
</div>
|
|
872
|
+
) : null}
|
|
873
|
+
|
|
874
|
+
{deleteConfirm ? (
|
|
875
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/20 px-4 backdrop-blur-[3px]">
|
|
876
|
+
<div className="w-full max-w-[520px] rounded-[30px] bg-white p-7 shadow-[0_44px_120px_rgba(15,23,42,0.18)] ring-1 ring-slate-200/80">
|
|
877
|
+
<div className="flex items-start justify-between gap-4">
|
|
878
|
+
<div>
|
|
879
|
+
<div className="inline-flex rounded-full bg-rose-50 px-3 py-1 text-xs font-medium text-rose-700">删除确认</div>
|
|
880
|
+
<h2 className="mt-3 text-[28px] font-semibold tracking-[-0.05em] text-slate-950">
|
|
881
|
+
删除{deleteConfirm.kind === "mcp" ? "MCP" : deleteConfirm.kind === "prompt" ? "提示词" : "技能"}
|
|
882
|
+
</h2>
|
|
883
|
+
<p className="mt-2 text-sm leading-6 text-slate-500">
|
|
884
|
+
将删除 <span className="font-medium text-slate-900">{deleteConfirm.name}</span>。此操作不可撤销。
|
|
885
|
+
</p>
|
|
886
|
+
</div>
|
|
887
|
+
<button type="button" onClick={closeDeleteConfirm} className="rounded-full p-2 text-slate-400 transition hover:bg-slate-100 hover:text-slate-700">
|
|
888
|
+
<X className="h-4 w-4" />
|
|
889
|
+
</button>
|
|
890
|
+
</div>
|
|
891
|
+
|
|
892
|
+
<div className="mt-6 flex items-center justify-end gap-3">
|
|
893
|
+
<button
|
|
894
|
+
type="button"
|
|
895
|
+
onClick={closeDeleteConfirm}
|
|
896
|
+
disabled={deleteSubmitPending}
|
|
897
|
+
className="inline-flex h-11 items-center justify-center rounded-2xl border border-slate-200 px-5 text-sm font-medium text-slate-600 transition hover:border-slate-300 hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-60"
|
|
898
|
+
>
|
|
899
|
+
取消
|
|
900
|
+
</button>
|
|
901
|
+
<button
|
|
902
|
+
type="button"
|
|
903
|
+
onClick={confirmDelete}
|
|
904
|
+
disabled={deleteSubmitPending}
|
|
905
|
+
className="inline-flex h-11 items-center justify-center rounded-2xl bg-rose-600 px-5 text-sm font-medium text-white shadow-[0_12px_24px_rgba(225,29,72,0.24)] transition hover:bg-rose-500 disabled:cursor-not-allowed disabled:opacity-60"
|
|
906
|
+
>
|
|
907
|
+
{deleteSubmitPending ? "删除中..." : "确认删除"}
|
|
908
|
+
</button>
|
|
909
|
+
</div>
|
|
910
|
+
</div>
|
|
911
|
+
</div>
|
|
912
|
+
) : null}
|
|
913
|
+
|
|
914
|
+
{resourceEditor ? (
|
|
915
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/20 px-4 backdrop-blur-[3px]">
|
|
916
|
+
<div className="w-full max-w-[620px] rounded-[30px] bg-white p-7 shadow-[0_44px_120px_rgba(15,23,42,0.18)] ring-1 ring-slate-200/80">
|
|
917
|
+
<div className="flex items-start justify-between gap-4">
|
|
918
|
+
<div>
|
|
919
|
+
<div className={cx("inline-flex rounded-full px-3 py-1 text-xs font-medium", styles.soft)}>
|
|
920
|
+
{resourceEditor.kind === "mcp" ? "MCP 编辑" : resourceEditor.kind === "prompt" ? "提示词编辑" : "技能编辑"}
|
|
921
|
+
</div>
|
|
922
|
+
<h2 className="mt-3 text-[28px] font-semibold tracking-[-0.05em] text-slate-950">
|
|
923
|
+
{resourceEditor.kind === "mcp" ? "MCP 配置" : resourceEditor.kind === "prompt" ? "提示词配置" : "技能配置"}
|
|
924
|
+
</h2>
|
|
925
|
+
<p className="mt-2 text-sm leading-6 text-slate-500">修改后会直接写入当前工作区配置,并刷新当前列表。</p>
|
|
926
|
+
</div>
|
|
927
|
+
<button type="button" onClick={closeResourceEditor} className="rounded-full p-2 text-slate-400 transition hover:bg-slate-100 hover:text-slate-700">
|
|
928
|
+
<X className="h-4 w-4" />
|
|
929
|
+
</button>
|
|
930
|
+
</div>
|
|
931
|
+
|
|
932
|
+
<form
|
|
933
|
+
className="mt-6 space-y-4"
|
|
934
|
+
onSubmit={(event) => {
|
|
935
|
+
event.preventDefault();
|
|
936
|
+
submitResourceEditor();
|
|
937
|
+
}}
|
|
938
|
+
>
|
|
939
|
+
{resourceEditor.kind === "mcp" ? (
|
|
940
|
+
<>
|
|
941
|
+
<label className="block">
|
|
942
|
+
<div className="mb-2 text-sm font-medium text-slate-600">MCP 名称</div>
|
|
943
|
+
<input
|
|
944
|
+
value={resourceEditor.name}
|
|
945
|
+
onChange={(event) =>
|
|
946
|
+
setResourceEditor((current) =>
|
|
947
|
+
current && current.kind === "mcp" ? { ...current, name: event.target.value, error: null } : current
|
|
948
|
+
)
|
|
949
|
+
}
|
|
950
|
+
placeholder="例如:filesystem"
|
|
951
|
+
className="h-12 w-full rounded-2xl border border-slate-200 bg-[#fbfbfc] px-4 text-sm text-slate-900 outline-none transition focus:border-[#1677ff] focus:bg-white"
|
|
952
|
+
/>
|
|
953
|
+
</label>
|
|
954
|
+
<label className="block">
|
|
955
|
+
<div className="mb-2 text-sm font-medium text-slate-600">描述</div>
|
|
956
|
+
<textarea
|
|
957
|
+
value={resourceEditor.description}
|
|
958
|
+
onChange={(event) =>
|
|
959
|
+
setResourceEditor((current) =>
|
|
960
|
+
current && current.kind === "mcp" ? { ...current, description: event.target.value, error: null } : current
|
|
961
|
+
)
|
|
962
|
+
}
|
|
963
|
+
rows={3}
|
|
964
|
+
className="w-full rounded-2xl border border-slate-200 bg-[#fbfbfc] px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-[#1677ff] focus:bg-white"
|
|
965
|
+
/>
|
|
966
|
+
</label>
|
|
967
|
+
<label className="block">
|
|
968
|
+
<div className="mb-2 text-sm font-medium text-slate-600">标签(逗号分隔)</div>
|
|
969
|
+
<input
|
|
970
|
+
value={resourceEditor.tags}
|
|
971
|
+
onChange={(event) =>
|
|
972
|
+
setResourceEditor((current) =>
|
|
973
|
+
current && current.kind === "mcp" ? { ...current, tags: event.target.value, error: null } : current
|
|
974
|
+
)
|
|
975
|
+
}
|
|
976
|
+
placeholder="files, docs"
|
|
977
|
+
className="h-12 w-full rounded-2xl border border-slate-200 bg-[#fbfbfc] px-4 text-sm text-slate-900 outline-none transition focus:border-[#1677ff] focus:bg-white"
|
|
978
|
+
/>
|
|
979
|
+
</label>
|
|
980
|
+
<label className="block">
|
|
981
|
+
<div className="mb-2 text-sm font-medium text-slate-600">主页链接(可选)</div>
|
|
982
|
+
<input
|
|
983
|
+
value={resourceEditor.homepage}
|
|
984
|
+
onChange={(event) =>
|
|
985
|
+
setResourceEditor((current) =>
|
|
986
|
+
current && current.kind === "mcp" ? { ...current, homepage: event.target.value, error: null } : current
|
|
987
|
+
)
|
|
988
|
+
}
|
|
989
|
+
placeholder="https://example.com"
|
|
990
|
+
className="h-12 w-full rounded-2xl border border-slate-200 bg-[#fbfbfc] px-4 text-sm text-slate-900 outline-none transition focus:border-[#1677ff] focus:bg-white"
|
|
991
|
+
/>
|
|
992
|
+
</label>
|
|
993
|
+
<div>
|
|
994
|
+
<div className="mb-2 text-sm font-medium text-slate-600">启用平台</div>
|
|
995
|
+
<div className="flex flex-wrap gap-2">
|
|
996
|
+
{allPlatforms.map((item) => {
|
|
997
|
+
const checked = resourceEditor.enabledPlatforms.includes(item);
|
|
998
|
+
return (
|
|
999
|
+
<button
|
|
1000
|
+
key={item}
|
|
1001
|
+
type="button"
|
|
1002
|
+
onClick={() => toggleResourcePlatform(item)}
|
|
1003
|
+
className={cx(
|
|
1004
|
+
"inline-flex h-10 items-center rounded-2xl px-4 text-sm font-medium transition",
|
|
1005
|
+
checked ? "bg-slate-950 text-white" : "border border-slate-200 bg-white text-slate-700 hover:bg-slate-100"
|
|
1006
|
+
)}
|
|
1007
|
+
>
|
|
1008
|
+
{platformLabel(item)}
|
|
1009
|
+
</button>
|
|
1010
|
+
);
|
|
1011
|
+
})}
|
|
1012
|
+
</div>
|
|
1013
|
+
</div>
|
|
1014
|
+
</>
|
|
1015
|
+
) : resourceEditor.kind === "prompt" ? (
|
|
1016
|
+
<>
|
|
1017
|
+
<label className="block">
|
|
1018
|
+
<div className="mb-2 text-sm font-medium text-slate-600">提示词名称</div>
|
|
1019
|
+
<input
|
|
1020
|
+
value={resourceEditor.name}
|
|
1021
|
+
onChange={(event) =>
|
|
1022
|
+
setResourceEditor((current) =>
|
|
1023
|
+
current && current.kind === "prompt" ? { ...current, name: event.target.value, error: null } : current
|
|
1024
|
+
)
|
|
1025
|
+
}
|
|
1026
|
+
placeholder="例如:默认编码"
|
|
1027
|
+
className="h-12 w-full rounded-2xl border border-slate-200 bg-[#fbfbfc] px-4 text-sm text-slate-900 outline-none transition focus:border-[#1677ff] focus:bg-white"
|
|
1028
|
+
/>
|
|
1029
|
+
</label>
|
|
1030
|
+
<label className="block">
|
|
1031
|
+
<div className="mb-2 text-sm font-medium text-slate-600">描述</div>
|
|
1032
|
+
<textarea
|
|
1033
|
+
value={resourceEditor.description}
|
|
1034
|
+
onChange={(event) =>
|
|
1035
|
+
setResourceEditor((current) =>
|
|
1036
|
+
current && current.kind === "prompt" ? { ...current, description: event.target.value, error: null } : current
|
|
1037
|
+
)
|
|
1038
|
+
}
|
|
1039
|
+
rows={3}
|
|
1040
|
+
className="w-full rounded-2xl border border-slate-200 bg-[#fbfbfc] px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-[#1677ff] focus:bg-white"
|
|
1041
|
+
/>
|
|
1042
|
+
</label>
|
|
1043
|
+
<label className="block">
|
|
1044
|
+
<div className="mb-2 text-sm font-medium text-slate-600">提示词内容</div>
|
|
1045
|
+
<textarea
|
|
1046
|
+
value={resourceEditor.content}
|
|
1047
|
+
onChange={(event) =>
|
|
1048
|
+
setResourceEditor((current) =>
|
|
1049
|
+
current && current.kind === "prompt" ? { ...current, content: event.target.value, error: null } : current
|
|
1050
|
+
)
|
|
1051
|
+
}
|
|
1052
|
+
rows={6}
|
|
1053
|
+
className="w-full rounded-2xl border border-slate-200 bg-[#fbfbfc] px-4 py-3 text-sm leading-7 text-slate-900 outline-none transition focus:border-[#1677ff] focus:bg-white"
|
|
1054
|
+
/>
|
|
1055
|
+
</label>
|
|
1056
|
+
<label className="block">
|
|
1057
|
+
<div className="mb-2 text-sm font-medium text-slate-600">作用范围</div>
|
|
1058
|
+
<select
|
|
1059
|
+
value={resourceEditor.appType}
|
|
1060
|
+
onChange={(event) =>
|
|
1061
|
+
setResourceEditor((current) => {
|
|
1062
|
+
if (!current || current.kind !== "prompt") return current;
|
|
1063
|
+
const value = event.target.value as "global" | PlatformId;
|
|
1064
|
+
return { ...current, appType: value, error: null };
|
|
1065
|
+
})
|
|
1066
|
+
}
|
|
1067
|
+
className="h-12 w-full rounded-2xl border border-slate-200 bg-[#fbfbfc] px-4 text-sm text-slate-900 outline-none transition focus:border-[#1677ff] focus:bg-white"
|
|
1068
|
+
>
|
|
1069
|
+
<option value="global">全局</option>
|
|
1070
|
+
{allPlatforms.map((item) => (
|
|
1071
|
+
<option key={item} value={item}>
|
|
1072
|
+
{platformLabel(item)}
|
|
1073
|
+
</option>
|
|
1074
|
+
))}
|
|
1075
|
+
</select>
|
|
1076
|
+
</label>
|
|
1077
|
+
<label className="flex items-center gap-3 rounded-2xl border border-slate-200 bg-[#fbfbfc] px-4 py-3">
|
|
1078
|
+
<input
|
|
1079
|
+
type="checkbox"
|
|
1080
|
+
checked={resourceEditor.enabled}
|
|
1081
|
+
onChange={(event) =>
|
|
1082
|
+
setResourceEditor((current) =>
|
|
1083
|
+
current && current.kind === "prompt" ? { ...current, enabled: event.target.checked, error: null } : current
|
|
1084
|
+
)
|
|
1085
|
+
}
|
|
1086
|
+
className="h-4 w-4 rounded border-slate-300 text-[#1677ff] focus:ring-[#1677ff]"
|
|
1087
|
+
/>
|
|
1088
|
+
<span className="text-sm text-slate-700">保存后立即启用</span>
|
|
1089
|
+
</label>
|
|
1090
|
+
</>
|
|
1091
|
+
) : (
|
|
1092
|
+
<>
|
|
1093
|
+
<label className="block">
|
|
1094
|
+
<div className="mb-2 text-sm font-medium text-slate-600">技能名称</div>
|
|
1095
|
+
<input
|
|
1096
|
+
value={resourceEditor.name}
|
|
1097
|
+
onChange={(event) =>
|
|
1098
|
+
setResourceEditor((current) =>
|
|
1099
|
+
current && current.kind === "skill" ? { ...current, name: event.target.value, error: null } : current
|
|
1100
|
+
)
|
|
1101
|
+
}
|
|
1102
|
+
placeholder="例如:openai-docs"
|
|
1103
|
+
className="h-12 w-full rounded-2xl border border-slate-200 bg-[#fbfbfc] px-4 text-sm text-slate-900 outline-none transition focus:border-[#1677ff] focus:bg-white"
|
|
1104
|
+
/>
|
|
1105
|
+
</label>
|
|
1106
|
+
<label className="block">
|
|
1107
|
+
<div className="mb-2 text-sm font-medium text-slate-600">描述</div>
|
|
1108
|
+
<textarea
|
|
1109
|
+
value={resourceEditor.description}
|
|
1110
|
+
onChange={(event) =>
|
|
1111
|
+
setResourceEditor((current) =>
|
|
1112
|
+
current && current.kind === "skill" ? { ...current, description: event.target.value, error: null } : current
|
|
1113
|
+
)
|
|
1114
|
+
}
|
|
1115
|
+
rows={3}
|
|
1116
|
+
className="w-full rounded-2xl border border-slate-200 bg-[#fbfbfc] px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-[#1677ff] focus:bg-white"
|
|
1117
|
+
/>
|
|
1118
|
+
</label>
|
|
1119
|
+
<label className="block">
|
|
1120
|
+
<div className="mb-2 text-sm font-medium text-slate-600">技能目录</div>
|
|
1121
|
+
<input
|
|
1122
|
+
value={resourceEditor.directory}
|
|
1123
|
+
onChange={(event) =>
|
|
1124
|
+
setResourceEditor((current) =>
|
|
1125
|
+
current && current.kind === "skill" ? { ...current, directory: event.target.value, error: null } : current
|
|
1126
|
+
)
|
|
1127
|
+
}
|
|
1128
|
+
placeholder="~/.codex/skills/..."
|
|
1129
|
+
className="h-12 w-full rounded-2xl border border-slate-200 bg-[#fbfbfc] px-4 text-sm text-slate-900 outline-none transition focus:border-[#1677ff] focus:bg-white"
|
|
1130
|
+
/>
|
|
1131
|
+
</label>
|
|
1132
|
+
<div>
|
|
1133
|
+
<div className="mb-2 text-sm font-medium text-slate-600">启用平台</div>
|
|
1134
|
+
<div className="flex flex-wrap gap-2">
|
|
1135
|
+
{allPlatforms.map((item) => {
|
|
1136
|
+
const checked = resourceEditor.enabledPlatforms.includes(item);
|
|
1137
|
+
return (
|
|
1138
|
+
<button
|
|
1139
|
+
key={item}
|
|
1140
|
+
type="button"
|
|
1141
|
+
onClick={() => toggleResourcePlatform(item)}
|
|
1142
|
+
className={cx(
|
|
1143
|
+
"inline-flex h-10 items-center rounded-2xl px-4 text-sm font-medium transition",
|
|
1144
|
+
checked ? "bg-slate-950 text-white" : "border border-slate-200 bg-white text-slate-700 hover:bg-slate-100"
|
|
1145
|
+
)}
|
|
1146
|
+
>
|
|
1147
|
+
{platformLabel(item)}
|
|
1148
|
+
</button>
|
|
1149
|
+
);
|
|
1150
|
+
})}
|
|
1151
|
+
</div>
|
|
1152
|
+
</div>
|
|
1153
|
+
</>
|
|
1154
|
+
)}
|
|
1155
|
+
|
|
1156
|
+
{resourceEditor.error ? <div className="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">{resourceEditor.error}</div> : null}
|
|
1157
|
+
|
|
1158
|
+
<div className="flex items-center justify-end gap-3 pt-3">
|
|
1159
|
+
<button
|
|
1160
|
+
type="button"
|
|
1161
|
+
onClick={closeResourceEditor}
|
|
1162
|
+
className="inline-flex h-11 items-center justify-center rounded-2xl border border-slate-200 px-5 text-sm font-medium text-slate-600 transition hover:border-slate-300 hover:text-slate-900"
|
|
1163
|
+
>
|
|
1164
|
+
取消
|
|
1165
|
+
</button>
|
|
1166
|
+
<button
|
|
1167
|
+
type="submit"
|
|
1168
|
+
disabled={resourceSubmitPending}
|
|
1169
|
+
className="inline-flex h-11 items-center justify-center rounded-2xl bg-[#1677ff] px-5 text-sm font-medium text-white shadow-[0_12px_24px_rgba(22,119,255,0.22)] transition hover:brightness-105 disabled:cursor-not-allowed disabled:opacity-60"
|
|
1170
|
+
>
|
|
1171
|
+
{resourceSubmitPending
|
|
1172
|
+
? "保存中..."
|
|
1173
|
+
: resourceEditor.kind === "mcp"
|
|
1174
|
+
? "保存 MCP"
|
|
1175
|
+
: resourceEditor.kind === "prompt"
|
|
1176
|
+
? "保存提示词"
|
|
1177
|
+
: "保存技能"}
|
|
1178
|
+
</button>
|
|
1179
|
+
</div>
|
|
1180
|
+
</form>
|
|
1181
|
+
</div>
|
|
1182
|
+
</div>
|
|
1183
|
+
) : null}
|
|
1184
|
+
</div>
|
|
1185
|
+
);
|
|
1186
|
+
}
|