@miphamai/cli 0.29.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/index.tsx +77 -4
- package/src/shared/constants.ts +27 -0
- package/src/ui/config-wizard.tsx +483 -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,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive first-run Config Wizard
|
|
3
|
+
*
|
|
4
|
+
* Replaces manual config.yml editing for non-technical users.
|
|
5
|
+
* Flow: Welcome → Mode → Provider → Model → API Key → Confirm → Done
|
|
6
|
+
*
|
|
7
|
+
* Local path: Ollama setup
|
|
8
|
+
* Cloud path: pick provider → pick model → paste API key → auto-configure
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React, { useState, useEffect } 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 } 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
|
+
type Step = 'welcome' | 'mode' | 'provider' | 'model' | 'apikey' | 'ollama' | 'confirm' | 'done'
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
onComplete: (config: { providerId: string; modelId: string; apiKey: string }) => void
|
|
27
|
+
onSkip: () => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Constants ──
|
|
31
|
+
|
|
32
|
+
const CLOUD_PROVIDERS = DEFAULT_PROVIDERS.filter((p) => p.id !== 'ollama' && p.id !== 'mipham')
|
|
33
|
+
|
|
34
|
+
const SELECTED_COLOR = 'cyan'
|
|
35
|
+
|
|
36
|
+
// ── Helpers ──
|
|
37
|
+
|
|
38
|
+
function getActiveModels(providerId: string): ModelInfo[] {
|
|
39
|
+
const provider = DEFAULT_PROVIDERS.find((p) => p.id === providerId)
|
|
40
|
+
return provider?.models.filter((m) => m.status === 'active') || []
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeConfigFile(providerId: string, modelId: string, apiKey: string): void {
|
|
44
|
+
const configDir = join(homedir(), '.mipham')
|
|
45
|
+
mkdirSync(configDir, { recursive: true })
|
|
46
|
+
|
|
47
|
+
const provider = DEFAULT_PROVIDERS.find((p) => p.id === providerId)
|
|
48
|
+
const models = getActiveModels(providerId)
|
|
49
|
+
const lines = [
|
|
50
|
+
'# Mipham Code Configuration',
|
|
51
|
+
`# Generated by Config Wizard — ${new Date().toISOString()}`,
|
|
52
|
+
'',
|
|
53
|
+
'version: 1',
|
|
54
|
+
`defaultProvider: ${providerId}`,
|
|
55
|
+
`defaultModel: ${modelId}`,
|
|
56
|
+
'permission: default',
|
|
57
|
+
'',
|
|
58
|
+
'providers:',
|
|
59
|
+
` - id: ${providerId}`,
|
|
60
|
+
` name: ${provider?.name || providerId}`,
|
|
61
|
+
` protocol: ${provider?.protocol || 'openai-compatible'}`,
|
|
62
|
+
...(provider?.baseUrl ? [` baseUrl: ${provider.baseUrl}`] : []),
|
|
63
|
+
` apiKey: ${apiKey}`,
|
|
64
|
+
` models:`,
|
|
65
|
+
...models.map((m) => ` - id: ${m.id}`),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
writeFileSync(join(configDir, 'config.yml'), lines.join('\n'), 'utf-8')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function checkOllama(): { installed: boolean; running: boolean; models: string[] } {
|
|
72
|
+
try {
|
|
73
|
+
execSync('ollama --version', { timeout: 5000, stdio: 'ignore' })
|
|
74
|
+
const out = execSync('ollama list', { timeout: 5000, encoding: 'utf-8' })
|
|
75
|
+
const models = out
|
|
76
|
+
.split('\n')
|
|
77
|
+
.slice(1)
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.map((l) => l.split(/\s+/)[0]!)
|
|
80
|
+
return { installed: true, running: true, models }
|
|
81
|
+
} catch {
|
|
82
|
+
try {
|
|
83
|
+
execSync('ollama --version', { timeout: 5000, stdio: 'ignore' })
|
|
84
|
+
return { installed: true, running: false, models: [] }
|
|
85
|
+
} catch {
|
|
86
|
+
return { installed: false, running: false, models: [] }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Component ──
|
|
92
|
+
|
|
93
|
+
export function ConfigWizard({ onComplete, onSkip }: Props) {
|
|
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)
|
|
102
|
+
|
|
103
|
+
// List navigation cursor
|
|
104
|
+
const [cursor, setCursor] = useState(0)
|
|
105
|
+
|
|
106
|
+
// Ollama
|
|
107
|
+
const [ollamaModel, setOllamaModel] = useState('')
|
|
108
|
+
const [ollamaStatus, setOllamaStatus] = useState<ReturnType<typeof checkOllama> | null>(null)
|
|
109
|
+
|
|
110
|
+
// ── Derived data ──
|
|
111
|
+
|
|
112
|
+
const providerList = CLOUD_PROVIDERS
|
|
113
|
+
const modelList = providerId ? getActiveModels(providerId) : []
|
|
114
|
+
|
|
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 ──
|
|
123
|
+
|
|
124
|
+
const finishCloud = () => {
|
|
125
|
+
if (!providerId || !modelId || !apiKey.trim()) {
|
|
126
|
+
setError('请填写 API Key')
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
writeConfigFile(providerId, modelId, apiKey.trim())
|
|
131
|
+
onComplete({ providerId, modelId, apiKey: apiKey.trim() })
|
|
132
|
+
} catch (err) {
|
|
133
|
+
setError(`写入配置失败: ${String(err)}`)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const finishLocal = () => {
|
|
138
|
+
const model = ollamaModel.trim() || 'llama3.2'
|
|
139
|
+
try {
|
|
140
|
+
writeConfigFile('ollama', model, 'ollama-local')
|
|
141
|
+
onComplete({ providerId: 'ollama', modelId: model, apiKey: 'ollama-local' })
|
|
142
|
+
} catch (err) {
|
|
143
|
+
setError(`写入配置失败: ${String(err)}`)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Ollama initialization ──
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (step === 'ollama' && !ollamaStatus) {
|
|
151
|
+
setOllamaStatus(checkOllama())
|
|
152
|
+
}
|
|
153
|
+
}, [step, ollamaStatus])
|
|
154
|
+
|
|
155
|
+
// ── Keyboard: list navigation steps (no TextInput) ──
|
|
156
|
+
|
|
157
|
+
useInput((_input, key) => {
|
|
158
|
+
// Esc always skips
|
|
159
|
+
if (key.escape) {
|
|
160
|
+
if (step === 'welcome') {
|
|
161
|
+
onSkip()
|
|
162
|
+
return
|
|
163
|
+
}
|
|
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
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Welcome ──
|
|
199
|
+
if (step === 'welcome') {
|
|
200
|
+
if (key.return) goStep('mode')
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
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))
|
|
208
|
+
if (key.return) {
|
|
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')
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
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))
|
|
227
|
+
if (key.return) {
|
|
228
|
+
const p = providerList[cursor]
|
|
229
|
+
if (p) {
|
|
230
|
+
setProviderId(p.id)
|
|
231
|
+
goStep('model')
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
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))
|
|
242
|
+
if (key.return) {
|
|
243
|
+
const m = modelList[cursor]
|
|
244
|
+
if (m) {
|
|
245
|
+
setModelId(m.id)
|
|
246
|
+
goStep('apikey')
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── Confirm ──
|
|
253
|
+
if (step === 'confirm') {
|
|
254
|
+
if (key.return) {
|
|
255
|
+
if (mode === 'cloud') finishCloud()
|
|
256
|
+
else finishLocal()
|
|
257
|
+
}
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// ── Render ──
|
|
263
|
+
|
|
264
|
+
const dim = 'gray'
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<Box flexDirection="column" padding={1}>
|
|
268
|
+
{/* Header */}
|
|
269
|
+
<Box marginBottom={1}>
|
|
270
|
+
<Text color="#FFD700" bold>
|
|
271
|
+
Mipham Code
|
|
272
|
+
</Text>
|
|
273
|
+
<Text dimColor> — 配置向导</Text>
|
|
274
|
+
</Box>
|
|
275
|
+
<Box marginBottom={1}>
|
|
276
|
+
<Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
|
|
277
|
+
</Box>
|
|
278
|
+
|
|
279
|
+
{/* ── Welcome ── */}
|
|
280
|
+
{step === 'welcome' && (
|
|
281
|
+
<Box flexDirection="column">
|
|
282
|
+
<Box marginBottom={1}>
|
|
283
|
+
<Text bold color="cyan">
|
|
284
|
+
欢迎使用 Mipham Code!
|
|
285
|
+
</Text>
|
|
286
|
+
</Box>
|
|
287
|
+
<Text>在开始之前,需要先配置 AI 模型连接。</Text>
|
|
288
|
+
<Text>此向导只需 1 分钟,需要您准备好 API Key。</Text>
|
|
289
|
+
<Box marginTop={1}>
|
|
290
|
+
<Text color="yellow">按 Enter 开始 →</Text>
|
|
291
|
+
</Box>
|
|
292
|
+
<Box marginTop={1}>
|
|
293
|
+
<Text dimColor>Esc 跳过配置</Text>
|
|
294
|
+
</Box>
|
|
295
|
+
</Box>
|
|
296
|
+
)}
|
|
297
|
+
|
|
298
|
+
{/* ── Mode ── */}
|
|
299
|
+
{step === 'mode' && (
|
|
300
|
+
<Box flexDirection="column">
|
|
301
|
+
<Box marginBottom={1}>
|
|
302
|
+
<Text bold>选择模型连接方式:</Text>
|
|
303
|
+
</Box>
|
|
304
|
+
|
|
305
|
+
<Box marginBottom={1}>
|
|
306
|
+
<Text color={cursor === 0 ? SELECTED_COLOR : undefined}>
|
|
307
|
+
{cursor === 0 ? '▶' : ' '} 🌐 云端 AI 模型(推荐)
|
|
308
|
+
</Text>
|
|
309
|
+
</Box>
|
|
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)
|
|
315
|
+
</Text>
|
|
316
|
+
</Box>
|
|
317
|
+
<Text dimColor> 需要提前安装 Ollama 并下载模型</Text>
|
|
318
|
+
|
|
319
|
+
<Box marginTop={2}>
|
|
320
|
+
<Text dimColor>↑↓ 选择 · Enter 确认 · Esc 返回</Text>
|
|
321
|
+
</Box>
|
|
322
|
+
</Box>
|
|
323
|
+
)}
|
|
324
|
+
|
|
325
|
+
{/* ── Cloud Provider ── */}
|
|
326
|
+
{step === 'provider' && (
|
|
327
|
+
<Box flexDirection="column">
|
|
328
|
+
<Box marginBottom={1}>
|
|
329
|
+
<Text bold>选择 AI 模型提供商:</Text>
|
|
330
|
+
</Box>
|
|
331
|
+
{providerList.map((p, i) => (
|
|
332
|
+
<Box key={p.id} marginBottom={1}>
|
|
333
|
+
<Text color={i === cursor ? SELECTED_COLOR : undefined}>
|
|
334
|
+
{i === cursor ? '▶' : ' '} {p.name}
|
|
335
|
+
</Text>
|
|
336
|
+
<Text dimColor> ({p.models.filter((m) => m.status === 'active').length} 个模型)</Text>
|
|
337
|
+
</Box>
|
|
338
|
+
))}
|
|
339
|
+
<Box marginTop={1}>
|
|
340
|
+
<Text dimColor>↑↓ 选择 · Enter 确认 · Esc 返回</Text>
|
|
341
|
+
</Box>
|
|
342
|
+
</Box>
|
|
343
|
+
)}
|
|
344
|
+
|
|
345
|
+
{/* ── Cloud Model ── */}
|
|
346
|
+
{step === 'model' && (
|
|
347
|
+
<Box flexDirection="column">
|
|
348
|
+
<Box marginBottom={1}>
|
|
349
|
+
<Text bold>
|
|
350
|
+
选择模型({DEFAULT_PROVIDERS.find((p) => p.id === providerId)?.name || providerId}):
|
|
351
|
+
</Text>
|
|
352
|
+
</Box>
|
|
353
|
+
{modelList.map((m, i) => (
|
|
354
|
+
<Box key={m.id} marginBottom={1}>
|
|
355
|
+
<Text color={i === cursor ? SELECTED_COLOR : undefined}>
|
|
356
|
+
{i === cursor ? '▶' : ' '} {m.name}
|
|
357
|
+
</Text>
|
|
358
|
+
<Text dimColor> ({m.id})</Text>
|
|
359
|
+
</Box>
|
|
360
|
+
))}
|
|
361
|
+
<Box marginTop={1}>
|
|
362
|
+
<Text dimColor>↑↓ 选择 · Enter 确认 · Esc 返回</Text>
|
|
363
|
+
</Box>
|
|
364
|
+
</Box>
|
|
365
|
+
)}
|
|
366
|
+
|
|
367
|
+
{/* ── API Key input ── */}
|
|
368
|
+
{step === 'apikey' && (
|
|
369
|
+
<Box flexDirection="column">
|
|
370
|
+
<Box marginBottom={1}>
|
|
371
|
+
<Text bold>粘贴 API Key:</Text>
|
|
372
|
+
</Box>
|
|
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 && (
|
|
378
|
+
<Box marginBottom={1}>
|
|
379
|
+
<Text color="red">⚠ {error}</Text>
|
|
380
|
+
</Box>
|
|
381
|
+
)}
|
|
382
|
+
<Box marginTop={1}>
|
|
383
|
+
<Text color="yellow">🔑 </Text>
|
|
384
|
+
<TextInput
|
|
385
|
+
value={apiKey}
|
|
386
|
+
onChange={setApiKey}
|
|
387
|
+
onSubmit={() => goStep('confirm')}
|
|
388
|
+
placeholder="粘贴 API Key 后按 Enter..."
|
|
389
|
+
/>
|
|
390
|
+
</Box>
|
|
391
|
+
<Box marginTop={1}>
|
|
392
|
+
<Text dimColor>Enter 继续 · Esc 返回选模型</Text>
|
|
393
|
+
</Box>
|
|
394
|
+
</Box>
|
|
395
|
+
)}
|
|
396
|
+
|
|
397
|
+
{/* ── Ollama ── */}
|
|
398
|
+
{step === 'ollama' && (
|
|
399
|
+
<Box flexDirection="column">
|
|
400
|
+
<Box marginBottom={1}>
|
|
401
|
+
<Text bold>Ollama 本地模型配置:</Text>
|
|
402
|
+
</Box>
|
|
403
|
+
{ollamaStatus ? (
|
|
404
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
405
|
+
<Text>
|
|
406
|
+
状态:{ollamaStatus.installed ? '✅ 已安装' : '❌ 未安装'}
|
|
407
|
+
{ollamaStatus.running ? ' · 运行中' : ''}
|
|
408
|
+
</Text>
|
|
409
|
+
{ollamaStatus.models.length > 0 && (
|
|
410
|
+
<Text dimColor>已下载模型:{ollamaStatus.models.join(', ')}</Text>
|
|
411
|
+
)}
|
|
412
|
+
</Box>
|
|
413
|
+
) : (
|
|
414
|
+
<Text dimColor>正在检测 Ollama...</Text>
|
|
415
|
+
)}
|
|
416
|
+
<Box marginTop={1}>
|
|
417
|
+
<Text dimColor>输入 Ollama 模型名称(如 llama3.2, qwen2.5, deepseek-r1):</Text>
|
|
418
|
+
</Box>
|
|
419
|
+
<Box marginTop={1}>
|
|
420
|
+
<Text color="yellow">🖥️ </Text>
|
|
421
|
+
<TextInput
|
|
422
|
+
value={ollamaModel}
|
|
423
|
+
onChange={setOllamaModel}
|
|
424
|
+
onSubmit={() => goStep('confirm')}
|
|
425
|
+
placeholder="模型名,默认 llama3.2"
|
|
426
|
+
/>
|
|
427
|
+
</Box>
|
|
428
|
+
<Box marginTop={1}>
|
|
429
|
+
<Text dimColor>Enter 继续 · Esc 返回</Text>
|
|
430
|
+
</Box>
|
|
431
|
+
</Box>
|
|
432
|
+
)}
|
|
433
|
+
|
|
434
|
+
{/* ── Confirm ── */}
|
|
435
|
+
{step === 'confirm' && (
|
|
436
|
+
<Box flexDirection="column">
|
|
437
|
+
<Box marginBottom={1}>
|
|
438
|
+
<Text bold color="cyan">
|
|
439
|
+
配置确认:
|
|
440
|
+
</Text>
|
|
441
|
+
</Box>
|
|
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}>
|
|
454
|
+
<Text dimColor>配置文件将保存到 ~/.mipham/config.yml</Text>
|
|
455
|
+
</Box>
|
|
456
|
+
{error && (
|
|
457
|
+
<Box marginBottom={1}>
|
|
458
|
+
<Text color="red">⚠ {error}</Text>
|
|
459
|
+
</Box>
|
|
460
|
+
)}
|
|
461
|
+
<Box marginTop={1}>
|
|
462
|
+
<Text color="yellow">按 Enter 保存并启动 →</Text>
|
|
463
|
+
</Box>
|
|
464
|
+
<Box marginTop={1}>
|
|
465
|
+
<Text dimColor>Esc 返回修改</Text>
|
|
466
|
+
</Box>
|
|
467
|
+
</Box>
|
|
468
|
+
)}
|
|
469
|
+
|
|
470
|
+
{/* ── Done ── */}
|
|
471
|
+
{step === 'done' && (
|
|
472
|
+
<Box flexDirection="column">
|
|
473
|
+
<Box marginBottom={1}>
|
|
474
|
+
<Text color="green" bold>
|
|
475
|
+
✅ 配置完成!
|
|
476
|
+
</Text>
|
|
477
|
+
</Box>
|
|
478
|
+
<Text>Mipham Code 正在启动...</Text>
|
|
479
|
+
</Box>
|
|
480
|
+
)}
|
|
481
|
+
</Box>
|
|
482
|
+
)
|
|
483
|
+
}
|