@miphamai/cli 0.28.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/skills/mipham/self-audit.mipham-skill.md +46 -46
- package/src/core/auto-memory.ts +8 -5
- package/src/core/crsi-sandbox.ts +11 -6
- package/src/index.tsx +77 -4
- package/src/shared/constants.ts +27 -0
- package/src/ui/app.tsx +41 -10
- package/src/ui/chat.tsx +13 -5
- package/src/ui/config-wizard.tsx +519 -0
package/package.json
CHANGED
|
@@ -24,58 +24,58 @@ Mipham Code 审计 Mipham Code — 这是 CRSI 递归自我改进的第一个闭
|
|
|
24
24
|
|
|
25
25
|
### 1. 代码质量
|
|
26
26
|
|
|
27
|
-
| 检查项
|
|
28
|
-
|
|
29
|
-
| Dead code
|
|
30
|
-
| 不一致模式
|
|
31
|
-
| 类型安全
|
|
32
|
-
| 错误处理
|
|
33
|
-
| Deep nesting | 搜索嵌套超过 4 层的 if/for/switch
|
|
27
|
+
| 检查项 | 方法 |
|
|
28
|
+
| ------------ | --------------------------------------------- |
|
|
29
|
+
| Dead code | Grep 搜索未被引用的 export、未使用的 import |
|
|
30
|
+
| 不一致模式 | 对比同一目录下多个文件的代码风格/模式差异 |
|
|
31
|
+
| 类型安全 | 搜索 `as any`、`@ts-ignore`、`unknown` 未收窄 |
|
|
32
|
+
| 错误处理 | 搜索裸 `catch`、无 `try/catch` 的 async 调用 |
|
|
33
|
+
| Deep nesting | 搜索嵌套超过 4 层的 if/for/switch |
|
|
34
34
|
|
|
35
35
|
### 2. 架构完整性
|
|
36
36
|
|
|
37
|
-
| 检查项
|
|
38
|
-
|
|
39
|
-
| 循环依赖
|
|
40
|
-
| 接口契约
|
|
41
|
-
| 模块边界
|
|
42
|
-
| God objects | 搜索超过 500 行的单个类/函数
|
|
37
|
+
| 检查项 | 方法 |
|
|
38
|
+
| ----------- | ---------------------------------------------------------- |
|
|
39
|
+
| 循环依赖 | 分析 import 图,检测 A→B→A |
|
|
40
|
+
| 接口契约 | 对比 `shared/types.ts` 中的类型定义与实际使用 |
|
|
41
|
+
| 模块边界 | 检查是否有跨层级直接访问(ui/ 直接 import core/ 内部实现) |
|
|
42
|
+
| God objects | 搜索超过 500 行的单个类/函数 |
|
|
43
43
|
|
|
44
44
|
### 3. 性能
|
|
45
45
|
|
|
46
|
-
| 检查项
|
|
47
|
-
|
|
48
|
-
| 同步阻塞
|
|
46
|
+
| 检查项 | 方法 |
|
|
47
|
+
| ------------ | ----------------------------------------------- |
|
|
48
|
+
| 同步阻塞 | 搜索 `readFileSync`、`execSync` 在主线程中 |
|
|
49
49
|
| 内存泄漏风险 | 搜索未清理的 setInterval、EventEmitter listener |
|
|
50
|
-
| 渲染性能
|
|
51
|
-
| N+1 模式
|
|
50
|
+
| 渲染性能 | 检查 React memo/callback 使用是否完整 |
|
|
51
|
+
| N+1 模式 | 搜索在循环内的 I/O 操作 |
|
|
52
52
|
|
|
53
53
|
### 4. 安全
|
|
54
54
|
|
|
55
|
-
| 检查项
|
|
56
|
-
|
|
57
|
-
| 硬编码凭据 | 搜索 API key、token、password 字符串
|
|
58
|
-
| 路径遍历
|
|
59
|
-
| 命令注入
|
|
60
|
-
| 许可合规
|
|
55
|
+
| 检查项 | 方法 |
|
|
56
|
+
| ---------- | ---------------------------------------- |
|
|
57
|
+
| 硬编码凭据 | 搜索 API key、token、password 字符串 |
|
|
58
|
+
| 路径遍历 | 搜索使用用户输入的 `join`/`resolve` 路径 |
|
|
59
|
+
| 命令注入 | 搜索字符串拼接的 shell 命令 |
|
|
60
|
+
| 许可合规 | 检查 package.json 中的 copyleft 依赖 |
|
|
61
61
|
|
|
62
62
|
### 5. 测试覆盖
|
|
63
63
|
|
|
64
|
-
| 检查项
|
|
65
|
-
|
|
66
|
-
| 未测试模块
|
|
67
|
-
| 关键路径覆盖
|
|
64
|
+
| 检查项 | 方法 |
|
|
65
|
+
| --------------- | ------------------------------------------------- |
|
|
66
|
+
| 未测试模块 | Glob 所有 `src/**/*.ts`,对比 `test/` 目录 |
|
|
67
|
+
| 关键路径覆盖 | 识别 engine、permission、tools 层,检查测试 |
|
|
68
68
|
| Flaky test 风险 | 搜索 `setTimeout`、`Math.random`、Date 依赖的测试 |
|
|
69
|
-
| 边界测试缺失
|
|
69
|
+
| 边界测试缺失 | 检查主要函数的 null/undefined/empty 参数测试 |
|
|
70
70
|
|
|
71
71
|
### 6. CRSI 集成健康
|
|
72
72
|
|
|
73
|
-
| 检查项
|
|
74
|
-
|
|
75
|
-
| Rule 引擎状态
|
|
76
|
-
| 效果追踪
|
|
77
|
-
| 模式分析器
|
|
78
|
-
| AutoMemory 状态 | 检查复盘文件数量、CRSI 洞察统计
|
|
73
|
+
| 检查项 | 方法 |
|
|
74
|
+
| --------------- | ----------------------------------------------------- |
|
|
75
|
+
| Rule 引擎状态 | 检查活跃规则数、禁用规则数、builtin vs auto-generated |
|
|
76
|
+
| 效果追踪 | 从 EffectivenessTracker 读取规则成功率 |
|
|
77
|
+
| 模式分析器 | 检查累积的 Agent 失败模式 |
|
|
78
|
+
| AutoMemory 状态 | 检查复盘文件数量、CRSI 洞察统计 |
|
|
79
79
|
|
|
80
80
|
## 执行流程
|
|
81
81
|
|
|
@@ -125,14 +125,14 @@ Mipham Code 审计 Mipham Code — 这是 CRSI 递归自我改进的第一个闭
|
|
|
125
125
|
|
|
126
126
|
## 摘要
|
|
127
127
|
|
|
128
|
-
| 维度
|
|
129
|
-
|
|
130
|
-
| 代码质量
|
|
131
|
-
| 架构完整性 | 8/10 | 3
|
|
132
|
-
| 性能
|
|
133
|
-
| 安全
|
|
134
|
-
| 测试覆盖
|
|
135
|
-
| CRSI 健康
|
|
128
|
+
| 维度 | 评分 | 发现数 | 严重 |
|
|
129
|
+
| ---------- | ---- | ------ | ---- |
|
|
130
|
+
| 代码质量 | 7/10 | 12 | 2 |
|
|
131
|
+
| 架构完整性 | 8/10 | 3 | 0 |
|
|
132
|
+
| 性能 | 7/10 | 5 | 1 |
|
|
133
|
+
| 安全 | 8/10 | 2 | 0 |
|
|
134
|
+
| 测试覆盖 | 7/10 | 8 | 1 |
|
|
135
|
+
| CRSI 健康 | 9/10 | 0 | 0 |
|
|
136
136
|
|
|
137
137
|
## 🔴 严重 (需要立即处理)
|
|
138
138
|
|
|
@@ -148,9 +148,9 @@ Mipham Code 审计 Mipham Code — 这是 CRSI 递归自我改进的第一个闭
|
|
|
148
148
|
|
|
149
149
|
## CRSI 规则更新
|
|
150
150
|
|
|
151
|
-
| 规则 ID | 类型 | 状态
|
|
152
|
-
|
|
153
|
-
| ...
|
|
151
|
+
| 规则 ID | 类型 | 状态 | 上次评估 |
|
|
152
|
+
| ------- | ---- | ------------------------ | -------- |
|
|
153
|
+
| ... | ... | active/degraded/disabled | ... |
|
|
154
154
|
```
|
|
155
155
|
|
|
156
156
|
## 安全约束
|
package/src/core/auto-memory.ts
CHANGED
|
@@ -419,10 +419,7 @@ export class AutoMemoryEngine {
|
|
|
419
419
|
return decisions.slice(0, 5)
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
-
private extractActionItems(
|
|
423
|
-
assistantContent: string,
|
|
424
|
-
toolCalls: ToolCallRecord[],
|
|
425
|
-
): string[] {
|
|
422
|
+
private extractActionItems(assistantContent: string, toolCalls: ToolCallRecord[]): string[] {
|
|
426
423
|
const items: string[] = []
|
|
427
424
|
|
|
428
425
|
// Check for TODO markers in assistant content
|
|
@@ -594,7 +591,13 @@ export class AutoMemoryEngine {
|
|
|
594
591
|
`| 成功率 | ${totalSuccesses + totalFailures > 0 ? Math.round((totalSuccesses / (totalSuccesses + totalFailures)) * 100) : 100}% |`,
|
|
595
592
|
'',
|
|
596
593
|
...(criticalInsights.length > 0
|
|
597
|
-
? [
|
|
594
|
+
? [
|
|
595
|
+
'## ⚠️ 关键洞察',
|
|
596
|
+
'',
|
|
597
|
+
...criticalInsights.map(
|
|
598
|
+
(i) => `- **${i.category}**: ${i.description}\n → ${i.suggestion}`,
|
|
599
|
+
),
|
|
600
|
+
]
|
|
598
601
|
: []),
|
|
599
602
|
'',
|
|
600
603
|
'---',
|
package/src/core/crsi-sandbox.ts
CHANGED
|
@@ -52,7 +52,8 @@ export interface CrsiModificationResult {
|
|
|
52
52
|
/** Error message if something failed */
|
|
53
53
|
error?: string
|
|
54
54
|
/** Current phase of the sandbox pipeline */
|
|
55
|
-
phase:
|
|
55
|
+
phase:
|
|
56
|
+
'pending' | 'applied' | 'testing' | 'passed' | 'failed' | 'approved' | 'merged' | 'rolled-back'
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export interface CrsiTestResult {
|
|
@@ -154,7 +155,12 @@ export class CrsiSandbox {
|
|
|
154
155
|
*/
|
|
155
156
|
applyModification(mod: CrsiModification): CrsiModificationResult {
|
|
156
157
|
if (!this.worktreePath) {
|
|
157
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
modification: mod,
|
|
160
|
+
applied: false,
|
|
161
|
+
phase: 'pending',
|
|
162
|
+
error: 'No worktree created. Call createWorktree() first.',
|
|
163
|
+
}
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
this.sessionReport.summary.total++
|
|
@@ -180,7 +186,8 @@ export class CrsiSandbox {
|
|
|
180
186
|
try {
|
|
181
187
|
const currentContent = readFileSync(targetPath, 'utf-8')
|
|
182
188
|
if (currentContent !== mod.originalContent && mod.originalContent) {
|
|
183
|
-
result.error =
|
|
189
|
+
result.error =
|
|
190
|
+
'Original content mismatch — file may have been modified since the audit. Aborting.'
|
|
184
191
|
result.phase = 'failed'
|
|
185
192
|
this.sessionReport.modifications.push(result)
|
|
186
193
|
return result
|
|
@@ -313,9 +320,7 @@ export class CrsiSandbox {
|
|
|
313
320
|
}
|
|
314
321
|
|
|
315
322
|
// Check that all modifications passed tests
|
|
316
|
-
const failedCount = this.sessionReport.modifications.filter(
|
|
317
|
-
(m) => m.phase === 'failed',
|
|
318
|
-
).length
|
|
323
|
+
const failedCount = this.sessionReport.modifications.filter((m) => m.phase === 'failed').length
|
|
319
324
|
if (failedCount > 0) {
|
|
320
325
|
return {
|
|
321
326
|
success: false,
|
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
|
{
|
package/src/ui/app.tsx
CHANGED
|
@@ -44,6 +44,8 @@ export interface ToolMeta {
|
|
|
44
44
|
input: string
|
|
45
45
|
output?: string
|
|
46
46
|
collapsed: boolean
|
|
47
|
+
/** Original tool name before Claude Code display renaming (e.g. Write→Update). */
|
|
48
|
+
originalName?: string
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export interface ChatMessage {
|
|
@@ -87,7 +89,8 @@ const PERMISSION_COLORS: Record<PermissionMode, string> = {
|
|
|
87
89
|
bypassPermissions: 'red',
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
/** Format a tool's input parameters into a compact one-line detail string.
|
|
92
|
+
/** Format a tool's input parameters into a compact one-line detail string.
|
|
93
|
+
* Tool display names follow Claude Code convention: Write/Edit → Update. */
|
|
91
94
|
function formatToolDetail(name: string, input: Record<string, unknown>): string {
|
|
92
95
|
switch (name) {
|
|
93
96
|
case 'Bash':
|
|
@@ -95,9 +98,9 @@ function formatToolDetail(name: string, input: Record<string, unknown>): string
|
|
|
95
98
|
case 'Read':
|
|
96
99
|
return (input.file_path as string) || ''
|
|
97
100
|
case 'Write':
|
|
98
|
-
return (input.file_path as string) || ''
|
|
101
|
+
return (input.file_path as string) || '' // displayed as "Update" in chat
|
|
99
102
|
case 'Edit':
|
|
100
|
-
return (input.file_path as string) || ''
|
|
103
|
+
return `${(input.file_path as string) || ''}: ${((input.old_string as string) || '').slice(0, 60)}` // displayed as "Update" in chat
|
|
101
104
|
case 'Grep':
|
|
102
105
|
return (input.pattern as string) || ''
|
|
103
106
|
case 'Glob':
|
|
@@ -115,6 +118,12 @@ function formatToolDetail(name: string, input: Record<string, unknown>): string
|
|
|
115
118
|
}
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
/** Claude Code display name: maps Write/Edit → Update for parity. */
|
|
122
|
+
function toolDisplayName(name: string): string {
|
|
123
|
+
if (name === 'Write' || name === 'Edit') return 'Update'
|
|
124
|
+
return name
|
|
125
|
+
}
|
|
126
|
+
|
|
118
127
|
export function App({
|
|
119
128
|
engine,
|
|
120
129
|
config,
|
|
@@ -462,7 +471,12 @@ export function App({
|
|
|
462
471
|
{
|
|
463
472
|
role: 'system' as const,
|
|
464
473
|
content: detail,
|
|
465
|
-
toolMeta: {
|
|
474
|
+
toolMeta: {
|
|
475
|
+
name: toolDisplayName(toolName),
|
|
476
|
+
input: detail,
|
|
477
|
+
collapsed: true,
|
|
478
|
+
originalName: toolName,
|
|
479
|
+
},
|
|
466
480
|
},
|
|
467
481
|
])
|
|
468
482
|
// Flush stream buffer before showing tool card
|
|
@@ -475,18 +489,35 @@ export function App({
|
|
|
475
489
|
setAgentProgress(null)
|
|
476
490
|
// Sync background agents (e.g. Agent tool may have spawned them)
|
|
477
491
|
syncBgAgents()
|
|
478
|
-
// Only show tool results with meaningful content.
|
|
479
|
-
// Skip empty results, "(no matches)", and other noise.
|
|
480
492
|
const output = chunk.content ? String(chunk.content).trim() : ''
|
|
481
493
|
if (output && output !== '(no matches)' && output.length > 20) {
|
|
482
|
-
|
|
483
|
-
|
|
494
|
+
// Generate diff-style summary for file operations (Claude Code parity)
|
|
495
|
+
let preview: string
|
|
496
|
+
const isFileOp = chunk.toolUse?.name === 'Write' || chunk.toolUse?.name === 'Edit'
|
|
497
|
+
if (isFileOp && chunk.toolUse?.input?.file_path) {
|
|
498
|
+
const filePath = String(chunk.toolUse.input.file_path)
|
|
499
|
+
const newContent = String(
|
|
500
|
+
chunk.toolUse.input.content || chunk.toolUse.input.new_string || '',
|
|
501
|
+
)
|
|
502
|
+
// Count lines added/removed for Write operations
|
|
503
|
+
const lineCount = newContent.split('\n').length
|
|
504
|
+
preview = `Updated ${filePath} · ${lineCount} lines`
|
|
505
|
+
} else {
|
|
506
|
+
const firstLine = output.split('\n')[0]!.slice(0, 200)
|
|
507
|
+
preview = firstLine.length < output.length ? `${firstLine}...` : firstLine
|
|
508
|
+
}
|
|
484
509
|
setMessages((prev) => [
|
|
485
510
|
...prev,
|
|
486
511
|
{
|
|
487
512
|
role: 'system' as const,
|
|
488
|
-
content:
|
|
489
|
-
toolMeta: {
|
|
513
|
+
content: preview,
|
|
514
|
+
toolMeta: {
|
|
515
|
+
name: '',
|
|
516
|
+
input: '',
|
|
517
|
+
output: preview,
|
|
518
|
+
collapsed: true,
|
|
519
|
+
originalName: chunk.toolUse?.name,
|
|
520
|
+
},
|
|
490
521
|
},
|
|
491
522
|
])
|
|
492
523
|
}
|
package/src/ui/chat.tsx
CHANGED
|
@@ -28,6 +28,7 @@ function toolColor(name: string): string {
|
|
|
28
28
|
return 'white'
|
|
29
29
|
case 'Write':
|
|
30
30
|
case 'Edit':
|
|
31
|
+
case 'Update':
|
|
31
32
|
return 'red'
|
|
32
33
|
case 'Agent':
|
|
33
34
|
case 'Task':
|
|
@@ -59,12 +60,19 @@ const MessageRow = React.memo(
|
|
|
59
60
|
>
|
|
60
61
|
{msg.toolMeta ? (
|
|
61
62
|
<Box flexDirection="column">
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
{
|
|
65
|
-
|
|
63
|
+
{/* Tool call line: ⏺ ToolName(detail) — Claude Code parity */}
|
|
64
|
+
{msg.toolMeta.name ? (
|
|
65
|
+
<Text color={toolColor(msg.toolMeta.name)}>
|
|
66
|
+
{msg.toolMeta.collapsed ? '⏺' : '⏺ ▼'} {msg.toolMeta.name}
|
|
67
|
+
{msg.toolMeta.input ? ` (${msg.toolMeta.input.slice(0, 120)})` : ''}
|
|
68
|
+
</Text>
|
|
69
|
+
) : null}
|
|
70
|
+
{/* Tool result line: ⎿ summary text */}
|
|
66
71
|
{msg.toolMeta.output ? (
|
|
67
|
-
<Text dimColor>
|
|
72
|
+
<Text dimColor>
|
|
73
|
+
{' ⎿ '}
|
|
74
|
+
{msg.toolMeta.output.slice(0, 300)}
|
|
75
|
+
</Text>
|
|
68
76
|
) : msg.content && !msg.toolMeta.collapsed ? (
|
|
69
77
|
<Text dimColor>{msg.content}</Text>
|
|
70
78
|
) : null}
|
|
@@ -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
|
+
}
|