@yivan-lab/pretty-please 1.0.0 → 1.2.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 +381 -28
- package/bin/pls.tsx +1138 -109
- package/dist/bin/pls.d.ts +1 -1
- package/dist/bin/pls.js +994 -91
- package/dist/package.json +80 -0
- package/dist/src/ai.d.ts +1 -41
- package/dist/src/ai.js +9 -190
- package/dist/src/alias.d.ts +41 -0
- package/dist/src/alias.js +240 -0
- 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 +35 -4
- package/dist/src/components/Chat.js +5 -4
- package/dist/src/components/CodeColorizer.js +26 -20
- package/dist/src/components/CommandBox.js +3 -17
- package/dist/src/components/ConfirmationPrompt.d.ts +2 -1
- package/dist/src/components/ConfirmationPrompt.js +9 -4
- package/dist/src/components/Duration.js +2 -1
- package/dist/src/components/InlineRenderer.js +2 -1
- package/dist/src/components/MarkdownDisplay.js +2 -1
- package/dist/src/components/MultiStepCommandGenerator.d.ts +5 -1
- package/dist/src/components/MultiStepCommandGenerator.js +127 -14
- package/dist/src/components/TableRenderer.js +2 -1
- package/dist/src/config.d.ts +59 -9
- package/dist/src/config.js +147 -48
- 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 +23 -7
- package/dist/src/multi-step.js +29 -6
- package/dist/src/prompts.d.ts +11 -0
- package/dist/src/prompts.js +140 -0
- package/dist/src/remote-history.d.ts +63 -0
- package/dist/src/remote-history.js +315 -0
- package/dist/src/remote.d.ts +113 -0
- package/dist/src/remote.js +634 -0
- package/dist/src/shell-hook.d.ts +87 -12
- package/dist/src/shell-hook.js +315 -17
- package/dist/src/sysinfo.d.ts +9 -5
- package/dist/src/sysinfo.js +2 -2
- package/dist/src/ui/theme.d.ts +27 -24
- package/dist/src/ui/theme.js +71 -21
- package/dist/src/upgrade.d.ts +41 -0
- package/dist/src/upgrade.js +348 -0
- package/dist/src/utils/console.d.ts +11 -11
- package/dist/src/utils/console.js +26 -17
- package/package.json +11 -9
- package/src/alias.ts +301 -0
- package/src/builtin-detector.ts +126 -0
- package/src/chat-history.ts +140 -0
- package/src/components/Chat.tsx +6 -5
- package/src/components/CodeColorizer.tsx +27 -19
- package/src/components/CommandBox.tsx +3 -17
- package/src/components/ConfirmationPrompt.tsx +11 -3
- package/src/components/Duration.tsx +2 -1
- package/src/components/InlineRenderer.tsx +2 -1
- package/src/components/MarkdownDisplay.tsx +2 -1
- package/src/components/MultiStepCommandGenerator.tsx +167 -16
- package/src/components/TableRenderer.tsx +2 -1
- package/src/config.ts +394 -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 +45 -8
- package/src/prompts.ts +154 -0
- package/src/remote-history.ts +390 -0
- package/src/remote.ts +800 -0
- package/src/shell-hook.ts +754 -0
- package/src/{sysinfo.js → sysinfo.ts} +28 -16
- package/src/ui/theme.ts +101 -24
- package/src/upgrade.ts +397 -0
- package/src/utils/{console.js → console.ts} +36 -27
- 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
package/src/config.ts
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
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
|
+
import { getCurrentTheme } from './ui/theme.js'
|
|
7
|
+
|
|
8
|
+
// 获取主题颜色
|
|
9
|
+
function getColors() {
|
|
10
|
+
const theme = getCurrentTheme()
|
|
11
|
+
return {
|
|
12
|
+
primary: theme.primary,
|
|
13
|
+
secondary: theme.secondary,
|
|
14
|
+
success: theme.success,
|
|
15
|
+
error: theme.error
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 配置文件路径
|
|
20
|
+
export const CONFIG_DIR = path.join(os.homedir(), '.please')
|
|
21
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
|
|
22
|
+
|
|
23
|
+
// 支持的 Provider 列表
|
|
24
|
+
const VALID_PROVIDERS = [
|
|
25
|
+
'openai',
|
|
26
|
+
'anthropic',
|
|
27
|
+
'deepseek',
|
|
28
|
+
'google',
|
|
29
|
+
'groq',
|
|
30
|
+
'mistral',
|
|
31
|
+
'cohere',
|
|
32
|
+
'fireworks',
|
|
33
|
+
'together',
|
|
34
|
+
] as const
|
|
35
|
+
|
|
36
|
+
type Provider = (typeof VALID_PROVIDERS)[number]
|
|
37
|
+
|
|
38
|
+
// 编辑模式
|
|
39
|
+
const VALID_EDIT_MODES = ['manual', 'auto'] as const
|
|
40
|
+
type EditMode = (typeof VALID_EDIT_MODES)[number]
|
|
41
|
+
|
|
42
|
+
// 主题
|
|
43
|
+
const VALID_THEMES = ['dark', 'light'] as const
|
|
44
|
+
export type ThemeName = (typeof VALID_THEMES)[number]
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 别名配置接口
|
|
48
|
+
*/
|
|
49
|
+
export interface AliasConfig {
|
|
50
|
+
prompt: string
|
|
51
|
+
description?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 远程服务器配置接口
|
|
56
|
+
*/
|
|
57
|
+
export interface RemoteConfig {
|
|
58
|
+
host: string
|
|
59
|
+
user: string
|
|
60
|
+
port: number
|
|
61
|
+
key?: string // SSH 私钥路径
|
|
62
|
+
password?: boolean // 是否使用密码认证(密码不存储,每次交互输入)
|
|
63
|
+
workDir?: string // 默认工作目录
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 远程服务器系统信息缓存
|
|
68
|
+
*/
|
|
69
|
+
export interface RemoteSysInfo {
|
|
70
|
+
os: string // 操作系统 (linux, darwin, etc.)
|
|
71
|
+
osVersion: string // 系统版本
|
|
72
|
+
shell: string // 默认 shell (bash, zsh, etc.)
|
|
73
|
+
hostname: string // 主机名
|
|
74
|
+
cachedAt: string // 缓存时间
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 配置接口
|
|
79
|
+
*/
|
|
80
|
+
export interface Config {
|
|
81
|
+
apiKey: string
|
|
82
|
+
baseUrl: string
|
|
83
|
+
model: string
|
|
84
|
+
provider: Provider
|
|
85
|
+
shellHook: boolean
|
|
86
|
+
chatHistoryLimit: number
|
|
87
|
+
commandHistoryLimit: number
|
|
88
|
+
shellHistoryLimit: number
|
|
89
|
+
editMode: EditMode
|
|
90
|
+
theme: ThemeName
|
|
91
|
+
aliases: Record<string, AliasConfig>
|
|
92
|
+
remotes: Record<string, RemoteConfig> // 远程服务器配置
|
|
93
|
+
defaultRemote?: string // 默认远程服务器名称
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 默认配置
|
|
98
|
+
*/
|
|
99
|
+
const DEFAULT_CONFIG: Config = {
|
|
100
|
+
apiKey: '',
|
|
101
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
102
|
+
model: 'gpt-4-turbo',
|
|
103
|
+
provider: 'openai',
|
|
104
|
+
shellHook: false,
|
|
105
|
+
chatHistoryLimit: 10,
|
|
106
|
+
commandHistoryLimit: 10,
|
|
107
|
+
shellHistoryLimit: 15,
|
|
108
|
+
editMode: 'manual',
|
|
109
|
+
theme: 'dark',
|
|
110
|
+
aliases: {},
|
|
111
|
+
remotes: {},
|
|
112
|
+
defaultRemote: '',
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 确保配置目录存在
|
|
117
|
+
*/
|
|
118
|
+
function ensureConfigDir(): void {
|
|
119
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
120
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 读取配置
|
|
126
|
+
* 优化:添加缓存,避免重复读取文件
|
|
127
|
+
*/
|
|
128
|
+
let cachedConfig: Config | null = null
|
|
129
|
+
|
|
130
|
+
export function getConfig(): Config {
|
|
131
|
+
// 如果已有缓存,直接返回
|
|
132
|
+
if (cachedConfig !== null) {
|
|
133
|
+
return cachedConfig
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
ensureConfigDir()
|
|
137
|
+
|
|
138
|
+
let config: Config
|
|
139
|
+
|
|
140
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
141
|
+
config = { ...DEFAULT_CONFIG }
|
|
142
|
+
} else {
|
|
143
|
+
try {
|
|
144
|
+
const content = fs.readFileSync(CONFIG_FILE, 'utf-8')
|
|
145
|
+
config = { ...DEFAULT_CONFIG, ...JSON.parse(content) }
|
|
146
|
+
} catch {
|
|
147
|
+
config = { ...DEFAULT_CONFIG }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
cachedConfig = config
|
|
152
|
+
return config
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 保存配置
|
|
157
|
+
*/
|
|
158
|
+
export function saveConfig(config: Config): void {
|
|
159
|
+
ensureConfigDir()
|
|
160
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 设置单个配置项
|
|
165
|
+
*/
|
|
166
|
+
export function setConfigValue(key: string, value: string | boolean | number): Config {
|
|
167
|
+
const config = getConfig()
|
|
168
|
+
|
|
169
|
+
if (!(key in DEFAULT_CONFIG)) {
|
|
170
|
+
throw new Error(`未知的配置项: ${key}`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 处理特殊类型
|
|
174
|
+
if (key === 'shellHook') {
|
|
175
|
+
config.shellHook = value === 'true' || value === true
|
|
176
|
+
} else if (key === 'chatHistoryLimit' || key === 'commandHistoryLimit' || key === 'shellHistoryLimit') {
|
|
177
|
+
const num = typeof value === 'number' ? value : parseInt(String(value), 10)
|
|
178
|
+
if (isNaN(num) || num < 1) {
|
|
179
|
+
throw new Error(`${key} 必须是大于 0 的整数`)
|
|
180
|
+
}
|
|
181
|
+
config[key] = num
|
|
182
|
+
} else if (key === 'provider') {
|
|
183
|
+
const strValue = String(value)
|
|
184
|
+
if (!VALID_PROVIDERS.includes(strValue as Provider)) {
|
|
185
|
+
throw new Error(`provider 必须是以下之一: ${VALID_PROVIDERS.join(', ')}`)
|
|
186
|
+
}
|
|
187
|
+
config.provider = strValue as Provider
|
|
188
|
+
} else if (key === 'editMode') {
|
|
189
|
+
const strValue = String(value)
|
|
190
|
+
if (!VALID_EDIT_MODES.includes(strValue as EditMode)) {
|
|
191
|
+
throw new Error(`editMode 必须是以下之一: ${VALID_EDIT_MODES.join(', ')}`)
|
|
192
|
+
}
|
|
193
|
+
config.editMode = strValue as EditMode
|
|
194
|
+
} else if (key === 'theme') {
|
|
195
|
+
const strValue = String(value)
|
|
196
|
+
if (!VALID_THEMES.includes(strValue as ThemeName)) {
|
|
197
|
+
throw new Error(`theme 必须是以下之一: ${VALID_THEMES.join(', ')}`)
|
|
198
|
+
}
|
|
199
|
+
config.theme = strValue as ThemeName
|
|
200
|
+
} else if (key === 'apiKey' || key === 'baseUrl' || key === 'model' || key === 'defaultRemote') {
|
|
201
|
+
config[key] = String(value)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
saveConfig(config)
|
|
205
|
+
|
|
206
|
+
// 清除缓存,下次读取时会重新加载
|
|
207
|
+
cachedConfig = null
|
|
208
|
+
|
|
209
|
+
return config
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 检查配置是否有效
|
|
214
|
+
*/
|
|
215
|
+
export function isConfigValid(): boolean {
|
|
216
|
+
const config = getConfig()
|
|
217
|
+
return config.apiKey.length > 0
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 隐藏 API Key 中间部分
|
|
222
|
+
*/
|
|
223
|
+
export function maskApiKey(apiKey: string): string {
|
|
224
|
+
if (!apiKey || apiKey.length < 10) return apiKey || '(未设置)'
|
|
225
|
+
return apiKey.slice(0, 6) + '****' + apiKey.slice(-4)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 显示当前配置
|
|
230
|
+
*/
|
|
231
|
+
export function displayConfig(): void {
|
|
232
|
+
const config = getConfig()
|
|
233
|
+
const colors = getColors()
|
|
234
|
+
console.log(chalk.bold('\n当前配置:'))
|
|
235
|
+
console.log(chalk.gray('━'.repeat(50)))
|
|
236
|
+
console.log(` ${chalk.hex(colors.primary)('apiKey')}: ${maskApiKey(config.apiKey)}`)
|
|
237
|
+
console.log(` ${chalk.hex(colors.primary)('baseUrl')}: ${config.baseUrl}`)
|
|
238
|
+
console.log(` ${chalk.hex(colors.primary)('provider')}: ${config.provider}`)
|
|
239
|
+
console.log(` ${chalk.hex(colors.primary)('model')}: ${config.model}`)
|
|
240
|
+
console.log(
|
|
241
|
+
` ${chalk.hex(colors.primary)('shellHook')}: ${config.shellHook ? chalk.hex(colors.success)('已启用') : chalk.gray('未启用')}`
|
|
242
|
+
)
|
|
243
|
+
console.log(
|
|
244
|
+
` ${chalk.hex(colors.primary)('editMode')}: ${
|
|
245
|
+
config.editMode === 'auto' ? chalk.hex(colors.primary)('auto (自动编辑)') : chalk.gray('manual (按E编辑)')
|
|
246
|
+
}`
|
|
247
|
+
)
|
|
248
|
+
console.log(` ${chalk.hex(colors.primary)('chatHistoryLimit')}: ${config.chatHistoryLimit} 轮`)
|
|
249
|
+
console.log(` ${chalk.hex(colors.primary)('commandHistoryLimit')}: ${config.commandHistoryLimit} 条`)
|
|
250
|
+
console.log(` ${chalk.hex(colors.primary)('shellHistoryLimit')}: ${config.shellHistoryLimit} 条`)
|
|
251
|
+
console.log(
|
|
252
|
+
` ${chalk.hex(colors.primary)('theme')}: ${
|
|
253
|
+
config.theme === 'dark' ? chalk.hex(colors.primary)('dark (深色)') : chalk.hex(colors.primary)('light (浅色)')
|
|
254
|
+
}`
|
|
255
|
+
)
|
|
256
|
+
console.log(chalk.gray('━'.repeat(50)))
|
|
257
|
+
console.log(chalk.gray(`配置文件: ${CONFIG_FILE}\n`))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 创建 readline 接口
|
|
262
|
+
*/
|
|
263
|
+
function createReadlineInterface(): readline.Interface {
|
|
264
|
+
return readline.createInterface({
|
|
265
|
+
input: process.stdin,
|
|
266
|
+
output: process.stdout,
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 异步提问
|
|
272
|
+
*/
|
|
273
|
+
function question(rl: readline.Interface, prompt: string): Promise<string> {
|
|
274
|
+
return new Promise((resolve) => {
|
|
275
|
+
rl.question(prompt, (answer) => {
|
|
276
|
+
resolve(answer)
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 交互式配置向导
|
|
283
|
+
*/
|
|
284
|
+
export async function runConfigWizard(): Promise<void> {
|
|
285
|
+
const rl = createReadlineInterface()
|
|
286
|
+
const config = getConfig()
|
|
287
|
+
const colors = getColors()
|
|
288
|
+
|
|
289
|
+
console.log(chalk.bold.hex(colors.primary)('\n🔧 Pretty Please 配置向导'))
|
|
290
|
+
console.log(chalk.gray('━'.repeat(50)))
|
|
291
|
+
console.log(chalk.gray('直接回车使用默认值,输入值后回车确认\n'))
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
// 1. Provider
|
|
295
|
+
const providerHint = chalk.gray(`(可选: ${VALID_PROVIDERS.join(', ')})`)
|
|
296
|
+
const providerPrompt = `${chalk.hex(colors.primary)('Provider')} ${providerHint}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.provider)} ${chalk.gray('→')} `
|
|
297
|
+
const provider = await question(rl, providerPrompt)
|
|
298
|
+
if (provider.trim()) {
|
|
299
|
+
if (!VALID_PROVIDERS.includes(provider.trim() as Provider)) {
|
|
300
|
+
console.log(chalk.hex(colors.error)(`\n✗ 无效的 provider,必须是以下之一: ${VALID_PROVIDERS.join(', ')}`))
|
|
301
|
+
console.log()
|
|
302
|
+
rl.close()
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
config.provider = provider.trim() as Provider
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 2. Base URL
|
|
309
|
+
const baseUrlPrompt = `${chalk.hex(colors.primary)('API Base URL')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.baseUrl)} ${chalk.gray('→')} `
|
|
310
|
+
const baseUrl = await question(rl, baseUrlPrompt)
|
|
311
|
+
if (baseUrl.trim()) {
|
|
312
|
+
config.baseUrl = baseUrl.trim()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 3. API Key
|
|
316
|
+
const currentKeyDisplay = config.apiKey ? maskApiKey(config.apiKey) : '(未设置)'
|
|
317
|
+
const apiKeyPrompt = `${chalk.hex(colors.primary)('API Key')} ${chalk.gray(`(当前: ${currentKeyDisplay})`)}\n${chalk.gray('→')} `
|
|
318
|
+
const apiKey = await question(rl, apiKeyPrompt)
|
|
319
|
+
if (apiKey.trim()) {
|
|
320
|
+
config.apiKey = apiKey.trim()
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 4. Model
|
|
324
|
+
const modelPrompt = `${chalk.hex(colors.primary)('Model')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.model)} ${chalk.gray('→')} `
|
|
325
|
+
const model = await question(rl, modelPrompt)
|
|
326
|
+
if (model.trim()) {
|
|
327
|
+
config.model = model.trim()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 5. Shell Hook
|
|
331
|
+
const shellHookPrompt = `${chalk.hex(colors.primary)('启用 Shell Hook')} ${chalk.gray('(记录终端命令历史)')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.shellHook ? 'true' : 'false')} ${chalk.gray('→')} `
|
|
332
|
+
const shellHook = await question(rl, shellHookPrompt)
|
|
333
|
+
if (shellHook.trim()) {
|
|
334
|
+
config.shellHook = shellHook.trim() === 'true'
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 6. Edit Mode
|
|
338
|
+
const editModeHint = chalk.gray('(manual=按E编辑, auto=自动编辑)')
|
|
339
|
+
const editModePrompt = `${chalk.hex(colors.primary)('编辑模式')} ${editModeHint}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.editMode)} ${chalk.gray('→')} `
|
|
340
|
+
const editMode = await question(rl, editModePrompt)
|
|
341
|
+
if (editMode.trim()) {
|
|
342
|
+
if (!VALID_EDIT_MODES.includes(editMode.trim() as EditMode)) {
|
|
343
|
+
console.log(chalk.hex(colors.error)(`\n✗ 无效的 editMode,必须是: manual 或 auto`))
|
|
344
|
+
console.log()
|
|
345
|
+
rl.close()
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
config.editMode = editMode.trim() as EditMode
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 7. Chat History Limit
|
|
352
|
+
const chatHistoryPrompt = `${chalk.hex(colors.primary)('Chat 历史保留轮数')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.chatHistoryLimit)} ${chalk.gray('→')} `
|
|
353
|
+
const chatHistoryLimit = await question(rl, chatHistoryPrompt)
|
|
354
|
+
if (chatHistoryLimit.trim()) {
|
|
355
|
+
const num = parseInt(chatHistoryLimit.trim(), 10)
|
|
356
|
+
if (!isNaN(num) && num > 0) {
|
|
357
|
+
config.chatHistoryLimit = num
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 8. Command History Limit
|
|
362
|
+
const commandHistoryPrompt = `${chalk.hex(colors.primary)('命令历史保留条数')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.commandHistoryLimit)} ${chalk.gray('→')} `
|
|
363
|
+
const commandHistoryLimit = await question(rl, commandHistoryPrompt)
|
|
364
|
+
if (commandHistoryLimit.trim()) {
|
|
365
|
+
const num = parseInt(commandHistoryLimit.trim(), 10)
|
|
366
|
+
if (!isNaN(num) && num > 0) {
|
|
367
|
+
config.commandHistoryLimit = num
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 9. Shell History Limit
|
|
372
|
+
const shellHistoryPrompt = `${chalk.hex(colors.primary)('Shell 历史保留条数')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.shellHistoryLimit)} ${chalk.gray('→')} `
|
|
373
|
+
const shellHistoryLimit = await question(rl, shellHistoryPrompt)
|
|
374
|
+
if (shellHistoryLimit.trim()) {
|
|
375
|
+
const num = parseInt(shellHistoryLimit.trim(), 10)
|
|
376
|
+
if (!isNaN(num) && num > 0) {
|
|
377
|
+
config.shellHistoryLimit = num
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
saveConfig(config)
|
|
382
|
+
|
|
383
|
+
console.log('\n' + chalk.gray('━'.repeat(50)))
|
|
384
|
+
console.log(chalk.hex(getColors().success)('✅ 配置已保存'))
|
|
385
|
+
console.log(chalk.gray(` ${CONFIG_FILE}`))
|
|
386
|
+
console.log()
|
|
387
|
+
} catch (error) {
|
|
388
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
389
|
+
console.log(chalk.hex(getColors().error)(`\n✗ 配置失败: ${message}`))
|
|
390
|
+
console.log()
|
|
391
|
+
} finally {
|
|
392
|
+
rl.close()
|
|
393
|
+
}
|
|
394
|
+
}
|
package/src/history.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import { getConfig } from './config.js'
|
|
5
|
+
|
|
6
|
+
const CONFIG_DIR = path.join(os.homedir(), '.please')
|
|
7
|
+
const HISTORY_FILE = path.join(CONFIG_DIR, 'history.json')
|
|
8
|
+
const MAX_HISTORY = 10
|
|
9
|
+
const MAX_OUTPUT_LENGTH = 500
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 历史记录项
|
|
13
|
+
*/
|
|
14
|
+
export interface HistoryRecord {
|
|
15
|
+
userPrompt: string
|
|
16
|
+
command: string
|
|
17
|
+
aiGeneratedCommand?: string
|
|
18
|
+
userModified?: boolean
|
|
19
|
+
executed: boolean
|
|
20
|
+
exitCode: number | null
|
|
21
|
+
output?: string
|
|
22
|
+
reason?: 'builtin' | string
|
|
23
|
+
timestamp?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 确保配置目录存在
|
|
28
|
+
*/
|
|
29
|
+
function ensureConfigDir(): void {
|
|
30
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
31
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 读取历史记录
|
|
37
|
+
*/
|
|
38
|
+
export function getHistory(): HistoryRecord[] {
|
|
39
|
+
ensureConfigDir()
|
|
40
|
+
|
|
41
|
+
if (!fs.existsSync(HISTORY_FILE)) {
|
|
42
|
+
return []
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const content = fs.readFileSync(HISTORY_FILE, 'utf-8')
|
|
47
|
+
return JSON.parse(content) as HistoryRecord[]
|
|
48
|
+
} catch {
|
|
49
|
+
return []
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 保存历史记录
|
|
55
|
+
*/
|
|
56
|
+
function saveHistory(history: HistoryRecord[]): void {
|
|
57
|
+
ensureConfigDir()
|
|
58
|
+
fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 添加一条历史记录
|
|
63
|
+
*/
|
|
64
|
+
export function addHistory(record: HistoryRecord): void {
|
|
65
|
+
const config = getConfig()
|
|
66
|
+
const history = getHistory()
|
|
67
|
+
|
|
68
|
+
// 截断输出
|
|
69
|
+
if (record.output && record.output.length > MAX_OUTPUT_LENGTH) {
|
|
70
|
+
record.output = record.output.slice(0, MAX_OUTPUT_LENGTH) + '...(截断)'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 添加时间戳
|
|
74
|
+
record.timestamp = new Date().toISOString()
|
|
75
|
+
|
|
76
|
+
// 添加到开头
|
|
77
|
+
history.unshift(record)
|
|
78
|
+
|
|
79
|
+
// 保留最近 N 条(从配置读取)
|
|
80
|
+
const maxHistory = config.commandHistoryLimit || MAX_HISTORY
|
|
81
|
+
if (history.length > maxHistory) {
|
|
82
|
+
history.length = maxHistory
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
saveHistory(history)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 清空历史记录
|
|
90
|
+
*/
|
|
91
|
+
export function clearHistory(): void {
|
|
92
|
+
saveHistory([])
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 格式化历史记录供 AI 使用
|
|
97
|
+
*/
|
|
98
|
+
export function formatHistoryForAI(): string {
|
|
99
|
+
const history = getHistory()
|
|
100
|
+
|
|
101
|
+
if (history.length === 0) {
|
|
102
|
+
return ''
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const lines = history
|
|
106
|
+
.map((item, index) => {
|
|
107
|
+
const timeAgo = getTimeAgo(item.timestamp || '')
|
|
108
|
+
|
|
109
|
+
let status: string
|
|
110
|
+
if (item.executed) {
|
|
111
|
+
status = item.exitCode === 0 ? '✓' : `✗ 退出码:${item.exitCode}`
|
|
112
|
+
} else if (item.reason === 'builtin') {
|
|
113
|
+
status = '(包含 builtin,未执行)'
|
|
114
|
+
} else {
|
|
115
|
+
status = '(用户取消执行)'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 检查是否用户修改了命令
|
|
119
|
+
if (item.userModified && item.aiGeneratedCommand) {
|
|
120
|
+
// 用户修改了命令
|
|
121
|
+
return `${index + 1}. [${timeAgo}] "${item.userPrompt}" → AI 生成: ${item.aiGeneratedCommand} / 用户修改为: ${item.command} ${status}`
|
|
122
|
+
} else {
|
|
123
|
+
// 未修改,使用原格式
|
|
124
|
+
let line = `${index + 1}. [${timeAgo}] "${item.userPrompt}" → ${item.command} ${status}`
|
|
125
|
+
|
|
126
|
+
// 如果有输出且命令失败,附加输出摘要
|
|
127
|
+
if (item.output && item.exitCode !== 0) {
|
|
128
|
+
line += `\n 输出: ${item.output.split('\n')[0]}` // 只取第一行
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return line
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
.reverse() // 从旧到新排列
|
|
135
|
+
|
|
136
|
+
return `【最近通过 pls 执行的命令】\n${lines.join('\n')}`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 计算时间差的友好显示
|
|
141
|
+
*/
|
|
142
|
+
function getTimeAgo(timestamp: string): string {
|
|
143
|
+
if (!timestamp) return '未知'
|
|
144
|
+
|
|
145
|
+
const now = Date.now()
|
|
146
|
+
const then = new Date(timestamp).getTime()
|
|
147
|
+
const diff = Math.floor((now - then) / 1000)
|
|
148
|
+
|
|
149
|
+
if (diff < 60) return '刚刚'
|
|
150
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`
|
|
151
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`
|
|
152
|
+
return `${Math.floor(diff / 86400)}天前`
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 获取历史记录文件路径(供显示用)
|
|
157
|
+
*/
|
|
158
|
+
export function getHistoryFilePath(): string {
|
|
159
|
+
return HISTORY_FILE
|
|
160
|
+
}
|
package/src/mastra-agent.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Agent } from '@mastra/core'
|
|
2
2
|
import { getConfig } from './config.js'
|
|
3
|
-
import {
|
|
3
|
+
import { buildCommandSystemPrompt } from './prompts.js'
|
|
4
4
|
import { formatSystemInfo } from './sysinfo.js'
|
|
5
5
|
import { formatHistoryForAI } from './history.js'
|
|
6
6
|
import { formatShellHistoryForAI, getShellHistory } from './shell-hook.js'
|
|
@@ -8,7 +8,6 @@ import { formatShellHistoryForAI, getShellHistory } from './shell-hook.js'
|
|
|
8
8
|
/**
|
|
9
9
|
* 创建 Mastra Shell Agent
|
|
10
10
|
* 根据用户配置的 API Key、Base URL、Provider 和 Model
|
|
11
|
-
* 使用 ai.js 中的统一提示词
|
|
12
11
|
*/
|
|
13
12
|
export function createShellAgent() {
|
|
14
13
|
const config = getConfig()
|
|
@@ -16,12 +15,12 @@ export function createShellAgent() {
|
|
|
16
15
|
// 组合 provider/model 格式(Mastra 要求)
|
|
17
16
|
const modelId = `${config.provider}/${config.model}` as `${string}/${string}`
|
|
18
17
|
|
|
19
|
-
//
|
|
18
|
+
// 构建系统提示词
|
|
20
19
|
const sysinfo = formatSystemInfo()
|
|
21
20
|
const plsHistory = formatHistoryForAI()
|
|
22
21
|
const shellHistory = formatShellHistoryForAI()
|
|
23
22
|
const shellHookEnabled = config.shellHook && getShellHistory().length > 0
|
|
24
|
-
const systemPrompt =
|
|
23
|
+
const systemPrompt = buildCommandSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled)
|
|
25
24
|
|
|
26
25
|
return new Agent({
|
|
27
26
|
name: 'shell-commander',
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Agent } from '@mastra/core'
|
|
2
|
+
import { getConfig } from './config.js'
|
|
3
|
+
import { buildChatSystemPrompt } from './prompts.js'
|
|
4
|
+
import { formatSystemInfo } from './sysinfo.js'
|
|
5
|
+
import { formatHistoryForAI } from './history.js'
|
|
6
|
+
import { formatShellHistoryForAI, getShellHistory } from './shell-hook.js'
|
|
7
|
+
import { getChatHistory, addChatMessage } from './chat-history.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 创建 Mastra Chat Agent
|
|
11
|
+
*/
|
|
12
|
+
export function createChatAgent() {
|
|
13
|
+
const config = getConfig()
|
|
14
|
+
|
|
15
|
+
// 组合 provider/model 格式(Mastra 要求)
|
|
16
|
+
const modelId = `${config.provider}/${config.model}` as `${string}/${string}`
|
|
17
|
+
|
|
18
|
+
// 构建系统提示词
|
|
19
|
+
const sysinfo = formatSystemInfo()
|
|
20
|
+
const plsHistory = formatHistoryForAI()
|
|
21
|
+
const shellHistory = formatShellHistoryForAI()
|
|
22
|
+
const shellHookEnabled = config.shellHook && getShellHistory().length > 0
|
|
23
|
+
const systemPrompt = buildChatSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled)
|
|
24
|
+
|
|
25
|
+
return new Agent({
|
|
26
|
+
name: 'chat-assistant',
|
|
27
|
+
instructions: systemPrompt,
|
|
28
|
+
model: {
|
|
29
|
+
url: config.baseUrl,
|
|
30
|
+
id: modelId,
|
|
31
|
+
apiKey: config.apiKey,
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 获取完整的系统提示词(用于调试)
|
|
38
|
+
*/
|
|
39
|
+
export function getChatSystemPrompt(): string {
|
|
40
|
+
const config = getConfig()
|
|
41
|
+
const sysinfo = formatSystemInfo()
|
|
42
|
+
const plsHistory = formatHistoryForAI()
|
|
43
|
+
const shellHistory = formatShellHistoryForAI()
|
|
44
|
+
const shellHookEnabled = config.shellHook && getShellHistory().length > 0
|
|
45
|
+
return buildChatSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 使用 Mastra 进行 AI 对话(支持流式输出)
|
|
50
|
+
*/
|
|
51
|
+
export async function chatWithMastra(
|
|
52
|
+
prompt: string,
|
|
53
|
+
options: {
|
|
54
|
+
debug?: boolean
|
|
55
|
+
onChunk?: (chunk: string) => void
|
|
56
|
+
} = {}
|
|
57
|
+
): Promise<{
|
|
58
|
+
reply: string
|
|
59
|
+
debug?: {
|
|
60
|
+
sysinfo: string
|
|
61
|
+
model: string
|
|
62
|
+
systemPrompt: string
|
|
63
|
+
chatHistory: Array<{ role: string; content: string }>
|
|
64
|
+
userPrompt: string
|
|
65
|
+
}
|
|
66
|
+
}> {
|
|
67
|
+
const config = getConfig()
|
|
68
|
+
const agent = createChatAgent()
|
|
69
|
+
|
|
70
|
+
// 获取对话历史
|
|
71
|
+
const chatHistory = getChatHistory()
|
|
72
|
+
|
|
73
|
+
// 构建消息数组(将历史和新消息合并)
|
|
74
|
+
const messages: string[] = []
|
|
75
|
+
|
|
76
|
+
// 添加历史对话
|
|
77
|
+
for (const msg of chatHistory) {
|
|
78
|
+
messages.push(msg.content)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 添加当前用户消息
|
|
82
|
+
messages.push(prompt)
|
|
83
|
+
|
|
84
|
+
let fullContent = ''
|
|
85
|
+
|
|
86
|
+
// 流式输出模式
|
|
87
|
+
if (options.onChunk) {
|
|
88
|
+
const stream = await agent.stream(messages)
|
|
89
|
+
|
|
90
|
+
for await (const chunk of stream.textStream) {
|
|
91
|
+
if (chunk) {
|
|
92
|
+
fullContent += chunk
|
|
93
|
+
options.onChunk(chunk)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
// 非流式模式
|
|
98
|
+
const response = await agent.generate(messages)
|
|
99
|
+
fullContent = response.text || ''
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!fullContent) {
|
|
103
|
+
throw new Error('AI 返回了空的响应')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 保存对话历史
|
|
107
|
+
addChatMessage(prompt, fullContent)
|
|
108
|
+
|
|
109
|
+
// 返回结果
|
|
110
|
+
if (options.debug) {
|
|
111
|
+
return {
|
|
112
|
+
reply: fullContent,
|
|
113
|
+
debug: {
|
|
114
|
+
sysinfo: formatSystemInfo(),
|
|
115
|
+
model: config.model,
|
|
116
|
+
systemPrompt: getChatSystemPrompt(),
|
|
117
|
+
chatHistory,
|
|
118
|
+
userPrompt: prompt,
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { reply: fullContent }
|
|
124
|
+
}
|