@yivan-lab/pretty-please 1.3.1 → 1.4.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 +226 -624
- package/bin/pls.tsx +25 -5
- package/dist/bin/pls.js +23 -4
- package/dist/package.json +1 -1
- package/dist/src/mastra-chat.js +4 -3
- package/dist/src/multi-step.js +4 -5
- package/dist/src/shell-hook.d.ts +19 -0
- package/dist/src/shell-hook.js +63 -0
- package/dist/src/system-history.d.ts +13 -0
- package/dist/src/system-history.js +105 -0
- package/package.json +1 -1
- package/src/mastra-chat.ts +4 -2
- package/src/multi-step.ts +4 -3
- package/src/shell-hook.ts +76 -0
- package/src/system-history.ts +117 -0
package/bin/pls.tsx
CHANGED
|
@@ -1240,7 +1240,7 @@ program
|
|
|
1240
1240
|
.argument('[prompt...]', '自然语言描述你想执行的操作')
|
|
1241
1241
|
.option('-d, --debug', '显示调试信息(系统信息、完整 prompt 等)')
|
|
1242
1242
|
.option('-r, --remote [name]', '在远程服务器上执行(不指定则使用默认服务器)')
|
|
1243
|
-
.action((promptArgs, options) => {
|
|
1243
|
+
.action(async (promptArgs, options) => {
|
|
1244
1244
|
// 智能处理 -r 参数:如果 -r 后面的值不是已注册的服务器名,把它当作 prompt 的一部分
|
|
1245
1245
|
if (typeof options.remote === 'string' && !getRemote(options.remote)) {
|
|
1246
1246
|
// "查看当前目录" 不是服务器名,放回 prompt
|
|
@@ -1248,12 +1248,32 @@ program
|
|
|
1248
1248
|
options.remote = true // 改为使用默认服务器
|
|
1249
1249
|
}
|
|
1250
1250
|
|
|
1251
|
+
let prompt = ''
|
|
1252
|
+
|
|
1251
1253
|
if (promptArgs.length === 0) {
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1254
|
+
// 无参数时:尝试自动检测上一条失败的命令
|
|
1255
|
+
const { getLastNonPlsCommand } = await import('../src/shell-hook.js')
|
|
1256
|
+
const lastCmd = getLastNonPlsCommand()
|
|
1257
|
+
|
|
1258
|
+
if (lastCmd && lastCmd.exit !== 0) {
|
|
1259
|
+
// 找到了失败的命令,自动生成 prompt
|
|
1260
|
+
prompt = `上一条命令「${lastCmd.cmd}」执行失败,退出码:${lastCmd.exit}。请生成正确的命令。`
|
|
1261
|
+
|
|
1262
|
+
if (options.debug) {
|
|
1263
|
+
console.log('')
|
|
1264
|
+
console2.muted(`自动检测到失败命令: ${lastCmd.cmd} (退出码: ${lastCmd.exit})`)
|
|
1265
|
+
console2.muted(`生成 prompt: ${prompt}`)
|
|
1266
|
+
}
|
|
1255
1267
|
|
|
1256
|
-
|
|
1268
|
+
// 继续执行命令生成流程(不 return)
|
|
1269
|
+
} else {
|
|
1270
|
+
// 没有失败的命令,显示帮助
|
|
1271
|
+
program.help()
|
|
1272
|
+
return
|
|
1273
|
+
}
|
|
1274
|
+
} else {
|
|
1275
|
+
prompt = promptArgs.join(' ')
|
|
1276
|
+
}
|
|
1257
1277
|
|
|
1258
1278
|
if (!prompt.trim()) {
|
|
1259
1279
|
console.log('')
|
package/dist/bin/pls.js
CHANGED
|
@@ -1030,18 +1030,37 @@ program
|
|
|
1030
1030
|
.argument('[prompt...]', '自然语言描述你想执行的操作')
|
|
1031
1031
|
.option('-d, --debug', '显示调试信息(系统信息、完整 prompt 等)')
|
|
1032
1032
|
.option('-r, --remote [name]', '在远程服务器上执行(不指定则使用默认服务器)')
|
|
1033
|
-
.action((promptArgs, options) => {
|
|
1033
|
+
.action(async (promptArgs, options) => {
|
|
1034
1034
|
// 智能处理 -r 参数:如果 -r 后面的值不是已注册的服务器名,把它当作 prompt 的一部分
|
|
1035
1035
|
if (typeof options.remote === 'string' && !getRemote(options.remote)) {
|
|
1036
1036
|
// "查看当前目录" 不是服务器名,放回 prompt
|
|
1037
1037
|
promptArgs.unshift(options.remote);
|
|
1038
1038
|
options.remote = true; // 改为使用默认服务器
|
|
1039
1039
|
}
|
|
1040
|
+
let prompt = '';
|
|
1040
1041
|
if (promptArgs.length === 0) {
|
|
1041
|
-
|
|
1042
|
-
|
|
1042
|
+
// 无参数时:尝试自动检测上一条失败的命令
|
|
1043
|
+
const { getLastNonPlsCommand } = await import('../src/shell-hook.js');
|
|
1044
|
+
const lastCmd = getLastNonPlsCommand();
|
|
1045
|
+
if (lastCmd && lastCmd.exit !== 0) {
|
|
1046
|
+
// 找到了失败的命令,自动生成 prompt
|
|
1047
|
+
prompt = `上一条命令「${lastCmd.cmd}」执行失败,退出码:${lastCmd.exit}。请生成正确的命令。`;
|
|
1048
|
+
if (options.debug) {
|
|
1049
|
+
console.log('');
|
|
1050
|
+
console2.muted(`自动检测到失败命令: ${lastCmd.cmd} (退出码: ${lastCmd.exit})`);
|
|
1051
|
+
console2.muted(`生成 prompt: ${prompt}`);
|
|
1052
|
+
}
|
|
1053
|
+
// 继续执行命令生成流程(不 return)
|
|
1054
|
+
}
|
|
1055
|
+
else {
|
|
1056
|
+
// 没有失败的命令,显示帮助
|
|
1057
|
+
program.help();
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
else {
|
|
1062
|
+
prompt = promptArgs.join(' ');
|
|
1043
1063
|
}
|
|
1044
|
-
let prompt = promptArgs.join(' ');
|
|
1045
1064
|
if (!prompt.trim()) {
|
|
1046
1065
|
console.log('');
|
|
1047
1066
|
console2.error('请提供你想执行的操作描述');
|
package/dist/package.json
CHANGED
package/dist/src/mastra-chat.js
CHANGED
|
@@ -3,7 +3,6 @@ import { getConfig } from './config.js';
|
|
|
3
3
|
import { CHAT_SYSTEM_PROMPT, buildChatUserContext } from './prompts.js';
|
|
4
4
|
import { formatSystemInfo } from './sysinfo.js';
|
|
5
5
|
import { formatHistoryForAI } from './history.js';
|
|
6
|
-
import { formatShellHistoryForAI, getShellHistory } from './shell-hook.js';
|
|
7
6
|
import { getChatHistory, addChatMessage } from './chat-history.js';
|
|
8
7
|
/**
|
|
9
8
|
* 创建 Mastra Chat Agent(使用静态系统提示词)
|
|
@@ -50,8 +49,10 @@ export async function chatWithMastra(prompt, options = {}) {
|
|
|
50
49
|
// 3. 构建最新消息(动态上下文 + 用户问题)
|
|
51
50
|
const sysinfo = formatSystemInfo();
|
|
52
51
|
const plsHistory = formatHistoryForAI();
|
|
53
|
-
|
|
54
|
-
const
|
|
52
|
+
// 使用统一的历史获取接口(自动降级到系统历史)
|
|
53
|
+
const { formatShellHistoryForAIWithFallback } = await import('./shell-hook.js');
|
|
54
|
+
const shellHistory = formatShellHistoryForAIWithFallback();
|
|
55
|
+
const shellHookEnabled = !!shellHistory; // 如果有 shell 历史就视为启用
|
|
55
56
|
const latestUserContext = buildChatUserContext(prompt, sysinfo, plsHistory, shellHistory, shellHookEnabled);
|
|
56
57
|
messages.push(latestUserContext);
|
|
57
58
|
// 4. 发送给 AI(流式或非流式)
|
package/dist/src/multi-step.js
CHANGED
|
@@ -3,8 +3,6 @@ import { createShellAgent } from './mastra-agent.js';
|
|
|
3
3
|
import { SHELL_COMMAND_SYSTEM_PROMPT, buildUserContextPrompt } from './prompts.js';
|
|
4
4
|
import { formatSystemInfo } from './sysinfo.js';
|
|
5
5
|
import { formatHistoryForAI } from './history.js';
|
|
6
|
-
import { formatShellHistoryForAI, getShellHistory } from './shell-hook.js';
|
|
7
|
-
import { getConfig } from './config.js';
|
|
8
6
|
import { formatRemoteHistoryForAI, formatRemoteShellHistoryForAI } from './remote-history.js';
|
|
9
7
|
import { formatRemoteSysInfoForAI } from './remote.js';
|
|
10
8
|
/**
|
|
@@ -47,10 +45,11 @@ export async function generateMultiStepCommand(userPrompt, previousSteps = [], o
|
|
|
47
45
|
else {
|
|
48
46
|
// 本地执行:格式化本地系统信息和历史
|
|
49
47
|
sysinfoStr = formatSystemInfo();
|
|
50
|
-
const config = getConfig();
|
|
51
48
|
const plsHistory = formatHistoryForAI();
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
// 使用统一的历史获取接口(自动降级到系统历史)
|
|
50
|
+
const { formatShellHistoryForAIWithFallback } = await import('./shell-hook.js');
|
|
51
|
+
const shellHistory = formatShellHistoryForAIWithFallback();
|
|
52
|
+
historyStr = shellHistory || plsHistory; // 优先使用 shell 历史,降级到 pls 历史
|
|
54
53
|
}
|
|
55
54
|
// 构建包含所有动态数据的 User Prompt(XML 格式)
|
|
56
55
|
const userContextPrompt = buildUserContextPrompt(userPrompt, sysinfoStr, historyStr, previousSteps);
|
package/dist/src/shell-hook.d.ts
CHANGED
|
@@ -59,6 +59,25 @@ export declare function reinstallHookForLimitChange(oldLimit: number, newLimit:
|
|
|
59
59
|
* 清空 shell 历史
|
|
60
60
|
*/
|
|
61
61
|
export declare function clearShellHistory(): void;
|
|
62
|
+
/**
|
|
63
|
+
* 获取 shell 历史(统一接口)
|
|
64
|
+
* 优先使用 shell hook,降级到系统历史文件
|
|
65
|
+
*
|
|
66
|
+
* 这是推荐的历史获取方式,会自动选择最佳来源:
|
|
67
|
+
* 1. 优先:shell hook(有退出码,最准确)
|
|
68
|
+
* 2. 降级:系统历史文件(无退出码,兼容未安装 hook 的情况)
|
|
69
|
+
*/
|
|
70
|
+
export declare function getShellHistoryWithFallback(): ShellHistoryItem[];
|
|
71
|
+
/**
|
|
72
|
+
* 获取最近一条非 pls 的命令(统一接口)
|
|
73
|
+
* 自动选择最佳历史来源
|
|
74
|
+
*/
|
|
75
|
+
export declare function getLastNonPlsCommand(): ShellHistoryItem | null;
|
|
76
|
+
/**
|
|
77
|
+
* 格式化 shell 历史供 AI 使用(统一接口)
|
|
78
|
+
* 自动选择最佳历史来源
|
|
79
|
+
*/
|
|
80
|
+
export declare function formatShellHistoryForAIWithFallback(): string;
|
|
62
81
|
/**
|
|
63
82
|
* 检测远程服务器的 shell 类型
|
|
64
83
|
*/
|
package/dist/src/shell-hook.js
CHANGED
|
@@ -479,6 +479,69 @@ export function clearShellHistory() {
|
|
|
479
479
|
console.log(chalk.hex(colors.success)('✓ Shell 历史已清空'));
|
|
480
480
|
console.log('');
|
|
481
481
|
}
|
|
482
|
+
// ================== 统一历史获取 ==================
|
|
483
|
+
/**
|
|
484
|
+
* 获取 shell 历史(统一接口)
|
|
485
|
+
* 优先使用 shell hook,降级到系统历史文件
|
|
486
|
+
*
|
|
487
|
+
* 这是推荐的历史获取方式,会自动选择最佳来源:
|
|
488
|
+
* 1. 优先:shell hook(有退出码,最准确)
|
|
489
|
+
* 2. 降级:系统历史文件(无退出码,兼容未安装 hook 的情况)
|
|
490
|
+
*/
|
|
491
|
+
export function getShellHistoryWithFallback() {
|
|
492
|
+
const config = getConfig();
|
|
493
|
+
// 优先使用 shell hook
|
|
494
|
+
if (config.shellHook) {
|
|
495
|
+
const history = getShellHistory();
|
|
496
|
+
if (history.length > 0) {
|
|
497
|
+
return history;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// 降级到系统历史文件
|
|
501
|
+
const { getSystemShellHistory } = require('./system-history.js');
|
|
502
|
+
return getSystemShellHistory();
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* 获取最近一条非 pls 的命令(统一接口)
|
|
506
|
+
* 自动选择最佳历史来源
|
|
507
|
+
*/
|
|
508
|
+
export function getLastNonPlsCommand() {
|
|
509
|
+
const history = getShellHistoryWithFallback();
|
|
510
|
+
// 从后往前找第一条非 pls 命令
|
|
511
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
512
|
+
const item = history[i];
|
|
513
|
+
if (!item.cmd.startsWith('pls') && !item.cmd.startsWith('please')) {
|
|
514
|
+
return item;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* 格式化 shell 历史供 AI 使用(统一接口)
|
|
521
|
+
* 自动选择最佳历史来源
|
|
522
|
+
*/
|
|
523
|
+
export function formatShellHistoryForAIWithFallback() {
|
|
524
|
+
const config = getConfig();
|
|
525
|
+
// 如果启用了 shell hook 且有记录,使用 hook 历史(包含详细信息)
|
|
526
|
+
if (config.shellHook) {
|
|
527
|
+
const hookHistory = getShellHistory();
|
|
528
|
+
if (hookHistory.length > 0) {
|
|
529
|
+
return formatShellHistoryForAI();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// 降级到系统历史
|
|
533
|
+
const { getSystemShellHistory } = require('./system-history.js');
|
|
534
|
+
const history = getSystemShellHistory();
|
|
535
|
+
if (history.length === 0) {
|
|
536
|
+
return '';
|
|
537
|
+
}
|
|
538
|
+
// 格式化系统历史(简单格式,无详细信息)
|
|
539
|
+
const lines = history.map((item, index) => {
|
|
540
|
+
const status = item.exit === 0 ? '✓' : `✗ 退出码:${item.exit}`;
|
|
541
|
+
return `${index + 1}. ${item.cmd} ${status}`;
|
|
542
|
+
});
|
|
543
|
+
return `【用户终端最近执行的命令(来自系统历史)】\n${lines.join('\n')}`;
|
|
544
|
+
}
|
|
482
545
|
// ================== 远程 Shell Hook ==================
|
|
483
546
|
/**
|
|
484
547
|
* 生成远程 zsh hook 脚本
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ShellHistoryItem } from './shell-hook.js';
|
|
2
|
+
/**
|
|
3
|
+
* 直接读取系统 shell 历史文件(类似 thefuck)
|
|
4
|
+
* 用于没有安装 shell hook 的情况
|
|
5
|
+
*
|
|
6
|
+
* 限制:系统历史文件不记录退出码,所以 exit 字段都是 0
|
|
7
|
+
*/
|
|
8
|
+
export declare function getSystemShellHistory(): ShellHistoryItem[];
|
|
9
|
+
/**
|
|
10
|
+
* 从系统历史中获取最近一条命令
|
|
11
|
+
* 排除 pls 命令本身
|
|
12
|
+
*/
|
|
13
|
+
export declare function getLastCommandFromSystem(): ShellHistoryItem | null;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { getConfig } from './config.js';
|
|
5
|
+
/**
|
|
6
|
+
* 直接读取系统 shell 历史文件(类似 thefuck)
|
|
7
|
+
* 用于没有安装 shell hook 的情况
|
|
8
|
+
*
|
|
9
|
+
* 限制:系统历史文件不记录退出码,所以 exit 字段都是 0
|
|
10
|
+
*/
|
|
11
|
+
export function getSystemShellHistory() {
|
|
12
|
+
const shell = process.env.SHELL || '';
|
|
13
|
+
const home = os.homedir();
|
|
14
|
+
let historyFile;
|
|
15
|
+
let parser;
|
|
16
|
+
if (shell.includes('zsh')) {
|
|
17
|
+
// zsh 历史文件
|
|
18
|
+
historyFile = process.env.HISTFILE || path.join(home, '.zsh_history');
|
|
19
|
+
parser = parseZshHistoryLine;
|
|
20
|
+
}
|
|
21
|
+
else if (shell.includes('bash')) {
|
|
22
|
+
// bash 历史文件
|
|
23
|
+
historyFile = process.env.HISTFILE || path.join(home, '.bash_history');
|
|
24
|
+
parser = parseBashHistoryLine;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// 不支持的 shell
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
if (!fs.existsSync(historyFile)) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const content = fs.readFileSync(historyFile, 'utf-8');
|
|
35
|
+
const lines = content.trim().split('\n');
|
|
36
|
+
const limit = getConfig().shellHistoryLimit || 10;
|
|
37
|
+
// 只取最后 N 条
|
|
38
|
+
const recentLines = lines.slice(-limit);
|
|
39
|
+
return recentLines
|
|
40
|
+
.map(line => parser(line))
|
|
41
|
+
.filter((item) => item !== null);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 解析 zsh 历史行
|
|
49
|
+
* 格式: ": 1234567890:0;ls -la"
|
|
50
|
+
* 或者: "ls -la" (简单格式)
|
|
51
|
+
*/
|
|
52
|
+
function parseZshHistoryLine(line) {
|
|
53
|
+
// 扩展格式: ": timestamp:duration;command"
|
|
54
|
+
const extendedMatch = line.match(/^:\s*(\d+):\d+;(.+)$/);
|
|
55
|
+
if (extendedMatch) {
|
|
56
|
+
const timestamp = parseInt(extendedMatch[1]);
|
|
57
|
+
const cmd = extendedMatch[2].trim();
|
|
58
|
+
return {
|
|
59
|
+
cmd,
|
|
60
|
+
exit: 0, // 系统历史文件不记录退出码
|
|
61
|
+
time: new Date(timestamp * 1000).toISOString(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// 简单格式
|
|
65
|
+
const cmd = line.trim();
|
|
66
|
+
if (cmd) {
|
|
67
|
+
return {
|
|
68
|
+
cmd,
|
|
69
|
+
exit: 0,
|
|
70
|
+
time: new Date().toISOString(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 解析 bash 历史行
|
|
77
|
+
* 格式: "ls -la"
|
|
78
|
+
* bash 历史文件默认不记录时间戳
|
|
79
|
+
*/
|
|
80
|
+
function parseBashHistoryLine(line) {
|
|
81
|
+
const cmd = line.trim();
|
|
82
|
+
if (cmd) {
|
|
83
|
+
return {
|
|
84
|
+
cmd,
|
|
85
|
+
exit: 0, // 系统历史文件不记录退出码
|
|
86
|
+
time: new Date().toISOString(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 从系统历史中获取最近一条命令
|
|
93
|
+
* 排除 pls 命令本身
|
|
94
|
+
*/
|
|
95
|
+
export function getLastCommandFromSystem() {
|
|
96
|
+
const history = getSystemShellHistory();
|
|
97
|
+
// 从后往前找第一条非 pls 命令
|
|
98
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
99
|
+
const item = history[i];
|
|
100
|
+
if (!item.cmd.startsWith('pls') && !item.cmd.startsWith('please')) {
|
|
101
|
+
return item;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
package/package.json
CHANGED
package/src/mastra-chat.ts
CHANGED
|
@@ -73,8 +73,10 @@ export async function chatWithMastra(
|
|
|
73
73
|
// 3. 构建最新消息(动态上下文 + 用户问题)
|
|
74
74
|
const sysinfo = formatSystemInfo()
|
|
75
75
|
const plsHistory = formatHistoryForAI()
|
|
76
|
-
|
|
77
|
-
const
|
|
76
|
+
// 使用统一的历史获取接口(自动降级到系统历史)
|
|
77
|
+
const { formatShellHistoryForAIWithFallback } = await import('./shell-hook.js')
|
|
78
|
+
const shellHistory = formatShellHistoryForAIWithFallback()
|
|
79
|
+
const shellHookEnabled = !!shellHistory // 如果有 shell 历史就视为启用
|
|
78
80
|
|
|
79
81
|
const latestUserContext = buildChatUserContext(
|
|
80
82
|
prompt,
|
package/src/multi-step.ts
CHANGED
|
@@ -75,10 +75,11 @@ export async function generateMultiStepCommand(
|
|
|
75
75
|
} else {
|
|
76
76
|
// 本地执行:格式化本地系统信息和历史
|
|
77
77
|
sysinfoStr = formatSystemInfo()
|
|
78
|
-
const config = getConfig()
|
|
79
78
|
const plsHistory = formatHistoryForAI()
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
// 使用统一的历史获取接口(自动降级到系统历史)
|
|
80
|
+
const { formatShellHistoryForAIWithFallback } = await import('./shell-hook.js')
|
|
81
|
+
const shellHistory = formatShellHistoryForAIWithFallback()
|
|
82
|
+
historyStr = shellHistory || plsHistory // 优先使用 shell 历史,降级到 pls 历史
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
// 构建包含所有动态数据的 User Prompt(XML 格式)
|
package/src/shell-hook.ts
CHANGED
|
@@ -573,6 +573,82 @@ export function clearShellHistory(): void {
|
|
|
573
573
|
console.log('')
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
+
// ================== 统一历史获取 ==================
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* 获取 shell 历史(统一接口)
|
|
580
|
+
* 优先使用 shell hook,降级到系统历史文件
|
|
581
|
+
*
|
|
582
|
+
* 这是推荐的历史获取方式,会自动选择最佳来源:
|
|
583
|
+
* 1. 优先:shell hook(有退出码,最准确)
|
|
584
|
+
* 2. 降级:系统历史文件(无退出码,兼容未安装 hook 的情况)
|
|
585
|
+
*/
|
|
586
|
+
export function getShellHistoryWithFallback(): ShellHistoryItem[] {
|
|
587
|
+
const config = getConfig()
|
|
588
|
+
|
|
589
|
+
// 优先使用 shell hook
|
|
590
|
+
if (config.shellHook) {
|
|
591
|
+
const history = getShellHistory()
|
|
592
|
+
if (history.length > 0) {
|
|
593
|
+
return history
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// 降级到系统历史文件
|
|
598
|
+
const { getSystemShellHistory } = require('./system-history.js')
|
|
599
|
+
return getSystemShellHistory()
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* 获取最近一条非 pls 的命令(统一接口)
|
|
604
|
+
* 自动选择最佳历史来源
|
|
605
|
+
*/
|
|
606
|
+
export function getLastNonPlsCommand(): ShellHistoryItem | null {
|
|
607
|
+
const history = getShellHistoryWithFallback()
|
|
608
|
+
|
|
609
|
+
// 从后往前找第一条非 pls 命令
|
|
610
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
611
|
+
const item = history[i]
|
|
612
|
+
if (!item.cmd.startsWith('pls') && !item.cmd.startsWith('please')) {
|
|
613
|
+
return item
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return null
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* 格式化 shell 历史供 AI 使用(统一接口)
|
|
622
|
+
* 自动选择最佳历史来源
|
|
623
|
+
*/
|
|
624
|
+
export function formatShellHistoryForAIWithFallback(): string {
|
|
625
|
+
const config = getConfig()
|
|
626
|
+
|
|
627
|
+
// 如果启用了 shell hook 且有记录,使用 hook 历史(包含详细信息)
|
|
628
|
+
if (config.shellHook) {
|
|
629
|
+
const hookHistory = getShellHistory()
|
|
630
|
+
if (hookHistory.length > 0) {
|
|
631
|
+
return formatShellHistoryForAI()
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// 降级到系统历史
|
|
636
|
+
const { getSystemShellHistory } = require('./system-history.js')
|
|
637
|
+
const history = getSystemShellHistory()
|
|
638
|
+
|
|
639
|
+
if (history.length === 0) {
|
|
640
|
+
return ''
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// 格式化系统历史(简单格式,无详细信息)
|
|
644
|
+
const lines = history.map((item, index) => {
|
|
645
|
+
const status = item.exit === 0 ? '✓' : `✗ 退出码:${item.exit}`
|
|
646
|
+
return `${index + 1}. ${item.cmd} ${status}`
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
return `【用户终端最近执行的命令(来自系统历史)】\n${lines.join('\n')}`
|
|
650
|
+
}
|
|
651
|
+
|
|
576
652
|
// ================== 远程 Shell Hook ==================
|
|
577
653
|
|
|
578
654
|
/**
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import { getConfig } from './config.js'
|
|
5
|
+
import type { ShellHistoryItem } from './shell-hook.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 直接读取系统 shell 历史文件(类似 thefuck)
|
|
9
|
+
* 用于没有安装 shell hook 的情况
|
|
10
|
+
*
|
|
11
|
+
* 限制:系统历史文件不记录退出码,所以 exit 字段都是 0
|
|
12
|
+
*/
|
|
13
|
+
export function getSystemShellHistory(): ShellHistoryItem[] {
|
|
14
|
+
const shell = process.env.SHELL || ''
|
|
15
|
+
const home = os.homedir()
|
|
16
|
+
|
|
17
|
+
let historyFile: string
|
|
18
|
+
let parser: (line: string) => ShellHistoryItem | null
|
|
19
|
+
|
|
20
|
+
if (shell.includes('zsh')) {
|
|
21
|
+
// zsh 历史文件
|
|
22
|
+
historyFile = process.env.HISTFILE || path.join(home, '.zsh_history')
|
|
23
|
+
parser = parseZshHistoryLine
|
|
24
|
+
} else if (shell.includes('bash')) {
|
|
25
|
+
// bash 历史文件
|
|
26
|
+
historyFile = process.env.HISTFILE || path.join(home, '.bash_history')
|
|
27
|
+
parser = parseBashHistoryLine
|
|
28
|
+
} else {
|
|
29
|
+
// 不支持的 shell
|
|
30
|
+
return []
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!fs.existsSync(historyFile)) {
|
|
34
|
+
return []
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const content = fs.readFileSync(historyFile, 'utf-8')
|
|
39
|
+
const lines = content.trim().split('\n')
|
|
40
|
+
const limit = getConfig().shellHistoryLimit || 10
|
|
41
|
+
|
|
42
|
+
// 只取最后 N 条
|
|
43
|
+
const recentLines = lines.slice(-limit)
|
|
44
|
+
|
|
45
|
+
return recentLines
|
|
46
|
+
.map(line => parser(line))
|
|
47
|
+
.filter((item): item is ShellHistoryItem => item !== null)
|
|
48
|
+
} catch {
|
|
49
|
+
return []
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 解析 zsh 历史行
|
|
55
|
+
* 格式: ": 1234567890:0;ls -la"
|
|
56
|
+
* 或者: "ls -la" (简单格式)
|
|
57
|
+
*/
|
|
58
|
+
function parseZshHistoryLine(line: string): ShellHistoryItem | null {
|
|
59
|
+
// 扩展格式: ": timestamp:duration;command"
|
|
60
|
+
const extendedMatch = line.match(/^:\s*(\d+):\d+;(.+)$/)
|
|
61
|
+
if (extendedMatch) {
|
|
62
|
+
const timestamp = parseInt(extendedMatch[1])
|
|
63
|
+
const cmd = extendedMatch[2].trim()
|
|
64
|
+
return {
|
|
65
|
+
cmd,
|
|
66
|
+
exit: 0, // 系统历史文件不记录退出码
|
|
67
|
+
time: new Date(timestamp * 1000).toISOString(),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 简单格式
|
|
72
|
+
const cmd = line.trim()
|
|
73
|
+
if (cmd) {
|
|
74
|
+
return {
|
|
75
|
+
cmd,
|
|
76
|
+
exit: 0,
|
|
77
|
+
time: new Date().toISOString(),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 解析 bash 历史行
|
|
86
|
+
* 格式: "ls -la"
|
|
87
|
+
* bash 历史文件默认不记录时间戳
|
|
88
|
+
*/
|
|
89
|
+
function parseBashHistoryLine(line: string): ShellHistoryItem | null {
|
|
90
|
+
const cmd = line.trim()
|
|
91
|
+
if (cmd) {
|
|
92
|
+
return {
|
|
93
|
+
cmd,
|
|
94
|
+
exit: 0, // 系统历史文件不记录退出码
|
|
95
|
+
time: new Date().toISOString(),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 从系统历史中获取最近一条命令
|
|
103
|
+
* 排除 pls 命令本身
|
|
104
|
+
*/
|
|
105
|
+
export function getLastCommandFromSystem(): ShellHistoryItem | null {
|
|
106
|
+
const history = getSystemShellHistory()
|
|
107
|
+
|
|
108
|
+
// 从后往前找第一条非 pls 命令
|
|
109
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
110
|
+
const item = history[i]
|
|
111
|
+
if (!item.cmd.startsWith('pls') && !item.cmd.startsWith('please')) {
|
|
112
|
+
return item
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return null
|
|
117
|
+
}
|