@yivan-lab/pretty-please 1.0.0 → 1.1.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/README.md +98 -27
- package/bin/pls.tsx +135 -24
- package/dist/bin/pls.d.ts +1 -1
- package/dist/bin/pls.js +117 -24
- package/dist/package.json +80 -0
- package/dist/src/ai.d.ts +1 -41
- package/dist/src/ai.js +9 -190
- package/dist/src/builtin-detector.d.ts +14 -8
- package/dist/src/builtin-detector.js +36 -16
- package/dist/src/chat-history.d.ts +16 -11
- package/dist/src/chat-history.js +26 -4
- package/dist/src/components/Chat.js +3 -3
- package/dist/src/components/CommandBox.js +1 -16
- package/dist/src/components/ConfirmationPrompt.d.ts +2 -1
- package/dist/src/components/ConfirmationPrompt.js +7 -3
- package/dist/src/components/MultiStepCommandGenerator.d.ts +2 -0
- package/dist/src/components/MultiStepCommandGenerator.js +110 -7
- package/dist/src/config.d.ts +27 -8
- package/dist/src/config.js +92 -33
- package/dist/src/history.d.ts +19 -5
- package/dist/src/history.js +26 -11
- package/dist/src/mastra-agent.d.ts +0 -1
- package/dist/src/mastra-agent.js +3 -4
- package/dist/src/mastra-chat.d.ts +28 -0
- package/dist/src/mastra-chat.js +93 -0
- package/dist/src/multi-step.d.ts +2 -2
- package/dist/src/multi-step.js +2 -2
- package/dist/src/prompts.d.ts +11 -0
- package/dist/src/prompts.js +140 -0
- package/dist/src/shell-hook.d.ts +35 -13
- package/dist/src/shell-hook.js +82 -7
- package/dist/src/sysinfo.d.ts +9 -5
- package/dist/src/sysinfo.js +2 -2
- package/dist/src/utils/console.d.ts +11 -11
- package/dist/src/utils/console.js +4 -6
- package/package.json +8 -6
- package/src/builtin-detector.ts +126 -0
- package/src/chat-history.ts +130 -0
- package/src/components/Chat.tsx +4 -4
- package/src/components/CommandBox.tsx +1 -16
- package/src/components/ConfirmationPrompt.tsx +9 -2
- package/src/components/MultiStepCommandGenerator.tsx +144 -7
- package/src/config.ts +309 -0
- package/src/history.ts +160 -0
- package/src/mastra-agent.ts +3 -4
- package/src/mastra-chat.ts +124 -0
- package/src/multi-step.ts +2 -2
- package/src/prompts.ts +154 -0
- package/src/shell-hook.ts +502 -0
- package/src/{sysinfo.js → sysinfo.ts} +28 -16
- package/src/utils/{console.js → console.ts} +16 -18
- package/bin/pls.js +0 -681
- package/src/ai.js +0 -324
- package/src/builtin-detector.js +0 -98
- package/src/chat-history.js +0 -94
- package/src/components/ChatStatus.tsx +0 -53
- package/src/components/CommandGenerator.tsx +0 -184
- package/src/components/ConfigDisplay.tsx +0 -64
- package/src/components/ConfigWizard.tsx +0 -101
- package/src/components/HistoryDisplay.tsx +0 -69
- package/src/components/HookManager.tsx +0 -150
- package/src/config.js +0 -221
- package/src/history.js +0 -131
- package/src/shell-hook.js +0 -393
|
@@ -6,16 +6,18 @@ interface ConfirmationPromptProps {
|
|
|
6
6
|
prompt: string
|
|
7
7
|
onConfirm: () => void
|
|
8
8
|
onCancel: () => void
|
|
9
|
+
onEdit?: () => void // 新增:编辑回调
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* ConfirmationPrompt 组件 - 单键确认提示
|
|
13
|
-
* 回车 = 确认,Esc = 取消,Ctrl+C = 退出
|
|
14
|
+
* 回车 = 确认,E = 编辑,Esc = 取消,Ctrl+C = 退出
|
|
14
15
|
*/
|
|
15
16
|
export const ConfirmationPrompt: React.FC<ConfirmationPromptProps> = ({
|
|
16
17
|
prompt,
|
|
17
18
|
onConfirm,
|
|
18
19
|
onCancel,
|
|
20
|
+
onEdit,
|
|
19
21
|
}) => {
|
|
20
22
|
useInput((input, key) => {
|
|
21
23
|
if (key.return) {
|
|
@@ -24,6 +26,9 @@ export const ConfirmationPrompt: React.FC<ConfirmationPromptProps> = ({
|
|
|
24
26
|
} else if (key.escape) {
|
|
25
27
|
// Esc 键
|
|
26
28
|
onCancel()
|
|
29
|
+
} else if ((input === 'e' || input === 'E') && onEdit) {
|
|
30
|
+
// E 键进入编辑模式
|
|
31
|
+
onEdit()
|
|
27
32
|
} else if (key.ctrl && input === 'c') {
|
|
28
33
|
// Ctrl+C
|
|
29
34
|
process.exit(0)
|
|
@@ -35,7 +40,9 @@ export const ConfirmationPrompt: React.FC<ConfirmationPromptProps> = ({
|
|
|
35
40
|
<Text bold color={theme.warning}>
|
|
36
41
|
{prompt}
|
|
37
42
|
</Text>
|
|
38
|
-
<Text color={theme.text.secondary}>
|
|
43
|
+
<Text color={theme.text.secondary}>
|
|
44
|
+
{onEdit ? ' [回车执行 / E 编辑 / Esc 取消] ' : ' [回车执行 / Esc 取消] '}
|
|
45
|
+
</Text>
|
|
39
46
|
</Text>
|
|
40
47
|
)
|
|
41
48
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react'
|
|
2
|
-
import { Box, Text } from 'ink'
|
|
2
|
+
import { Box, Text, useInput } from 'ink'
|
|
3
|
+
import TextInput from 'ink-text-input'
|
|
3
4
|
import Spinner from 'ink-spinner'
|
|
4
5
|
import { generateMultiStepCommand, type CommandStep, type ExecutedStep } from '../multi-step.js'
|
|
5
6
|
import { detectBuiltin, formatBuiltins } from '../builtin-detector.js'
|
|
@@ -7,12 +8,15 @@ import { CommandBox } from './CommandBox.js'
|
|
|
7
8
|
import { ConfirmationPrompt } from './ConfirmationPrompt.js'
|
|
8
9
|
import { Duration } from './Duration.js'
|
|
9
10
|
import { theme } from '../ui/theme.js'
|
|
11
|
+
import { getConfig } from '../config.js'
|
|
10
12
|
|
|
11
13
|
interface MultiStepCommandGeneratorProps {
|
|
12
14
|
prompt: string
|
|
13
15
|
debug?: boolean
|
|
14
16
|
onStepComplete: (step: {
|
|
15
17
|
command: string
|
|
18
|
+
aiGeneratedCommand?: string // 新增:AI 生成的原始命令
|
|
19
|
+
userModified?: boolean // 新增:用户是否修改
|
|
16
20
|
confirmed: boolean
|
|
17
21
|
cancelled?: boolean
|
|
18
22
|
hasBuiltin?: boolean
|
|
@@ -29,6 +33,7 @@ interface MultiStepCommandGeneratorProps {
|
|
|
29
33
|
type State =
|
|
30
34
|
| { type: 'thinking' }
|
|
31
35
|
| { type: 'showing_command'; stepData: CommandStep }
|
|
36
|
+
| { type: 'editing'; stepData: CommandStep } // 新增:编辑状态
|
|
32
37
|
| { type: 'cancelled'; command: string }
|
|
33
38
|
| { type: 'error'; error: string }
|
|
34
39
|
|
|
@@ -46,6 +51,17 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
|
|
|
46
51
|
const [state, setState] = useState<State>({ type: 'thinking' })
|
|
47
52
|
const [thinkDuration, setThinkDuration] = useState(0)
|
|
48
53
|
const [debugInfo, setDebugInfo] = useState<any>(null)
|
|
54
|
+
const [editedCommand, setEditedCommand] = useState('') // 新增:编辑后的命令
|
|
55
|
+
|
|
56
|
+
// 监听编辑模式下的 Esc 键
|
|
57
|
+
useInput(
|
|
58
|
+
(input, key) => {
|
|
59
|
+
if (state.type === 'editing' && key.escape) {
|
|
60
|
+
handleEditCancel()
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{ isActive: state.type === 'editing' }
|
|
64
|
+
)
|
|
49
65
|
|
|
50
66
|
// 初始化:调用 Mastra 生成命令
|
|
51
67
|
useEffect(() => {
|
|
@@ -61,15 +77,29 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
|
|
|
61
77
|
setDebugInfo(result.debugInfo)
|
|
62
78
|
}
|
|
63
79
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
80
|
+
// 如果 AI 返回空命令且决定不继续,说明 AI 放弃了
|
|
81
|
+
// 直接结束,不显示命令框
|
|
82
|
+
if (!result.stepData.command.trim() && result.stepData.continue === false) {
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
onStepComplete({
|
|
85
|
+
command: '',
|
|
86
|
+
confirmed: false,
|
|
87
|
+
reasoning: result.stepData.reasoning,
|
|
88
|
+
needsContinue: false,
|
|
89
|
+
})
|
|
90
|
+
}, 100)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
68
93
|
|
|
69
|
-
// 检测 builtin
|
|
94
|
+
// 检测 builtin(优先检测)
|
|
70
95
|
const { hasBuiltin, builtins } = detectBuiltin(result.stepData.command)
|
|
71
96
|
|
|
72
97
|
if (hasBuiltin) {
|
|
98
|
+
// 有 builtin,不管什么模式都不编辑,直接提示
|
|
99
|
+
setState({
|
|
100
|
+
type: 'showing_command',
|
|
101
|
+
stepData: result.stepData,
|
|
102
|
+
})
|
|
73
103
|
setTimeout(() => {
|
|
74
104
|
onStepComplete({
|
|
75
105
|
command: result.stepData.command,
|
|
@@ -80,6 +110,26 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
|
|
|
80
110
|
needsContinue: result.stepData.continue,
|
|
81
111
|
})
|
|
82
112
|
}, 100)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 根据 editMode 决定进入哪个状态
|
|
117
|
+
const config = getConfig()
|
|
118
|
+
const autoEdit = config.editMode === 'auto'
|
|
119
|
+
|
|
120
|
+
if (autoEdit) {
|
|
121
|
+
// auto 模式:直接进入编辑状态
|
|
122
|
+
setEditedCommand(result.stepData.command)
|
|
123
|
+
setState({
|
|
124
|
+
type: 'editing',
|
|
125
|
+
stepData: result.stepData,
|
|
126
|
+
})
|
|
127
|
+
} else {
|
|
128
|
+
// manual 模式:显示命令,等待用户操作
|
|
129
|
+
setState({
|
|
130
|
+
type: 'showing_command',
|
|
131
|
+
stepData: result.stepData,
|
|
132
|
+
})
|
|
83
133
|
}
|
|
84
134
|
})
|
|
85
135
|
.catch((error: any) => {
|
|
@@ -99,6 +149,33 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
|
|
|
99
149
|
if (state.type === 'showing_command') {
|
|
100
150
|
onStepComplete({
|
|
101
151
|
command: state.stepData.command,
|
|
152
|
+
aiGeneratedCommand: state.stepData.command, // 原始命令
|
|
153
|
+
userModified: false,
|
|
154
|
+
confirmed: true,
|
|
155
|
+
reasoning: state.stepData.reasoning,
|
|
156
|
+
needsContinue: state.stepData.continue,
|
|
157
|
+
nextStepHint: state.stepData.nextStepHint,
|
|
158
|
+
debugInfo: debugInfo,
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 处理编辑
|
|
164
|
+
const handleEdit = () => {
|
|
165
|
+
if (state.type === 'showing_command') {
|
|
166
|
+
setEditedCommand(state.stepData.command) // 初始化为 AI 生成的命令
|
|
167
|
+
setState({ type: 'editing', stepData: state.stepData })
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 编辑完成确认
|
|
172
|
+
const handleEditConfirm = () => {
|
|
173
|
+
if (state.type === 'editing') {
|
|
174
|
+
const modified = editedCommand !== state.stepData.command
|
|
175
|
+
onStepComplete({
|
|
176
|
+
command: editedCommand, // 使用编辑后的命令
|
|
177
|
+
aiGeneratedCommand: state.stepData.command, // 保存 AI 原始命令
|
|
178
|
+
userModified: modified,
|
|
102
179
|
confirmed: true,
|
|
103
180
|
reasoning: state.stepData.reasoning,
|
|
104
181
|
needsContinue: state.stepData.continue,
|
|
@@ -108,6 +185,28 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
|
|
|
108
185
|
}
|
|
109
186
|
}
|
|
110
187
|
|
|
188
|
+
// 取消编辑
|
|
189
|
+
const handleEditCancel = () => {
|
|
190
|
+
if (state.type === 'editing') {
|
|
191
|
+
const config = getConfig()
|
|
192
|
+
|
|
193
|
+
if (config.editMode === 'auto') {
|
|
194
|
+
// auto 模式:Esc 直接取消整个操作
|
|
195
|
+
setState({ type: 'cancelled', command: state.stepData.command })
|
|
196
|
+
setTimeout(() => {
|
|
197
|
+
onStepComplete({
|
|
198
|
+
command: state.stepData.command,
|
|
199
|
+
confirmed: false,
|
|
200
|
+
cancelled: true,
|
|
201
|
+
})
|
|
202
|
+
}, 100)
|
|
203
|
+
} else {
|
|
204
|
+
// manual 模式:Esc 返回到 showing_command 状态
|
|
205
|
+
setState({ type: 'showing_command', stepData: state.stepData })
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
111
210
|
// 处理取消
|
|
112
211
|
const handleCancel = () => {
|
|
113
212
|
if (state.type === 'showing_command') {
|
|
@@ -206,8 +305,46 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
|
|
|
206
305
|
|
|
207
306
|
{/* 确认提示 */}
|
|
208
307
|
{!detectBuiltin(state.stepData.command).hasBuiltin && (
|
|
209
|
-
<ConfirmationPrompt
|
|
308
|
+
<ConfirmationPrompt
|
|
309
|
+
prompt="执行?"
|
|
310
|
+
onConfirm={handleConfirm}
|
|
311
|
+
onCancel={handleCancel}
|
|
312
|
+
onEdit={handleEdit} // 新增:编辑回调
|
|
313
|
+
/>
|
|
314
|
+
)}
|
|
315
|
+
</>
|
|
316
|
+
)}
|
|
317
|
+
|
|
318
|
+
{/* 编辑模式 */}
|
|
319
|
+
{state.type === 'editing' && (
|
|
320
|
+
<>
|
|
321
|
+
{/* 步骤信息(仅多步骤时显示) */}
|
|
322
|
+
{state.stepData.continue === true && (
|
|
323
|
+
<Box flexDirection="column" marginTop={1}>
|
|
324
|
+
<Text color={theme.text.secondary}>步骤 {currentStepNumber}/?</Text>
|
|
325
|
+
{state.stepData.reasoning && (
|
|
326
|
+
<Text color={theme.text.muted}>原因: {state.stepData.reasoning}</Text>
|
|
327
|
+
)}
|
|
328
|
+
</Box>
|
|
210
329
|
)}
|
|
330
|
+
|
|
331
|
+
{/* 命令框(AI 建议) */}
|
|
332
|
+
<CommandBox command={state.stepData.command} />
|
|
333
|
+
|
|
334
|
+
{/* 编辑框 */}
|
|
335
|
+
<Box flexDirection="row">
|
|
336
|
+
<Text color={theme.primary}>{'> '}</Text>
|
|
337
|
+
<TextInput
|
|
338
|
+
value={editedCommand}
|
|
339
|
+
onChange={setEditedCommand}
|
|
340
|
+
onSubmit={handleEditConfirm}
|
|
341
|
+
/>
|
|
342
|
+
</Box>
|
|
343
|
+
<Box marginTop={1}>
|
|
344
|
+
<Text color={theme.text.secondary}>
|
|
345
|
+
{getConfig().editMode === 'auto' ? '[回车执行 / Esc 取消]' : '[回车执行 / Esc 返回]'}
|
|
346
|
+
</Text>
|
|
347
|
+
</Box>
|
|
211
348
|
</>
|
|
212
349
|
)}
|
|
213
350
|
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import readline from 'readline'
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
|
|
7
|
+
// 配置文件路径
|
|
8
|
+
export const CONFIG_DIR = path.join(os.homedir(), '.please')
|
|
9
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
|
|
10
|
+
|
|
11
|
+
// 支持的 Provider 列表
|
|
12
|
+
const VALID_PROVIDERS = [
|
|
13
|
+
'openai',
|
|
14
|
+
'anthropic',
|
|
15
|
+
'deepseek',
|
|
16
|
+
'google',
|
|
17
|
+
'groq',
|
|
18
|
+
'mistral',
|
|
19
|
+
'cohere',
|
|
20
|
+
'fireworks',
|
|
21
|
+
'together',
|
|
22
|
+
] as const
|
|
23
|
+
|
|
24
|
+
type Provider = (typeof VALID_PROVIDERS)[number]
|
|
25
|
+
|
|
26
|
+
// 编辑模式
|
|
27
|
+
const VALID_EDIT_MODES = ['manual', 'auto'] as const
|
|
28
|
+
type EditMode = (typeof VALID_EDIT_MODES)[number]
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 配置接口
|
|
32
|
+
*/
|
|
33
|
+
export interface Config {
|
|
34
|
+
apiKey: string
|
|
35
|
+
baseUrl: string
|
|
36
|
+
model: string
|
|
37
|
+
provider: Provider
|
|
38
|
+
shellHook: boolean
|
|
39
|
+
chatHistoryLimit: number
|
|
40
|
+
commandHistoryLimit: number
|
|
41
|
+
shellHistoryLimit: number
|
|
42
|
+
editMode: EditMode
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 默认配置
|
|
47
|
+
*/
|
|
48
|
+
const DEFAULT_CONFIG: Config = {
|
|
49
|
+
apiKey: '',
|
|
50
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
51
|
+
model: 'gpt-4-turbo',
|
|
52
|
+
provider: 'openai',
|
|
53
|
+
shellHook: false,
|
|
54
|
+
chatHistoryLimit: 10,
|
|
55
|
+
commandHistoryLimit: 10,
|
|
56
|
+
shellHistoryLimit: 15,
|
|
57
|
+
editMode: 'manual',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 确保配置目录存在
|
|
62
|
+
*/
|
|
63
|
+
function ensureConfigDir(): void {
|
|
64
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
65
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 读取配置
|
|
71
|
+
*/
|
|
72
|
+
export function getConfig(): Config {
|
|
73
|
+
ensureConfigDir()
|
|
74
|
+
|
|
75
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
76
|
+
return { ...DEFAULT_CONFIG }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const content = fs.readFileSync(CONFIG_FILE, 'utf-8')
|
|
81
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(content) }
|
|
82
|
+
} catch {
|
|
83
|
+
return { ...DEFAULT_CONFIG }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 保存配置
|
|
89
|
+
*/
|
|
90
|
+
export function saveConfig(config: Config): void {
|
|
91
|
+
ensureConfigDir()
|
|
92
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 设置单个配置项
|
|
97
|
+
*/
|
|
98
|
+
export function setConfigValue(key: string, value: string | boolean | number): Config {
|
|
99
|
+
const config = getConfig()
|
|
100
|
+
|
|
101
|
+
if (!(key in DEFAULT_CONFIG)) {
|
|
102
|
+
throw new Error(`未知的配置项: ${key}`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 处理特殊类型
|
|
106
|
+
if (key === 'shellHook') {
|
|
107
|
+
config.shellHook = value === 'true' || value === true
|
|
108
|
+
} else if (key === 'chatHistoryLimit' || key === 'commandHistoryLimit' || key === 'shellHistoryLimit') {
|
|
109
|
+
const num = typeof value === 'number' ? value : parseInt(String(value), 10)
|
|
110
|
+
if (isNaN(num) || num < 1) {
|
|
111
|
+
throw new Error(`${key} 必须是大于 0 的整数`)
|
|
112
|
+
}
|
|
113
|
+
config[key] = num
|
|
114
|
+
} else if (key === 'provider') {
|
|
115
|
+
const strValue = String(value)
|
|
116
|
+
if (!VALID_PROVIDERS.includes(strValue as Provider)) {
|
|
117
|
+
throw new Error(`provider 必须是以下之一: ${VALID_PROVIDERS.join(', ')}`)
|
|
118
|
+
}
|
|
119
|
+
config.provider = strValue as Provider
|
|
120
|
+
} else if (key === 'editMode') {
|
|
121
|
+
const strValue = String(value)
|
|
122
|
+
if (!VALID_EDIT_MODES.includes(strValue as EditMode)) {
|
|
123
|
+
throw new Error(`editMode 必须是以下之一: ${VALID_EDIT_MODES.join(', ')}`)
|
|
124
|
+
}
|
|
125
|
+
config.editMode = strValue as EditMode
|
|
126
|
+
} else if (key === 'apiKey' || key === 'baseUrl' || key === 'model') {
|
|
127
|
+
config[key] = String(value)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
saveConfig(config)
|
|
131
|
+
return config
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 检查配置是否有效
|
|
136
|
+
*/
|
|
137
|
+
export function isConfigValid(): boolean {
|
|
138
|
+
const config = getConfig()
|
|
139
|
+
return config.apiKey.length > 0
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 隐藏 API Key 中间部分
|
|
144
|
+
*/
|
|
145
|
+
export function maskApiKey(apiKey: string): string {
|
|
146
|
+
if (!apiKey || apiKey.length < 10) return apiKey || '(未设置)'
|
|
147
|
+
return apiKey.slice(0, 6) + '****' + apiKey.slice(-4)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 显示当前配置
|
|
152
|
+
*/
|
|
153
|
+
export function displayConfig(): void {
|
|
154
|
+
const config = getConfig()
|
|
155
|
+
console.log(chalk.bold('\n当前配置:'))
|
|
156
|
+
console.log(chalk.gray('━'.repeat(50)))
|
|
157
|
+
console.log(` ${chalk.cyan('apiKey')}: ${maskApiKey(config.apiKey)}`)
|
|
158
|
+
console.log(` ${chalk.cyan('baseUrl')}: ${config.baseUrl}`)
|
|
159
|
+
console.log(` ${chalk.cyan('provider')}: ${config.provider}`)
|
|
160
|
+
console.log(` ${chalk.cyan('model')}: ${config.model}`)
|
|
161
|
+
console.log(
|
|
162
|
+
` ${chalk.cyan('shellHook')}: ${config.shellHook ? chalk.green('已启用') : chalk.gray('未启用')}`
|
|
163
|
+
)
|
|
164
|
+
console.log(
|
|
165
|
+
` ${chalk.cyan('editMode')}: ${
|
|
166
|
+
config.editMode === 'auto' ? chalk.hex('#00D9FF')('auto (自动编辑)') : chalk.gray('manual (按E编辑)')
|
|
167
|
+
}`
|
|
168
|
+
)
|
|
169
|
+
console.log(` ${chalk.cyan('chatHistoryLimit')}: ${config.chatHistoryLimit} 轮`)
|
|
170
|
+
console.log(` ${chalk.cyan('commandHistoryLimit')}: ${config.commandHistoryLimit} 条`)
|
|
171
|
+
console.log(` ${chalk.cyan('shellHistoryLimit')}: ${config.shellHistoryLimit} 条`)
|
|
172
|
+
console.log(chalk.gray('━'.repeat(50)))
|
|
173
|
+
console.log(chalk.gray(`配置文件: ${CONFIG_FILE}\n`))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 创建 readline 接口
|
|
178
|
+
*/
|
|
179
|
+
function createReadlineInterface(): readline.Interface {
|
|
180
|
+
return readline.createInterface({
|
|
181
|
+
input: process.stdin,
|
|
182
|
+
output: process.stdout,
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 异步提问
|
|
188
|
+
*/
|
|
189
|
+
function question(rl: readline.Interface, prompt: string): Promise<string> {
|
|
190
|
+
return new Promise((resolve) => {
|
|
191
|
+
rl.question(prompt, (answer) => {
|
|
192
|
+
resolve(answer)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 交互式配置向导
|
|
199
|
+
*/
|
|
200
|
+
export async function runConfigWizard(): Promise<void> {
|
|
201
|
+
const rl = createReadlineInterface()
|
|
202
|
+
const config = getConfig()
|
|
203
|
+
|
|
204
|
+
console.log(chalk.bold.hex('#00D9FF')('\n🔧 Pretty Please 配置向导'))
|
|
205
|
+
console.log(chalk.gray('━'.repeat(50)))
|
|
206
|
+
console.log(chalk.gray('直接回车使用默认值,输入值后回车确认\n'))
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
// 1. Provider
|
|
210
|
+
const providerHint = chalk.gray(`(可选: ${VALID_PROVIDERS.join(', ')})`)
|
|
211
|
+
const providerPrompt = `${chalk.cyan('Provider')} ${providerHint}\n${chalk.gray('默认:')} ${chalk.yellow(config.provider)} ${chalk.gray('→')} `
|
|
212
|
+
const provider = await question(rl, providerPrompt)
|
|
213
|
+
if (provider.trim()) {
|
|
214
|
+
if (!VALID_PROVIDERS.includes(provider.trim() as Provider)) {
|
|
215
|
+
console.log(chalk.hex('#EF4444')(`\n✗ 无效的 provider,必须是以下之一: ${VALID_PROVIDERS.join(', ')}`))
|
|
216
|
+
console.log()
|
|
217
|
+
rl.close()
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
config.provider = provider.trim() as Provider
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 2. Base URL
|
|
224
|
+
const baseUrlPrompt = `${chalk.cyan('API Base URL')}\n${chalk.gray('默认:')} ${chalk.yellow(config.baseUrl)} ${chalk.gray('→')} `
|
|
225
|
+
const baseUrl = await question(rl, baseUrlPrompt)
|
|
226
|
+
if (baseUrl.trim()) {
|
|
227
|
+
config.baseUrl = baseUrl.trim()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 3. API Key
|
|
231
|
+
const currentKeyDisplay = config.apiKey ? maskApiKey(config.apiKey) : '(未设置)'
|
|
232
|
+
const apiKeyPrompt = `${chalk.cyan('API Key')} ${chalk.gray(`(当前: ${currentKeyDisplay})`)}\n${chalk.gray('→')} `
|
|
233
|
+
const apiKey = await question(rl, apiKeyPrompt)
|
|
234
|
+
if (apiKey.trim()) {
|
|
235
|
+
config.apiKey = apiKey.trim()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 4. Model
|
|
239
|
+
const modelPrompt = `${chalk.cyan('Model')}\n${chalk.gray('默认:')} ${chalk.yellow(config.model)} ${chalk.gray('→')} `
|
|
240
|
+
const model = await question(rl, modelPrompt)
|
|
241
|
+
if (model.trim()) {
|
|
242
|
+
config.model = model.trim()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 5. Shell Hook
|
|
246
|
+
const shellHookPrompt = `${chalk.cyan('启用 Shell Hook')} ${chalk.gray('(记录终端命令历史)')}\n${chalk.gray('默认:')} ${chalk.yellow(config.shellHook ? 'true' : 'false')} ${chalk.gray('→')} `
|
|
247
|
+
const shellHook = await question(rl, shellHookPrompt)
|
|
248
|
+
if (shellHook.trim()) {
|
|
249
|
+
config.shellHook = shellHook.trim() === 'true'
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 6. Edit Mode
|
|
253
|
+
const editModeHint = chalk.gray('(manual=按E编辑, auto=自动编辑)')
|
|
254
|
+
const editModePrompt = `${chalk.cyan('编辑模式')} ${editModeHint}\n${chalk.gray('默认:')} ${chalk.yellow(config.editMode)} ${chalk.gray('→')} `
|
|
255
|
+
const editMode = await question(rl, editModePrompt)
|
|
256
|
+
if (editMode.trim()) {
|
|
257
|
+
if (!VALID_EDIT_MODES.includes(editMode.trim() as EditMode)) {
|
|
258
|
+
console.log(chalk.hex('#EF4444')(`\n✗ 无效的 editMode,必须是: manual 或 auto`))
|
|
259
|
+
console.log()
|
|
260
|
+
rl.close()
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
config.editMode = editMode.trim() as EditMode
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 7. Chat History Limit
|
|
267
|
+
const chatHistoryPrompt = `${chalk.cyan('Chat 历史保留轮数')}\n${chalk.gray('默认:')} ${chalk.yellow(config.chatHistoryLimit)} ${chalk.gray('→')} `
|
|
268
|
+
const chatHistoryLimit = await question(rl, chatHistoryPrompt)
|
|
269
|
+
if (chatHistoryLimit.trim()) {
|
|
270
|
+
const num = parseInt(chatHistoryLimit.trim(), 10)
|
|
271
|
+
if (!isNaN(num) && num > 0) {
|
|
272
|
+
config.chatHistoryLimit = num
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 8. Command History Limit
|
|
277
|
+
const commandHistoryPrompt = `${chalk.cyan('命令历史保留条数')}\n${chalk.gray('默认:')} ${chalk.yellow(config.commandHistoryLimit)} ${chalk.gray('→')} `
|
|
278
|
+
const commandHistoryLimit = await question(rl, commandHistoryPrompt)
|
|
279
|
+
if (commandHistoryLimit.trim()) {
|
|
280
|
+
const num = parseInt(commandHistoryLimit.trim(), 10)
|
|
281
|
+
if (!isNaN(num) && num > 0) {
|
|
282
|
+
config.commandHistoryLimit = num
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 9. Shell History Limit
|
|
287
|
+
const shellHistoryPrompt = `${chalk.cyan('Shell 历史保留条数')}\n${chalk.gray('默认:')} ${chalk.yellow(config.shellHistoryLimit)} ${chalk.gray('→')} `
|
|
288
|
+
const shellHistoryLimit = await question(rl, shellHistoryPrompt)
|
|
289
|
+
if (shellHistoryLimit.trim()) {
|
|
290
|
+
const num = parseInt(shellHistoryLimit.trim(), 10)
|
|
291
|
+
if (!isNaN(num) && num > 0) {
|
|
292
|
+
config.shellHistoryLimit = num
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
saveConfig(config)
|
|
297
|
+
|
|
298
|
+
console.log('\n' + chalk.gray('━'.repeat(50)))
|
|
299
|
+
console.log(chalk.hex('#10B981')('✅ 配置已保存'))
|
|
300
|
+
console.log(chalk.gray(` ${CONFIG_FILE}`))
|
|
301
|
+
console.log()
|
|
302
|
+
} catch (error) {
|
|
303
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
304
|
+
console.log(chalk.hex('#EF4444')(`\n✗ 配置失败: ${message}`))
|
|
305
|
+
console.log()
|
|
306
|
+
} finally {
|
|
307
|
+
rl.close()
|
|
308
|
+
}
|
|
309
|
+
}
|