@miphamai/cli 0.30.0 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/ui/config-wizard.tsx +222 -258
package/package.json
CHANGED
package/src/ui/config-wizard.tsx
CHANGED
|
@@ -2,97 +2,77 @@
|
|
|
2
2
|
* Interactive first-run Config Wizard
|
|
3
3
|
*
|
|
4
4
|
* Replaces manual config.yml editing for non-technical users.
|
|
5
|
-
* Flow: Welcome →
|
|
5
|
+
* Flow: Welcome → Mode → Provider → Model → API Key → Confirm → Done
|
|
6
6
|
*
|
|
7
|
-
* Local path: Ollama setup
|
|
7
|
+
* Local path: Ollama setup
|
|
8
8
|
* Cloud path: pick provider → pick model → paste API key → auto-configure
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import React, { useState,
|
|
11
|
+
import React, { useState, useEffect } from 'react'
|
|
12
12
|
import { Box, Text, useInput } from 'ink'
|
|
13
13
|
import TextInput from 'ink-text-input'
|
|
14
14
|
import { DEFAULT_PROVIDERS } from '../shared/constants'
|
|
15
15
|
import type { ProviderConfig, ModelInfo } from '../shared/types'
|
|
16
|
-
import { mkdirSync, writeFileSync
|
|
16
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
17
17
|
import { join } from 'node:path'
|
|
18
18
|
import { homedir } from 'node:os'
|
|
19
19
|
import { execSync } from 'node:child_process'
|
|
20
20
|
|
|
21
21
|
// ── Types ──
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
step:
|
|
25
|
-
| 'welcome'
|
|
26
|
-
| 'mode'
|
|
27
|
-
| 'cloud-provider'
|
|
28
|
-
| 'cloud-model'
|
|
29
|
-
| 'cloud-key'
|
|
30
|
-
| 'local-ollama'
|
|
31
|
-
| 'confirm'
|
|
32
|
-
| 'done'
|
|
33
|
-
mode: 'local' | 'cloud' | null
|
|
34
|
-
providerId: string | null
|
|
35
|
-
modelId: string | null
|
|
36
|
-
apiKey: string
|
|
37
|
-
ollamaEndpoint: string
|
|
38
|
-
ollamaModel: string
|
|
39
|
-
error: string | null
|
|
40
|
-
}
|
|
23
|
+
type Step = 'welcome' | 'mode' | 'provider' | 'model' | 'apikey' | 'ollama' | 'confirm' | 'done'
|
|
41
24
|
|
|
42
25
|
interface Props {
|
|
43
26
|
onComplete: (config: { providerId: string; modelId: string; apiKey: string }) => void
|
|
44
27
|
onSkip: () => void
|
|
45
28
|
}
|
|
46
29
|
|
|
47
|
-
// ──
|
|
30
|
+
// ── Constants ──
|
|
48
31
|
|
|
49
|
-
/** Filter to cloud-only providers (exclude ollama/mipham local-only). */
|
|
50
32
|
const CLOUD_PROVIDERS = DEFAULT_PROVIDERS.filter((p) => p.id !== 'ollama' && p.id !== 'mipham')
|
|
51
33
|
|
|
52
|
-
|
|
53
|
-
|
|
34
|
+
const SELECTED_COLOR = 'cyan'
|
|
35
|
+
|
|
36
|
+
// ── Helpers ──
|
|
37
|
+
|
|
38
|
+
function getActiveModels(providerId: string): ModelInfo[] {
|
|
54
39
|
const provider = DEFAULT_PROVIDERS.find((p) => p.id === providerId)
|
|
55
40
|
return provider?.models.filter((m) => m.status === 'active') || []
|
|
56
41
|
}
|
|
57
42
|
|
|
58
|
-
|
|
59
|
-
function writeConfig(providerId: string, modelId: string, apiKey: string): void {
|
|
43
|
+
function writeConfigFile(providerId: string, modelId: string, apiKey: string): void {
|
|
60
44
|
const configDir = join(homedir(), '.mipham')
|
|
61
45
|
mkdirSync(configDir, { recursive: true })
|
|
62
46
|
|
|
63
47
|
const provider = DEFAULT_PROVIDERS.find((p) => p.id === providerId)
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
`# ${new Date().toISOString()}`,
|
|
68
|
-
|
|
69
|
-
|
|
48
|
+
const models = getActiveModels(providerId)
|
|
49
|
+
const lines = [
|
|
50
|
+
'# Mipham Code Configuration',
|
|
51
|
+
`# Generated by Config Wizard — ${new Date().toISOString()}`,
|
|
52
|
+
'',
|
|
53
|
+
'version: 1',
|
|
70
54
|
`defaultProvider: ${providerId}`,
|
|
71
55
|
`defaultModel: ${modelId}`,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
56
|
+
'permission: default',
|
|
57
|
+
'',
|
|
58
|
+
'providers:',
|
|
75
59
|
` - id: ${providerId}`,
|
|
76
60
|
` name: ${provider?.name || providerId}`,
|
|
77
61
|
` protocol: ${provider?.protocol || 'openai-compatible'}`,
|
|
78
62
|
...(provider?.baseUrl ? [` baseUrl: ${provider.baseUrl}`] : []),
|
|
79
63
|
` apiKey: ${apiKey}`,
|
|
80
64
|
` models:`,
|
|
65
|
+
...models.map((m) => ` - id: ${m.id}`),
|
|
81
66
|
]
|
|
82
67
|
|
|
83
|
-
|
|
84
|
-
for (const m of models) {
|
|
85
|
-
configLines.push(` - id: ${m.id}`)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
writeFileSync(join(configDir, 'config.yml'), configLines.join('\n'), 'utf-8')
|
|
68
|
+
writeFileSync(join(configDir, 'config.yml'), lines.join('\n'), 'utf-8')
|
|
89
69
|
}
|
|
90
70
|
|
|
91
|
-
/** Check if Ollama is installed and running. */
|
|
92
71
|
function checkOllama(): { installed: boolean; running: boolean; models: string[] } {
|
|
93
72
|
try {
|
|
94
73
|
execSync('ollama --version', { timeout: 5000, stdio: 'ignore' })
|
|
95
|
-
const
|
|
74
|
+
const out = execSync('ollama list', { timeout: 5000, encoding: 'utf-8' })
|
|
75
|
+
const models = out
|
|
96
76
|
.split('\n')
|
|
97
77
|
.slice(1)
|
|
98
78
|
.filter(Boolean)
|
|
@@ -111,166 +91,169 @@ function checkOllama(): { installed: boolean; running: boolean; models: string[]
|
|
|
111
91
|
// ── Component ──
|
|
112
92
|
|
|
113
93
|
export function ConfigWizard({ onComplete, onSkip }: Props) {
|
|
114
|
-
const [
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
error: null,
|
|
123
|
-
})
|
|
94
|
+
const [step, setStep] = useState<Step>('welcome')
|
|
95
|
+
|
|
96
|
+
// Selections
|
|
97
|
+
const [mode, setMode] = useState<'cloud' | 'local' | null>(null)
|
|
98
|
+
const [providerId, setProviderId] = useState<string | null>(null)
|
|
99
|
+
const [modelId, setModelId] = useState<string | null>(null)
|
|
100
|
+
const [apiKey, setApiKey] = useState('')
|
|
101
|
+
const [error, setError] = useState<string | null>(null)
|
|
124
102
|
|
|
125
|
-
|
|
126
|
-
const [
|
|
103
|
+
// List navigation cursor
|
|
104
|
+
const [cursor, setCursor] = useState(0)
|
|
105
|
+
|
|
106
|
+
// Ollama
|
|
107
|
+
const [ollamaModel, setOllamaModel] = useState('')
|
|
127
108
|
const [ollamaStatus, setOllamaStatus] = useState<ReturnType<typeof checkOllama> | null>(null)
|
|
128
109
|
|
|
129
|
-
// ──
|
|
130
|
-
|
|
131
|
-
const goTo = useCallback((step: WizardState['step']) => {
|
|
132
|
-
setSelectedIdx(0)
|
|
133
|
-
setInputValue('')
|
|
134
|
-
setState((s) => ({ ...s, step, error: null }))
|
|
135
|
-
}, [])
|
|
136
|
-
|
|
137
|
-
const setMode = useCallback(
|
|
138
|
-
(mode: 'local' | 'cloud') => {
|
|
139
|
-
setState((s) => ({ ...s, mode }))
|
|
140
|
-
if (mode === 'cloud') {
|
|
141
|
-
goTo('cloud-provider')
|
|
142
|
-
} else {
|
|
143
|
-
const status = checkOllama()
|
|
144
|
-
setOllamaStatus(status)
|
|
145
|
-
goTo('local-ollama')
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
[goTo],
|
|
149
|
-
)
|
|
110
|
+
// ── Derived data ──
|
|
150
111
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
setState((s) => ({ ...s, providerId }))
|
|
154
|
-
goTo('cloud-model')
|
|
155
|
-
},
|
|
156
|
-
[goTo],
|
|
157
|
-
)
|
|
112
|
+
const providerList = CLOUD_PROVIDERS
|
|
113
|
+
const modelList = providerId ? getActiveModels(providerId) : []
|
|
158
114
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
115
|
+
// Reset cursor when step changes
|
|
116
|
+
const goStep = (s: Step) => {
|
|
117
|
+
setCursor(0)
|
|
118
|
+
setError(null)
|
|
119
|
+
setStep(s)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Completion handlers ──
|
|
166
123
|
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
setState((s) => ({ ...s, error: '请输入 API Key' }))
|
|
124
|
+
const finishCloud = () => {
|
|
125
|
+
if (!providerId || !modelId || !apiKey.trim()) {
|
|
126
|
+
setError('请填写 API Key')
|
|
171
127
|
return
|
|
172
128
|
}
|
|
173
|
-
setState((s) => ({ ...s, apiKey: key, error: null }))
|
|
174
|
-
goTo('confirm')
|
|
175
|
-
}, [inputValue, goTo])
|
|
176
|
-
|
|
177
|
-
const finishLocal = useCallback(() => {
|
|
178
|
-
const endpoint = inputValue.trim() || 'http://localhost:11434'
|
|
179
|
-
const model = state.ollamaModel || 'llama3.2'
|
|
180
129
|
try {
|
|
181
|
-
|
|
182
|
-
onComplete({ providerId
|
|
130
|
+
writeConfigFile(providerId, modelId, apiKey.trim())
|
|
131
|
+
onComplete({ providerId, modelId, apiKey: apiKey.trim() })
|
|
183
132
|
} catch (err) {
|
|
184
|
-
|
|
133
|
+
setError(`写入配置失败: ${String(err)}`)
|
|
185
134
|
}
|
|
186
|
-
}
|
|
135
|
+
}
|
|
187
136
|
|
|
188
|
-
const
|
|
189
|
-
|
|
137
|
+
const finishLocal = () => {
|
|
138
|
+
const model = ollamaModel.trim() || 'llama3.2'
|
|
190
139
|
try {
|
|
191
|
-
|
|
192
|
-
onComplete({ providerId:
|
|
140
|
+
writeConfigFile('ollama', model, 'ollama-local')
|
|
141
|
+
onComplete({ providerId: 'ollama', modelId: model, apiKey: 'ollama-local' })
|
|
193
142
|
} catch (err) {
|
|
194
|
-
|
|
143
|
+
setError(`写入配置失败: ${String(err)}`)
|
|
195
144
|
}
|
|
196
|
-
}
|
|
145
|
+
}
|
|
197
146
|
|
|
198
|
-
// ──
|
|
147
|
+
// ── Ollama initialization ──
|
|
199
148
|
|
|
200
|
-
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
return
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (step === 'ollama' && !ollamaStatus) {
|
|
151
|
+
setOllamaStatus(checkOllama())
|
|
204
152
|
}
|
|
153
|
+
}, [step, ollamaStatus])
|
|
205
154
|
|
|
206
|
-
|
|
207
|
-
if (key.return) goTo('mode')
|
|
208
|
-
return
|
|
209
|
-
}
|
|
155
|
+
// ── Keyboard: list navigation steps (no TextInput) ──
|
|
210
156
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
157
|
+
useInput((_input, key) => {
|
|
158
|
+
// Esc always skips
|
|
159
|
+
if (key.escape) {
|
|
160
|
+
if (step === 'welcome') {
|
|
161
|
+
onSkip()
|
|
162
|
+
return
|
|
214
163
|
}
|
|
215
|
-
|
|
216
|
-
|
|
164
|
+
// Go back one step
|
|
165
|
+
if (step === 'mode') {
|
|
166
|
+
goStep('welcome')
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
if (step === 'provider') {
|
|
170
|
+
goStep('mode')
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
if (step === 'model') {
|
|
174
|
+
goStep('provider')
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
if (step === 'apikey') {
|
|
178
|
+
goStep('model')
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
if (step === 'ollama') {
|
|
182
|
+
goStep('mode')
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
if (step === 'confirm') {
|
|
186
|
+
if (mode === 'cloud') {
|
|
187
|
+
goStep('apikey')
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
if (mode === 'local') {
|
|
191
|
+
goStep('ollama')
|
|
192
|
+
return
|
|
193
|
+
}
|
|
217
194
|
}
|
|
218
195
|
return
|
|
219
196
|
}
|
|
220
197
|
|
|
221
|
-
//
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
: state.providerId
|
|
227
|
-
? getModels(state.providerId)
|
|
228
|
-
: []
|
|
229
|
-
|
|
230
|
-
if (items.length === 0) return
|
|
198
|
+
// ── Welcome ──
|
|
199
|
+
if (step === 'welcome') {
|
|
200
|
+
if (key.return) goStep('mode')
|
|
201
|
+
return
|
|
202
|
+
}
|
|
231
203
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (key.downArrow)
|
|
236
|
-
setSelectedIdx((i) => (i < items.length - 1 ? i + 1 : 0))
|
|
237
|
-
}
|
|
204
|
+
// ── Mode selection ──
|
|
205
|
+
if (step === 'mode') {
|
|
206
|
+
if (key.upArrow) setCursor((c) => (c === 0 ? 1 : 0))
|
|
207
|
+
if (key.downArrow) setCursor((c) => (c === 0 ? 1 : 0))
|
|
238
208
|
if (key.return) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
209
|
+
if (cursor === 0) {
|
|
210
|
+
// Cloud
|
|
211
|
+
setMode('cloud')
|
|
212
|
+
goStep('provider')
|
|
213
|
+
} else {
|
|
214
|
+
// Local
|
|
215
|
+
setMode('local')
|
|
216
|
+
setOllamaStatus(checkOllama())
|
|
217
|
+
goStep('ollama')
|
|
246
218
|
}
|
|
247
219
|
}
|
|
248
220
|
return
|
|
249
221
|
}
|
|
250
222
|
|
|
251
|
-
//
|
|
252
|
-
if (
|
|
223
|
+
// ── Cloud provider selection ──
|
|
224
|
+
if (step === 'provider') {
|
|
225
|
+
if (key.upArrow) setCursor((c) => (c > 0 ? c - 1 : providerList.length - 1))
|
|
226
|
+
if (key.downArrow) setCursor((c) => (c < providerList.length - 1 ? c + 1 : 0))
|
|
253
227
|
if (key.return) {
|
|
254
|
-
|
|
228
|
+
const p = providerList[cursor]
|
|
229
|
+
if (p) {
|
|
230
|
+
setProviderId(p.id)
|
|
231
|
+
goStep('model')
|
|
232
|
+
}
|
|
255
233
|
}
|
|
256
234
|
return
|
|
257
235
|
}
|
|
258
236
|
|
|
259
|
-
//
|
|
260
|
-
if (
|
|
237
|
+
// ── Cloud model selection ──
|
|
238
|
+
if (step === 'model') {
|
|
239
|
+
if (modelList.length === 0) return
|
|
240
|
+
if (key.upArrow) setCursor((c) => (c > 0 ? c - 1 : modelList.length - 1))
|
|
241
|
+
if (key.downArrow) setCursor((c) => (c < modelList.length - 1 ? c + 1 : 0))
|
|
261
242
|
if (key.return) {
|
|
262
|
-
|
|
243
|
+
const m = modelList[cursor]
|
|
244
|
+
if (m) {
|
|
245
|
+
setModelId(m.id)
|
|
246
|
+
goStep('apikey')
|
|
247
|
+
}
|
|
263
248
|
}
|
|
264
249
|
return
|
|
265
250
|
}
|
|
266
251
|
|
|
267
|
-
// Confirm
|
|
268
|
-
if (
|
|
252
|
+
// ── Confirm ──
|
|
253
|
+
if (step === 'confirm') {
|
|
269
254
|
if (key.return) {
|
|
270
|
-
finishCloud()
|
|
271
|
-
|
|
272
|
-
if (key.backspace) {
|
|
273
|
-
goTo('cloud-key')
|
|
255
|
+
if (mode === 'cloud') finishCloud()
|
|
256
|
+
else finishLocal()
|
|
274
257
|
}
|
|
275
258
|
return
|
|
276
259
|
}
|
|
@@ -278,8 +261,7 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
|
|
|
278
261
|
|
|
279
262
|
// ── Render ──
|
|
280
263
|
|
|
281
|
-
const
|
|
282
|
-
const dimColor = 'gray'
|
|
264
|
+
const dim = 'gray'
|
|
283
265
|
|
|
284
266
|
return (
|
|
285
267
|
<Box flexDirection="column" padding={1}>
|
|
@@ -290,69 +272,66 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
|
|
|
290
272
|
</Text>
|
|
291
273
|
<Text dimColor> — 配置向导</Text>
|
|
292
274
|
</Box>
|
|
293
|
-
|
|
294
275
|
<Box marginBottom={1}>
|
|
295
|
-
<Text dimColor
|
|
276
|
+
<Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
|
|
296
277
|
</Box>
|
|
297
278
|
|
|
298
279
|
{/* ── Welcome ── */}
|
|
299
|
-
{
|
|
280
|
+
{step === 'welcome' && (
|
|
300
281
|
<Box flexDirection="column">
|
|
301
282
|
<Box marginBottom={1}>
|
|
302
283
|
<Text bold color="cyan">
|
|
303
284
|
欢迎使用 Mipham Code!
|
|
304
285
|
</Text>
|
|
305
286
|
</Box>
|
|
306
|
-
<
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
<Text>此向导只需 1-2 分钟,需要您准备好 API Key。</Text>
|
|
287
|
+
<Text>在开始之前,需要先配置 AI 模型连接。</Text>
|
|
288
|
+
<Text>此向导只需 1 分钟,需要您准备好 API Key。</Text>
|
|
289
|
+
<Box marginTop={1}>
|
|
290
|
+
<Text color="yellow">按 Enter 开始 →</Text>
|
|
311
291
|
</Box>
|
|
312
292
|
<Box marginTop={1}>
|
|
313
|
-
<Text
|
|
293
|
+
<Text dimColor>Esc 跳过配置</Text>
|
|
314
294
|
</Box>
|
|
315
295
|
</Box>
|
|
316
296
|
)}
|
|
317
297
|
|
|
318
|
-
{/* ── Mode
|
|
319
|
-
{
|
|
298
|
+
{/* ── Mode ── */}
|
|
299
|
+
{step === 'mode' && (
|
|
320
300
|
<Box flexDirection="column">
|
|
321
301
|
<Box marginBottom={1}>
|
|
322
302
|
<Text bold>选择模型连接方式:</Text>
|
|
323
303
|
</Box>
|
|
304
|
+
|
|
324
305
|
<Box marginBottom={1}>
|
|
325
|
-
<Text color={
|
|
326
|
-
{
|
|
306
|
+
<Text color={cursor === 0 ? SELECTED_COLOR : undefined}>
|
|
307
|
+
{cursor === 0 ? '▶' : ' '} 🌐 云端 AI 模型(推荐)
|
|
327
308
|
</Text>
|
|
328
309
|
</Box>
|
|
329
|
-
<
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
{selectedIdx === 1 ? '▶' : ' '} 🖥️ 本地模型(Ollama)
|
|
310
|
+
<Text dimColor> Claude · DeepSeek · Gemini · 豆包 · Qwen · 混元 · OpenAI</Text>
|
|
311
|
+
|
|
312
|
+
<Box marginTop={2} marginBottom={1}>
|
|
313
|
+
<Text color={cursor === 1 ? SELECTED_COLOR : undefined}>
|
|
314
|
+
{cursor === 1 ? '▶' : ' '} 🖥️ 本地模型(Ollama)
|
|
335
315
|
</Text>
|
|
336
316
|
</Box>
|
|
337
|
-
<
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
<Text dimColor>↑↓ 选择 · Enter 确认 · Esc 跳过</Text>
|
|
317
|
+
<Text dimColor> 需要提前安装 Ollama 并下载模型</Text>
|
|
318
|
+
|
|
319
|
+
<Box marginTop={2}>
|
|
320
|
+
<Text dimColor>↑↓ 选择 · Enter 确认 · Esc 返回</Text>
|
|
342
321
|
</Box>
|
|
343
322
|
</Box>
|
|
344
323
|
)}
|
|
345
324
|
|
|
346
|
-
{/* ── Cloud
|
|
347
|
-
{
|
|
325
|
+
{/* ── Cloud Provider ── */}
|
|
326
|
+
{step === 'provider' && (
|
|
348
327
|
<Box flexDirection="column">
|
|
349
328
|
<Box marginBottom={1}>
|
|
350
329
|
<Text bold>选择 AI 模型提供商:</Text>
|
|
351
330
|
</Box>
|
|
352
|
-
{
|
|
331
|
+
{providerList.map((p, i) => (
|
|
353
332
|
<Box key={p.id} marginBottom={1}>
|
|
354
|
-
<Text color={i ===
|
|
355
|
-
{i ===
|
|
333
|
+
<Text color={i === cursor ? SELECTED_COLOR : undefined}>
|
|
334
|
+
{i === cursor ? '▶' : ' '} {p.name}
|
|
356
335
|
</Text>
|
|
357
336
|
<Text dimColor> ({p.models.filter((m) => m.status === 'active').length} 个模型)</Text>
|
|
358
337
|
</Box>
|
|
@@ -363,18 +342,18 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
|
|
|
363
342
|
</Box>
|
|
364
343
|
)}
|
|
365
344
|
|
|
366
|
-
{/* ── Cloud
|
|
367
|
-
{
|
|
345
|
+
{/* ── Cloud Model ── */}
|
|
346
|
+
{step === 'model' && (
|
|
368
347
|
<Box flexDirection="column">
|
|
369
348
|
<Box marginBottom={1}>
|
|
370
349
|
<Text bold>
|
|
371
|
-
选择模型({DEFAULT_PROVIDERS.find((p) => p.id ===
|
|
350
|
+
选择模型({DEFAULT_PROVIDERS.find((p) => p.id === providerId)?.name || providerId}):
|
|
372
351
|
</Text>
|
|
373
352
|
</Box>
|
|
374
|
-
{
|
|
353
|
+
{modelList.map((m, i) => (
|
|
375
354
|
<Box key={m.id} marginBottom={1}>
|
|
376
|
-
<Text color={i ===
|
|
377
|
-
{i ===
|
|
355
|
+
<Text color={i === cursor ? SELECTED_COLOR : undefined}>
|
|
356
|
+
{i === cursor ? '▶' : ' '} {m.name}
|
|
378
357
|
</Text>
|
|
379
358
|
<Text dimColor> ({m.id})</Text>
|
|
380
359
|
</Box>
|
|
@@ -385,44 +364,38 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
|
|
|
385
364
|
</Box>
|
|
386
365
|
)}
|
|
387
366
|
|
|
388
|
-
{/* ──
|
|
389
|
-
{
|
|
367
|
+
{/* ── API Key input ── */}
|
|
368
|
+
{step === 'apikey' && (
|
|
390
369
|
<Box flexDirection="column">
|
|
391
370
|
<Box marginBottom={1}>
|
|
392
371
|
<Text bold>粘贴 API Key:</Text>
|
|
393
372
|
</Box>
|
|
394
|
-
<
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
<Box marginBottom={1}>
|
|
400
|
-
<Text dimColor>
|
|
401
|
-
模型:{getModels(state.providerId).find((m) => m.id === state.modelId)?.name}
|
|
402
|
-
</Text>
|
|
403
|
-
</Box>
|
|
404
|
-
{state.error && (
|
|
373
|
+
<Text dimColor>提供商:{DEFAULT_PROVIDERS.find((p) => p.id === providerId)?.name}</Text>
|
|
374
|
+
<Text dimColor>
|
|
375
|
+
模型:{getActiveModels(providerId!).find((m) => m.id === modelId)?.name}
|
|
376
|
+
</Text>
|
|
377
|
+
{error && (
|
|
405
378
|
<Box marginBottom={1}>
|
|
406
|
-
<Text color="red">⚠ {
|
|
379
|
+
<Text color="red">⚠ {error}</Text>
|
|
407
380
|
</Box>
|
|
408
381
|
)}
|
|
409
|
-
<Box>
|
|
382
|
+
<Box marginTop={1}>
|
|
410
383
|
<Text color="yellow">🔑 </Text>
|
|
411
384
|
<TextInput
|
|
412
|
-
value={
|
|
413
|
-
onChange={
|
|
414
|
-
onSubmit={
|
|
385
|
+
value={apiKey}
|
|
386
|
+
onChange={setApiKey}
|
|
387
|
+
onSubmit={() => goStep('confirm')}
|
|
415
388
|
placeholder="粘贴 API Key 后按 Enter..."
|
|
416
389
|
/>
|
|
417
390
|
</Box>
|
|
418
391
|
<Box marginTop={1}>
|
|
419
|
-
<Text dimColor>Enter
|
|
392
|
+
<Text dimColor>Enter 继续 · Esc 返回选模型</Text>
|
|
420
393
|
</Box>
|
|
421
394
|
</Box>
|
|
422
395
|
)}
|
|
423
396
|
|
|
424
|
-
{/* ──
|
|
425
|
-
{
|
|
397
|
+
{/* ── Ollama ── */}
|
|
398
|
+
{step === 'ollama' && (
|
|
426
399
|
<Box flexDirection="column">
|
|
427
400
|
<Box marginBottom={1}>
|
|
428
401
|
<Text bold>Ollama 本地模型配置:</Text>
|
|
@@ -434,84 +407,75 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
|
|
|
434
407
|
{ollamaStatus.running ? ' · 运行中' : ''}
|
|
435
408
|
</Text>
|
|
436
409
|
{ollamaStatus.models.length > 0 && (
|
|
437
|
-
<
|
|
438
|
-
<Text dimColor>已下载模型:{ollamaStatus.models.join(', ')}</Text>
|
|
439
|
-
</Box>
|
|
410
|
+
<Text dimColor>已下载模型:{ollamaStatus.models.join(', ')}</Text>
|
|
440
411
|
)}
|
|
441
412
|
</Box>
|
|
442
413
|
) : (
|
|
443
|
-
<
|
|
444
|
-
<Text dimColor>正在检测 Ollama...</Text>
|
|
445
|
-
</Box>
|
|
414
|
+
<Text dimColor>正在检测 Ollama...</Text>
|
|
446
415
|
)}
|
|
447
|
-
<Box
|
|
448
|
-
<Text dimColor
|
|
416
|
+
<Box marginTop={1}>
|
|
417
|
+
<Text dimColor>输入 Ollama 模型名称(如 llama3.2, qwen2.5, deepseek-r1):</Text>
|
|
449
418
|
</Box>
|
|
450
|
-
<Box>
|
|
419
|
+
<Box marginTop={1}>
|
|
451
420
|
<Text color="yellow">🖥️ </Text>
|
|
452
421
|
<TextInput
|
|
453
|
-
value={
|
|
454
|
-
onChange={
|
|
455
|
-
onSubmit={
|
|
456
|
-
placeholder="
|
|
422
|
+
value={ollamaModel}
|
|
423
|
+
onChange={setOllamaModel}
|
|
424
|
+
onSubmit={() => goStep('confirm')}
|
|
425
|
+
placeholder="模型名,默认 llama3.2"
|
|
457
426
|
/>
|
|
458
427
|
</Box>
|
|
459
428
|
<Box marginTop={1}>
|
|
460
|
-
<Text dimColor>Enter
|
|
429
|
+
<Text dimColor>Enter 继续 · Esc 返回</Text>
|
|
461
430
|
</Box>
|
|
462
431
|
</Box>
|
|
463
432
|
)}
|
|
464
433
|
|
|
465
434
|
{/* ── Confirm ── */}
|
|
466
|
-
{
|
|
435
|
+
{step === 'confirm' && (
|
|
467
436
|
<Box flexDirection="column">
|
|
468
437
|
<Box marginBottom={1}>
|
|
469
438
|
<Text bold color="cyan">
|
|
470
439
|
配置确认:
|
|
471
440
|
</Text>
|
|
472
441
|
</Box>
|
|
473
|
-
<
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
</Text>
|
|
486
|
-
</Box>
|
|
487
|
-
<Box marginBottom={1}>
|
|
442
|
+
<Text>连接方式:{mode === 'cloud' ? '🌐 云端' : '🖥️ 本地'}</Text>
|
|
443
|
+
{mode === 'cloud' && (
|
|
444
|
+
<>
|
|
445
|
+
<Text>提供商:{DEFAULT_PROVIDERS.find((p) => p.id === providerId)?.name}</Text>
|
|
446
|
+
<Text>模型:{modelId}</Text>
|
|
447
|
+
<Text>
|
|
448
|
+
API Key:{apiKey.slice(0, 8)}...{apiKey.slice(-4)}
|
|
449
|
+
</Text>
|
|
450
|
+
</>
|
|
451
|
+
)}
|
|
452
|
+
{mode === 'local' && <Text>Ollama 模型:{ollamaModel.trim() || 'llama3.2'}</Text>}
|
|
453
|
+
<Box marginTop={1}>
|
|
488
454
|
<Text dimColor>配置文件将保存到 ~/.mipham/config.yml</Text>
|
|
489
455
|
</Box>
|
|
490
|
-
{
|
|
456
|
+
{error && (
|
|
491
457
|
<Box marginBottom={1}>
|
|
492
|
-
<Text color="red">⚠ {
|
|
458
|
+
<Text color="red">⚠ {error}</Text>
|
|
493
459
|
</Box>
|
|
494
460
|
)}
|
|
495
461
|
<Box marginTop={1}>
|
|
496
|
-
<Text color="yellow">按 Enter
|
|
462
|
+
<Text color="yellow">按 Enter 保存并启动 →</Text>
|
|
497
463
|
</Box>
|
|
498
464
|
<Box marginTop={1}>
|
|
499
|
-
<Text dimColor>
|
|
465
|
+
<Text dimColor>Esc 返回修改</Text>
|
|
500
466
|
</Box>
|
|
501
467
|
</Box>
|
|
502
468
|
)}
|
|
503
469
|
|
|
504
470
|
{/* ── Done ── */}
|
|
505
|
-
{
|
|
471
|
+
{step === 'done' && (
|
|
506
472
|
<Box flexDirection="column">
|
|
507
473
|
<Box marginBottom={1}>
|
|
508
474
|
<Text color="green" bold>
|
|
509
475
|
✅ 配置完成!
|
|
510
476
|
</Text>
|
|
511
477
|
</Box>
|
|
512
|
-
<
|
|
513
|
-
<Text>Mipham Code 正在启动...</Text>
|
|
514
|
-
</Box>
|
|
478
|
+
<Text>Mipham Code 正在启动...</Text>
|
|
515
479
|
</Box>
|
|
516
480
|
)}
|
|
517
481
|
</Box>
|