@geminilight/mindos 0.5.8 → 0.5.10
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 +9 -10
- package/README_zh.md +8 -9
- package/app/app/api/mcp/agents/route.ts +7 -0
- package/app/app/api/mcp/install-skill/route.ts +6 -0
- package/app/app/api/setup/check-port/route.ts +27 -3
- package/app/app/api/setup/route.ts +2 -9
- package/app/app/api/skills/route.ts +1 -1
- package/app/app/globals.css +28 -4
- package/app/app/login/page.tsx +2 -2
- package/app/app/view/[...path]/ViewPageClient.tsx +15 -10
- package/app/app/view/[...path]/not-found.tsx +1 -1
- package/app/components/AskModal.tsx +5 -5
- package/app/components/Breadcrumb.tsx +2 -2
- package/app/components/DirView.tsx +6 -6
- package/app/components/FileTree.tsx +7 -7
- package/app/components/HomeContent.tsx +8 -8
- package/app/components/OnboardingView.tsx +1 -1
- package/app/components/SearchModal.tsx +1 -1
- package/app/components/SettingsModal.tsx +2 -2
- package/app/components/SetupWizard.tsx +1 -1258
- package/app/components/Sidebar.tsx +4 -4
- package/app/components/SidebarLayout.tsx +9 -0
- package/app/components/SyncStatusBar.tsx +6 -6
- package/app/components/TableOfContents.tsx +1 -1
- package/app/components/UpdateBanner.tsx +1 -1
- package/app/components/ask/FileChip.tsx +1 -1
- package/app/components/ask/MentionPopover.tsx +4 -4
- package/app/components/ask/MessageList.tsx +3 -3
- package/app/components/ask/SessionHistory.tsx +3 -3
- package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +5 -5
- package/app/components/renderers/config/ConfigRenderer.tsx +4 -4
- package/app/components/renderers/csv/BoardView.tsx +2 -2
- package/app/components/renderers/csv/ConfigPanel.tsx +5 -5
- package/app/components/renderers/csv/GalleryView.tsx +1 -1
- package/app/components/renderers/csv/types.ts +1 -1
- package/app/components/renderers/diff/DiffRenderer.tsx +9 -9
- package/app/components/renderers/graph/GraphRenderer.tsx +1 -1
- package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
- package/app/components/renderers/timeline/TimelineRenderer.tsx +1 -1
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +4 -4
- package/app/components/settings/KnowledgeTab.tsx +1 -1
- package/app/components/settings/McpTab.tsx +93 -47
- package/app/components/settings/PluginsTab.tsx +4 -4
- package/app/components/settings/Primitives.tsx +4 -4
- package/app/components/settings/SyncTab.tsx +13 -13
- package/app/components/setup/StepAI.tsx +67 -0
- package/app/components/setup/StepAgents.tsx +237 -0
- package/app/components/setup/StepDots.tsx +39 -0
- package/app/components/setup/StepKB.tsx +237 -0
- package/app/components/setup/StepPorts.tsx +121 -0
- package/app/components/setup/StepReview.tsx +211 -0
- package/app/components/setup/StepSecurity.tsx +78 -0
- package/app/components/setup/constants.tsx +13 -0
- package/app/components/setup/index.tsx +464 -0
- package/app/components/setup/types.ts +53 -0
- package/app/lib/i18n.ts +52 -8
- package/app/lib/mcp-agents.ts +81 -0
- package/bin/lib/gateway.js +44 -4
- package/bin/lib/mcp-agents.js +81 -0
- package/bin/lib/mcp-install.js +34 -4
- package/package.json +3 -1
- package/scripts/setup.js +43 -6
- package/skills/project-wiki/SKILL.md +92 -63
- package/app/public/landing/index.html +0 -353
- package/app/public/landing/style.css +0 -216
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { Sparkles, Loader2, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
5
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
6
|
+
import type { SetupState, PortStatus, AgentEntry, AgentInstallStatus } from './types';
|
|
7
|
+
import { TOTAL_STEPS, STEP_KB, STEP_PORTS, STEP_AGENTS } from './constants';
|
|
8
|
+
import StepKB from './StepKB';
|
|
9
|
+
import StepAI from './StepAI';
|
|
10
|
+
import StepPorts from './StepPorts';
|
|
11
|
+
import StepSecurity from './StepSecurity';
|
|
12
|
+
import StepAgents from './StepAgents';
|
|
13
|
+
import StepReview from './StepReview';
|
|
14
|
+
import StepDots from './StepDots';
|
|
15
|
+
|
|
16
|
+
// ─── Helpers (shared by handleComplete + retryAgent) ─────────────────────────
|
|
17
|
+
|
|
18
|
+
/** Build a single agent's install payload */
|
|
19
|
+
function buildAgentPayload(
|
|
20
|
+
key: string,
|
|
21
|
+
agents: AgentEntry[],
|
|
22
|
+
transport: 'auto' | 'stdio' | 'http',
|
|
23
|
+
scope: 'global' | 'project',
|
|
24
|
+
): { key: string; scope: string; transport: string } {
|
|
25
|
+
const agent = agents.find(a => a.key === key);
|
|
26
|
+
const effectiveTransport = transport === 'auto'
|
|
27
|
+
? (agent?.preferredTransport || 'stdio')
|
|
28
|
+
: transport;
|
|
29
|
+
return { key, scope, transport: effectiveTransport };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Parse a single install API result into AgentInstallStatus */
|
|
33
|
+
function parseInstallResult(
|
|
34
|
+
r: { agent: string; status: string; message?: string; transport?: string; verified?: boolean; verifyError?: string },
|
|
35
|
+
): AgentInstallStatus {
|
|
36
|
+
return {
|
|
37
|
+
state: r.status === 'ok' ? 'ok' : 'error',
|
|
38
|
+
message: r.message,
|
|
39
|
+
transport: r.transport,
|
|
40
|
+
verified: r.verified,
|
|
41
|
+
verifyError: r.verifyError,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Phase runners (pure async, no setState — results consumed by caller) ────
|
|
46
|
+
|
|
47
|
+
/** Phase 1: Save setup config. Returns whether restart is needed. Throws on failure. */
|
|
48
|
+
async function saveConfig(state: SetupState): Promise<boolean> {
|
|
49
|
+
const payload = {
|
|
50
|
+
mindRoot: state.mindRoot,
|
|
51
|
+
template: state.template || undefined,
|
|
52
|
+
port: state.webPort,
|
|
53
|
+
mcpPort: state.mcpPort,
|
|
54
|
+
authToken: state.authToken,
|
|
55
|
+
webPassword: state.webPassword,
|
|
56
|
+
ai: state.provider === 'skip' ? undefined : {
|
|
57
|
+
provider: state.provider,
|
|
58
|
+
providers: {
|
|
59
|
+
anthropic: { apiKey: state.anthropicKey, model: state.anthropicModel },
|
|
60
|
+
openai: { apiKey: state.openaiKey, model: state.openaiModel, baseUrl: state.openaiBaseUrl },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
const res = await fetch('/api/setup', {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/json' },
|
|
67
|
+
body: JSON.stringify(payload),
|
|
68
|
+
});
|
|
69
|
+
const data = await res.json();
|
|
70
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
71
|
+
return !!data.needsRestart;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Phase 2: Install selected agents. Returns status map. */
|
|
75
|
+
async function installAgents(
|
|
76
|
+
keys: string[],
|
|
77
|
+
agents: AgentEntry[],
|
|
78
|
+
transport: 'auto' | 'stdio' | 'http',
|
|
79
|
+
scope: 'global' | 'project',
|
|
80
|
+
mcpPort: number,
|
|
81
|
+
authToken: string,
|
|
82
|
+
): Promise<Record<string, AgentInstallStatus>> {
|
|
83
|
+
const agentsPayload = keys.map(k => buildAgentPayload(k, agents, transport, scope));
|
|
84
|
+
const res = await fetch('/api/mcp/install', {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { 'Content-Type': 'application/json' },
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
agents: agentsPayload,
|
|
89
|
+
transport,
|
|
90
|
+
url: `http://localhost:${mcpPort}/mcp`,
|
|
91
|
+
token: authToken || undefined,
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
const data = await res.json();
|
|
95
|
+
const updated: Record<string, AgentInstallStatus> = {};
|
|
96
|
+
if (data.results) {
|
|
97
|
+
for (const r of data.results as Array<{ agent: string; status: string; message?: string; transport?: string; verified?: boolean; verifyError?: string }>) {
|
|
98
|
+
updated[r.agent] = parseInstallResult(r);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return updated;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Phase 3: Install skill to agents. Returns result. */
|
|
105
|
+
async function installSkill(
|
|
106
|
+
template: string,
|
|
107
|
+
agentKeys: string[],
|
|
108
|
+
): Promise<{ ok?: boolean; skill?: string; error?: string }> {
|
|
109
|
+
const skillName = template === 'zh' ? 'mindos-zh' : 'mindos';
|
|
110
|
+
const res = await fetch('/api/mcp/install-skill', {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({ skill: skillName, agents: agentKeys }),
|
|
114
|
+
});
|
|
115
|
+
return await res.json();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Component ───────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
export default function SetupWizard() {
|
|
121
|
+
const { t } = useLocale();
|
|
122
|
+
const s = t.setup;
|
|
123
|
+
|
|
124
|
+
const [step, setStep] = useState(0);
|
|
125
|
+
const [state, setState] = useState<SetupState>({
|
|
126
|
+
mindRoot: '~/MindOS/mind',
|
|
127
|
+
template: 'en',
|
|
128
|
+
provider: 'anthropic',
|
|
129
|
+
anthropicKey: '',
|
|
130
|
+
anthropicModel: 'claude-sonnet-4-6',
|
|
131
|
+
openaiKey: '',
|
|
132
|
+
openaiModel: 'gpt-5.4',
|
|
133
|
+
openaiBaseUrl: '',
|
|
134
|
+
webPort: 3000,
|
|
135
|
+
mcpPort: 8787,
|
|
136
|
+
authToken: '',
|
|
137
|
+
webPassword: '',
|
|
138
|
+
});
|
|
139
|
+
const [homeDir, setHomeDir] = useState('~');
|
|
140
|
+
const [tokenCopied, setTokenCopied] = useState(false);
|
|
141
|
+
const [submitting, setSubmitting] = useState(false);
|
|
142
|
+
const [completed, setCompleted] = useState(false);
|
|
143
|
+
const [error, setError] = useState('');
|
|
144
|
+
const [needsRestart, setNeedsRestart] = useState(false);
|
|
145
|
+
|
|
146
|
+
const [webPortStatus, setWebPortStatus] = useState<PortStatus>({ checking: false, available: null, isSelf: false, suggestion: null });
|
|
147
|
+
const [mcpPortStatus, setMcpPortStatus] = useState<PortStatus>({ checking: false, available: null, isSelf: false, suggestion: null });
|
|
148
|
+
|
|
149
|
+
const [agents, setAgents] = useState<AgentEntry[]>([]);
|
|
150
|
+
const [agentsLoading, setAgentsLoading] = useState(false);
|
|
151
|
+
const [agentsLoaded, setAgentsLoaded] = useState(false);
|
|
152
|
+
const [selectedAgents, setSelectedAgents] = useState<Set<string>>(new Set());
|
|
153
|
+
const [agentTransport, setAgentTransport] = useState<'auto' | 'stdio' | 'http'>('auto');
|
|
154
|
+
const [agentScope, setAgentScope] = useState<'global' | 'project'>('global');
|
|
155
|
+
const [agentStatuses, setAgentStatuses] = useState<Record<string, AgentInstallStatus>>({});
|
|
156
|
+
const [skillInstallResult, setSkillInstallResult] = useState<{ ok?: boolean; skill?: string; error?: string } | null>(null);
|
|
157
|
+
const [setupPhase, setSetupPhase] = useState<'review' | 'saving' | 'agents' | 'skill' | 'done'>('review');
|
|
158
|
+
|
|
159
|
+
// Load existing config as defaults on mount, generate token if none exists
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
fetch('/api/setup')
|
|
162
|
+
.then(r => r.json())
|
|
163
|
+
.then(data => {
|
|
164
|
+
if (data.homeDir) setHomeDir(data.homeDir);
|
|
165
|
+
setState(prev => ({
|
|
166
|
+
...prev,
|
|
167
|
+
mindRoot: data.mindRoot || prev.mindRoot,
|
|
168
|
+
webPort: typeof data.port === 'number' ? data.port : prev.webPort,
|
|
169
|
+
mcpPort: typeof data.mcpPort === 'number' ? data.mcpPort : prev.mcpPort,
|
|
170
|
+
authToken: data.authToken || prev.authToken,
|
|
171
|
+
webPassword: data.webPassword || prev.webPassword,
|
|
172
|
+
provider: (data.provider === 'anthropic' || data.provider === 'openai') ? data.provider : prev.provider,
|
|
173
|
+
anthropicModel: data.anthropicModel || prev.anthropicModel,
|
|
174
|
+
openaiModel: data.openaiModel || prev.openaiModel,
|
|
175
|
+
openaiBaseUrl: data.openaiBaseUrl ?? prev.openaiBaseUrl,
|
|
176
|
+
}));
|
|
177
|
+
// Generate a new token only if none exists yet
|
|
178
|
+
if (!data.authToken) {
|
|
179
|
+
fetch('/api/setup/generate-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
|
|
180
|
+
.then(r => r.json())
|
|
181
|
+
.then(tokenData => { if (tokenData.token) setState(p => ({ ...p, authToken: tokenData.token })); })
|
|
182
|
+
.catch(e => console.warn('[SetupWizard] Token generation failed:', e));
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
.catch(e => {
|
|
186
|
+
console.warn('[SetupWizard] Failed to load config, generating token as fallback:', e);
|
|
187
|
+
// Fallback: generate token on failure
|
|
188
|
+
fetch('/api/setup/generate-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
|
|
189
|
+
.then(r => r.json())
|
|
190
|
+
.then(data => { if (data.token) setState(prev => ({ ...prev, authToken: data.token })); })
|
|
191
|
+
.catch(e2 => console.warn('[SetupWizard] Fallback token generation also failed:', e2));
|
|
192
|
+
});
|
|
193
|
+
}, []);
|
|
194
|
+
|
|
195
|
+
// Auto-check ports when entering Step 3
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (step === STEP_PORTS) {
|
|
198
|
+
checkPort(state.webPort, 'web');
|
|
199
|
+
checkPort(state.mcpPort, 'mcp');
|
|
200
|
+
}
|
|
201
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
202
|
+
}, [step]);
|
|
203
|
+
|
|
204
|
+
// Load agents when entering Step 5
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (step === STEP_AGENTS && !agentsLoaded && !agentsLoading) {
|
|
207
|
+
setAgentsLoading(true);
|
|
208
|
+
fetch('/api/mcp/agents')
|
|
209
|
+
.then(r => r.json())
|
|
210
|
+
.then(data => {
|
|
211
|
+
if (data.agents) {
|
|
212
|
+
setAgents(data.agents);
|
|
213
|
+
setSelectedAgents(new Set(
|
|
214
|
+
(data.agents as AgentEntry[]).filter(a => a.installed || a.present).map(a => a.key)
|
|
215
|
+
));
|
|
216
|
+
}
|
|
217
|
+
setAgentsLoaded(true);
|
|
218
|
+
})
|
|
219
|
+
.catch(e => { console.warn('[SetupWizard] Failed to load agents:', e); setAgentsLoaded(true); })
|
|
220
|
+
.finally(() => setAgentsLoading(false));
|
|
221
|
+
}
|
|
222
|
+
}, [step, agentsLoaded, agentsLoading]);
|
|
223
|
+
|
|
224
|
+
const update = useCallback(<K extends keyof SetupState>(key: K, val: SetupState[K]) => {
|
|
225
|
+
setState(prev => ({ ...prev, [key]: val }));
|
|
226
|
+
}, []);
|
|
227
|
+
|
|
228
|
+
const generateToken = useCallback(async (seed?: string) => {
|
|
229
|
+
try {
|
|
230
|
+
const res = await fetch('/api/setup/generate-token', {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: { 'Content-Type': 'application/json' },
|
|
233
|
+
body: JSON.stringify({ seed: seed || undefined }),
|
|
234
|
+
});
|
|
235
|
+
const data = await res.json();
|
|
236
|
+
if (data.token) setState(prev => ({ ...prev, authToken: data.token }));
|
|
237
|
+
} catch (e) { console.warn('[SetupWizard] generateToken failed:', e); }
|
|
238
|
+
}, []);
|
|
239
|
+
|
|
240
|
+
const copyToken = useCallback(() => {
|
|
241
|
+
navigator.clipboard.writeText(state.authToken).catch(() => { /* clipboard unavailable in insecure context */ });
|
|
242
|
+
setTokenCopied(true);
|
|
243
|
+
setTimeout(() => setTokenCopied(false), 2000);
|
|
244
|
+
}, [state.authToken]);
|
|
245
|
+
|
|
246
|
+
const checkPort = useCallback(async (port: number, which: 'web' | 'mcp') => {
|
|
247
|
+
if (port < 1024 || port > 65535) return;
|
|
248
|
+
const setStatus = which === 'web' ? setWebPortStatus : setMcpPortStatus;
|
|
249
|
+
setStatus({ checking: true, available: null, isSelf: false, suggestion: null });
|
|
250
|
+
try {
|
|
251
|
+
const res = await fetch('/api/setup/check-port', {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
headers: { 'Content-Type': 'application/json' },
|
|
254
|
+
body: JSON.stringify({ port }),
|
|
255
|
+
});
|
|
256
|
+
const data = await res.json();
|
|
257
|
+
setStatus({ checking: false, available: data.available ?? null, isSelf: !!data.isSelf, suggestion: data.suggestion ?? null });
|
|
258
|
+
} catch (e) {
|
|
259
|
+
console.warn('[SetupWizard] checkPort failed:', e);
|
|
260
|
+
setStatus({ checking: false, available: null, isSelf: false, suggestion: null });
|
|
261
|
+
}
|
|
262
|
+
}, []);
|
|
263
|
+
|
|
264
|
+
const portConflict = state.webPort === state.mcpPort;
|
|
265
|
+
|
|
266
|
+
const canNext = () => {
|
|
267
|
+
if (step === STEP_KB) return state.mindRoot.trim().length > 0;
|
|
268
|
+
if (step === STEP_PORTS) {
|
|
269
|
+
if (portConflict) return false;
|
|
270
|
+
if (webPortStatus.checking || mcpPortStatus.checking) return false;
|
|
271
|
+
if (webPortStatus.available !== true || mcpPortStatus.available !== true) return false;
|
|
272
|
+
return (
|
|
273
|
+
state.webPort >= 1024 && state.webPort <= 65535 &&
|
|
274
|
+
state.mcpPort >= 1024 && state.mcpPort <= 65535
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return true;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const handleComplete = async () => {
|
|
281
|
+
setSubmitting(true);
|
|
282
|
+
setError('');
|
|
283
|
+
const agentKeys = Array.from(selectedAgents);
|
|
284
|
+
|
|
285
|
+
// Phase 1: Save config
|
|
286
|
+
setSetupPhase('saving');
|
|
287
|
+
let restartNeeded = false;
|
|
288
|
+
try {
|
|
289
|
+
restartNeeded = await saveConfig(state);
|
|
290
|
+
if (restartNeeded) setNeedsRestart(true);
|
|
291
|
+
} catch (e) {
|
|
292
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
293
|
+
setSetupPhase('review');
|
|
294
|
+
setSubmitting(false);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Phase 2: Install agents
|
|
299
|
+
setSetupPhase('agents');
|
|
300
|
+
if (agentKeys.length > 0) {
|
|
301
|
+
const initialStatuses: Record<string, AgentInstallStatus> = {};
|
|
302
|
+
for (const key of agentKeys) initialStatuses[key] = { state: 'installing' };
|
|
303
|
+
setAgentStatuses(initialStatuses);
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const statuses = await installAgents(agentKeys, agents, agentTransport, agentScope, state.mcpPort, state.authToken);
|
|
307
|
+
setAgentStatuses(statuses);
|
|
308
|
+
} catch (e) {
|
|
309
|
+
console.warn('[SetupWizard] agent batch install failed:', e);
|
|
310
|
+
const errStatuses: Record<string, AgentInstallStatus> = {};
|
|
311
|
+
for (const key of agentKeys) errStatuses[key] = { state: 'error' };
|
|
312
|
+
setAgentStatuses(errStatuses);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Phase 3: Install skill
|
|
317
|
+
setSetupPhase('skill');
|
|
318
|
+
try {
|
|
319
|
+
const skillData = await installSkill(state.template, agentKeys);
|
|
320
|
+
setSkillInstallResult(skillData);
|
|
321
|
+
} catch (e) {
|
|
322
|
+
console.warn('[SetupWizard] skill install failed:', e);
|
|
323
|
+
setSkillInstallResult({ error: 'Failed to install skill' });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
setSubmitting(false);
|
|
327
|
+
setCompleted(true);
|
|
328
|
+
setSetupPhase('done');
|
|
329
|
+
|
|
330
|
+
if (restartNeeded) {
|
|
331
|
+
// Config changed requiring restart — stay on page, show restart block
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
window.location.href = '/?welcome=1';
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const retryAgent = useCallback(async (key: string) => {
|
|
338
|
+
setAgentStatuses(prev => ({ ...prev, [key]: { state: 'installing' } }));
|
|
339
|
+
try {
|
|
340
|
+
const payload = buildAgentPayload(key, agents, agentTransport, agentScope);
|
|
341
|
+
const res = await fetch('/api/mcp/install', {
|
|
342
|
+
method: 'POST',
|
|
343
|
+
headers: { 'Content-Type': 'application/json' },
|
|
344
|
+
body: JSON.stringify({
|
|
345
|
+
agents: [payload],
|
|
346
|
+
transport: agentTransport,
|
|
347
|
+
url: `http://localhost:${state.mcpPort}/mcp`,
|
|
348
|
+
token: state.authToken || undefined,
|
|
349
|
+
}),
|
|
350
|
+
});
|
|
351
|
+
const data = await res.json();
|
|
352
|
+
if (data.results?.[0]) {
|
|
353
|
+
const r = data.results[0] as { agent: string; status: string; message?: string; transport?: string; verified?: boolean; verifyError?: string };
|
|
354
|
+
setAgentStatuses(prev => ({ ...prev, [key]: parseInstallResult(r) }));
|
|
355
|
+
}
|
|
356
|
+
} catch (e) {
|
|
357
|
+
console.warn('[SetupWizard] retryAgent failed:', e);
|
|
358
|
+
setAgentStatuses(prev => ({ ...prev, [key]: { state: 'error' } }));
|
|
359
|
+
}
|
|
360
|
+
}, [agents, agentScope, agentTransport, state.mcpPort, state.authToken]);
|
|
361
|
+
|
|
362
|
+
return (
|
|
363
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
|
|
364
|
+
role="dialog" aria-modal="true" aria-labelledby="setup-title"
|
|
365
|
+
style={{ background: 'var(--background)' }}>
|
|
366
|
+
<div className="w-full max-w-xl mx-auto px-6 py-12">
|
|
367
|
+
<div className="text-center mb-8">
|
|
368
|
+
<div className="inline-flex items-center gap-2 mb-2">
|
|
369
|
+
<Sparkles size={18} style={{ color: 'var(--amber)' }} />
|
|
370
|
+
<h1 id="setup-title" className="text-2xl font-semibold tracking-tight font-display" style={{ color: 'var(--foreground)' }}>
|
|
371
|
+
MindOS
|
|
372
|
+
</h1>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<div className="flex justify-center">
|
|
377
|
+
<StepDots step={step} setStep={setStep} stepTitles={s.stepTitles} disabled={submitting || completed} />
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<h2 className="text-lg font-semibold mb-5" style={{ color: 'var(--foreground)' }}>
|
|
381
|
+
{s.stepTitles[step]}
|
|
382
|
+
</h2>
|
|
383
|
+
|
|
384
|
+
{step === 0 && <StepKB state={state} update={update} t={t} homeDir={homeDir} />}
|
|
385
|
+
{step === 1 && <StepAI state={state} update={update} s={s} />}
|
|
386
|
+
{step === 2 && (
|
|
387
|
+
<StepPorts
|
|
388
|
+
state={state} update={update}
|
|
389
|
+
webPortStatus={webPortStatus} mcpPortStatus={mcpPortStatus}
|
|
390
|
+
setWebPortStatus={setWebPortStatus} setMcpPortStatus={setMcpPortStatus}
|
|
391
|
+
checkPort={checkPort} portConflict={portConflict} s={s}
|
|
392
|
+
/>
|
|
393
|
+
)}
|
|
394
|
+
{step === 3 && (
|
|
395
|
+
<StepSecurity
|
|
396
|
+
authToken={state.authToken} tokenCopied={tokenCopied}
|
|
397
|
+
onCopy={copyToken} onGenerate={generateToken}
|
|
398
|
+
webPassword={state.webPassword} onPasswordChange={v => update('webPassword', v)}
|
|
399
|
+
s={s}
|
|
400
|
+
/>
|
|
401
|
+
)}
|
|
402
|
+
{step === 4 && (
|
|
403
|
+
<StepAgents
|
|
404
|
+
agents={agents} agentsLoading={agentsLoading}
|
|
405
|
+
selectedAgents={selectedAgents} setSelectedAgents={setSelectedAgents}
|
|
406
|
+
agentTransport={agentTransport} setAgentTransport={setAgentTransport}
|
|
407
|
+
agentScope={agentScope} setAgentScope={setAgentScope}
|
|
408
|
+
agentStatuses={agentStatuses} s={s} settingsMcp={t.settings.mcp}
|
|
409
|
+
template={state.template}
|
|
410
|
+
/>
|
|
411
|
+
)}
|
|
412
|
+
{step === 5 && (
|
|
413
|
+
<StepReview
|
|
414
|
+
state={state} selectedAgents={selectedAgents}
|
|
415
|
+
agentStatuses={agentStatuses} onRetryAgent={retryAgent}
|
|
416
|
+
error={error} needsRestart={needsRestart}
|
|
417
|
+
s={s}
|
|
418
|
+
skillInstallResult={skillInstallResult}
|
|
419
|
+
setupPhase={setupPhase}
|
|
420
|
+
/>
|
|
421
|
+
)}
|
|
422
|
+
|
|
423
|
+
{/* Navigation */}
|
|
424
|
+
<div className="flex items-center justify-between mt-8 pt-6" style={{ borderTop: '1px solid var(--border)' }}>
|
|
425
|
+
<button
|
|
426
|
+
onClick={() => setStep(step - 1)}
|
|
427
|
+
disabled={step === 0 || submitting || completed}
|
|
428
|
+
className="flex items-center gap-1 px-4 py-2 text-sm rounded-lg border border-border hover:bg-muted transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
429
|
+
style={{ color: 'var(--foreground)' }}>
|
|
430
|
+
<ChevronLeft size={14} /> {s.back}
|
|
431
|
+
</button>
|
|
432
|
+
|
|
433
|
+
{step < TOTAL_STEPS - 1 ? (
|
|
434
|
+
<button
|
|
435
|
+
onClick={() => setStep(step + 1)}
|
|
436
|
+
disabled={!canNext()}
|
|
437
|
+
className="flex items-center gap-1 px-4 py-2 text-sm rounded-lg transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
438
|
+
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}>
|
|
439
|
+
{s.next} <ChevronRight size={14} />
|
|
440
|
+
</button>
|
|
441
|
+
) : completed ? (
|
|
442
|
+
// After completing: show Done link (no restart needed) or nothing (RestartBlock handles it)
|
|
443
|
+
!needsRestart ? (
|
|
444
|
+
<a href="/?welcome=1"
|
|
445
|
+
className="flex items-center gap-1 px-5 py-2 text-sm font-medium rounded-lg transition-colors"
|
|
446
|
+
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}>
|
|
447
|
+
{s.completeDone} →
|
|
448
|
+
</a>
|
|
449
|
+
) : null
|
|
450
|
+
) : (
|
|
451
|
+
<button
|
|
452
|
+
onClick={handleComplete}
|
|
453
|
+
disabled={submitting}
|
|
454
|
+
className="flex items-center gap-1 px-5 py-2 text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
|
|
455
|
+
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}>
|
|
456
|
+
{submitting && <Loader2 size={14} className="animate-spin" />}
|
|
457
|
+
{submitting ? s.completing : s.complete}
|
|
458
|
+
</button>
|
|
459
|
+
)}
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
);
|
|
464
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Messages } from '@/lib/i18n';
|
|
2
|
+
|
|
3
|
+
// ─── i18n type aliases ───────────────────────────────────────────────────────
|
|
4
|
+
export type SetupMessages = Messages['setup'];
|
|
5
|
+
export type McpMessages = Messages['settings']['mcp'];
|
|
6
|
+
|
|
7
|
+
// ─── Template ────────────────────────────────────────────────────────────────
|
|
8
|
+
export type Template = 'en' | 'zh' | 'empty' | '';
|
|
9
|
+
|
|
10
|
+
// ─── Setup state ─────────────────────────────────────────────────────────────
|
|
11
|
+
export interface SetupState {
|
|
12
|
+
mindRoot: string;
|
|
13
|
+
template: Template;
|
|
14
|
+
provider: 'anthropic' | 'openai' | 'skip';
|
|
15
|
+
anthropicKey: string;
|
|
16
|
+
anthropicModel: string;
|
|
17
|
+
openaiKey: string;
|
|
18
|
+
openaiModel: string;
|
|
19
|
+
openaiBaseUrl: string;
|
|
20
|
+
webPort: number;
|
|
21
|
+
mcpPort: number;
|
|
22
|
+
authToken: string;
|
|
23
|
+
webPassword: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Port check ──────────────────────────────────────────────────────────────
|
|
27
|
+
export interface PortStatus {
|
|
28
|
+
checking: boolean;
|
|
29
|
+
available: boolean | null;
|
|
30
|
+
isSelf: boolean;
|
|
31
|
+
suggestion: number | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Agent types ─────────────────────────────────────────────────────────────
|
|
35
|
+
export interface AgentEntry {
|
|
36
|
+
key: string;
|
|
37
|
+
name: string;
|
|
38
|
+
present: boolean;
|
|
39
|
+
installed: boolean;
|
|
40
|
+
hasProjectScope: boolean;
|
|
41
|
+
hasGlobalScope: boolean;
|
|
42
|
+
preferredTransport: 'stdio' | 'http';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type AgentInstallState = 'pending' | 'installing' | 'ok' | 'error';
|
|
46
|
+
|
|
47
|
+
export interface AgentInstallStatus {
|
|
48
|
+
state: AgentInstallState;
|
|
49
|
+
message?: string;
|
|
50
|
+
transport?: string;
|
|
51
|
+
verified?: boolean;
|
|
52
|
+
verifyError?: string;
|
|
53
|
+
}
|
package/app/lib/i18n.ts
CHANGED
|
@@ -228,7 +228,10 @@ export const messages = {
|
|
|
228
228
|
skillLanguage: 'Skill Language',
|
|
229
229
|
skillLangEn: 'English',
|
|
230
230
|
skillLangZh: '中文',
|
|
231
|
-
|
|
231
|
+
selectDetected: 'Select Detected',
|
|
232
|
+
clearSelection: 'Clear',
|
|
233
|
+
},
|
|
234
|
+
save: 'Save',
|
|
232
235
|
saved: 'Saved',
|
|
233
236
|
saveFailed: 'Save failed',
|
|
234
237
|
reconfigure: 'Reconfigure',
|
|
@@ -283,7 +286,10 @@ export const messages = {
|
|
|
283
286
|
kbPath: 'Knowledge base path',
|
|
284
287
|
kbPathHint: 'Absolute path to your notes directory.',
|
|
285
288
|
kbPathDefault: '~/MindOS/mind',
|
|
286
|
-
|
|
289
|
+
kbPathUseDefault: (path: string) => `Use ${path}`,
|
|
290
|
+
kbPathHasFiles: (n: number) => `This directory already contains ${n} file${n > 1 ? 's' : ''}. You can skip the template or merge (existing files won't be overwritten).`,
|
|
291
|
+
kbTemplateSkip: 'Skip template',
|
|
292
|
+
kbTemplateMerge: 'Choose a template to merge',
|
|
287
293
|
template: 'Starter template',
|
|
288
294
|
templateSkip: 'Skip (directory already has files)',
|
|
289
295
|
// Step 2
|
|
@@ -325,7 +331,16 @@ export const messages = {
|
|
|
325
331
|
agentToolsLoading: 'Loading agents…',
|
|
326
332
|
agentToolsEmpty: 'No supported agents detected.',
|
|
327
333
|
agentNoneSelected: 'No agents selected — you can configure later in Settings → MCP.',
|
|
328
|
-
agentSkipLater: 'Skip —
|
|
334
|
+
agentSkipLater: 'Skip — install agents later in Settings > MCP',
|
|
335
|
+
agentSelectDetected: 'Select detected agents',
|
|
336
|
+
agentNoneDetected: 'No agents detected on your system.',
|
|
337
|
+
agentShowMore: (n: number) => `Show ${n} more agents`,
|
|
338
|
+
agentAdvanced: 'Advanced options',
|
|
339
|
+
agentScopeGlobal: 'Install for all projects',
|
|
340
|
+
agentScopeProject: 'This project only',
|
|
341
|
+
badgeInstalled: 'Installed',
|
|
342
|
+
badgeDetected: 'Detected',
|
|
343
|
+
badgeNotFound: 'Not found',
|
|
329
344
|
agentNotInstalled: 'not installed',
|
|
330
345
|
agentDetected: 'detected',
|
|
331
346
|
agentNotFound: 'not found',
|
|
@@ -338,6 +353,7 @@ export const messages = {
|
|
|
338
353
|
agentUnverified: 'unverified',
|
|
339
354
|
agentVerifyNote: 'stdio agents are verified after restart',
|
|
340
355
|
// Skill auto-install
|
|
356
|
+
skillWhat: 'Skills teach AI agents how to use your knowledge base — like instructions for reading, writing, and organizing your notes.',
|
|
341
357
|
skillAutoHint: (name: string) => `Based on your template, the "${name}" skill will be installed to selected agents.`,
|
|
342
358
|
skillLabel: 'Skill',
|
|
343
359
|
skillInstalling: 'Installing skill…',
|
|
@@ -347,9 +363,15 @@ export const messages = {
|
|
|
347
363
|
aiSkipTitle: 'Skip for now',
|
|
348
364
|
aiSkipDesc: 'You can add an API key later in Settings → AI.',
|
|
349
365
|
// Step 6 — Review
|
|
350
|
-
reviewHint: 'Verify your settings
|
|
366
|
+
reviewHint: 'Verify your settings, then press Complete Setup.',
|
|
351
367
|
reviewInstallResults: 'Agent configuration results:',
|
|
368
|
+
phaseSaving: 'Saving configuration…',
|
|
369
|
+
phaseAgents: 'Configuring agents…',
|
|
370
|
+
phaseSkill: 'Installing skill…',
|
|
371
|
+
phaseDone: 'Setup complete!',
|
|
352
372
|
retryAgent: 'Retry',
|
|
373
|
+
agentFailedCount: (n: number) => `${n} agent${n > 1 ? 's' : ''} failed`,
|
|
374
|
+
agentCountSummary: (n: number) => `${n} agent${n > 1 ? 's' : ''}`,
|
|
353
375
|
agentFailureNote: 'Agent failures are non-blocking — you can enter MindOS and retry from Settings → MCP.',
|
|
354
376
|
portAvailable: 'Available',
|
|
355
377
|
portChanged: 'Port changed — please restart the server for it to take effect.',
|
|
@@ -601,7 +623,10 @@ export const messages = {
|
|
|
601
623
|
skillLanguage: 'Skill 语言',
|
|
602
624
|
skillLangEn: 'English',
|
|
603
625
|
skillLangZh: '中文',
|
|
604
|
-
|
|
626
|
+
selectDetected: '选择已检测',
|
|
627
|
+
clearSelection: '清除',
|
|
628
|
+
},
|
|
629
|
+
save: '保存',
|
|
605
630
|
saved: '已保存',
|
|
606
631
|
saveFailed: '保存失败',
|
|
607
632
|
reconfigure: '重新配置',
|
|
@@ -656,7 +681,10 @@ export const messages = {
|
|
|
656
681
|
kbPath: '知识库路径',
|
|
657
682
|
kbPathHint: '笔记目录的绝对路径。',
|
|
658
683
|
kbPathDefault: '~/MindOS/mind',
|
|
659
|
-
|
|
684
|
+
kbPathUseDefault: (path: string) => `使用 ${path}`,
|
|
685
|
+
kbPathHasFiles: (n: number) => `该目录已有 ${n} 个文件。可以跳过模板,或选择合并(已有文件不会被覆盖)。`,
|
|
686
|
+
kbTemplateSkip: '跳过模板',
|
|
687
|
+
kbTemplateMerge: '选择模板合并',
|
|
660
688
|
template: '初始模板',
|
|
661
689
|
templateSkip: '跳过(目录已有文件)',
|
|
662
690
|
// Step 2
|
|
@@ -698,7 +726,16 @@ export const messages = {
|
|
|
698
726
|
agentToolsLoading: '正在加载 Agent…',
|
|
699
727
|
agentToolsEmpty: '未检测到受支持的 Agent。',
|
|
700
728
|
agentNoneSelected: '未选择 agent — 可稍后在 设置 → MCP 中配置。',
|
|
701
|
-
agentSkipLater: '跳过 —
|
|
729
|
+
agentSkipLater: '跳过 — 稍后在 设置 > MCP 中安装',
|
|
730
|
+
agentSelectDetected: '选择已检测到的 Agent',
|
|
731
|
+
agentNoneDetected: '未在系统中检测到已安装的 Agent。',
|
|
732
|
+
agentShowMore: (n: number) => `显示另外 ${n} 个 Agent`,
|
|
733
|
+
agentAdvanced: '高级选项',
|
|
734
|
+
agentScopeGlobal: '为所有项目安装',
|
|
735
|
+
agentScopeProject: '仅当前项目',
|
|
736
|
+
badgeInstalled: '已安装',
|
|
737
|
+
badgeDetected: '已检测到',
|
|
738
|
+
badgeNotFound: '未找到',
|
|
702
739
|
agentNotInstalled: '未安装',
|
|
703
740
|
agentDetected: '已检测到',
|
|
704
741
|
agentNotFound: '未找到',
|
|
@@ -711,6 +748,7 @@ export const messages = {
|
|
|
711
748
|
agentUnverified: '未验证',
|
|
712
749
|
agentVerifyNote: 'stdio agent 需重启后验证',
|
|
713
750
|
// Skill auto-install
|
|
751
|
+
skillWhat: 'Skill 教 AI Agent 如何使用你的知识库 — 包括笔记的读取、写入和整理规则。',
|
|
714
752
|
skillAutoHint: (name: string) => `根据您选择的模板,将向选中的 Agent 安装「${name}」Skill。`,
|
|
715
753
|
skillLabel: 'Skill',
|
|
716
754
|
skillInstalling: '正在安装 Skill…',
|
|
@@ -720,9 +758,15 @@ export const messages = {
|
|
|
720
758
|
aiSkipTitle: '暂时跳过',
|
|
721
759
|
aiSkipDesc: '稍后可在 设置 → AI 中添加 API 密钥。',
|
|
722
760
|
// Step 6 — Review
|
|
723
|
-
reviewHint: '
|
|
761
|
+
reviewHint: '确认设置无误后,点击完成。',
|
|
724
762
|
reviewInstallResults: 'Agent 配置结果:',
|
|
763
|
+
phaseSaving: '正在保存配置…',
|
|
764
|
+
phaseAgents: '正在配置 Agent…',
|
|
765
|
+
phaseSkill: '正在安装 Skill…',
|
|
766
|
+
phaseDone: '设置完成!',
|
|
725
767
|
retryAgent: '重试',
|
|
768
|
+
agentFailedCount: (n: number) => `${n} 个 Agent 配置失败`,
|
|
769
|
+
agentCountSummary: (n: number) => `${n} 个 Agent`,
|
|
726
770
|
agentFailureNote: 'Agent 安装失败不影响进入 MindOS — 可稍后在 设置 → MCP 中重试。',
|
|
727
771
|
portAvailable: '可用',
|
|
728
772
|
portChanged: '端口已变更 — 请重启服务以使其生效。',
|