@miphamai/cli 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/config/loader.ts +55 -1
- package/src/i18n-core/locales/en-US.json +11 -2
- package/src/i18n-core/locales/zh-CN.json +11 -2
- package/src/providers/openai-compat.ts +51 -0
- package/src/shared/constants.ts +21 -0
- package/src/ui/app.tsx +136 -8
- package/src/ui/commands.ts +93 -0
- package/src/ui/config-wizard.tsx +338 -260
- package/src/ui/picker.tsx +11 -1
package/src/ui/config-wizard.tsx
CHANGED
|
@@ -2,97 +2,123 @@
|
|
|
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
|
-
import { DEFAULT_PROVIDERS } from '../shared/constants'
|
|
14
|
+
import { DEFAULT_PROVIDERS, OLLAMA_PRESET_MODELS } 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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
48
|
+
const models =
|
|
49
|
+
providerId === 'ollama' ? getOllamaModelListForConfig() : getActiveModels(providerId)
|
|
50
|
+
|
|
51
|
+
function getOllamaModelListForConfig(): ModelInfo[] {
|
|
52
|
+
// 与 getOllamaModelList 逻辑一致,但用于配置写入
|
|
53
|
+
const seen = new Set<string>()
|
|
54
|
+
const result: ModelInfo[] = []
|
|
55
|
+
|
|
56
|
+
// ollama list
|
|
57
|
+
try {
|
|
58
|
+
const out = execSync('ollama list', { timeout: 5000, encoding: 'utf-8' })
|
|
59
|
+
const lines = out.split('\n').slice(1).filter(Boolean)
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
const name = line.split(/\s+/)[0]!
|
|
62
|
+
if (!seen.has(name)) {
|
|
63
|
+
seen.add(name)
|
|
64
|
+
result.push({
|
|
65
|
+
id: name,
|
|
66
|
+
name,
|
|
67
|
+
providerId: 'ollama',
|
|
68
|
+
contextWindow: 128_000,
|
|
69
|
+
maxOutput: 32_000,
|
|
70
|
+
vision: false,
|
|
71
|
+
status: 'active',
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
/* ollama not available */
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const p of OLLAMA_PRESET_MODELS) {
|
|
80
|
+
if (!seen.has(p.id)) {
|
|
81
|
+
seen.add(p.id)
|
|
82
|
+
result.push({
|
|
83
|
+
id: p.id,
|
|
84
|
+
name: p.id,
|
|
85
|
+
providerId: 'ollama',
|
|
86
|
+
contextWindow: 128_000,
|
|
87
|
+
maxOutput: 32_000,
|
|
88
|
+
vision: false,
|
|
89
|
+
status: 'active',
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result
|
|
94
|
+
}
|
|
95
|
+
const lines = [
|
|
96
|
+
'# Mipham Code Configuration',
|
|
97
|
+
`# Generated by Config Wizard — ${new Date().toISOString()}`,
|
|
98
|
+
'',
|
|
99
|
+
'version: 1',
|
|
70
100
|
`defaultProvider: ${providerId}`,
|
|
71
101
|
`defaultModel: ${modelId}`,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
102
|
+
'permission: default',
|
|
103
|
+
'',
|
|
104
|
+
'providers:',
|
|
75
105
|
` - id: ${providerId}`,
|
|
76
106
|
` name: ${provider?.name || providerId}`,
|
|
77
107
|
` protocol: ${provider?.protocol || 'openai-compatible'}`,
|
|
78
108
|
...(provider?.baseUrl ? [` baseUrl: ${provider.baseUrl}`] : []),
|
|
79
109
|
` apiKey: ${apiKey}`,
|
|
80
110
|
` models:`,
|
|
111
|
+
...models.map((m) => ` - id: ${m.id}`),
|
|
81
112
|
]
|
|
82
113
|
|
|
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')
|
|
114
|
+
writeFileSync(join(configDir, 'config.yml'), lines.join('\n'), 'utf-8')
|
|
89
115
|
}
|
|
90
116
|
|
|
91
|
-
/** Check if Ollama is installed and running. */
|
|
92
117
|
function checkOllama(): { installed: boolean; running: boolean; models: string[] } {
|
|
93
118
|
try {
|
|
94
119
|
execSync('ollama --version', { timeout: 5000, stdio: 'ignore' })
|
|
95
|
-
const
|
|
120
|
+
const out = execSync('ollama list', { timeout: 5000, encoding: 'utf-8' })
|
|
121
|
+
const models = out
|
|
96
122
|
.split('\n')
|
|
97
123
|
.slice(1)
|
|
98
124
|
.filter(Boolean)
|
|
@@ -108,169 +134,224 @@ function checkOllama(): { installed: boolean; running: boolean; models: string[]
|
|
|
108
134
|
}
|
|
109
135
|
}
|
|
110
136
|
|
|
137
|
+
// ── Ollama model list helpers ──
|
|
138
|
+
|
|
139
|
+
interface OllamaModelItem {
|
|
140
|
+
id: string
|
|
141
|
+
source: 'local' | 'MiphamAI' | '热门'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getOllamaModelList(installedModels: string[]): OllamaModelItem[] {
|
|
145
|
+
const seen = new Set<string>()
|
|
146
|
+
const result: OllamaModelItem[] = []
|
|
147
|
+
|
|
148
|
+
// ollama list 返回的本地模型
|
|
149
|
+
for (const name of installedModels) {
|
|
150
|
+
if (!seen.has(name)) {
|
|
151
|
+
seen.add(name)
|
|
152
|
+
result.push({ id: name, source: 'local' })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 预置模型(去重)
|
|
157
|
+
for (const preset of OLLAMA_PRESET_MODELS) {
|
|
158
|
+
if (!seen.has(preset.id)) {
|
|
159
|
+
seen.add(preset.id)
|
|
160
|
+
result.push({ id: preset.id, source: preset.source as 'MiphamAI' | '热门' })
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return result
|
|
165
|
+
}
|
|
166
|
+
|
|
111
167
|
// ── Component ──
|
|
112
168
|
|
|
113
169
|
export function ConfigWizard({ onComplete, onSkip }: Props) {
|
|
114
|
-
const [
|
|
115
|
-
step: 'welcome',
|
|
116
|
-
mode: null,
|
|
117
|
-
providerId: null,
|
|
118
|
-
modelId: null,
|
|
119
|
-
apiKey: '',
|
|
120
|
-
ollamaEndpoint: 'http://localhost:11434',
|
|
121
|
-
ollamaModel: '',
|
|
122
|
-
error: null,
|
|
123
|
-
})
|
|
170
|
+
const [step, setStep] = useState<Step>('welcome')
|
|
124
171
|
|
|
125
|
-
|
|
126
|
-
const [
|
|
172
|
+
// Selections
|
|
173
|
+
const [mode, setMode] = useState<'cloud' | 'local' | null>(null)
|
|
174
|
+
const [providerId, setProviderId] = useState<string | null>(null)
|
|
175
|
+
const [modelId, setModelId] = useState<string | null>(null)
|
|
176
|
+
const [apiKey, setApiKey] = useState('')
|
|
177
|
+
const [error, setError] = useState<string | null>(null)
|
|
178
|
+
|
|
179
|
+
// List navigation cursor
|
|
180
|
+
const [cursor, setCursor] = useState(0)
|
|
181
|
+
|
|
182
|
+
// Ollama
|
|
183
|
+
const [ollamaModel, _setOllamaModel] = useState('')
|
|
127
184
|
const [ollamaStatus, setOllamaStatus] = useState<ReturnType<typeof checkOllama> | null>(null)
|
|
185
|
+
const [ollamaCursor, setOllamaCursor] = useState(0)
|
|
186
|
+
const ollamaModelList = ollamaStatus ? getOllamaModelList(ollamaStatus.models) : []
|
|
128
187
|
|
|
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
|
-
)
|
|
188
|
+
// ── Derived data ──
|
|
150
189
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
setState((s) => ({ ...s, providerId }))
|
|
154
|
-
goTo('cloud-model')
|
|
155
|
-
},
|
|
156
|
-
[goTo],
|
|
157
|
-
)
|
|
190
|
+
const providerList = CLOUD_PROVIDERS
|
|
191
|
+
const modelList = providerId ? getActiveModels(providerId) : []
|
|
158
192
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
193
|
+
// Reset cursor when step changes
|
|
194
|
+
const goStep = (s: Step) => {
|
|
195
|
+
setCursor(0)
|
|
196
|
+
setOllamaCursor(0)
|
|
197
|
+
setError(null)
|
|
198
|
+
setStep(s)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Completion handlers ──
|
|
166
202
|
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
setState((s) => ({ ...s, error: '请输入 API Key' }))
|
|
203
|
+
const finishCloud = () => {
|
|
204
|
+
if (!providerId || !modelId || !apiKey.trim()) {
|
|
205
|
+
setError('请填写 API Key')
|
|
171
206
|
return
|
|
172
207
|
}
|
|
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
208
|
try {
|
|
181
|
-
|
|
182
|
-
onComplete({ providerId
|
|
209
|
+
writeConfigFile(providerId, modelId, apiKey.trim())
|
|
210
|
+
onComplete({ providerId, modelId, apiKey: apiKey.trim() })
|
|
183
211
|
} catch (err) {
|
|
184
|
-
|
|
212
|
+
setError(`写入配置失败: ${String(err)}`)
|
|
185
213
|
}
|
|
186
|
-
}
|
|
214
|
+
}
|
|
187
215
|
|
|
188
|
-
const
|
|
189
|
-
|
|
216
|
+
const finishLocal = () => {
|
|
217
|
+
const selectedModel = ollamaModelList[ollamaCursor]
|
|
218
|
+
const model = selectedModel?.id || ollamaModel.trim() || 'llama3.2'
|
|
190
219
|
try {
|
|
191
|
-
|
|
192
|
-
onComplete({ providerId:
|
|
220
|
+
writeConfigFile('ollama', model, 'ollama-local')
|
|
221
|
+
onComplete({ providerId: 'ollama', modelId: model, apiKey: 'ollama-local' })
|
|
193
222
|
} catch (err) {
|
|
194
|
-
|
|
223
|
+
setError(`写入配置失败: ${String(err)}`)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Ollama initialization ──
|
|
228
|
+
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (step === 'ollama' && !ollamaStatus) {
|
|
231
|
+
setOllamaStatus(checkOllama())
|
|
195
232
|
}
|
|
196
|
-
}, [
|
|
233
|
+
}, [step, ollamaStatus])
|
|
197
234
|
|
|
198
|
-
// ── Keyboard ──
|
|
235
|
+
// ── Keyboard: list navigation steps (no TextInput) ──
|
|
199
236
|
|
|
200
237
|
useInput((_input, key) => {
|
|
238
|
+
// Esc always skips
|
|
201
239
|
if (key.escape) {
|
|
202
|
-
|
|
240
|
+
if (step === 'welcome') {
|
|
241
|
+
onSkip()
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
// Go back one step
|
|
245
|
+
if (step === 'mode') {
|
|
246
|
+
goStep('welcome')
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
if (step === 'provider') {
|
|
250
|
+
goStep('mode')
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
if (step === 'model') {
|
|
254
|
+
goStep('provider')
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
if (step === 'apikey') {
|
|
258
|
+
goStep('model')
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
if (step === 'ollama') {
|
|
262
|
+
goStep('mode')
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
if (step === 'confirm') {
|
|
266
|
+
if (mode === 'cloud') {
|
|
267
|
+
goStep('apikey')
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
if (mode === 'local') {
|
|
271
|
+
goStep('ollama')
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
}
|
|
203
275
|
return
|
|
204
276
|
}
|
|
205
277
|
|
|
206
|
-
|
|
207
|
-
|
|
278
|
+
// ── Welcome ──
|
|
279
|
+
if (step === 'welcome') {
|
|
280
|
+
if (key.return) goStep('mode')
|
|
208
281
|
return
|
|
209
282
|
}
|
|
210
283
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
284
|
+
// ── Mode selection ──
|
|
285
|
+
if (step === 'mode') {
|
|
286
|
+
if (key.upArrow) setCursor((c) => (c === 0 ? 1 : 0))
|
|
287
|
+
if (key.downArrow) setCursor((c) => (c === 0 ? 1 : 0))
|
|
215
288
|
if (key.return) {
|
|
216
|
-
|
|
289
|
+
if (cursor === 0) {
|
|
290
|
+
// Cloud
|
|
291
|
+
setMode('cloud')
|
|
292
|
+
goStep('provider')
|
|
293
|
+
} else {
|
|
294
|
+
// Local
|
|
295
|
+
setMode('local')
|
|
296
|
+
setOllamaStatus(checkOllama())
|
|
297
|
+
goStep('ollama')
|
|
298
|
+
}
|
|
217
299
|
}
|
|
218
300
|
return
|
|
219
301
|
}
|
|
220
302
|
|
|
221
|
-
// Cloud provider
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
? CLOUD_PROVIDERS
|
|
226
|
-
: state.providerId
|
|
227
|
-
? getModels(state.providerId)
|
|
228
|
-
: []
|
|
229
|
-
|
|
230
|
-
if (items.length === 0) return
|
|
231
|
-
|
|
232
|
-
if (key.upArrow) {
|
|
233
|
-
setSelectedIdx((i) => (i > 0 ? i - 1 : items.length - 1))
|
|
234
|
-
}
|
|
235
|
-
if (key.downArrow) {
|
|
236
|
-
setSelectedIdx((i) => (i < items.length - 1 ? i + 1 : 0))
|
|
237
|
-
}
|
|
303
|
+
// ── Cloud provider selection ──
|
|
304
|
+
if (step === 'provider') {
|
|
305
|
+
if (key.upArrow) setCursor((c) => (c > 0 ? c - 1 : providerList.length - 1))
|
|
306
|
+
if (key.downArrow) setCursor((c) => (c < providerList.length - 1 ? c + 1 : 0))
|
|
238
307
|
if (key.return) {
|
|
239
|
-
const
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
} else {
|
|
244
|
-
selectModel((item as ModelInfo).id)
|
|
245
|
-
}
|
|
308
|
+
const p = providerList[cursor]
|
|
309
|
+
if (p) {
|
|
310
|
+
setProviderId(p.id)
|
|
311
|
+
goStep('model')
|
|
246
312
|
}
|
|
247
313
|
}
|
|
248
314
|
return
|
|
249
315
|
}
|
|
250
316
|
|
|
251
|
-
//
|
|
252
|
-
if (
|
|
317
|
+
// ── Cloud model selection ──
|
|
318
|
+
if (step === 'model') {
|
|
319
|
+
if (modelList.length === 0) return
|
|
320
|
+
if (key.upArrow) setCursor((c) => (c > 0 ? c - 1 : modelList.length - 1))
|
|
321
|
+
if (key.downArrow) setCursor((c) => (c < modelList.length - 1 ? c + 1 : 0))
|
|
253
322
|
if (key.return) {
|
|
254
|
-
|
|
323
|
+
const m = modelList[cursor]
|
|
324
|
+
if (m) {
|
|
325
|
+
setModelId(m.id)
|
|
326
|
+
goStep('apikey')
|
|
327
|
+
}
|
|
255
328
|
}
|
|
256
329
|
return
|
|
257
330
|
}
|
|
258
331
|
|
|
259
|
-
//
|
|
260
|
-
if (
|
|
332
|
+
// ── Ollama model selection ──
|
|
333
|
+
if (step === 'ollama') {
|
|
334
|
+
if (ollamaModelList.length === 0) return
|
|
335
|
+
if (key.upArrow) {
|
|
336
|
+
setOllamaCursor((c) => (c > 0 ? c - 1 : ollamaModelList.length - 1))
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
if (key.downArrow) {
|
|
340
|
+
setOllamaCursor((c) => (c < ollamaModelList.length - 1 ? c + 1 : 0))
|
|
341
|
+
return
|
|
342
|
+
}
|
|
261
343
|
if (key.return) {
|
|
262
|
-
|
|
344
|
+
goStep('confirm')
|
|
345
|
+
return
|
|
263
346
|
}
|
|
264
347
|
return
|
|
265
348
|
}
|
|
266
349
|
|
|
267
|
-
// Confirm
|
|
268
|
-
if (
|
|
350
|
+
// ── Confirm ──
|
|
351
|
+
if (step === 'confirm') {
|
|
269
352
|
if (key.return) {
|
|
270
|
-
finishCloud()
|
|
271
|
-
|
|
272
|
-
if (key.backspace) {
|
|
273
|
-
goTo('cloud-key')
|
|
353
|
+
if (mode === 'cloud') finishCloud()
|
|
354
|
+
else finishLocal()
|
|
274
355
|
}
|
|
275
356
|
return
|
|
276
357
|
}
|
|
@@ -278,9 +359,6 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
|
|
|
278
359
|
|
|
279
360
|
// ── Render ──
|
|
280
361
|
|
|
281
|
-
const selectedColor = 'cyan'
|
|
282
|
-
const dimColor = 'gray'
|
|
283
|
-
|
|
284
362
|
return (
|
|
285
363
|
<Box flexDirection="column" padding={1}>
|
|
286
364
|
{/* Header */}
|
|
@@ -290,69 +368,66 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
|
|
|
290
368
|
</Text>
|
|
291
369
|
<Text dimColor> — 配置向导</Text>
|
|
292
370
|
</Box>
|
|
293
|
-
|
|
294
371
|
<Box marginBottom={1}>
|
|
295
|
-
<Text dimColor
|
|
372
|
+
<Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
|
|
296
373
|
</Box>
|
|
297
374
|
|
|
298
375
|
{/* ── Welcome ── */}
|
|
299
|
-
{
|
|
376
|
+
{step === 'welcome' && (
|
|
300
377
|
<Box flexDirection="column">
|
|
301
378
|
<Box marginBottom={1}>
|
|
302
379
|
<Text bold color="cyan">
|
|
303
380
|
欢迎使用 Mipham Code!
|
|
304
381
|
</Text>
|
|
305
382
|
</Box>
|
|
306
|
-
<
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
<Text>此向导只需 1-2 分钟,需要您准备好 API Key。</Text>
|
|
383
|
+
<Text>在开始之前,需要先配置 AI 模型连接。</Text>
|
|
384
|
+
<Text>此向导只需 1 分钟,需要您准备好 API Key。</Text>
|
|
385
|
+
<Box marginTop={1}>
|
|
386
|
+
<Text color="yellow">按 Enter 开始 →</Text>
|
|
311
387
|
</Box>
|
|
312
388
|
<Box marginTop={1}>
|
|
313
|
-
<Text
|
|
389
|
+
<Text dimColor>Esc 跳过配置</Text>
|
|
314
390
|
</Box>
|
|
315
391
|
</Box>
|
|
316
392
|
)}
|
|
317
393
|
|
|
318
|
-
{/* ── Mode
|
|
319
|
-
{
|
|
394
|
+
{/* ── Mode ── */}
|
|
395
|
+
{step === 'mode' && (
|
|
320
396
|
<Box flexDirection="column">
|
|
321
397
|
<Box marginBottom={1}>
|
|
322
398
|
<Text bold>选择模型连接方式:</Text>
|
|
323
399
|
</Box>
|
|
400
|
+
|
|
324
401
|
<Box marginBottom={1}>
|
|
325
|
-
<Text color={
|
|
326
|
-
{
|
|
402
|
+
<Text color={cursor === 0 ? SELECTED_COLOR : undefined}>
|
|
403
|
+
{cursor === 0 ? '▶' : ' '} 🌐 云端 AI 模型(推荐)
|
|
327
404
|
</Text>
|
|
328
405
|
</Box>
|
|
329
|
-
<
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
{selectedIdx === 1 ? '▶' : ' '} 🖥️ 本地模型(Ollama)
|
|
406
|
+
<Text dimColor> Claude · DeepSeek · Gemini · 豆包 · Qwen · 混元 · OpenAI</Text>
|
|
407
|
+
|
|
408
|
+
<Box marginTop={2} marginBottom={1}>
|
|
409
|
+
<Text color={cursor === 1 ? SELECTED_COLOR : undefined}>
|
|
410
|
+
{cursor === 1 ? '▶' : ' '} 🖥️ 本地模型(Ollama)
|
|
335
411
|
</Text>
|
|
336
412
|
</Box>
|
|
337
|
-
<
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
<Text dimColor>↑↓ 选择 · Enter 确认 · Esc 跳过</Text>
|
|
413
|
+
<Text dimColor> 需要提前安装 Ollama 并下载模型</Text>
|
|
414
|
+
|
|
415
|
+
<Box marginTop={2}>
|
|
416
|
+
<Text dimColor>↑↓ 选择 · Enter 确认 · Esc 返回</Text>
|
|
342
417
|
</Box>
|
|
343
418
|
</Box>
|
|
344
419
|
)}
|
|
345
420
|
|
|
346
|
-
{/* ── Cloud
|
|
347
|
-
{
|
|
421
|
+
{/* ── Cloud Provider ── */}
|
|
422
|
+
{step === 'provider' && (
|
|
348
423
|
<Box flexDirection="column">
|
|
349
424
|
<Box marginBottom={1}>
|
|
350
425
|
<Text bold>选择 AI 模型提供商:</Text>
|
|
351
426
|
</Box>
|
|
352
|
-
{
|
|
427
|
+
{providerList.map((p, i) => (
|
|
353
428
|
<Box key={p.id} marginBottom={1}>
|
|
354
|
-
<Text color={i ===
|
|
355
|
-
{i ===
|
|
429
|
+
<Text color={i === cursor ? SELECTED_COLOR : undefined}>
|
|
430
|
+
{i === cursor ? '▶' : ' '} {p.name}
|
|
356
431
|
</Text>
|
|
357
432
|
<Text dimColor> ({p.models.filter((m) => m.status === 'active').length} 个模型)</Text>
|
|
358
433
|
</Box>
|
|
@@ -363,18 +438,18 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
|
|
|
363
438
|
</Box>
|
|
364
439
|
)}
|
|
365
440
|
|
|
366
|
-
{/* ── Cloud
|
|
367
|
-
{
|
|
441
|
+
{/* ── Cloud Model ── */}
|
|
442
|
+
{step === 'model' && (
|
|
368
443
|
<Box flexDirection="column">
|
|
369
444
|
<Box marginBottom={1}>
|
|
370
445
|
<Text bold>
|
|
371
|
-
选择模型({DEFAULT_PROVIDERS.find((p) => p.id ===
|
|
446
|
+
选择模型({DEFAULT_PROVIDERS.find((p) => p.id === providerId)?.name || providerId}):
|
|
372
447
|
</Text>
|
|
373
448
|
</Box>
|
|
374
|
-
{
|
|
449
|
+
{modelList.map((m, i) => (
|
|
375
450
|
<Box key={m.id} marginBottom={1}>
|
|
376
|
-
<Text color={i ===
|
|
377
|
-
{i ===
|
|
451
|
+
<Text color={i === cursor ? SELECTED_COLOR : undefined}>
|
|
452
|
+
{i === cursor ? '▶' : ' '} {m.name}
|
|
378
453
|
</Text>
|
|
379
454
|
<Text dimColor> ({m.id})</Text>
|
|
380
455
|
</Box>
|
|
@@ -385,44 +460,38 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
|
|
|
385
460
|
</Box>
|
|
386
461
|
)}
|
|
387
462
|
|
|
388
|
-
{/* ──
|
|
389
|
-
{
|
|
463
|
+
{/* ── API Key input ── */}
|
|
464
|
+
{step === 'apikey' && (
|
|
390
465
|
<Box flexDirection="column">
|
|
391
466
|
<Box marginBottom={1}>
|
|
392
467
|
<Text bold>粘贴 API Key:</Text>
|
|
393
468
|
</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 && (
|
|
469
|
+
<Text dimColor>提供商:{DEFAULT_PROVIDERS.find((p) => p.id === providerId)?.name}</Text>
|
|
470
|
+
<Text dimColor>
|
|
471
|
+
模型:{getActiveModels(providerId!).find((m) => m.id === modelId)?.name}
|
|
472
|
+
</Text>
|
|
473
|
+
{error && (
|
|
405
474
|
<Box marginBottom={1}>
|
|
406
|
-
<Text color="red">⚠ {
|
|
475
|
+
<Text color="red">⚠ {error}</Text>
|
|
407
476
|
</Box>
|
|
408
477
|
)}
|
|
409
|
-
<Box>
|
|
478
|
+
<Box marginTop={1}>
|
|
410
479
|
<Text color="yellow">🔑 </Text>
|
|
411
480
|
<TextInput
|
|
412
|
-
value={
|
|
413
|
-
onChange={
|
|
414
|
-
onSubmit={
|
|
481
|
+
value={apiKey}
|
|
482
|
+
onChange={setApiKey}
|
|
483
|
+
onSubmit={() => goStep('confirm')}
|
|
415
484
|
placeholder="粘贴 API Key 后按 Enter..."
|
|
416
485
|
/>
|
|
417
486
|
</Box>
|
|
418
487
|
<Box marginTop={1}>
|
|
419
|
-
<Text dimColor>Enter
|
|
488
|
+
<Text dimColor>Enter 继续 · Esc 返回选模型</Text>
|
|
420
489
|
</Box>
|
|
421
490
|
</Box>
|
|
422
491
|
)}
|
|
423
492
|
|
|
424
|
-
{/* ──
|
|
425
|
-
{
|
|
493
|
+
{/* ── Ollama ── */}
|
|
494
|
+
{step === 'ollama' && (
|
|
426
495
|
<Box flexDirection="column">
|
|
427
496
|
<Box marginBottom={1}>
|
|
428
497
|
<Text bold>Ollama 本地模型配置:</Text>
|
|
@@ -431,87 +500,96 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
|
|
|
431
500
|
<Box flexDirection="column" marginBottom={1}>
|
|
432
501
|
<Text>
|
|
433
502
|
状态:{ollamaStatus.installed ? '✅ 已安装' : '❌ 未安装'}
|
|
434
|
-
{ollamaStatus.running
|
|
503
|
+
{ollamaStatus.running
|
|
504
|
+
? ' · 运行中'
|
|
505
|
+
: ollamaStatus.installed
|
|
506
|
+
? ' · 未运行(请执行 ollama serve)'
|
|
507
|
+
: ''}
|
|
435
508
|
</Text>
|
|
436
|
-
{ollamaStatus.models.length > 0 && (
|
|
437
|
-
<Box marginTop={1}>
|
|
438
|
-
<Text dimColor>已下载模型:{ollamaStatus.models.join(', ')}</Text>
|
|
439
|
-
</Box>
|
|
440
|
-
)}
|
|
441
509
|
</Box>
|
|
442
510
|
) : (
|
|
511
|
+
<Text dimColor>正在检测 Ollama...</Text>
|
|
512
|
+
)}
|
|
513
|
+
{ollamaModelList.length > 0 && (
|
|
514
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
515
|
+
<Box marginBottom={1}>
|
|
516
|
+
<Text dimColor>已下载模型(↑↓ 选择 · Enter 确认):</Text>
|
|
517
|
+
</Box>
|
|
518
|
+
{ollamaModelList.map((m, i) => (
|
|
519
|
+
<Box key={m.id}>
|
|
520
|
+
<Text color={i === ollamaCursor ? 'cyan' : undefined}>
|
|
521
|
+
{i === ollamaCursor ? '▶' : ' '} {m.id}
|
|
522
|
+
</Text>
|
|
523
|
+
{m.source !== 'local' && (
|
|
524
|
+
<Text dimColor> [{m.source === 'MiphamAI' ? 'MiphamAI' : '热门'}]</Text>
|
|
525
|
+
)}
|
|
526
|
+
</Box>
|
|
527
|
+
))}
|
|
528
|
+
</Box>
|
|
529
|
+
)}
|
|
530
|
+
{ollamaStatus && ollamaModelList.length === 0 && (
|
|
443
531
|
<Box marginBottom={1}>
|
|
444
|
-
<Text dimColor
|
|
532
|
+
<Text dimColor>
|
|
533
|
+
未检测到已下载模型。请先运行 ollama pull <model> 下载模型。
|
|
534
|
+
</Text>
|
|
535
|
+
<Text dimColor>以下为预置模型列表,选择后可在对话中通过 /models 切换。</Text>
|
|
445
536
|
</Box>
|
|
446
537
|
)}
|
|
447
|
-
<Box marginBottom={1}>
|
|
448
|
-
<Text dimColor>请输入 Ollama 模型名称(如 llama3.2, qwen2.5, deepseek-r1):</Text>
|
|
449
|
-
</Box>
|
|
450
|
-
<Box>
|
|
451
|
-
<Text color="yellow">🖥️ </Text>
|
|
452
|
-
<TextInput
|
|
453
|
-
value={inputValue}
|
|
454
|
-
onChange={setInputValue}
|
|
455
|
-
onSubmit={finishLocal}
|
|
456
|
-
placeholder="ollama 模型名,默认 llama3.2"
|
|
457
|
-
/>
|
|
458
|
-
</Box>
|
|
459
538
|
<Box marginTop={1}>
|
|
460
|
-
<Text dimColor
|
|
539
|
+
<Text dimColor>↑↓ 选择 · Enter 确认 · Esc 返回</Text>
|
|
461
540
|
</Box>
|
|
462
541
|
</Box>
|
|
463
542
|
)}
|
|
464
543
|
|
|
465
544
|
{/* ── Confirm ── */}
|
|
466
|
-
{
|
|
545
|
+
{step === 'confirm' && (
|
|
467
546
|
<Box flexDirection="column">
|
|
468
547
|
<Box marginBottom={1}>
|
|
469
548
|
<Text bold color="cyan">
|
|
470
549
|
配置确认:
|
|
471
550
|
</Text>
|
|
472
551
|
</Box>
|
|
473
|
-
<
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
552
|
+
<Text>连接方式:{mode === 'cloud' ? '🌐 云端' : '🖥️ 本地'}</Text>
|
|
553
|
+
{mode === 'cloud' && (
|
|
554
|
+
<>
|
|
555
|
+
<Text>提供商:{DEFAULT_PROVIDERS.find((p) => p.id === providerId)?.name}</Text>
|
|
556
|
+
<Text>模型:{modelId}</Text>
|
|
557
|
+
<Text>
|
|
558
|
+
API Key:{apiKey.slice(0, 8)}...{apiKey.slice(-4)}
|
|
559
|
+
</Text>
|
|
560
|
+
</>
|
|
561
|
+
)}
|
|
562
|
+
{mode === 'local' && (
|
|
483
563
|
<Text>
|
|
484
|
-
|
|
564
|
+
Ollama 模型:{ollamaModelList[ollamaCursor]?.id || ollamaModel.trim() || 'llama3.2'}
|
|
485
565
|
</Text>
|
|
486
|
-
|
|
487
|
-
<Box
|
|
566
|
+
)}
|
|
567
|
+
<Box marginTop={1}>
|
|
488
568
|
<Text dimColor>配置文件将保存到 ~/.mipham/config.yml</Text>
|
|
489
569
|
</Box>
|
|
490
|
-
{
|
|
570
|
+
{error && (
|
|
491
571
|
<Box marginBottom={1}>
|
|
492
|
-
<Text color="red">⚠ {
|
|
572
|
+
<Text color="red">⚠ {error}</Text>
|
|
493
573
|
</Box>
|
|
494
574
|
)}
|
|
495
575
|
<Box marginTop={1}>
|
|
496
|
-
<Text color="yellow">按 Enter
|
|
576
|
+
<Text color="yellow">按 Enter 保存并启动 →</Text>
|
|
497
577
|
</Box>
|
|
498
578
|
<Box marginTop={1}>
|
|
499
|
-
<Text dimColor>
|
|
579
|
+
<Text dimColor>Esc 返回修改</Text>
|
|
500
580
|
</Box>
|
|
501
581
|
</Box>
|
|
502
582
|
)}
|
|
503
583
|
|
|
504
584
|
{/* ── Done ── */}
|
|
505
|
-
{
|
|
585
|
+
{step === 'done' && (
|
|
506
586
|
<Box flexDirection="column">
|
|
507
587
|
<Box marginBottom={1}>
|
|
508
588
|
<Text color="green" bold>
|
|
509
589
|
✅ 配置完成!
|
|
510
590
|
</Text>
|
|
511
591
|
</Box>
|
|
512
|
-
<
|
|
513
|
-
<Text>Mipham Code 正在启动...</Text>
|
|
514
|
-
</Box>
|
|
592
|
+
<Text>Mipham Code 正在启动...</Text>
|
|
515
593
|
</Box>
|
|
516
594
|
)}
|
|
517
595
|
</Box>
|