@yivan-lab/pretty-please 1.0.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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +380 -0
  3. package/bin/pls.js +681 -0
  4. package/bin/pls.tsx +541 -0
  5. package/dist/bin/pls.d.ts +2 -0
  6. package/dist/bin/pls.js +429 -0
  7. package/dist/src/ai.d.ts +48 -0
  8. package/dist/src/ai.js +295 -0
  9. package/dist/src/builtin-detector.d.ts +15 -0
  10. package/dist/src/builtin-detector.js +83 -0
  11. package/dist/src/chat-history.d.ts +26 -0
  12. package/dist/src/chat-history.js +81 -0
  13. package/dist/src/components/Chat.d.ts +13 -0
  14. package/dist/src/components/Chat.js +80 -0
  15. package/dist/src/components/ChatStatus.d.ts +9 -0
  16. package/dist/src/components/ChatStatus.js +34 -0
  17. package/dist/src/components/CodeColorizer.d.ts +12 -0
  18. package/dist/src/components/CodeColorizer.js +82 -0
  19. package/dist/src/components/CommandBox.d.ts +10 -0
  20. package/dist/src/components/CommandBox.js +45 -0
  21. package/dist/src/components/CommandGenerator.d.ts +20 -0
  22. package/dist/src/components/CommandGenerator.js +116 -0
  23. package/dist/src/components/ConfigDisplay.d.ts +9 -0
  24. package/dist/src/components/ConfigDisplay.js +42 -0
  25. package/dist/src/components/ConfigWizard.d.ts +9 -0
  26. package/dist/src/components/ConfigWizard.js +72 -0
  27. package/dist/src/components/ConfirmationPrompt.d.ts +12 -0
  28. package/dist/src/components/ConfirmationPrompt.js +26 -0
  29. package/dist/src/components/Duration.d.ts +9 -0
  30. package/dist/src/components/Duration.js +21 -0
  31. package/dist/src/components/HistoryDisplay.d.ts +9 -0
  32. package/dist/src/components/HistoryDisplay.js +51 -0
  33. package/dist/src/components/HookManager.d.ts +10 -0
  34. package/dist/src/components/HookManager.js +88 -0
  35. package/dist/src/components/InlineRenderer.d.ts +12 -0
  36. package/dist/src/components/InlineRenderer.js +75 -0
  37. package/dist/src/components/MarkdownDisplay.d.ts +13 -0
  38. package/dist/src/components/MarkdownDisplay.js +197 -0
  39. package/dist/src/components/MultiStepCommandGenerator.d.ts +25 -0
  40. package/dist/src/components/MultiStepCommandGenerator.js +142 -0
  41. package/dist/src/components/TableRenderer.d.ts +12 -0
  42. package/dist/src/components/TableRenderer.js +66 -0
  43. package/dist/src/config.d.ts +29 -0
  44. package/dist/src/config.js +203 -0
  45. package/dist/src/history.d.ts +20 -0
  46. package/dist/src/history.js +113 -0
  47. package/dist/src/mastra-agent.d.ts +7 -0
  48. package/dist/src/mastra-agent.js +31 -0
  49. package/dist/src/multi-step.d.ts +41 -0
  50. package/dist/src/multi-step.js +67 -0
  51. package/dist/src/shell-hook.d.ts +35 -0
  52. package/dist/src/shell-hook.js +348 -0
  53. package/dist/src/sysinfo.d.ts +15 -0
  54. package/dist/src/sysinfo.js +52 -0
  55. package/dist/src/ui/theme.d.ts +26 -0
  56. package/dist/src/ui/theme.js +31 -0
  57. package/dist/src/utils/console.d.ts +44 -0
  58. package/dist/src/utils/console.js +114 -0
  59. package/package.json +78 -0
  60. package/src/ai.js +324 -0
  61. package/src/builtin-detector.js +98 -0
  62. package/src/chat-history.js +94 -0
  63. package/src/components/Chat.tsx +122 -0
  64. package/src/components/ChatStatus.tsx +53 -0
  65. package/src/components/CodeColorizer.tsx +128 -0
  66. package/src/components/CommandBox.tsx +60 -0
  67. package/src/components/CommandGenerator.tsx +184 -0
  68. package/src/components/ConfigDisplay.tsx +64 -0
  69. package/src/components/ConfigWizard.tsx +101 -0
  70. package/src/components/ConfirmationPrompt.tsx +41 -0
  71. package/src/components/Duration.tsx +24 -0
  72. package/src/components/HistoryDisplay.tsx +69 -0
  73. package/src/components/HookManager.tsx +150 -0
  74. package/src/components/InlineRenderer.tsx +123 -0
  75. package/src/components/MarkdownDisplay.tsx +288 -0
  76. package/src/components/MultiStepCommandGenerator.tsx +229 -0
  77. package/src/components/TableRenderer.tsx +110 -0
  78. package/src/config.js +221 -0
  79. package/src/history.js +131 -0
  80. package/src/mastra-agent.ts +35 -0
  81. package/src/multi-step.ts +93 -0
  82. package/src/shell-hook.js +393 -0
  83. package/src/sysinfo.js +57 -0
  84. package/src/ui/theme.ts +37 -0
  85. package/src/utils/console.js +130 -0
  86. package/tsconfig.json +23 -0
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@yivan-lab/pretty-please",
3
+ "version": "1.0.0",
4
+ "description": "AI 驱动的命令行工具,将自然语言转换为可执行的 Shell 命令",
5
+ "type": "module",
6
+ "bin": {
7
+ "pls": "./dist/bin/pls.js",
8
+ "please": "./dist/bin/pls.js"
9
+ },
10
+ "scripts": {
11
+ "dev": "tsx bin/pls.tsx",
12
+ "build": "tsc",
13
+ "start": "node dist/bin/pls.js",
14
+ "prepublishOnly": "pnpm build"
15
+ },
16
+ "keywords": [
17
+ "cli",
18
+ "ai",
19
+ "shell",
20
+ "openai",
21
+ "command-line",
22
+ "natural-language",
23
+ "terminal",
24
+ "assistant",
25
+ "mastra",
26
+ "deepseek"
27
+ ],
28
+ "author": "yivan-lab",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/yivan-lab/pretty-please.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/yivan-lab/pretty-please/issues"
36
+ },
37
+ "homepage": "https://github.com/yivan-lab/pretty-please#readme",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "files": [
42
+ "bin",
43
+ "dist",
44
+ "src",
45
+ "README.md",
46
+ "LICENSE",
47
+ "package.json",
48
+ "tsconfig.json"
49
+ ],
50
+ "dependencies": {
51
+ "@mastra/core": "^0.24.8",
52
+ "chalk": "^5.6.2",
53
+ "cli-highlight": "^2.1.11",
54
+ "commander": "^14.0.2",
55
+ "ink": "^6.5.1",
56
+ "ink-box": "^2.0.0",
57
+ "ink-markdown": "^1.0.4",
58
+ "ink-select-input": "^6.2.0",
59
+ "ink-spinner": "^5.0.0",
60
+ "ink-text-input": "^6.0.0",
61
+ "lowlight": "^3.3.0",
62
+ "marked": "^17.0.1",
63
+ "openai": "^6.10.0",
64
+ "ora": "^9.0.0",
65
+ "react": "^19.2.3",
66
+ "shiki": "^3.20.0",
67
+ "string-width": "^8.1.0",
68
+ "wrap-ansi": "^9.0.2",
69
+ "zod": "^3.25.76"
70
+ },
71
+ "devDependencies": {
72
+ "@types/hast": "^3.0.4",
73
+ "@types/node": "^25.0.2",
74
+ "@types/react": "^19.2.7",
75
+ "tsx": "^4.21.0",
76
+ "typescript": "^5.9.3"
77
+ }
78
+ }
package/src/ai.js ADDED
@@ -0,0 +1,324 @@
1
+ import OpenAI from 'openai';
2
+ import { getConfig } from './config.js';
3
+ import { formatSystemInfo } from './sysinfo.js';
4
+ import { formatHistoryForAI } from './history.js';
5
+ import { formatShellHistoryForAI, getShellHistory } from './shell-hook.js';
6
+ import { getChatHistory, addChatMessage } from './chat-history.js';
7
+
8
+ /**
9
+ * 创建 OpenAI 客户端
10
+ */
11
+ function createClient() {
12
+ const config = getConfig();
13
+
14
+ return new OpenAI({
15
+ apiKey: config.apiKey,
16
+ baseURL: config.baseUrl
17
+ });
18
+ }
19
+
20
+ /**
21
+ * 生成系统提示词
22
+ * @param {string} sysinfo - 系统信息
23
+ * @param {string} plsHistory - pls 命令历史
24
+ * @param {string} shellHistory - shell 终端历史
25
+ * @param {boolean} shellHookEnabled - 是否启用了 shell hook
26
+ */
27
+ function buildSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled) {
28
+ let prompt = `你是一个专业的 shell 脚本生成器。用户会提供他们的系统信息和一个命令需求。
29
+ 你的任务是返回一个可执行的、原始的 shell 命令或脚本来完成他们的目标。
30
+
31
+ 重要规则:
32
+ 1. 返回 JSON 格式,command 字段必须是可直接执行的命令(无解释、无注释、无 markdown)
33
+ 2. 不要添加 shebang(如 #!/bin/bash)
34
+ 3. command 可以包含多条命令(用 && 连接),但整体算一个命令
35
+ 4. 根据用户的系统信息选择合适的命令(如包管理器)
36
+ 5. 如果用户引用了之前的操作(如"刚才的"、"上一个"),请参考历史记录
37
+ 6. 绝对不要输出 pls 或 please 命令!
38
+
39
+ 【输出格式 - 非常重要】
40
+
41
+ 单步模式(一个命令完成):
42
+ 如果任务只需要一个命令就能完成,只返回:
43
+ {
44
+ "command": "ls -la"
45
+ }
46
+
47
+ 多步模式(需要多个命令,后续依赖前面的结果):
48
+ 如果任务需要多个命令,且后续命令必须根据前面的执行结果来决定,则返回:
49
+
50
+ 【多步骤完整示例】
51
+ 用户:"查找大于100MB的日志文件并压缩"
52
+
53
+ 第一步你返回:
54
+ {
55
+ "command": "find . -name '*.log' -size +100M",
56
+ "continue": true,
57
+ "reasoning": "查找大日志",
58
+ "nextStepHint": "压缩找到的文件"
59
+ }
60
+
61
+ 执行后你会收到:
62
+ 命令已执行
63
+ 退出码: 0
64
+ 输出:
65
+ ./app.log
66
+ ./system.log
67
+
68
+ 然后你返回第二步:
69
+ {
70
+ "command": "tar -czf logs.tar.gz ./app.log ./system.log",
71
+ "continue": false,
72
+ "reasoning": "压缩日志文件"
73
+ }
74
+
75
+ 关键判断标准:
76
+ - 多步 = 后续命令依赖前面的输出(如先 find 看有哪些,再根据结果操作具体文件)
77
+ - 单步 = 一个命令就能完成(即使命令里有 && 连接多条,也算一个命令)
78
+
79
+ 常见场景举例:
80
+ - "删除空文件夹" → 单步:find . -empty -delete (一个命令完成)
81
+ - "查找大文件并压缩" → 多步:先 find 看有哪些,再 tar 压缩具体文件
82
+ - "安装 git" → 单步:brew install git
83
+ - "备份并删除旧日志" → 多步:先 mkdir backup,再 mv 文件到 backup
84
+ - "查看目录" → 单步:ls -la
85
+
86
+ 严格要求:单步模式只返回 {"command": "xxx"},绝对不要输出 continue/reasoning/nextStepHint!
87
+
88
+ 【错误处理】
89
+ 如果你收到命令执行失败的信息(退出码非0),你应该:
90
+ 1. 分析错误原因
91
+ 2. 调整命令策略,返回修正后的命令
92
+ 3. 设置 continue: true 重试,或设置 continue: false 放弃
93
+
94
+ 错误处理示例:
95
+ 上一步失败,你收到:
96
+ 命令已执行
97
+ 退出码: 1
98
+ 输出:
99
+ mv: rename ./test.zip to ./c/test.zip: No such file or directory
100
+
101
+ 你分析后返回修正:
102
+ {
103
+ "command": "cp test.zip a/ && cp test.zip b/ && cp test.zip c/",
104
+ "continue": false,
105
+ "reasoning": "改用 cp 复制而非 mv"
106
+ }
107
+
108
+ 或者放弃:
109
+ {
110
+ "command": "echo '任务无法完成:文件已被移动'",
111
+ "continue": false,
112
+ "reasoning": "文件不存在,无法继续"
113
+ }
114
+
115
+ 关于 pls/please 工具:
116
+ 用户正在使用 pls(pretty-please)工具,这是一个将自然语言转换为 shell 命令的 AI 助手。
117
+ 当用户输入 "pls <描述>" 时,AI(也就是你)会生成对应的 shell 命令供用户确认执行。
118
+ 历史记录中标记为 [pls] 的条目表示用户通过 pls 工具执行的命令。
119
+
120
+ 用户的系统信息:${sysinfo}`;
121
+
122
+ // 根据是否启用 shell hook 决定展示哪个历史
123
+ if (shellHookEnabled && shellHistory) {
124
+ // 启用了 shell hook:只展示 shell history(已包含增强的 pls 信息)
125
+ prompt += `\n\n${shellHistory}`;
126
+ } else if (plsHistory) {
127
+ // 未启用 shell hook:只展示 pls history
128
+ prompt += `\n\n${plsHistory}`;
129
+ }
130
+
131
+ return prompt;
132
+ }
133
+
134
+ // 导出给其他模块使用
135
+ export { buildSystemPrompt };
136
+
137
+ /**
138
+ * 调用 AI 生成命令
139
+ * @param {string} prompt 用户输入的自然语言描述
140
+ * @param {object} options 选项
141
+ * @param {boolean} options.debug 是否返回调试信息
142
+ */
143
+ export async function generateCommand(prompt, options = {}) {
144
+ const config = getConfig();
145
+ const client = createClient();
146
+ const sysinfo = formatSystemInfo();
147
+ const plsHistory = formatHistoryForAI();
148
+ const shellHistory = formatShellHistoryForAI();
149
+ const shellHookEnabled = config.shellHook && getShellHistory().length > 0;
150
+ const systemPrompt = buildSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled);
151
+
152
+ const response = await client.chat.completions.create({
153
+ model: config.model,
154
+ messages: [
155
+ {
156
+ role: 'system',
157
+ content: systemPrompt
158
+ },
159
+ {
160
+ role: 'user',
161
+ content: prompt
162
+ }
163
+ ],
164
+ max_tokens: 1024,
165
+ temperature: 0.2
166
+ });
167
+
168
+ const command = response.choices[0]?.message?.content?.trim();
169
+
170
+ if (!command) {
171
+ throw new Error('AI 返回了空的响应');
172
+ }
173
+
174
+ if (options.debug) {
175
+ return {
176
+ command,
177
+ debug: {
178
+ sysinfo,
179
+ model: config.model,
180
+ systemPrompt,
181
+ userPrompt: prompt
182
+ }
183
+ };
184
+ }
185
+
186
+ return command;
187
+ }
188
+
189
+ /**
190
+ * 生成 chat 模式的系统提示词
191
+ * @param {string} sysinfo - 系统信息
192
+ * @param {string} plsHistory - pls 命令历史
193
+ * @param {string} shellHistory - shell 终端历史
194
+ * @param {boolean} shellHookEnabled - 是否启用了 shell hook
195
+ */
196
+ function buildChatSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled) {
197
+ let prompt = `你是一个命令行专家助手,帮助用户理解和使用命令行工具。
198
+
199
+ 【你的能力】
200
+ - 解释命令的含义、参数、用法
201
+ - 分析命令的执行效果和潜在风险
202
+ - 回答命令行、Shell、系统管理相关问题
203
+ - 根据用户需求推荐合适的命令并解释
204
+
205
+ 【回答要求】
206
+ - 简洁清晰,避免冗余
207
+ - 危险操作要明确警告
208
+ - 适当给出示例命令
209
+ - 结合用户的系统环境给出针对性建议
210
+
211
+ 【用户系统信息】
212
+ ${sysinfo}`;
213
+
214
+ // 根据是否启用 shell hook 决定展示哪个历史
215
+ if (shellHookEnabled && shellHistory) {
216
+ prompt += `\n\n${shellHistory}`;
217
+ } else if (plsHistory) {
218
+ prompt += `\n\n${plsHistory}`;
219
+ }
220
+
221
+ return prompt;
222
+ }
223
+
224
+ /**
225
+ * 调用 AI 进行对话(chat 模式,支持流式输出)
226
+ * @param {string} prompt 用户输入的问题
227
+ * @param {object} options 选项
228
+ * @param {boolean} options.debug 是否返回调试信息
229
+ * @param {function} options.onChunk 流式输出回调,接收每个文本片段
230
+ */
231
+ export async function chatWithAI(prompt, options = {}) {
232
+ const config = getConfig();
233
+ const client = createClient();
234
+ const sysinfo = formatSystemInfo();
235
+ const plsHistory = formatHistoryForAI();
236
+ const shellHistory = formatShellHistoryForAI();
237
+ const shellHookEnabled = config.shellHook && getShellHistory().length > 0;
238
+ const systemPrompt = buildChatSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled);
239
+
240
+ // 获取对话历史
241
+ const chatHistory = getChatHistory();
242
+
243
+ // 构建消息数组
244
+ const messages = [
245
+ { role: 'system', content: systemPrompt },
246
+ ...chatHistory,
247
+ { role: 'user', content: prompt }
248
+ ];
249
+
250
+ // 流式输出模式
251
+ if (options.onChunk) {
252
+ const stream = await client.chat.completions.create({
253
+ model: config.model,
254
+ messages,
255
+ max_tokens: 2048,
256
+ temperature: 0.7,
257
+ stream: true
258
+ });
259
+
260
+ let fullContent = '';
261
+
262
+ for await (const chunk of stream) {
263
+ const content = chunk.choices[0]?.delta?.content || '';
264
+ if (content) {
265
+ fullContent += content;
266
+ options.onChunk(content);
267
+ }
268
+ }
269
+
270
+ if (!fullContent) {
271
+ throw new Error('AI 返回了空的响应');
272
+ }
273
+
274
+ // 保存对话历史
275
+ addChatMessage(prompt, fullContent);
276
+
277
+ if (options.debug) {
278
+ return {
279
+ reply: fullContent,
280
+ debug: {
281
+ sysinfo,
282
+ model: config.model,
283
+ systemPrompt,
284
+ chatHistory,
285
+ userPrompt: prompt
286
+ }
287
+ };
288
+ }
289
+
290
+ return fullContent;
291
+ }
292
+
293
+ // 非流式模式(保持兼容)
294
+ const response = await client.chat.completions.create({
295
+ model: config.model,
296
+ messages,
297
+ max_tokens: 2048,
298
+ temperature: 0.7
299
+ });
300
+
301
+ const reply = response.choices[0]?.message?.content?.trim();
302
+
303
+ if (!reply) {
304
+ throw new Error('AI 返回了空的响应');
305
+ }
306
+
307
+ // 保存对话历史
308
+ addChatMessage(prompt, reply);
309
+
310
+ if (options.debug) {
311
+ return {
312
+ reply,
313
+ debug: {
314
+ sysinfo,
315
+ model: config.model,
316
+ systemPrompt,
317
+ chatHistory,
318
+ userPrompt: prompt
319
+ }
320
+ };
321
+ }
322
+
323
+ return reply;
324
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Shell builtin 命令检测器
3
+ *
4
+ * 用于检测命令中是否包含 shell 内置命令(builtin)
5
+ * 这些命令在子进程中执行可能无效或行为异常
6
+ */
7
+
8
+ // Shell 内置命令列表
9
+ const SHELL_BUILTINS = [
10
+ // 目录相关
11
+ 'cd', 'pushd', 'popd', 'dirs',
12
+
13
+ // 历史相关
14
+ 'history',
15
+
16
+ // 别名和函数
17
+ 'alias', 'unalias',
18
+
19
+ // 环境变量
20
+ 'export', 'set', 'unset', 'declare', 'local', 'readonly',
21
+
22
+ // 脚本执行
23
+ 'source', '.',
24
+
25
+ // 任务控制
26
+ 'jobs', 'fg', 'bg', 'disown',
27
+
28
+ // 其他
29
+ 'ulimit', 'umask', 'builtin', 'command', 'type',
30
+ 'enable', 'hash', 'help', 'let', 'read', 'wait',
31
+ 'eval', 'exec', 'trap', 'times', 'shopt'
32
+ ];
33
+
34
+ /**
35
+ * 提取命令中的所有命令名
36
+ * @param {string} command - 完整命令字符串
37
+ * @returns {string[]} 命令名数组
38
+ */
39
+ function extractCommandNames(command) {
40
+ // 按分隔符拆分(&&, ||, ;, |, &, 换行)
41
+ // 注意:| 是管道,两边都在子进程中执行,所以也算
42
+ const parts = command.split(/[;&|]+|\n+/);
43
+
44
+ const commandNames = [];
45
+
46
+ for (const part of parts) {
47
+ const trimmed = part.trim();
48
+ if (!trimmed) continue;
49
+
50
+ // 提取第一个单词(命令名)
51
+ // 处理 sudo、env 等前缀
52
+ let words = trimmed.split(/\s+/);
53
+
54
+ // 跳过 sudo、env 等
55
+ let i = 0;
56
+ while (i < words.length && ['sudo', 'env', 'nohup', 'nice'].includes(words[i])) {
57
+ i++;
58
+ }
59
+
60
+ if (i < words.length) {
61
+ commandNames.push(words[i]);
62
+ }
63
+ }
64
+
65
+ return commandNames;
66
+ }
67
+
68
+ /**
69
+ * 检测命令中是否包含 builtin
70
+ * @param {string} command - 要检测的命令
71
+ * @returns {{ hasBuiltin: boolean, builtins: string[] }} 检测结果
72
+ */
73
+ export function detectBuiltin(command) {
74
+ const commandNames = extractCommandNames(command);
75
+ const foundBuiltins = [];
76
+
77
+ for (const name of commandNames) {
78
+ if (SHELL_BUILTINS.includes(name)) {
79
+ foundBuiltins.push(name);
80
+ }
81
+ }
82
+
83
+ return {
84
+ hasBuiltin: foundBuiltins.length > 0,
85
+ builtins: [...new Set(foundBuiltins)] // 去重
86
+ };
87
+ }
88
+
89
+ /**
90
+ * 格式化 builtin 列表为易读的字符串
91
+ * @param {string[]} builtins - builtin 命令数组
92
+ * @returns {string} 格式化后的字符串
93
+ */
94
+ export function formatBuiltins(builtins) {
95
+ if (builtins.length === 0) return '';
96
+ if (builtins.length === 1) return builtins[0];
97
+ return builtins.join(', ');
98
+ }
@@ -0,0 +1,94 @@
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 CHAT_HISTORY_FILE = path.join(CONFIG_DIR, 'chat_history.json');
8
+
9
+ /**
10
+ * 确保配置目录存在
11
+ */
12
+ function ensureConfigDir() {
13
+ if (!fs.existsSync(CONFIG_DIR)) {
14
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
15
+ }
16
+ }
17
+
18
+ /**
19
+ * 读取对话历史
20
+ * @returns {Array<{role: string, content: string}>} messages 数组
21
+ */
22
+ export function getChatHistory() {
23
+ ensureConfigDir();
24
+
25
+ if (!fs.existsSync(CHAT_HISTORY_FILE)) {
26
+ return [];
27
+ }
28
+
29
+ try {
30
+ const content = fs.readFileSync(CHAT_HISTORY_FILE, 'utf-8');
31
+ return JSON.parse(content);
32
+ } catch {
33
+ return [];
34
+ }
35
+ }
36
+
37
+ /**
38
+ * 保存对话历史
39
+ * @param {Array<{role: string, content: string}>} history
40
+ */
41
+ function saveChatHistory(history) {
42
+ ensureConfigDir();
43
+ fs.writeFileSync(CHAT_HISTORY_FILE, JSON.stringify(history, null, 2));
44
+ }
45
+
46
+ /**
47
+ * 添加一轮对话(用户问题 + AI 回答)
48
+ * @param {string} userMessage - 用户消息
49
+ * @param {string} assistantMessage - AI 回复
50
+ */
51
+ export function addChatMessage(userMessage, assistantMessage) {
52
+ const config = getConfig();
53
+ const history = getChatHistory();
54
+
55
+ // 添加新的对话
56
+ history.push({ role: 'user', content: userMessage });
57
+ history.push({ role: 'assistant', content: assistantMessage });
58
+
59
+ // 计算当前轮数(每 2 条消息 = 1 轮)
60
+ const currentRounds = Math.floor(history.length / 2);
61
+ const maxRounds = config.chatHistoryLimit || 10;
62
+
63
+ // 如果超出限制,移除最早的对话
64
+ if (currentRounds > maxRounds) {
65
+ // 需要移除的轮数
66
+ const removeRounds = currentRounds - maxRounds;
67
+ // 移除最早的 N 轮(N*2 条消息)
68
+ history.splice(0, removeRounds * 2);
69
+ }
70
+
71
+ saveChatHistory(history);
72
+ }
73
+
74
+ /**
75
+ * 清空对话历史
76
+ */
77
+ export function clearChatHistory() {
78
+ saveChatHistory([]);
79
+ }
80
+
81
+ /**
82
+ * 获取对话历史文件路径
83
+ */
84
+ export function getChatHistoryFilePath() {
85
+ return CHAT_HISTORY_FILE;
86
+ }
87
+
88
+ /**
89
+ * 获取当前对话轮数
90
+ */
91
+ export function getChatRoundCount() {
92
+ const history = getChatHistory();
93
+ return Math.floor(history.length / 2);
94
+ }