deepfish-ai 1.0.8

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.
@@ -0,0 +1,93 @@
1
+ const ExtensionManager = require('./extension/ExtensionManager')
2
+ const readline = require('readline')
3
+ const { logError } = require('./utils')
4
+ const { GlobalVariable } = require('./globalVariable')
5
+ const AiRecorder = require('./ai-services/AiWorker/AiRecorder')
6
+ const AIService = require('./ai-services/AIService')
7
+
8
+ class AICLI {
9
+ constructor(config) {
10
+ this.config = config
11
+ // 初始化扩展
12
+ this.extensionManager = new ExtensionManager(this)
13
+ this.Tools = this.extensionManager.extensions.functions
14
+ this.aiRecorder = new AiRecorder(this);
15
+ const currentName = this.config.currentAi || 'default'
16
+ const currentAiConfig = this.config.ai.find(
17
+ (cfg) => cfg.name === currentName,
18
+ )
19
+ this.aiConfig = currentAiConfig || this.config.ai[0]
20
+ const serviceType = this.aiConfig?.type || 'ollama'
21
+ this.aiService = new AIService(serviceType, this)
22
+ GlobalVariable.aiCli = this
23
+ GlobalVariable.aiRecorder = this.aiRecorder
24
+ GlobalVariable.isRecordHistory = this.config.isRecordHistory || false
25
+ GlobalVariable.isLog = this.config.isLog || false
26
+ }
27
+
28
+ // 单轮对话
29
+ async run(userPrompt) {
30
+ try {
31
+ await this.aiService.mainWorkflow(userPrompt)
32
+ } catch (error) {
33
+ logError(error.stack)
34
+ throw error
35
+ }
36
+ }
37
+ // 多轮对话
38
+ startInteractive() {
39
+ const rl = readline.createInterface({
40
+ input: process.stdin,
41
+ output: process.stdout,
42
+ prompt: '> ',
43
+ })
44
+
45
+ console.log('AI CLI Assistant')
46
+ console.log('Type your question or command. Type "exit" to quit.')
47
+ console.log('='.repeat(50))
48
+ rl.prompt()
49
+
50
+ rl.on('line', async (line) => {
51
+ const input = line.trim()
52
+
53
+ if (input.toLowerCase() === 'exit') {
54
+ rl.close()
55
+ return
56
+ }
57
+
58
+ try {
59
+ await this.run(input)
60
+ } catch (error) {
61
+ console.error('Error:', error.message)
62
+ }
63
+
64
+ console.log('='.repeat(50))
65
+ rl.prompt()
66
+ })
67
+
68
+ rl.on('close', () => {
69
+ console.log('Goodbye!')
70
+ process.exit(0)
71
+ })
72
+ }
73
+
74
+ _parseResponse(response) {
75
+ if (!response) {
76
+ throw new Error('AI returned empty data')
77
+ }
78
+ response = response.trim().replace(/^```json\n|```$/g, '')
79
+ try {
80
+ const steps = JSON.parse(response)
81
+ if (Array.isArray(steps)) {
82
+ return steps
83
+ } else {
84
+ return [{ type: 1, content: response, description: '' }]
85
+ }
86
+ } catch (error) {
87
+ logError('返回数据解析错误,' + error.stack)
88
+ return [{ type: 1, content: response, description: '' }]
89
+ }
90
+ }
91
+ }
92
+
93
+ module.exports = AICLI
@@ -0,0 +1,14 @@
1
+ function getDefaultConfig() {
2
+ return {
3
+ ai: [],
4
+ currentAi: "",
5
+ maxIterations: -1, // ai完成工作流的最大迭代次数
6
+ maxMessagesLength: 50000, // 最大压缩长度
7
+ maxMessagesCount: 40, // 最大压缩数量
8
+ extensions: [],
9
+ isRecordHistory: false, // 是否创建工作流执行记录文件,用于因意外终止恢复工作流
10
+ isLog: false // 是否创建工作流执行日志
11
+ };
12
+ }
13
+
14
+ module.exports = getDefaultConfig;
@@ -0,0 +1,10 @@
1
+ const GlobalVariable = {
2
+ aiCli: null,
3
+ aiRecorder: null,
4
+ isRecordHistory: false,
5
+ isLog: false,
6
+ }
7
+
8
+ module.exports = {
9
+ GlobalVariable
10
+ };
@@ -0,0 +1,25 @@
1
+ const { OpenAI } = require("openai");
2
+ const AiWorker = require("./AiWorker");
3
+
4
+ class AIService {
5
+ constructor(type = "deepseek", aiCli) {
6
+ this.type = type;
7
+ this.aiCli = aiCli;
8
+ this.config = aiCli.config;
9
+ this.aiConfig = aiCli.aiConfig;
10
+ this.name = this.config.currentAi || "I";
11
+ this.config.name = this.name;
12
+ this.client = new OpenAI({
13
+ baseURL: this.aiConfig.baseUrl,
14
+ apiKey: this.aiConfig.apiKey || "",
15
+ });
16
+ this.aiWorker = new AiWorker(this.aiCli, this.client);
17
+ this.aiWorker.aiService = this;
18
+ }
19
+
20
+ mainWorkflow(goal) {
21
+ return this.aiWorker.main(goal);
22
+ }
23
+ }
24
+
25
+ module.exports = AIService;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * @Author: Roman 306863030@qq.com
3
+ * @Date: 2026-03-16 09:18:05
4
+ * @LastEditors: Roman 306863030@qq.com
5
+ * @LastEditTime: 2026-03-17 10:00:50
6
+ * @FilePath: \src\core\ai-services\AiWorker\AIMessageManager.js
7
+ * @Description: 上下文管理-添加、自动压缩
8
+ * @
9
+ */
10
+ const { cloneDeep } = require('lodash')
11
+ const { logError, logInfo } = require('../../utils')
12
+ const { aiRequestSingle } = require('./AiTools')
13
+ const { GlobalVariable } = require('../../globalVariable')
14
+
15
+ class AIMessageManager {
16
+ aiClient
17
+ aiConfig
18
+ config
19
+ constructor(aiClient, config, aiConfig, messages) {
20
+ this.aiClient = aiClient
21
+ this.aiConfig = aiConfig
22
+ this.config = config
23
+ this.messages = messages
24
+ }
25
+ reLinkMsgs(messages) {
26
+ this.messages = messages
27
+ }
28
+ // 添加消息
29
+ addMsg(message) {
30
+ this.messages.push(message)
31
+ GlobalVariable.aiRecorder.record(this.messages)
32
+ GlobalVariable.aiRecorder.log(message)
33
+ }
34
+ // 添加tool
35
+ addTool(id, content) {
36
+ if (typeof content === 'object') {
37
+ content = JSON.stringify(content)
38
+ }
39
+ const message = {
40
+ role: 'tool',
41
+ tool_call_id: id,
42
+ content: content,
43
+ }
44
+ this.messages.push(message)
45
+ GlobalVariable.aiRecorder.record(this.messages)
46
+ GlobalVariable.aiRecorder.log(message)
47
+ }
48
+ /**
49
+ * 压缩消息,根据配置压缩消息长度和数量
50
+ * @param {*} messages
51
+ * @returns
52
+ */
53
+ async compress(messages) {
54
+ const currentLength = this._getLength(messages)
55
+ const currentCount = messages.length
56
+ if (
57
+ currentLength > this.config.maxMessagesLength ||
58
+ currentCount > this.config.maxMessagesCount
59
+ ) {
60
+ logInfo(
61
+ `Managing messages: current length ${currentLength}, count ${currentCount}`,
62
+ )
63
+ const systemMessage = messages[0]
64
+ const userMessage = messages[1]
65
+ const goal = messages[1].content
66
+ const messagesToSummarize = messages.slice(2, -2)
67
+
68
+ if (messagesToSummarize.length > 0) {
69
+ messages = cloneDeep(messages)
70
+ const summary = await this._getSummary(
71
+ goal,
72
+ messages.slice(-2),
73
+ messagesToSummarize,
74
+ )
75
+
76
+ const newMessages = [
77
+ systemMessage,
78
+ userMessage,
79
+ {
80
+ role: 'user',
81
+ content: `[CONVERSATION SUMMARY]: ${summary}`,
82
+ },
83
+ ...messages.slice(-2),
84
+ ]
85
+ logInfo(
86
+ `Messages compressed: ${messages.length} -> ${newMessages.length}`,
87
+ )
88
+ GlobalVariable.aiRecorder.record(newMessages)
89
+ GlobalVariable.aiRecorder.log(newMessages)
90
+ return newMessages
91
+ }
92
+ }
93
+ return messages
94
+ }
95
+ // 计算Messges的总长度
96
+ _getLength(messages) {
97
+ return messages.reduce((total, msg) => {
98
+ let length = 0
99
+ if (msg.content) {
100
+ length +=
101
+ typeof msg.content === 'string'
102
+ ? msg.content.length
103
+ : JSON.stringify(msg.content).length
104
+ }
105
+ if (msg.tool_calls) {
106
+ length += JSON.stringify(msg.tool_calls).length
107
+ }
108
+ return total + length
109
+ }, 0)
110
+ }
111
+ // 合并消息
112
+ async _getSummary(goal, lastTwoMessages, messages) {
113
+ lastTwoMessages = lastTwoMessages
114
+ .map((m) => {
115
+ if (m.role === 'system') return `[SYSTEM]: ${m.content}`
116
+ if (m.role === 'user') return `[USER]: ${m.content}`
117
+ if (m.role === 'assistant')
118
+ return `[ASSISTANT]: ${m.content ? m.content : '[Tool calls]'}`
119
+ if (m.role === 'tool') return `[TOOL RESULT]: ${m.content}`
120
+ return ''
121
+ })
122
+ .join('\n')
123
+ const summaryPrompt = `请结合任务目标${goal},和最后两轮的对话${lastTwoMessages}, 总结以下对话历史,重点:
124
+ 1. 删除不需要的信息,如程序报错、冗余表述、语气词、闲聊等信息
125
+ 2. 关注当前进度和状态
126
+ 3. 总结后续任务所需的重要背景信息并以及所需要的内容
127
+ 结果只保留对上下文有用的内容,保持摘要简短且全面,保证后续任务有效进行。.
128
+
129
+ Conversation history:
130
+ ${messages
131
+ .map((m) => {
132
+ if (m.role === 'system') return `[SYSTEM]: ${m.content}`
133
+ if (m.role === 'user') return `[USER]: ${m.content}`
134
+ if (m.role === 'assistant')
135
+ return `[ASSISTANT]: ${m.content ? m.content : '[Tool calls]'}`
136
+ if (m.role === 'tool') return `[TOOL RESULT]: ${m.content}`
137
+ return ''
138
+ })
139
+ .join('\n')}`
140
+ try {
141
+ const summary = await aiRequestSingle(
142
+ this.aiClient,
143
+ this.aiConfig,
144
+ 'You are a helpful assistant that creates concise summaries of conversations.',
145
+ summaryPrompt,
146
+ )
147
+ return summary
148
+ } catch (error) {
149
+ logError('Failed to summarize messages: ' + error.message)
150
+ return 'Previous conversation history was too long and has been summarized. Please continue with the current task.'
151
+ }
152
+ }
153
+ }
154
+
155
+ module.exports = AIMessageManager
@@ -0,0 +1,151 @@
1
+ /**
2
+ * @Author: Roman 306863030@qq.com
3
+ * @Date: 2026-03-16 09:18:05
4
+ * @LastEditors: Roman 306863030@qq.com
5
+ * @LastEditTime: 2026-03-17 10:00:20
6
+ * @FilePath: \src\core\ai-services\AiWorker\AiAgent.js
7
+ * @Description: 工作流循环
8
+ * @
9
+ */
10
+
11
+ const { logError, logInfo, loading } = require('../../utils')
12
+ const AIMessageManager = require('./AIMessageManager')
13
+ const { AiAgentSystemPrompt } = require('./AiPrompt')
14
+ const { aiRequestByTools } = require('./AiTools')
15
+
16
+ class AiAgent {
17
+ // 工作流提示词
18
+ prompt
19
+ messages
20
+ maxIterations
21
+ goal
22
+ aiMessageManager
23
+ constructor(
24
+ aiClient,
25
+ config,
26
+ aiConfig,
27
+ extensionTools = { descriptions: [], functions: {} },
28
+ ) {
29
+ this.aiClient = aiClient
30
+ this.config = config
31
+ this.aiConfig = aiConfig
32
+ this.prompt = AiAgentSystemPrompt
33
+ this.maxIterations =
34
+ config.maxIterations === -1 ? Infinity : config.maxIterations
35
+ this.aiMessageManager = new AIMessageManager(aiClient, config, aiConfig, [])
36
+ this.extensionTools = extensionTools
37
+ this.name = config.name
38
+ }
39
+
40
+ // 工作流循环
41
+ async work(messages) {
42
+ this.aiMessageManager.reLinkMsgs(messages)
43
+ let maxIterations = this.maxIterations
44
+ let loadingStop
45
+ try {
46
+ while (maxIterations-- > 0) {
47
+ // 压缩上下文
48
+ const newMessages = await this.aiMessageManager.compress(messages)
49
+ if (messages !== newMessages) {
50
+ messages.splice(0, messages.length, ...newMessages)
51
+ }
52
+ if (!this.aiConfig.stream) {
53
+ loadingStop = loading('Thinking...')
54
+ }
55
+ const { message, content, tool_calls } = await aiRequestByTools(
56
+ this.aiClient,
57
+ this.aiConfig,
58
+ messages,
59
+ this.extensionTools.descriptions,
60
+ )
61
+ this.aiMessageManager.addMsg(message)
62
+ if (loadingStop) {
63
+ loadingStop(`${this.name} have finished thinking.`)
64
+ loadingStop = null
65
+ }
66
+ logInfo(content)
67
+ // 检查是否是任务完成的总结响应(没有工具调用且有内容)
68
+ if (tool_calls) {
69
+ // 执行函数
70
+ await this.execTools(tool_calls)
71
+ } else {
72
+ // 没有工具调用,结束
73
+ break
74
+ }
75
+ }
76
+ return messages[messages.length - 1]?.content || ''
77
+ } catch (error) {
78
+ if (loadingStop) {
79
+ loadingStop(
80
+ 'AI process terminated unexpectedly: ' + error.message,
81
+ true,
82
+ )
83
+ } else {
84
+ logError('AI process terminated unexpectedly: ' + error.message)
85
+ }
86
+ throw error
87
+ }
88
+ }
89
+
90
+ // 执行函数
91
+ async execTools(tool_calls) {
92
+ for (const toolCall of tool_calls) {
93
+ const { id, function: func } = toolCall
94
+ const { name, arguments: args } = func
95
+ let toolFunction = this.extensionTools.functions[name]
96
+ logInfo(`Calling tool ${toolCall.function.name}`)
97
+ if (toolFunction) {
98
+ try {
99
+ const parsedArgs = typeof args === 'string' ? JSON.parse(args) : args
100
+ if (name === 'readFile') {
101
+ const fileInfo = await this.extensionTools.functions['getFileInfo'](
102
+ parsedArgs.filePath,
103
+ )
104
+ if (fileInfo && fileInfo.isFile && fileInfo.size > 10 * 1024) {
105
+ this.aiMessageManager.addTool(id, {
106
+ error:
107
+ '文件过大,请使用executeJSCode工具编写脚本分块读取和处理文件,避免一次性读取整个文件内容到对话中。建议使用fs.createReadStream逐行或分块读取,仅返回必要的结果或总结。',
108
+ fileSize: fileInfo.size,
109
+ fileSizeKB: Math.round(fileInfo.size / 1024),
110
+ })
111
+ continue
112
+ }
113
+ }
114
+ let result = await toolFunction(...Object.values(parsedArgs))
115
+ let toolContent = JSON.stringify(result)
116
+ if (name !== 'requestAI') {
117
+ const MAX_CONTENT_SIZE = 100000
118
+ if (toolContent.length > MAX_CONTENT_SIZE) {
119
+ if (
120
+ typeof result === 'string' &&
121
+ result.length > MAX_CONTENT_SIZE
122
+ ) {
123
+ toolContent = {
124
+ truncated: true,
125
+ message:
126
+ '文件内容过大,请使用executeJSCode工具编写脚本分块读取和处理文件,避免一次性读取整个文件内容到对话中。',
127
+ preview: toolContent.substring(0, MAX_CONTENT_SIZE) + '...',
128
+ }
129
+ } else {
130
+ toolContent = {
131
+ truncated: true,
132
+ message: '结果数据量过大,请使用更具体的查询或分块处理。',
133
+ preview: toolContent.substring(0, MAX_CONTENT_SIZE) + '...',
134
+ }
135
+ }
136
+ }
137
+ }
138
+ this.aiMessageManager.addTool(id, toolContent)
139
+ } catch (error) {
140
+ this.aiMessageManager.addTool(id, { error: error.message })
141
+ }
142
+ logInfo(`Tool ${toolCall.function.name} finished.`)
143
+ } else {
144
+ this.aiMessageManager.addTool(id, { error: `Tool ${name} not found` })
145
+ logError(`Tool ${toolCall.function.name} not found.`)
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ module.exports = AiAgent
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @Author: Roman 306863030@qq.com
3
+ * @Date: 2026-03-17 09:12:22
4
+ * @LastEditors: Roman 306863030@qq.com
5
+ * @LastEditTime: 2026-03-17 10:27:12
6
+ * @FilePath: \cmd\src\core\ai-services\AiWorker\AiPrompt.js
7
+ * @Description: AI请求提示词
8
+ * @
9
+ */
10
+ const currentDir = process.cwd()
11
+ const osType = process.platform
12
+
13
+ const AiAgentSystemPrompt = `
14
+ 你是一个严格按规则执行任务的智能体,不能违反任何系统限制。
15
+ ### 基础环境信息
16
+ 当前工作目录:${currentDir}
17
+ 操作系统类型:${osType}
18
+
19
+ ### 工具使用规则
20
+ 优先使用工具完成任务:可调用 executeJSCode 运行 Node.js 代码处理复杂逻辑;可调用 executeCommand 运行系统命令行工具(如 git、npm 等),工具调用需确保语法/指令符合当前操作系统规范(Windows/macOS/Linux 区分)。
21
+
22
+ ### 大文件处理规则(分步执行)
23
+ 处理长文档等大文件(单文件>20KB)时,必须按以下步骤分块处理:
24
+ 1. 预处理:先执行文件大小/结构检查(如通过命令行/JS 代码获取文件大小、判断文件格式),输出检查结果;
25
+ 2. 分块规则:按5KB-10KB/块拆分文件,拆分后每个块生成独立临时文件(命名格式:{原文件名}_chunk{序号}.tmp);
26
+ 3. 处理逻辑:翻译/总结/分析类任务逐块处理,每块处理完成后记录结果,最后合并所有块的结果生成最终文件;
27
+ 4. 合并校验:合并后需校验结果完整性(如总字符数匹配、无内容缺失),确保分块处理无遗漏。
28
+
29
+ ### 核心执行原则
30
+ 1. 最优路径优先:执行前必须先规划最少步骤的操作路径,明确「先做什么、再做什么、哪些可省略」,避免重复操作和无效步骤;
31
+ 2. 异常反馈:操作失败(如命令执行报错、文件不存在)时,需明确说明「失败原因+可尝试的解决方案」,而非仅提示“操作失败”;
32
+ 3. 结果校验:任务完成后,需简单校验结果是否符合用户目标(如文件是否生成、内容是否完整),并向用户反馈校验结果。
33
+ `
34
+
35
+ module.exports = {
36
+ AiAgentSystemPrompt
37
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * @Author: Roman 306863030@qq.com
3
+ * @Date: 2026-03-16 09:18:05
4
+ * @LastEditors: Roman 306863030@qq.com
5
+ * @LastEditTime: 2026-03-17 10:01:45
6
+ * @FilePath: \src\core\ai-services\AiWorker\AiRecorder.js
7
+ * @Description: 对话历史记录、恢复
8
+ * @
9
+ */
10
+ const fs = require("fs-extra");
11
+ const path = require("path");
12
+ const inquirer = require("inquirer");
13
+ const dayjs = require("dayjs");
14
+ const { GlobalVariable } = require("../../globalVariable");
15
+
16
+ class AiRecorder {
17
+ constructor(aiCli) {
18
+ this.aiCli = aiCli;
19
+ }
20
+
21
+ record(messages) {
22
+ if (!GlobalVariable.isRecordHistory) {
23
+ return false;
24
+ }
25
+ const recordDir = path.join(process.cwd(), "ai-history");
26
+ const recordFile = path.join(recordDir, "history.json");
27
+
28
+ try {
29
+ fs.ensureDirSync(recordDir);
30
+ const recordData = {
31
+ timestamp: dayjs().format("YYYY-MM-DD HH:mm:ss"),
32
+ messages: messages,
33
+ };
34
+
35
+ fs.writeJSONSync(recordFile, recordData, { spaces: 2 });
36
+ return true;
37
+ } catch (error) {
38
+ console.error("Failed to record:", error.message);
39
+ return false;
40
+ }
41
+ }
42
+
43
+ async recover() {
44
+ if (!GlobalVariable.isRecordHistory) {
45
+ return null;
46
+ }
47
+ const recordDir = path.join(process.cwd(), "ai-history");
48
+ const recordFile = path.join(recordDir, "history.json");
49
+
50
+ try {
51
+ const exists = fs.pathExistsSync(recordFile);
52
+ if (!exists) {
53
+ return null;
54
+ }
55
+ const recordData = fs.readJSONSync(recordFile);
56
+ const answer = await inquirer.default.prompt([
57
+ {
58
+ type: "confirm",
59
+ name: "recover",
60
+ message: "发现之前的任务记录,是否恢复?",
61
+ default: true,
62
+ },
63
+ ]);
64
+ if (answer.recover) {
65
+ return {
66
+ goal: recordData.goal,
67
+ messages: recordData.messages,
68
+ };
69
+ } else {
70
+ return null;
71
+ }
72
+ } catch (error) {
73
+ console.error("Failed to recover:", error.message);
74
+ return null;
75
+ }
76
+ }
77
+
78
+ // 记录message以及压缩后的messages
79
+ log(message) {
80
+ if (GlobalVariable.isLog) {
81
+ const time = dayjs();
82
+ const logDir = path.join(
83
+ process.cwd(),
84
+ `ai-log/${time.format("YYYY-MM-DD")}`,
85
+ );
86
+ const logFile = path.join(logDir, `log-${time.format("HH")}.txt`);
87
+
88
+ try {
89
+ fs.ensureDirSync(logDir);
90
+ if (typeof content === "object") {
91
+ message = JSON.stringify(message);
92
+ } else if (Array.isArray(message)) {
93
+ message = '###压缩上下文###' + "\n" + JSON.stringify(message);
94
+ }
95
+ const logEntry = `[${new Date().toISOString()}] ${message}\n`;
96
+ fs.appendFileSync(logFile, logEntry);
97
+ return true;
98
+ } catch (error) {
99
+ console.error("Failed to log:", error.message);
100
+ return false;
101
+ }
102
+ }
103
+ }
104
+
105
+ clear() {
106
+ const recordDir = path.join(process.cwd(), "ai-history");
107
+ try {
108
+ const exists = fs.pathExistsSync(recordDir);
109
+ if (exists) {
110
+ fs.removeSync(recordDir);
111
+ }
112
+ return true;
113
+ } catch (error) {
114
+ console.error("Failed to clear:", error.message);
115
+ return false;
116
+ }
117
+ }
118
+ }
119
+
120
+ module.exports = AiRecorder;