@miphamai/cli 0.29.0 → 0.30.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/index.tsx +77 -4
- package/src/shared/constants.ts +27 -0
- package/src/ui/config-wizard.tsx +519 -0
package/package.json
CHANGED
package/src/index.tsx
CHANGED
|
@@ -24,7 +24,7 @@ import { ContextManager } from './core/context'
|
|
|
24
24
|
import { QueryEngine } from './core/engine'
|
|
25
25
|
import { ExperienceRuleEngine } from './core/rule-engine.js'
|
|
26
26
|
import { SessionStore } from './core/session-store'
|
|
27
|
-
import type { PermissionLevel } from './shared/types'
|
|
27
|
+
import type { PermissionLevel, MiphamConfig } from './shared/types'
|
|
28
28
|
import { SkillsLoader } from './skills/loader'
|
|
29
29
|
import { PluginManager } from './plugin/plugin-manager'
|
|
30
30
|
import { loadPlugins } from './plugin/plugin-loader'
|
|
@@ -42,6 +42,7 @@ import { AgentViewDashboard } from './agent-view/dashboard'
|
|
|
42
42
|
import { createT } from './i18n-core/t'
|
|
43
43
|
import { detectLocale } from './i18n-core/detect'
|
|
44
44
|
import { I18nProvider } from './i18n-context'
|
|
45
|
+
import { ConfigWizard } from './ui/config-wizard'
|
|
45
46
|
import enUS from './i18n-core/locales/en-US.json' with { type: 'json' }
|
|
46
47
|
import zhCN from './i18n-core/locales/zh-CN.json' with { type: 'json' }
|
|
47
48
|
import type { TranslationMap } from './i18n-core/types'
|
|
@@ -111,6 +112,70 @@ async function checkWorkspaceTrust(): Promise<void> {
|
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
// ── SetupGate: first-run wizard → App bridge ──
|
|
116
|
+
|
|
117
|
+
interface SetupGateProps {
|
|
118
|
+
needsSetup: boolean
|
|
119
|
+
engine: QueryEngine
|
|
120
|
+
config: MiphamConfig
|
|
121
|
+
defaultProvider: string
|
|
122
|
+
defaultModel: string
|
|
123
|
+
lang?: string
|
|
124
|
+
skillsLoader?: SkillsLoader
|
|
125
|
+
pluginManager?: PluginManager
|
|
126
|
+
version?: string
|
|
127
|
+
sessionId: string
|
|
128
|
+
agentViewManager?: AgentViewManager
|
|
129
|
+
locale: string
|
|
130
|
+
t: (key: string) => string
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function SetupGate(props: SetupGateProps) {
|
|
134
|
+
const [showWizard, setShowWizard] = React.useState(props.needsSetup)
|
|
135
|
+
const [wizardProvider, setWizardProvider] = React.useState<string | null>(null)
|
|
136
|
+
const [wizardModel, setWizardModel] = React.useState<string | null>(null)
|
|
137
|
+
|
|
138
|
+
// When wizard completes: write config, switch provider, proceed to App
|
|
139
|
+
const handleWizardComplete = React.useCallback(
|
|
140
|
+
(wizardConfig: { providerId: string; modelId: string; apiKey: string }) => {
|
|
141
|
+
// Switch the engine to the newly configured provider
|
|
142
|
+
try {
|
|
143
|
+
props.engine.switchProvider(wizardConfig.providerId, wizardConfig.modelId)
|
|
144
|
+
} catch {
|
|
145
|
+
// If switch fails (e.g. provider not yet in registry), just use as-is
|
|
146
|
+
}
|
|
147
|
+
setWizardProvider(wizardConfig.providerId)
|
|
148
|
+
setWizardModel(wizardConfig.modelId)
|
|
149
|
+
setShowWizard(false)
|
|
150
|
+
},
|
|
151
|
+
[props.engine],
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
const handleWizardSkip = React.useCallback(() => {
|
|
155
|
+
setShowWizard(false)
|
|
156
|
+
}, [])
|
|
157
|
+
|
|
158
|
+
if (showWizard) {
|
|
159
|
+
return React.createElement(ConfigWizard, {
|
|
160
|
+
onComplete: handleWizardComplete,
|
|
161
|
+
onSkip: handleWizardSkip,
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return React.createElement(App, {
|
|
166
|
+
engine: props.engine,
|
|
167
|
+
config: props.config,
|
|
168
|
+
initialProvider: wizardProvider || props.defaultProvider,
|
|
169
|
+
initialModel: wizardModel || props.defaultModel,
|
|
170
|
+
lang: props.lang,
|
|
171
|
+
skillsLoader: props.skillsLoader,
|
|
172
|
+
pluginManager: props.pluginManager,
|
|
173
|
+
version: props.version,
|
|
174
|
+
sessionId: props.sessionId,
|
|
175
|
+
agentViewManager: props.agentViewManager,
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
114
179
|
export async function runApp(options: RunOptions): Promise<void> {
|
|
115
180
|
// Metrics: count CLI invocation
|
|
116
181
|
getMetrics().cliInvocations.inc()
|
|
@@ -481,21 +546,29 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
481
546
|
}
|
|
482
547
|
})
|
|
483
548
|
|
|
549
|
+
// ── First-run detection: show interactive ConfigWizard if no config exists ──
|
|
550
|
+
const hasUserConfig = existsSync(join(homedir(), '.mipham', 'config.yml'))
|
|
551
|
+
const hasProjectConfig = existsSync(join(process.cwd(), '.mipham', 'config.yml'))
|
|
552
|
+
const needsSetup = !hasUserConfig && !hasProjectConfig
|
|
553
|
+
|
|
484
554
|
const { waitUntilExit } = render(
|
|
485
555
|
React.createElement(I18nProvider, {
|
|
486
556
|
locale,
|
|
487
557
|
t,
|
|
488
|
-
children: React.createElement(
|
|
558
|
+
children: React.createElement(SetupGate, {
|
|
559
|
+
needsSetup,
|
|
489
560
|
engine,
|
|
490
561
|
config,
|
|
491
|
-
|
|
492
|
-
|
|
562
|
+
defaultProvider,
|
|
563
|
+
defaultModel,
|
|
493
564
|
lang: options.lang,
|
|
494
565
|
skillsLoader,
|
|
495
566
|
pluginManager,
|
|
496
567
|
version: options.version,
|
|
497
568
|
sessionId: sessionName,
|
|
498
569
|
agentViewManager,
|
|
570
|
+
locale,
|
|
571
|
+
t,
|
|
499
572
|
}),
|
|
500
573
|
}),
|
|
501
574
|
)
|
package/src/shared/constants.ts
CHANGED
|
@@ -35,6 +35,33 @@ export const DEFAULT_PROVIDERS: ProviderConfig[] = [
|
|
|
35
35
|
vision: true,
|
|
36
36
|
status: 'active',
|
|
37
37
|
},
|
|
38
|
+
{
|
|
39
|
+
id: 'claude-fable-5',
|
|
40
|
+
name: 'Claude Fable 5',
|
|
41
|
+
providerId: 'anthropic',
|
|
42
|
+
contextWindow: 1_000_000,
|
|
43
|
+
maxOutput: 128_000,
|
|
44
|
+
vision: true,
|
|
45
|
+
status: 'active',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'claude-sonnet-5',
|
|
49
|
+
name: 'Claude Sonnet 5',
|
|
50
|
+
providerId: 'anthropic',
|
|
51
|
+
contextWindow: 1_000_000,
|
|
52
|
+
maxOutput: 128_000,
|
|
53
|
+
vision: true,
|
|
54
|
+
status: 'active',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'claude-opus-5',
|
|
58
|
+
name: 'Claude Opus 5',
|
|
59
|
+
providerId: 'anthropic',
|
|
60
|
+
contextWindow: 1_000_000,
|
|
61
|
+
maxOutput: 128_000,
|
|
62
|
+
vision: true,
|
|
63
|
+
status: 'active',
|
|
64
|
+
},
|
|
38
65
|
],
|
|
39
66
|
},
|
|
40
67
|
{
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive first-run Config Wizard
|
|
3
|
+
*
|
|
4
|
+
* Replaces manual config.yml editing for non-technical users.
|
|
5
|
+
* Flow: Welcome → Local/Cloud → Provider → Model → API Key → Done
|
|
6
|
+
*
|
|
7
|
+
* Local path: Ollama setup (endpoint + model pull)
|
|
8
|
+
* Cloud path: pick provider → pick model → paste API key → auto-configure
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React, { useState, useCallback } from 'react'
|
|
12
|
+
import { Box, Text, useInput } from 'ink'
|
|
13
|
+
import TextInput from 'ink-text-input'
|
|
14
|
+
import { DEFAULT_PROVIDERS } from '../shared/constants'
|
|
15
|
+
import type { ProviderConfig, ModelInfo } from '../shared/types'
|
|
16
|
+
import { mkdirSync, writeFileSync, existsSync } from 'node:fs'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
import { homedir } from 'node:os'
|
|
19
|
+
import { execSync } from 'node:child_process'
|
|
20
|
+
|
|
21
|
+
// ── Types ──
|
|
22
|
+
|
|
23
|
+
interface WizardState {
|
|
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
|
+
}
|
|
41
|
+
|
|
42
|
+
interface Props {
|
|
43
|
+
onComplete: (config: { providerId: string; modelId: string; apiKey: string }) => void
|
|
44
|
+
onSkip: () => void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Helpers ──
|
|
48
|
+
|
|
49
|
+
/** Filter to cloud-only providers (exclude ollama/mipham local-only). */
|
|
50
|
+
const CLOUD_PROVIDERS = DEFAULT_PROVIDERS.filter((p) => p.id !== 'ollama' && p.id !== 'mipham')
|
|
51
|
+
|
|
52
|
+
/** Get active models for a provider. */
|
|
53
|
+
function getModels(providerId: string): ModelInfo[] {
|
|
54
|
+
const provider = DEFAULT_PROVIDERS.find((p) => p.id === providerId)
|
|
55
|
+
return provider?.models.filter((m) => m.status === 'active') || []
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Write config.yml to ~/.mipham/ */
|
|
59
|
+
function writeConfig(providerId: string, modelId: string, apiKey: string): void {
|
|
60
|
+
const configDir = join(homedir(), '.mipham')
|
|
61
|
+
mkdirSync(configDir, { recursive: true })
|
|
62
|
+
|
|
63
|
+
const provider = DEFAULT_PROVIDERS.find((p) => p.id === providerId)
|
|
64
|
+
const configLines = [
|
|
65
|
+
`# Mipham Code Configuration`,
|
|
66
|
+
`# Generated by Config Wizard`,
|
|
67
|
+
`# ${new Date().toISOString()}`,
|
|
68
|
+
``,
|
|
69
|
+
`version: 1`,
|
|
70
|
+
`defaultProvider: ${providerId}`,
|
|
71
|
+
`defaultModel: ${modelId}`,
|
|
72
|
+
`permission: default`,
|
|
73
|
+
``,
|
|
74
|
+
`providers:`,
|
|
75
|
+
` - id: ${providerId}`,
|
|
76
|
+
` name: ${provider?.name || providerId}`,
|
|
77
|
+
` protocol: ${provider?.protocol || 'openai-compatible'}`,
|
|
78
|
+
...(provider?.baseUrl ? [` baseUrl: ${provider.baseUrl}`] : []),
|
|
79
|
+
` apiKey: ${apiKey}`,
|
|
80
|
+
` models:`,
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
const models = getModels(providerId)
|
|
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')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Check if Ollama is installed and running. */
|
|
92
|
+
function checkOllama(): { installed: boolean; running: boolean; models: string[] } {
|
|
93
|
+
try {
|
|
94
|
+
execSync('ollama --version', { timeout: 5000, stdio: 'ignore' })
|
|
95
|
+
const models = execSync('ollama list', { timeout: 5000, encoding: 'utf-8' })
|
|
96
|
+
.split('\n')
|
|
97
|
+
.slice(1)
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.map((l) => l.split(/\s+/)[0]!)
|
|
100
|
+
return { installed: true, running: true, models }
|
|
101
|
+
} catch {
|
|
102
|
+
try {
|
|
103
|
+
execSync('ollama --version', { timeout: 5000, stdio: 'ignore' })
|
|
104
|
+
return { installed: true, running: false, models: [] }
|
|
105
|
+
} catch {
|
|
106
|
+
return { installed: false, running: false, models: [] }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Component ──
|
|
112
|
+
|
|
113
|
+
export function ConfigWizard({ onComplete, onSkip }: Props) {
|
|
114
|
+
const [state, setState] = useState<WizardState>({
|
|
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
|
+
})
|
|
124
|
+
|
|
125
|
+
const [selectedIdx, setSelectedIdx] = useState(0)
|
|
126
|
+
const [inputValue, setInputValue] = useState('')
|
|
127
|
+
const [ollamaStatus, setOllamaStatus] = useState<ReturnType<typeof checkOllama> | null>(null)
|
|
128
|
+
|
|
129
|
+
// ── Navigation helpers ──
|
|
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
|
+
)
|
|
150
|
+
|
|
151
|
+
const selectProvider = useCallback(
|
|
152
|
+
(providerId: string) => {
|
|
153
|
+
setState((s) => ({ ...s, providerId }))
|
|
154
|
+
goTo('cloud-model')
|
|
155
|
+
},
|
|
156
|
+
[goTo],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
const selectModel = useCallback(
|
|
160
|
+
(modelId: string) => {
|
|
161
|
+
setState((s) => ({ ...s, modelId }))
|
|
162
|
+
goTo('cloud-key')
|
|
163
|
+
},
|
|
164
|
+
[goTo],
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
const confirmApiKey = useCallback(() => {
|
|
168
|
+
const key = inputValue.trim()
|
|
169
|
+
if (!key) {
|
|
170
|
+
setState((s) => ({ ...s, error: '请输入 API Key' }))
|
|
171
|
+
return
|
|
172
|
+
}
|
|
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
|
+
try {
|
|
181
|
+
writeConfig('ollama', model, 'ollama-local')
|
|
182
|
+
onComplete({ providerId: 'ollama', modelId: model, apiKey: 'ollama-local' })
|
|
183
|
+
} catch (err) {
|
|
184
|
+
setState((s) => ({ ...s, error: `配置写入失败: ${String(err)}` }))
|
|
185
|
+
}
|
|
186
|
+
}, [inputValue, state.ollamaModel, onComplete])
|
|
187
|
+
|
|
188
|
+
const finishCloud = useCallback(() => {
|
|
189
|
+
if (!state.providerId || !state.modelId || !state.apiKey) return
|
|
190
|
+
try {
|
|
191
|
+
writeConfig(state.providerId, state.modelId, state.apiKey)
|
|
192
|
+
onComplete({ providerId: state.providerId, modelId: state.modelId, apiKey: state.apiKey })
|
|
193
|
+
} catch (err) {
|
|
194
|
+
setState((s) => ({ ...s, error: `配置写入失败: ${String(err)}` }))
|
|
195
|
+
}
|
|
196
|
+
}, [state, onComplete])
|
|
197
|
+
|
|
198
|
+
// ── Keyboard ──
|
|
199
|
+
|
|
200
|
+
useInput((_input, key) => {
|
|
201
|
+
if (key.escape) {
|
|
202
|
+
onSkip()
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (state.step === 'welcome') {
|
|
207
|
+
if (key.return) goTo('mode')
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (state.step === 'mode') {
|
|
212
|
+
if (key.upArrow || key.downArrow) {
|
|
213
|
+
setSelectedIdx((i) => (i === 0 ? 1 : 0))
|
|
214
|
+
}
|
|
215
|
+
if (key.return) {
|
|
216
|
+
setMode(selectedIdx === 0 ? 'cloud' : 'local')
|
|
217
|
+
}
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Cloud provider / model selection (list navigation)
|
|
222
|
+
if (state.step === 'cloud-provider' || state.step === 'cloud-model') {
|
|
223
|
+
const items =
|
|
224
|
+
state.step === 'cloud-provider'
|
|
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
|
+
}
|
|
238
|
+
if (key.return) {
|
|
239
|
+
const item = items[selectedIdx]
|
|
240
|
+
if (item) {
|
|
241
|
+
if (state.step === 'cloud-provider') {
|
|
242
|
+
selectProvider((item as ProviderConfig).id)
|
|
243
|
+
} else {
|
|
244
|
+
selectModel((item as ModelInfo).id)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// API key input: Enter submits
|
|
252
|
+
if (state.step === 'cloud-key') {
|
|
253
|
+
if (key.return) {
|
|
254
|
+
confirmApiKey()
|
|
255
|
+
}
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Local Ollama input
|
|
260
|
+
if (state.step === 'local-ollama') {
|
|
261
|
+
if (key.return) {
|
|
262
|
+
finishLocal()
|
|
263
|
+
}
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Confirm
|
|
268
|
+
if (state.step === 'confirm') {
|
|
269
|
+
if (key.return) {
|
|
270
|
+
finishCloud()
|
|
271
|
+
}
|
|
272
|
+
if (key.backspace) {
|
|
273
|
+
goTo('cloud-key')
|
|
274
|
+
}
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
// ── Render ──
|
|
280
|
+
|
|
281
|
+
const selectedColor = 'cyan'
|
|
282
|
+
const dimColor = 'gray'
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<Box flexDirection="column" padding={1}>
|
|
286
|
+
{/* Header */}
|
|
287
|
+
<Box marginBottom={1}>
|
|
288
|
+
<Text color="#FFD700" bold>
|
|
289
|
+
Mipham Code
|
|
290
|
+
</Text>
|
|
291
|
+
<Text dimColor> — 配置向导</Text>
|
|
292
|
+
</Box>
|
|
293
|
+
|
|
294
|
+
<Box marginBottom={1}>
|
|
295
|
+
<Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
|
|
296
|
+
</Box>
|
|
297
|
+
|
|
298
|
+
{/* ── Welcome ── */}
|
|
299
|
+
{state.step === 'welcome' && (
|
|
300
|
+
<Box flexDirection="column">
|
|
301
|
+
<Box marginBottom={1}>
|
|
302
|
+
<Text bold color="cyan">
|
|
303
|
+
欢迎使用 Mipham Code!
|
|
304
|
+
</Text>
|
|
305
|
+
</Box>
|
|
306
|
+
<Box marginBottom={1}>
|
|
307
|
+
<Text>在开始之前,需要先配置 AI 模型连接。</Text>
|
|
308
|
+
</Box>
|
|
309
|
+
<Box marginBottom={1}>
|
|
310
|
+
<Text>此向导只需 1-2 分钟,需要您准备好 API Key。</Text>
|
|
311
|
+
</Box>
|
|
312
|
+
<Box marginTop={1}>
|
|
313
|
+
<Text color="yellow">按 Enter 开始配置 →</Text>
|
|
314
|
+
</Box>
|
|
315
|
+
</Box>
|
|
316
|
+
)}
|
|
317
|
+
|
|
318
|
+
{/* ── Mode Selection ── */}
|
|
319
|
+
{state.step === 'mode' && (
|
|
320
|
+
<Box flexDirection="column">
|
|
321
|
+
<Box marginBottom={1}>
|
|
322
|
+
<Text bold>选择模型连接方式:</Text>
|
|
323
|
+
</Box>
|
|
324
|
+
<Box marginBottom={1}>
|
|
325
|
+
<Text color={selectedIdx === 0 ? selectedColor : dimColor}>
|
|
326
|
+
{selectedIdx === 0 ? '▶' : ' '} 🌐 云端 AI 模型(推荐)
|
|
327
|
+
</Text>
|
|
328
|
+
</Box>
|
|
329
|
+
<Box marginBottom={1}>
|
|
330
|
+
<Text dimColor> Claude · DeepSeek · Gemini · 豆包 · Qwen · 混元 · OpenAI</Text>
|
|
331
|
+
</Box>
|
|
332
|
+
<Box marginBottom={1}>
|
|
333
|
+
<Text color={selectedIdx === 1 ? selectedColor : dimColor}>
|
|
334
|
+
{selectedIdx === 1 ? '▶' : ' '} 🖥️ 本地模型(Ollama)
|
|
335
|
+
</Text>
|
|
336
|
+
</Box>
|
|
337
|
+
<Box marginBottom={1}>
|
|
338
|
+
<Text dimColor> 需要提前安装 Ollama 并下载模型</Text>
|
|
339
|
+
</Box>
|
|
340
|
+
<Box marginTop={1}>
|
|
341
|
+
<Text dimColor>↑↓ 选择 · Enter 确认 · Esc 跳过</Text>
|
|
342
|
+
</Box>
|
|
343
|
+
</Box>
|
|
344
|
+
)}
|
|
345
|
+
|
|
346
|
+
{/* ── Cloud: Provider Selection ── */}
|
|
347
|
+
{state.step === 'cloud-provider' && (
|
|
348
|
+
<Box flexDirection="column">
|
|
349
|
+
<Box marginBottom={1}>
|
|
350
|
+
<Text bold>选择 AI 模型提供商:</Text>
|
|
351
|
+
</Box>
|
|
352
|
+
{CLOUD_PROVIDERS.map((p, i) => (
|
|
353
|
+
<Box key={p.id} marginBottom={1}>
|
|
354
|
+
<Text color={i === selectedIdx ? selectedColor : undefined}>
|
|
355
|
+
{i === selectedIdx ? '▶' : ' '} {p.name}
|
|
356
|
+
</Text>
|
|
357
|
+
<Text dimColor> ({p.models.filter((m) => m.status === 'active').length} 个模型)</Text>
|
|
358
|
+
</Box>
|
|
359
|
+
))}
|
|
360
|
+
<Box marginTop={1}>
|
|
361
|
+
<Text dimColor>↑↓ 选择 · Enter 确认 · Esc 返回</Text>
|
|
362
|
+
</Box>
|
|
363
|
+
</Box>
|
|
364
|
+
)}
|
|
365
|
+
|
|
366
|
+
{/* ── Cloud: Model Selection ── */}
|
|
367
|
+
{state.step === 'cloud-model' && state.providerId && (
|
|
368
|
+
<Box flexDirection="column">
|
|
369
|
+
<Box marginBottom={1}>
|
|
370
|
+
<Text bold>
|
|
371
|
+
选择模型({DEFAULT_PROVIDERS.find((p) => p.id === state.providerId)?.name}):
|
|
372
|
+
</Text>
|
|
373
|
+
</Box>
|
|
374
|
+
{getModels(state.providerId).map((m, i) => (
|
|
375
|
+
<Box key={m.id} marginBottom={1}>
|
|
376
|
+
<Text color={i === selectedIdx ? selectedColor : undefined}>
|
|
377
|
+
{i === selectedIdx ? '▶' : ' '} {m.name}
|
|
378
|
+
</Text>
|
|
379
|
+
<Text dimColor> ({m.id})</Text>
|
|
380
|
+
</Box>
|
|
381
|
+
))}
|
|
382
|
+
<Box marginTop={1}>
|
|
383
|
+
<Text dimColor>↑↓ 选择 · Enter 确认 · Esc 返回</Text>
|
|
384
|
+
</Box>
|
|
385
|
+
</Box>
|
|
386
|
+
)}
|
|
387
|
+
|
|
388
|
+
{/* ── Cloud: API Key ── */}
|
|
389
|
+
{state.step === 'cloud-key' && state.providerId && state.modelId && (
|
|
390
|
+
<Box flexDirection="column">
|
|
391
|
+
<Box marginBottom={1}>
|
|
392
|
+
<Text bold>粘贴 API Key:</Text>
|
|
393
|
+
</Box>
|
|
394
|
+
<Box marginBottom={1}>
|
|
395
|
+
<Text dimColor>
|
|
396
|
+
提供商:{DEFAULT_PROVIDERS.find((p) => p.id === state.providerId)?.name}
|
|
397
|
+
</Text>
|
|
398
|
+
</Box>
|
|
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 && (
|
|
405
|
+
<Box marginBottom={1}>
|
|
406
|
+
<Text color="red">⚠ {state.error}</Text>
|
|
407
|
+
</Box>
|
|
408
|
+
)}
|
|
409
|
+
<Box>
|
|
410
|
+
<Text color="yellow">🔑 </Text>
|
|
411
|
+
<TextInput
|
|
412
|
+
value={inputValue}
|
|
413
|
+
onChange={setInputValue}
|
|
414
|
+
onSubmit={confirmApiKey}
|
|
415
|
+
placeholder="粘贴 API Key 后按 Enter..."
|
|
416
|
+
/>
|
|
417
|
+
</Box>
|
|
418
|
+
<Box marginTop={1}>
|
|
419
|
+
<Text dimColor>Enter 确认 · Esc 返回</Text>
|
|
420
|
+
</Box>
|
|
421
|
+
</Box>
|
|
422
|
+
)}
|
|
423
|
+
|
|
424
|
+
{/* ── Local: Ollama ── */}
|
|
425
|
+
{state.step === 'local-ollama' && (
|
|
426
|
+
<Box flexDirection="column">
|
|
427
|
+
<Box marginBottom={1}>
|
|
428
|
+
<Text bold>Ollama 本地模型配置:</Text>
|
|
429
|
+
</Box>
|
|
430
|
+
{ollamaStatus ? (
|
|
431
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
432
|
+
<Text>
|
|
433
|
+
状态:{ollamaStatus.installed ? '✅ 已安装' : '❌ 未安装'}
|
|
434
|
+
{ollamaStatus.running ? ' · 运行中' : ''}
|
|
435
|
+
</Text>
|
|
436
|
+
{ollamaStatus.models.length > 0 && (
|
|
437
|
+
<Box marginTop={1}>
|
|
438
|
+
<Text dimColor>已下载模型:{ollamaStatus.models.join(', ')}</Text>
|
|
439
|
+
</Box>
|
|
440
|
+
)}
|
|
441
|
+
</Box>
|
|
442
|
+
) : (
|
|
443
|
+
<Box marginBottom={1}>
|
|
444
|
+
<Text dimColor>正在检测 Ollama...</Text>
|
|
445
|
+
</Box>
|
|
446
|
+
)}
|
|
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
|
+
<Box marginTop={1}>
|
|
460
|
+
<Text dimColor>Enter 确认 · Esc 返回</Text>
|
|
461
|
+
</Box>
|
|
462
|
+
</Box>
|
|
463
|
+
)}
|
|
464
|
+
|
|
465
|
+
{/* ── Confirm ── */}
|
|
466
|
+
{state.step === 'confirm' && (
|
|
467
|
+
<Box flexDirection="column">
|
|
468
|
+
<Box marginBottom={1}>
|
|
469
|
+
<Text bold color="cyan">
|
|
470
|
+
配置确认:
|
|
471
|
+
</Text>
|
|
472
|
+
</Box>
|
|
473
|
+
<Box marginBottom={1}>
|
|
474
|
+
<Text>连接方式:{state.mode === 'cloud' ? '🌐 云端' : '🖥️ 本地'}</Text>
|
|
475
|
+
</Box>
|
|
476
|
+
<Box marginBottom={1}>
|
|
477
|
+
<Text>提供商:{DEFAULT_PROVIDERS.find((p) => p.id === state.providerId)?.name}</Text>
|
|
478
|
+
</Box>
|
|
479
|
+
<Box marginBottom={1}>
|
|
480
|
+
<Text>模型:{state.modelId}</Text>
|
|
481
|
+
</Box>
|
|
482
|
+
<Box marginBottom={1}>
|
|
483
|
+
<Text>
|
|
484
|
+
API Key:{state.apiKey.slice(0, 8)}...{state.apiKey.slice(-4)}
|
|
485
|
+
</Text>
|
|
486
|
+
</Box>
|
|
487
|
+
<Box marginBottom={1}>
|
|
488
|
+
<Text dimColor>配置文件将保存到 ~/.mipham/config.yml</Text>
|
|
489
|
+
</Box>
|
|
490
|
+
{state.error && (
|
|
491
|
+
<Box marginBottom={1}>
|
|
492
|
+
<Text color="red">⚠ {state.error}</Text>
|
|
493
|
+
</Box>
|
|
494
|
+
)}
|
|
495
|
+
<Box marginTop={1}>
|
|
496
|
+
<Text color="yellow">按 Enter 保存并开始 →</Text>
|
|
497
|
+
</Box>
|
|
498
|
+
<Box marginTop={1}>
|
|
499
|
+
<Text dimColor>Backspace 返回修改 · Esc 跳过配置</Text>
|
|
500
|
+
</Box>
|
|
501
|
+
</Box>
|
|
502
|
+
)}
|
|
503
|
+
|
|
504
|
+
{/* ── Done ── */}
|
|
505
|
+
{state.step === 'done' && (
|
|
506
|
+
<Box flexDirection="column">
|
|
507
|
+
<Box marginBottom={1}>
|
|
508
|
+
<Text color="green" bold>
|
|
509
|
+
✅ 配置完成!
|
|
510
|
+
</Text>
|
|
511
|
+
</Box>
|
|
512
|
+
<Box marginBottom={1}>
|
|
513
|
+
<Text>Mipham Code 正在启动...</Text>
|
|
514
|
+
</Box>
|
|
515
|
+
</Box>
|
|
516
|
+
)}
|
|
517
|
+
</Box>
|
|
518
|
+
)
|
|
519
|
+
}
|