@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/multi-step.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { createShellAgent } from './mastra-agent.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'
|
|
7
|
-
import { getConfig } from './config.js'
|
|
7
|
+
import { getConfig, type RemoteSysInfo } from './config.js'
|
|
8
|
+
import { formatRemoteHistoryForAI, formatRemoteShellHistoryForAI, type RemoteShellHistoryItem } from './remote-history.js'
|
|
9
|
+
import { formatRemoteSysInfoForAI } from './remote.js'
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* 多步骤命令的 Zod Schema
|
|
13
|
+
* 注意:optional 字段使用 .default() 是为了绕过 Mastra 0.24.8 对 optional 字段的验证 bug
|
|
11
14
|
*/
|
|
12
15
|
export const CommandStepSchema = z.object({
|
|
13
16
|
command: z.string(),
|
|
14
|
-
continue: z.boolean().optional(),
|
|
15
|
-
reasoning: z.string().optional(),
|
|
16
|
-
nextStepHint: z.string().optional(),
|
|
17
|
+
continue: z.boolean().optional().default(false),
|
|
18
|
+
reasoning: z.string().optional().default(''),
|
|
19
|
+
nextStepHint: z.string().optional().default(''),
|
|
17
20
|
})
|
|
18
21
|
|
|
19
22
|
export type CommandStep = z.infer<typeof CommandStepSchema>
|
|
@@ -26,6 +29,15 @@ export interface ExecutedStep extends CommandStep {
|
|
|
26
29
|
output: string
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
/**
|
|
33
|
+
* 远程执行上下文
|
|
34
|
+
*/
|
|
35
|
+
export interface RemoteContext {
|
|
36
|
+
name: string
|
|
37
|
+
sysInfo: RemoteSysInfo
|
|
38
|
+
shellHistory: RemoteShellHistoryItem[]
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
/**
|
|
30
42
|
* 生成系统上下文信息(供 Mastra 使用)
|
|
31
43
|
*/
|
|
@@ -36,7 +48,24 @@ export function getFullSystemPrompt() {
|
|
|
36
48
|
const shellHistory = formatShellHistoryForAI()
|
|
37
49
|
const shellHookEnabled = config.shellHook && getShellHistory().length > 0
|
|
38
50
|
|
|
39
|
-
return
|
|
51
|
+
return buildCommandSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 生成远程系统上下文信息
|
|
56
|
+
*/
|
|
57
|
+
export function getRemoteFullSystemPrompt(remoteContext: RemoteContext) {
|
|
58
|
+
// 格式化远程系统信息
|
|
59
|
+
const sysinfo = formatRemoteSysInfoForAI(remoteContext.name, remoteContext.sysInfo)
|
|
60
|
+
|
|
61
|
+
// 格式化远程 pls 命令历史
|
|
62
|
+
const plsHistory = formatRemoteHistoryForAI(remoteContext.name)
|
|
63
|
+
|
|
64
|
+
// 格式化远程 shell 历史
|
|
65
|
+
const shellHistory = formatRemoteShellHistoryForAI(remoteContext.shellHistory)
|
|
66
|
+
const shellHookEnabled = remoteContext.shellHistory.length > 0
|
|
67
|
+
|
|
68
|
+
return buildCommandSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled)
|
|
40
69
|
}
|
|
41
70
|
|
|
42
71
|
/**
|
|
@@ -45,10 +74,14 @@ export function getFullSystemPrompt() {
|
|
|
45
74
|
export async function generateMultiStepCommand(
|
|
46
75
|
userPrompt: string,
|
|
47
76
|
previousSteps: ExecutedStep[] = [],
|
|
48
|
-
options: { debug?: boolean } = {}
|
|
77
|
+
options: { debug?: boolean; remoteContext?: RemoteContext } = {}
|
|
49
78
|
): Promise<{ stepData: CommandStep; debugInfo?: any }> {
|
|
50
79
|
const agent = createShellAgent()
|
|
51
|
-
|
|
80
|
+
|
|
81
|
+
// 根据是否有远程上下文选择不同的系统提示词
|
|
82
|
+
const fullSystemPrompt = options.remoteContext
|
|
83
|
+
? getRemoteFullSystemPrompt(options.remoteContext)
|
|
84
|
+
: getFullSystemPrompt()
|
|
52
85
|
|
|
53
86
|
// 构建消息数组(string[] 格式)
|
|
54
87
|
const messages: string[] = [userPrompt]
|
|
@@ -85,6 +118,10 @@ export async function generateMultiStepCommand(
|
|
|
85
118
|
userPrompt,
|
|
86
119
|
previousStepsCount: previousSteps.length,
|
|
87
120
|
response: stepData,
|
|
121
|
+
remoteContext: options.remoteContext ? {
|
|
122
|
+
name: options.remoteContext.name,
|
|
123
|
+
sysInfo: options.remoteContext.sysInfo,
|
|
124
|
+
} : undefined,
|
|
88
125
|
},
|
|
89
126
|
}
|
|
90
127
|
}
|
package/src/prompts.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 统一管理所有 AI 系统提示词
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 构建命令生成模式的系统提示词
|
|
7
|
+
*/
|
|
8
|
+
export function buildCommandSystemPrompt(
|
|
9
|
+
sysinfo: string,
|
|
10
|
+
plsHistory: string,
|
|
11
|
+
shellHistory: string,
|
|
12
|
+
shellHookEnabled: boolean
|
|
13
|
+
): string {
|
|
14
|
+
let prompt = `你是一个专业的 shell 脚本生成器。用户会提供他们的系统信息和一个命令需求。
|
|
15
|
+
你的任务是返回一个可执行的、原始的 shell 命令或脚本来完成他们的目标。
|
|
16
|
+
|
|
17
|
+
重要规则:
|
|
18
|
+
1. 返回 JSON 格式,command 字段必须是可直接执行的命令(无解释、无注释、无 markdown)
|
|
19
|
+
2. 不要添加 shebang(如 #!/bin/bash)
|
|
20
|
+
3. command 可以包含多条命令(用 && 连接),但整体算一个命令
|
|
21
|
+
4. 根据用户的系统信息选择合适的命令(如包管理器)
|
|
22
|
+
5. 如果用户引用了之前的操作(如"刚才的"、"上一个"),请参考历史记录
|
|
23
|
+
6. 绝对不要输出 pls 或 please 命令!
|
|
24
|
+
|
|
25
|
+
【输出格式 - 非常重要】
|
|
26
|
+
|
|
27
|
+
单步模式(一个命令完成):
|
|
28
|
+
如果任务只需要一个命令就能完成,只返回:
|
|
29
|
+
{
|
|
30
|
+
"command": "ls -la"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
多步模式(需要多个命令,后续依赖前面的结果):
|
|
34
|
+
如果任务需要多个命令,且后续命令必须根据前面的执行结果来决定,则返回:
|
|
35
|
+
|
|
36
|
+
【多步骤完整示例】
|
|
37
|
+
用户:"查找大于100MB的日志文件并压缩"
|
|
38
|
+
|
|
39
|
+
第一步你返回:
|
|
40
|
+
{
|
|
41
|
+
"command": "find . -name '*.log' -size +100M",
|
|
42
|
+
"continue": true,
|
|
43
|
+
"reasoning": "查找大日志", (精简即可)
|
|
44
|
+
"nextStepHint": "压缩找到的文件" (精简即可)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
执行后你会收到:
|
|
48
|
+
命令已执行
|
|
49
|
+
退出码: 0
|
|
50
|
+
输出:
|
|
51
|
+
./app.log
|
|
52
|
+
./system.log
|
|
53
|
+
|
|
54
|
+
然后你返回第二步:
|
|
55
|
+
{
|
|
56
|
+
"command": "tar -czf logs.tar.gz ./app.log ./system.log",
|
|
57
|
+
"continue": false,
|
|
58
|
+
"reasoning": "压缩日志文件"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
关键判断标准:
|
|
62
|
+
- 多步 = 后续命令依赖前面的输出(如先 find 看有哪些,再根据结果操作具体文件)
|
|
63
|
+
- 单步 = 一个命令就能完成(即使命令里有 && 连接多条,也算一个命令)
|
|
64
|
+
|
|
65
|
+
常见场景举例:
|
|
66
|
+
- "删除空文件夹" → 单步:find . -empty -delete (一个命令完成)
|
|
67
|
+
- "查找大文件并压缩" → 多步:先 find 看有哪些,再 tar 压缩具体文件
|
|
68
|
+
- "安装 git" → 单步:brew install git
|
|
69
|
+
- "备份并删除旧日志" → 多步:先 mkdir backup,再 mv 文件到 backup
|
|
70
|
+
- "查看目录" → 单步:ls -la
|
|
71
|
+
|
|
72
|
+
严格要求:单步模式只返回 {"command": "xxx"},绝对不要输出 continue/reasoning/nextStepHint!
|
|
73
|
+
|
|
74
|
+
【错误处理】
|
|
75
|
+
如果你收到命令执行失败的信息(退出码非0),你应该:
|
|
76
|
+
1. 分析错误原因
|
|
77
|
+
2. 调整命令策略,返回修正后的命令
|
|
78
|
+
3. 设置 continue: true 重试,或设置 continue: false 放弃
|
|
79
|
+
|
|
80
|
+
错误处理示例:
|
|
81
|
+
上一步失败,你收到:
|
|
82
|
+
命令已执行
|
|
83
|
+
退出码: 1
|
|
84
|
+
输出:
|
|
85
|
+
mv: rename ./test.zip to ./c/test.zip: No such file or directory
|
|
86
|
+
|
|
87
|
+
你分析后返回修正:
|
|
88
|
+
{
|
|
89
|
+
"command": "cp test.zip a/ && cp test.zip b/ && cp test.zip c/",
|
|
90
|
+
"continue": false,
|
|
91
|
+
"reasoning": "改用 cp 复制而非 mv"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
或者如果决定放弃(无法修正),返回:
|
|
95
|
+
{
|
|
96
|
+
"command": "",
|
|
97
|
+
"continue": false,
|
|
98
|
+
"reasoning": "文件不存在且无法恢复,任务无法继续"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
重要:当 continue: false 且决定放弃时,command 可以留空,重点是在 reasoning 中说明为什么放弃。
|
|
102
|
+
|
|
103
|
+
关于 pls/please 工具:
|
|
104
|
+
用户正在使用 pls(pretty-please)工具,这是一个将自然语言转换为 shell 命令的 AI 助手。
|
|
105
|
+
当用户输入 "pls <描述>" 时,AI(也就是你)会生成对应的 shell 命令供用户确认执行。
|
|
106
|
+
历史记录中标记为 [pls] 的条目表示用户通过 pls 工具执行的命令。
|
|
107
|
+
|
|
108
|
+
用户的系统信息:${sysinfo}`
|
|
109
|
+
|
|
110
|
+
// 根据是否启用 shell hook 决定展示哪个历史
|
|
111
|
+
if (shellHookEnabled && shellHistory) {
|
|
112
|
+
prompt += `\n\n${shellHistory}`
|
|
113
|
+
} else if (plsHistory) {
|
|
114
|
+
prompt += `\n\n${plsHistory}`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return prompt
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 构建 Chat 对话模式的系统提示词
|
|
122
|
+
*/
|
|
123
|
+
export function buildChatSystemPrompt(
|
|
124
|
+
sysinfo: string,
|
|
125
|
+
plsHistory: string,
|
|
126
|
+
shellHistory: string,
|
|
127
|
+
shellHookEnabled: boolean
|
|
128
|
+
): string {
|
|
129
|
+
let prompt = `你是一个命令行专家助手,帮助用户理解和使用命令行工具。
|
|
130
|
+
|
|
131
|
+
【你的能力】
|
|
132
|
+
- 解释命令的含义、参数、用法
|
|
133
|
+
- 分析命令的执行效果和潜在风险
|
|
134
|
+
- 回答命令行、Shell、系统管理相关问题
|
|
135
|
+
- 根据用户需求推荐合适的命令并解释
|
|
136
|
+
|
|
137
|
+
【回答要求】
|
|
138
|
+
- 简洁清晰,避免冗余
|
|
139
|
+
- 危险操作要明确警告
|
|
140
|
+
- 适当给出示例命令
|
|
141
|
+
- 结合用户的系统环境给出针对性建议
|
|
142
|
+
|
|
143
|
+
【用户系统信息】
|
|
144
|
+
${sysinfo}`
|
|
145
|
+
|
|
146
|
+
// 根据是否启用 shell hook 决定展示哪个历史
|
|
147
|
+
if (shellHookEnabled && shellHistory) {
|
|
148
|
+
prompt += `\n\n${shellHistory}`
|
|
149
|
+
} else if (plsHistory) {
|
|
150
|
+
prompt += `\n\n${plsHistory}`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return prompt
|
|
154
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 远程服务器历史管理模块
|
|
3
|
+
* 管理每个远程服务器的命令历史
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs'
|
|
7
|
+
import path from 'path'
|
|
8
|
+
import chalk from 'chalk'
|
|
9
|
+
import { CONFIG_DIR, getConfig } from './config.js'
|
|
10
|
+
import { getCurrentTheme } from './ui/theme.js'
|
|
11
|
+
import { sshExec, getRemote } from './remote.js'
|
|
12
|
+
|
|
13
|
+
// 获取主题颜色
|
|
14
|
+
function getColors() {
|
|
15
|
+
const theme = getCurrentTheme()
|
|
16
|
+
return {
|
|
17
|
+
primary: theme.primary,
|
|
18
|
+
secondary: theme.secondary,
|
|
19
|
+
success: theme.success,
|
|
20
|
+
error: theme.error,
|
|
21
|
+
warning: theme.warning,
|
|
22
|
+
muted: theme.text.muted,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 远程服务器数据目录
|
|
27
|
+
const REMOTES_DIR = path.join(CONFIG_DIR, 'remotes')
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 远程命令历史记录
|
|
31
|
+
*/
|
|
32
|
+
export interface RemoteHistoryRecord {
|
|
33
|
+
userPrompt: string
|
|
34
|
+
command: string
|
|
35
|
+
aiGeneratedCommand?: string // AI 原始命令
|
|
36
|
+
userModified?: boolean // 用户是否修改
|
|
37
|
+
executed: boolean
|
|
38
|
+
exitCode: number | null
|
|
39
|
+
output: string
|
|
40
|
+
timestamp: string
|
|
41
|
+
reason?: string // 未执行原因
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Shell 历史记录项
|
|
46
|
+
*/
|
|
47
|
+
export interface RemoteShellHistoryItem {
|
|
48
|
+
cmd: string
|
|
49
|
+
exit: number
|
|
50
|
+
time: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ================== 命令历史管理 ==================
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 获取远程服务器历史文件路径
|
|
57
|
+
*/
|
|
58
|
+
function getRemoteHistoryPath(name: string): string {
|
|
59
|
+
return path.join(REMOTES_DIR, name, 'history.json')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 获取远程服务器命令历史
|
|
64
|
+
*/
|
|
65
|
+
export function getRemoteHistory(name: string): RemoteHistoryRecord[] {
|
|
66
|
+
const historyPath = getRemoteHistoryPath(name)
|
|
67
|
+
|
|
68
|
+
if (!fs.existsSync(historyPath)) {
|
|
69
|
+
return []
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const content = fs.readFileSync(historyPath, 'utf-8')
|
|
74
|
+
return JSON.parse(content) as RemoteHistoryRecord[]
|
|
75
|
+
} catch {
|
|
76
|
+
return []
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 添加远程命令历史记录
|
|
82
|
+
*/
|
|
83
|
+
export function addRemoteHistory(name: string, record: Omit<RemoteHistoryRecord, 'timestamp'>): void {
|
|
84
|
+
const config = getConfig()
|
|
85
|
+
const historyPath = getRemoteHistoryPath(name)
|
|
86
|
+
|
|
87
|
+
// 确保目录存在
|
|
88
|
+
const dir = path.dirname(historyPath)
|
|
89
|
+
if (!fs.existsSync(dir)) {
|
|
90
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let history = getRemoteHistory(name)
|
|
94
|
+
|
|
95
|
+
// 添加新记录
|
|
96
|
+
history.push({
|
|
97
|
+
...record,
|
|
98
|
+
timestamp: new Date().toISOString(),
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// 限制历史数量
|
|
102
|
+
const limit = config.commandHistoryLimit || 10
|
|
103
|
+
if (history.length > limit) {
|
|
104
|
+
history = history.slice(-limit)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fs.writeFileSync(historyPath, JSON.stringify(history, null, 2))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 清空远程命令历史
|
|
112
|
+
*/
|
|
113
|
+
export function clearRemoteHistory(name: string): void {
|
|
114
|
+
const historyPath = getRemoteHistoryPath(name)
|
|
115
|
+
if (fs.existsSync(historyPath)) {
|
|
116
|
+
fs.unlinkSync(historyPath)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 格式化远程命令历史供 AI 使用
|
|
122
|
+
*/
|
|
123
|
+
export function formatRemoteHistoryForAI(name: string): string {
|
|
124
|
+
const history = getRemoteHistory(name)
|
|
125
|
+
|
|
126
|
+
if (history.length === 0) {
|
|
127
|
+
return ''
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const lines = history.map((record, index) => {
|
|
131
|
+
let status = ''
|
|
132
|
+
if (record.reason === 'builtin') {
|
|
133
|
+
status = '(包含 builtin,未执行)'
|
|
134
|
+
} else if (record.executed) {
|
|
135
|
+
status = record.exitCode === 0 ? '✓' : `✗ 退出码:${record.exitCode}`
|
|
136
|
+
} else {
|
|
137
|
+
status = '(用户取消执行)'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 显示用户修改信息
|
|
141
|
+
if (record.userModified && record.aiGeneratedCommand) {
|
|
142
|
+
return `${index + 1}. "${record.userPrompt}" → AI 生成: ${record.aiGeneratedCommand} / 用户修改为: ${record.command} ${status}`
|
|
143
|
+
} else {
|
|
144
|
+
return `${index + 1}. "${record.userPrompt}" → ${record.command} ${status}`
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return `【该服务器最近通过 pls 执行的命令】\n${lines.join('\n')}`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 显示远程命令历史
|
|
153
|
+
*/
|
|
154
|
+
export function displayRemoteHistory(name: string): void {
|
|
155
|
+
const remote = getRemote(name)
|
|
156
|
+
const history = getRemoteHistory(name)
|
|
157
|
+
const colors = getColors()
|
|
158
|
+
|
|
159
|
+
if (!remote) {
|
|
160
|
+
console.log('')
|
|
161
|
+
console.log(chalk.hex(colors.error)(`✗ 服务器 "${name}" 不存在`))
|
|
162
|
+
console.log('')
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log('')
|
|
167
|
+
|
|
168
|
+
if (history.length === 0) {
|
|
169
|
+
console.log(chalk.gray(` 服务器 "${name}" 暂无命令历史`))
|
|
170
|
+
console.log('')
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log(chalk.bold(`📜 服务器 "${name}" 命令历史:`))
|
|
175
|
+
console.log(chalk.gray('━'.repeat(50)))
|
|
176
|
+
|
|
177
|
+
history.forEach((item, index) => {
|
|
178
|
+
const status = item.executed
|
|
179
|
+
? item.exitCode === 0
|
|
180
|
+
? chalk.hex(colors.success)('✓')
|
|
181
|
+
: chalk.hex(colors.error)(`✗ 退出码:${item.exitCode}`)
|
|
182
|
+
: chalk.gray('(未执行)')
|
|
183
|
+
|
|
184
|
+
console.log(`\n${chalk.gray(`${index + 1}.`)} ${chalk.hex(colors.primary)(item.userPrompt)}`)
|
|
185
|
+
|
|
186
|
+
// 显示用户修改信息
|
|
187
|
+
if (item.userModified && item.aiGeneratedCommand) {
|
|
188
|
+
console.log(` ${chalk.dim('AI 生成:')} ${chalk.gray(item.aiGeneratedCommand)}`)
|
|
189
|
+
console.log(` ${chalk.dim('用户修改为:')} ${item.command} ${status} ${chalk.hex(colors.warning)('(已修改)')}`)
|
|
190
|
+
} else {
|
|
191
|
+
console.log(` ${chalk.dim('→')} ${item.command} ${status}`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log(` ${chalk.gray(item.timestamp)}`)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
console.log('')
|
|
198
|
+
console.log(chalk.gray('━'.repeat(50)))
|
|
199
|
+
console.log(chalk.gray(`历史文件: ${getRemoteHistoryPath(name)}`))
|
|
200
|
+
console.log('')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ================== Shell 历史管理 ==================
|
|
204
|
+
|
|
205
|
+
// 远程 shell 历史的本地缓存文件
|
|
206
|
+
function getRemoteShellHistoryPath(name: string): string {
|
|
207
|
+
return path.join(REMOTES_DIR, name, 'shell_history.jsonl')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 从远程服务器读取 shell 历史
|
|
212
|
+
* 读取远程 ~/.please/shell_history.jsonl
|
|
213
|
+
*/
|
|
214
|
+
export async function fetchRemoteShellHistory(name: string): Promise<RemoteShellHistoryItem[]> {
|
|
215
|
+
const config = getConfig()
|
|
216
|
+
const limit = config.shellHistoryLimit || 15
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
// 读取远程 shell 历史文件
|
|
220
|
+
const result = await sshExec(name, `tail -n ${limit} ~/.please/shell_history.jsonl 2>/dev/null || echo ""`, {
|
|
221
|
+
timeout: 10000,
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
if (result.exitCode !== 0 || !result.stdout.trim()) {
|
|
225
|
+
return []
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const lines = result.stdout.trim().split('\n').filter(line => line.trim())
|
|
229
|
+
const items: RemoteShellHistoryItem[] = []
|
|
230
|
+
|
|
231
|
+
for (const line of lines) {
|
|
232
|
+
try {
|
|
233
|
+
const item = JSON.parse(line) as RemoteShellHistoryItem
|
|
234
|
+
items.push(item)
|
|
235
|
+
} catch {
|
|
236
|
+
// 跳过无效行
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 缓存到本地
|
|
241
|
+
saveRemoteShellHistoryCache(name, items)
|
|
242
|
+
|
|
243
|
+
return items
|
|
244
|
+
} catch {
|
|
245
|
+
// 如果无法连接,尝试返回缓存
|
|
246
|
+
return getRemoteShellHistoryCache(name)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 保存远程 shell 历史缓存到本地
|
|
252
|
+
*/
|
|
253
|
+
function saveRemoteShellHistoryCache(name: string, items: RemoteShellHistoryItem[]): void {
|
|
254
|
+
const cachePath = getRemoteShellHistoryPath(name)
|
|
255
|
+
|
|
256
|
+
// 确保目录存在
|
|
257
|
+
const dir = path.dirname(cachePath)
|
|
258
|
+
if (!fs.existsSync(dir)) {
|
|
259
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const content = items.map(item => JSON.stringify(item)).join('\n')
|
|
263
|
+
fs.writeFileSync(cachePath, content)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 获取本地缓存的远程 shell 历史
|
|
268
|
+
*/
|
|
269
|
+
function getRemoteShellHistoryCache(name: string): RemoteShellHistoryItem[] {
|
|
270
|
+
const cachePath = getRemoteShellHistoryPath(name)
|
|
271
|
+
|
|
272
|
+
if (!fs.existsSync(cachePath)) {
|
|
273
|
+
return []
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const content = fs.readFileSync(cachePath, 'utf-8')
|
|
278
|
+
const lines = content.trim().split('\n').filter(line => line.trim())
|
|
279
|
+
|
|
280
|
+
return lines.map(line => {
|
|
281
|
+
try {
|
|
282
|
+
return JSON.parse(line) as RemoteShellHistoryItem
|
|
283
|
+
} catch {
|
|
284
|
+
return null
|
|
285
|
+
}
|
|
286
|
+
}).filter((item): item is RemoteShellHistoryItem => item !== null)
|
|
287
|
+
} catch {
|
|
288
|
+
return []
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 格式化远程 shell 历史供 AI 使用
|
|
294
|
+
*/
|
|
295
|
+
export function formatRemoteShellHistoryForAI(items: RemoteShellHistoryItem[]): string {
|
|
296
|
+
if (items.length === 0) {
|
|
297
|
+
return ''
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const lines = items.map((item, index) => {
|
|
301
|
+
const status = item.exit === 0 ? '✓' : `✗ 退出码:${item.exit}`
|
|
302
|
+
return `${index + 1}. ${item.cmd} ${status}`
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
return `【该服务器终端最近执行的命令】\n${lines.join('\n')}`
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* 显示远程 shell 历史
|
|
310
|
+
*/
|
|
311
|
+
export async function displayRemoteShellHistory(name: string): Promise<void> {
|
|
312
|
+
const remote = getRemote(name)
|
|
313
|
+
const colors = getColors()
|
|
314
|
+
|
|
315
|
+
if (!remote) {
|
|
316
|
+
console.log('')
|
|
317
|
+
console.log(chalk.hex(colors.error)(`✗ 服务器 "${name}" 不存在`))
|
|
318
|
+
console.log('')
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
console.log('')
|
|
323
|
+
console.log(chalk.gray(`正在从 ${name} 读取 shell 历史...`))
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const history = await fetchRemoteShellHistory(name)
|
|
327
|
+
|
|
328
|
+
if (history.length === 0) {
|
|
329
|
+
console.log('')
|
|
330
|
+
console.log(chalk.gray(` 服务器 "${name}" 暂无 shell 历史`))
|
|
331
|
+
console.log(chalk.gray(' 请先安装远程 hook: pls remote hook install ' + name))
|
|
332
|
+
console.log('')
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
console.log('')
|
|
337
|
+
console.log(chalk.bold(`终端历史 - ${name}(最近 ${history.length} 条):`))
|
|
338
|
+
console.log(chalk.gray('━'.repeat(50)))
|
|
339
|
+
|
|
340
|
+
history.forEach((item, index) => {
|
|
341
|
+
const num = index + 1
|
|
342
|
+
const status = item.exit === 0 ? chalk.hex(colors.success)('✓') : chalk.hex(colors.error)(`✗ (${item.exit})`)
|
|
343
|
+
console.log(` ${chalk.hex(colors.primary)(num.toString().padStart(2, ' '))}. ${item.cmd} ${status}`)
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
console.log(chalk.gray('━'.repeat(50)))
|
|
347
|
+
console.log(chalk.gray(`远程文件: ~/.please/shell_history.jsonl`))
|
|
348
|
+
console.log('')
|
|
349
|
+
} catch (error) {
|
|
350
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
351
|
+
console.log('')
|
|
352
|
+
console.log(chalk.hex(colors.error)(`✗ 无法读取远程 shell 历史: ${message}`))
|
|
353
|
+
console.log('')
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 清空远程 shell 历史
|
|
359
|
+
*/
|
|
360
|
+
export async function clearRemoteShellHistory(name: string): Promise<void> {
|
|
361
|
+
const remote = getRemote(name)
|
|
362
|
+
const colors = getColors()
|
|
363
|
+
|
|
364
|
+
if (!remote) {
|
|
365
|
+
console.log('')
|
|
366
|
+
console.log(chalk.hex(colors.error)(`✗ 服务器 "${name}" 不存在`))
|
|
367
|
+
console.log('')
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
// 清空远程文件
|
|
373
|
+
await sshExec(name, 'rm -f ~/.please/shell_history.jsonl', { timeout: 10000 })
|
|
374
|
+
|
|
375
|
+
// 清空本地缓存
|
|
376
|
+
const cachePath = getRemoteShellHistoryPath(name)
|
|
377
|
+
if (fs.existsSync(cachePath)) {
|
|
378
|
+
fs.unlinkSync(cachePath)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
console.log('')
|
|
382
|
+
console.log(chalk.hex(colors.success)(`✓ 服务器 "${name}" 的 shell 历史已清空`))
|
|
383
|
+
console.log('')
|
|
384
|
+
} catch (error) {
|
|
385
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
386
|
+
console.log('')
|
|
387
|
+
console.log(chalk.hex(colors.error)(`✗ 无法清空远程 shell 历史: ${message}`))
|
|
388
|
+
console.log('')
|
|
389
|
+
}
|
|
390
|
+
}
|