@geminilight/mindos 0.5.9 → 0.5.11
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 +1 -1
- package/app/app/api/settings/test-key/route.ts +111 -0
- package/app/app/api/skills/route.ts +1 -1
- package/app/app/api/sync/route.ts +16 -31
- package/app/app/globals.css +10 -2
- package/app/app/login/page.tsx +1 -1
- package/app/app/view/[...path]/ViewPageClient.tsx +6 -1
- package/app/app/view/[...path]/not-found.tsx +1 -1
- package/app/components/AskModal.tsx +4 -4
- package/app/components/Breadcrumb.tsx +2 -2
- package/app/components/DirView.tsx +6 -6
- package/app/components/FileTree.tsx +2 -2
- package/app/components/HomeContent.tsx +7 -7
- 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 -1400
- package/app/components/Sidebar.tsx +4 -4
- package/app/components/SidebarLayout.tsx +9 -0
- package/app/components/SyncStatusBar.tsx +3 -3
- 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 +1 -1
- package/app/components/ask/SessionHistory.tsx +2 -2
- package/app/components/renderers/config/ConfigRenderer.tsx +1 -1
- 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/graph/GraphRenderer.tsx +1 -1
- package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +2 -2
- package/app/components/settings/AiTab.tsx +120 -2
- package/app/components/settings/KnowledgeTab.tsx +1 -1
- package/app/components/settings/McpTab.tsx +27 -23
- package/app/components/settings/PluginsTab.tsx +4 -4
- package/app/components/settings/Primitives.tsx +1 -1
- package/app/components/settings/SyncTab.tsx +8 -8
- 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/instrumentation.ts +19 -0
- package/app/lib/i18n.ts +22 -4
- package/app/next.config.ts +1 -1
- package/bin/cli.js +8 -1
- package/bin/lib/sync.js +61 -11
- package/package.json +4 -2
- package/skills/project-wiki/SKILL.md +92 -63
- package/assets/images/demo-flow-dark.png +0 -0
- package/assets/images/demo-flow-light.png +0 -0
- package/assets/images/demo-flow-zh-dark.png +0 -0
- package/assets/images/demo-flow-zh-light.png +0 -0
- package/assets/images/gui-sync-cv.png +0 -0
- package/assets/images/wechat-qr.png +0 -0
- package/mcp/package-lock.json +0 -1717
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export async function register() {
|
|
2
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
3
|
+
const { readFileSync } = await import('fs');
|
|
4
|
+
const { join, resolve } = await import('path');
|
|
5
|
+
const { homedir } = await import('os');
|
|
6
|
+
try {
|
|
7
|
+
const configPath = join(homedir(), '.mindos', 'config.json');
|
|
8
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
9
|
+
if (config.sync?.enabled && config.mindRoot) {
|
|
10
|
+
// Resolve absolute path to avoid Turbopack bundling issues
|
|
11
|
+
const syncModule = resolve(process.cwd(), '..', 'bin', 'lib', 'sync.js');
|
|
12
|
+
const { startSyncDaemon } = await import(/* webpackIgnore: true */ syncModule);
|
|
13
|
+
await startSyncDaemon(config.mindRoot);
|
|
14
|
+
}
|
|
15
|
+
} catch {
|
|
16
|
+
// Sync not configured or failed to start — silently skip
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
package/app/lib/i18n.ts
CHANGED
|
@@ -119,6 +119,15 @@ export const messages = {
|
|
|
119
119
|
resetToEnv: 'Reset to env value',
|
|
120
120
|
restoreFromEnv: 'Restore from env',
|
|
121
121
|
noApiKey: 'API key is not set. AI features will be unavailable until you add one.',
|
|
122
|
+
testKey: 'Test',
|
|
123
|
+
testKeyTesting: 'Testing...',
|
|
124
|
+
testKeyOk: (ms: number) => `\u2713 ${ms}ms`,
|
|
125
|
+
testKeyAuthError: 'Invalid API key',
|
|
126
|
+
testKeyModelNotFound: 'Model not found',
|
|
127
|
+
testKeyRateLimited: 'Rate limited, try again later',
|
|
128
|
+
testKeyNetworkError: 'Network error',
|
|
129
|
+
testKeyNoKey: 'No API key configured',
|
|
130
|
+
testKeyUnknown: 'Test failed',
|
|
122
131
|
},
|
|
123
132
|
appearance: {
|
|
124
133
|
readingFont: 'Reading font',
|
|
@@ -230,7 +239,8 @@ export const messages = {
|
|
|
230
239
|
skillLangZh: '中文',
|
|
231
240
|
selectDetected: 'Select Detected',
|
|
232
241
|
clearSelection: 'Clear',
|
|
233
|
-
},
|
|
242
|
+
},
|
|
243
|
+
save: 'Save',
|
|
234
244
|
saved: 'Saved',
|
|
235
245
|
saveFailed: 'Save failed',
|
|
236
246
|
reconfigure: 'Reconfigure',
|
|
@@ -286,7 +296,6 @@ export const messages = {
|
|
|
286
296
|
kbPathHint: 'Absolute path to your notes directory.',
|
|
287
297
|
kbPathDefault: '~/MindOS/mind',
|
|
288
298
|
kbPathUseDefault: (path: string) => `Use ${path}`,
|
|
289
|
-
kbPathExists: (n: number) => `Directory already has ${n} file(s) — template will not be applied.`,
|
|
290
299
|
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
300
|
kbTemplateSkip: 'Skip template',
|
|
292
301
|
kbTemplateMerge: 'Choose a template to merge',
|
|
@@ -514,6 +523,15 @@ export const messages = {
|
|
|
514
523
|
resetToEnv: '恢复为环境变量',
|
|
515
524
|
restoreFromEnv: '从环境变量恢复',
|
|
516
525
|
noApiKey: 'API 密钥未设置,AI 功能暂不可用,请在此填写。',
|
|
526
|
+
testKey: '测试',
|
|
527
|
+
testKeyTesting: '测试中...',
|
|
528
|
+
testKeyOk: (ms: number) => `\u2713 ${ms}ms`,
|
|
529
|
+
testKeyAuthError: 'API Key 无效',
|
|
530
|
+
testKeyModelNotFound: '模型不存在',
|
|
531
|
+
testKeyRateLimited: '请求频率限制,稍后重试',
|
|
532
|
+
testKeyNetworkError: '网络错误',
|
|
533
|
+
testKeyNoKey: '未配置 API Key',
|
|
534
|
+
testKeyUnknown: '测试失败',
|
|
517
535
|
},
|
|
518
536
|
appearance: {
|
|
519
537
|
readingFont: '正文字体',
|
|
@@ -625,7 +643,8 @@ export const messages = {
|
|
|
625
643
|
skillLangZh: '中文',
|
|
626
644
|
selectDetected: '选择已检测',
|
|
627
645
|
clearSelection: '清除',
|
|
628
|
-
},
|
|
646
|
+
},
|
|
647
|
+
save: '保存',
|
|
629
648
|
saved: '已保存',
|
|
630
649
|
saveFailed: '保存失败',
|
|
631
650
|
reconfigure: '重新配置',
|
|
@@ -681,7 +700,6 @@ export const messages = {
|
|
|
681
700
|
kbPathHint: '笔记目录的绝对路径。',
|
|
682
701
|
kbPathDefault: '~/MindOS/mind',
|
|
683
702
|
kbPathUseDefault: (path: string) => `使用 ${path}`,
|
|
684
|
-
kbPathExists: (n: number) => `目录已有 ${n} 个文件 — 将不会应用模板。`,
|
|
685
703
|
kbPathHasFiles: (n: number) => `该目录已有 ${n} 个文件。可以跳过模板,或选择合并(已有文件不会被覆盖)。`,
|
|
686
704
|
kbTemplateSkip: '跳过模板',
|
|
687
705
|
kbTemplateMerge: '选择模板合并',
|
package/app/next.config.ts
CHANGED
|
@@ -3,7 +3,7 @@ import path from "path";
|
|
|
3
3
|
|
|
4
4
|
const nextConfig: NextConfig = {
|
|
5
5
|
transpilePackages: ['github-slugger'],
|
|
6
|
-
serverExternalPackages: ['pdfjs-dist', 'pdf-parse'],
|
|
6
|
+
serverExternalPackages: ['pdfjs-dist', 'pdf-parse', 'chokidar'],
|
|
7
7
|
outputFileTracingRoot: path.join(__dirname),
|
|
8
8
|
turbopack: {
|
|
9
9
|
root: path.join(__dirname),
|
package/bin/cli.js
CHANGED
|
@@ -803,7 +803,14 @@ ${bold('Examples:')}
|
|
|
803
803
|
}
|
|
804
804
|
|
|
805
805
|
if (sub === 'now') {
|
|
806
|
-
|
|
806
|
+
try {
|
|
807
|
+
console.log(dim('Pulling...'));
|
|
808
|
+
manualSync(mindRoot);
|
|
809
|
+
console.log(green('✔ Sync complete'));
|
|
810
|
+
} catch (err) {
|
|
811
|
+
console.error(red(err.message));
|
|
812
|
+
process.exit(1);
|
|
813
|
+
}
|
|
807
814
|
return;
|
|
808
815
|
}
|
|
809
816
|
|