deepfish-ai 1.0.21 → 1.0.23

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 (28) hide show
  1. package/package.json +1 -1
  2. package/src/AgentRobot/AgentRobotFactory/MainAgentRobot.js +4 -7
  3. package/src/AgentRobot/AgentRobotFactory/SubAgentRobot.js +3 -8
  4. package/src/AgentRobot/AgentRobotFactory/SubSkillAgentRobot.js +3 -8
  5. package/src/AgentRobot/BaseAgentRobot/Brain.js +8 -8
  6. package/src/AgentRobot/BaseAgentRobot/Hand.js +3 -3
  7. package/src/AgentRobot/BaseAgentRobot/index.js +25 -95
  8. package/src/AgentRobot/BaseAgentRobot/lazy-tools/doc-transform.js +204 -0
  9. package/src/AgentRobot/BaseAgentRobot/lazy-tools/docx.js +552 -1
  10. package/src/AgentRobot/BaseAgentRobot/lazy-tools/embedding.js +763 -0
  11. package/src/AgentRobot/BaseAgentRobot/lazy-tools/img.js +1 -0
  12. package/src/AgentRobot/BaseAgentRobot/lazy-tools/pdf.js +1 -0
  13. package/src/AgentRobot/BaseAgentRobot/lazy-tools/pptx.js +1 -0
  14. package/src/AgentRobot/BaseAgentRobot/lazy-tools/xlsx.js +1 -0
  15. package/src/AgentRobot/BaseAgentRobot/tools/BaseTools.js +1 -0
  16. package/src/AgentRobot/BaseAgentRobot/tools/CreateAgentTools.js +3 -2
  17. package/src/AgentRobot/BaseAgentRobot/tools/FileTools.js +1 -0
  18. package/src/AgentRobot/BaseAgentRobot/tools/GenerateTools.js +4 -2
  19. package/src/AgentRobot/BaseAgentRobot/tools/InquirerTools.js +1 -0
  20. package/src/AgentRobot/BaseAgentRobot/tools/SystemTools.js +3 -4
  21. package/src/AgentRobot/BaseAgentRobot/tools/TaskTools.js +1 -0
  22. package/src/AgentRobot/BaseAgentRobot/tools/TestTools.js +1 -0
  23. package/src/AgentRobot/BaseAgentRobot/tools/UserTool.js +87 -0
  24. package/src/AgentRobot/BaseAgentRobot/tools/WebTools.js +257 -0
  25. package/src/AgentRobot/BaseAgentRobot/utils/AIRequest.js +9 -2
  26. package/src/AgentRobot/BaseAgentRobot/utils/AIToolManager.js +128 -0
  27. package/src/AgentRobot/BaseAgentRobot/utils/AttachmentToolScanner.js +4 -5
  28. package/src/cli/DefaultConfig.js +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepfish-ai",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "This is an AI-driven command-line tool built on Node.js, equipped with AI agent and workflow capabilities. It is compatible with a wide range of AI models, can convert natural language into cross-system terminal and file operation commands, and features high extensibility. It supports complex tasks such as translation, content creation, and format conversion, while allowing custom extensions to be automatically generated via AI.",
5
5
  "main": "src/index.js",
6
6
  "type": "commonjs",
@@ -18,9 +18,12 @@ class MainAgentRobot extends BaseAgentRobot {
18
18
  _initFiles(opt) {
19
19
  this.workspace = opt.workspace || process.cwd() // 工作空间,目录
20
20
  this.basespace = opt.basespace || path.join(os.homedir(), '.deepfish-ai') // 记忆空间,目录
21
+ this.userspace = path.join(this.basespace, 'user-info') // 用户空间,目录
22
+ this.userInfoFilePath = path.join(this.userspace, 'user.md')
21
23
  this.memorySpace = path.join(this.basespace, 'memory') // 记忆空间,目录
22
24
  this.agentRecordFilePath = path.join(this.memorySpace, 'agentRecord.json')
23
25
  fs.ensureDirSync(this.memorySpace)
26
+ fs.ensureDirSync(this.userspace)
24
27
  // 查看agentRecord.json文件是否存在,不存在则创建
25
28
  if (!fs.pathExistsSync(this.agentRecordFilePath)) {
26
29
  fs.writeJsonSync(this.agentRecordFilePath, [], { spaces: 2 })
@@ -68,12 +71,6 @@ class MainAgentRobot extends BaseAgentRobot {
68
71
  fs.writeJsonSync(this.agentRecordFilePath, agentRecord, { spaces: 2 })
69
72
  this.logger = new Logger(this) // 初始化日志系统
70
73
  this.logger.clearAllLogs()
71
- this.toolCollection = AttachmentToolScanner.getToolCollection(
72
- this.workspace,
73
- ) // 加载工具集合
74
- this.clawSkillCollection = AttachmentToolScanner.getClawSkillCollection(
75
- this.basespace,
76
- ) // 加载Claw技能集合
77
74
  }
78
75
 
79
76
  _getDefaultSystemPrompt(opt) {
@@ -82,7 +79,7 @@ class MainAgentRobot extends BaseAgentRobot {
82
79
  ${systemPrompt}
83
80
  ### 工具调用
84
81
  对于复杂的任务,先从可以使用的Skills中查找并使用合适的Skill,如果没有合适的Skill,再使用内置工具函数,使用时请严格按照工具函数的调用方式进行调用。
85
- ${AttachmentToolScanner.getAttachToolPrompt(this.toolCollection, this.clawSkillCollection)}
82
+ ${AttachmentToolScanner.getAttachToolPrompt(this.toolManager.toolCollection, this.toolManager.clawSkillCollection)}
86
83
  `
87
84
  }
88
85
  }
@@ -1,6 +1,5 @@
1
1
  const path = require('path')
2
2
  const os = require('os')
3
- const fs = require('fs-extra')
4
3
  const BaseAgentRobot = require('../BaseAgentRobot/index.js')
5
4
  const Logger = require('../BaseAgentRobot/Logger.js')
6
5
  const {AttachmentToolScanner} = require('../BaseAgentRobot/utils/AttachmentToolScanner.js')
@@ -20,6 +19,8 @@ class SubAgentRobot extends BaseAgentRobot {
20
19
  this.attachTools = opt.attachTools || []
21
20
  this.workspace = opt.workspace || process.cwd() // 工作空间,目录
22
21
  this.basespace = opt.basespace || path.join(os.homedir(), '.deepfish-ai') // 记忆空间,目录
22
+ this.userspace = path.join(this.basespace, 'user-info') // 用户空间,目录
23
+ this.userInfoFilePath = path.join(this.userspace, 'user.md')
23
24
  this.memorySpace = path.join(this.basespace, 'memory') // 记忆空间,目录
24
25
  this.agentRecordFilePath = path.join(this.memorySpace, 'agentRecord.json')
25
26
  this.agentSpace = path.join(this.memorySpace, this.root.id) // Agent空间,目录
@@ -28,12 +29,6 @@ class SubAgentRobot extends BaseAgentRobot {
28
29
  this.logger = new Logger(this) // 初始化日志系统
29
30
  this.agentTree = new AgentTree(this)
30
31
  this.agentTree.init()
31
- this.toolCollection = AttachmentToolScanner.getToolCollection(
32
- this.workspace,
33
- ) // 加载工具集合
34
- this.clawSkillCollection = AttachmentToolScanner.getClawSkillCollection(
35
- this.basespace,
36
- ) // 加载Claw技能集合
37
32
  }
38
33
 
39
34
  _getDefaultSystemPrompt(opt) {
@@ -42,7 +37,7 @@ class SubAgentRobot extends BaseAgentRobot {
42
37
  ${systemPrompt}
43
38
  ### 工具调用
44
39
  对于复杂的任务,先从可以使用的Skills中查找并使用合适的Skill,如果没有合适的Skill,再使用内置工具函数,使用时请严格按照工具函数的调用方式进行调用。
45
- ${AttachmentToolScanner.getAttachToolPrompt(this.toolCollection, this.clawSkillCollection)}
40
+ ${AttachmentToolScanner.getAttachToolPrompt(this.toolManager.toolCollection, this.toolManager.clawSkillCollection)}
46
41
  `
47
42
  }
48
43
  }
@@ -20,6 +20,8 @@ class SubSkillAgentRobot extends BaseAgentRobot {
20
20
  this.attachTools = opt.attachTools || []
21
21
  this.workspace = opt.workspace || process.cwd() // 工作空间,目录
22
22
  this.basespace = opt.basespace || path.join(os.homedir(), '.deepfish-ai') // 记忆空间,目录
23
+ this.userspace = path.join(this.basespace, 'user-info') // 用户空间,目录
24
+ this.userInfoFilePath = path.join(this.userspace, 'user.md')
23
25
  this.memorySpace = path.join(this.basespace, 'memory') // 记忆空间,目录
24
26
  this.agentRecordFilePath = path.join(this.memorySpace, 'agentRecord.json')
25
27
  this.agentSpace = path.join(this.memorySpace, this.root.id) // Agent空间,目录
@@ -29,20 +31,13 @@ class SubSkillAgentRobot extends BaseAgentRobot {
29
31
  this.logger = new Logger(this) // 初始化日志系统
30
32
  this.agentTree = new AgentTree(this)
31
33
  this.agentTree.init()
32
-
33
- this.toolCollection = AttachmentToolScanner.getToolCollection(
34
- this.workspace,
35
- ) // 加载工具集合
36
- this.clawSkillCollection = AttachmentToolScanner.getClawSkillCollection(
37
- this.basespace,
38
- ) // 加载Claw技能集合
39
34
  }
40
35
 
41
36
  _getDefaultSystemPrompt(opt) {
42
37
  const clawSkills = opt.clawSkills || []
43
38
  let systemPrompt = super._getDefaultSystemPrompt(opt)
44
39
  systemPrompt =
45
- systemPrompt + '\n' + AttachmentToolScanner.getClawSkillPrompt(clawSkills, this.toolCollection, this.clawSkillCollection)
40
+ systemPrompt + '\n' + AttachmentToolScanner.getClawSkillPrompt(clawSkills, this.toolManager.toolCollection, this.toolManager.clawSkillCollection)
46
41
  return systemPrompt
47
42
  }
48
43
  }
@@ -70,10 +70,17 @@ class Brain extends EventEmitterSuper {
70
70
  if (maxIterations === -1) {
71
71
  maxIterations = Infinity
72
72
  }
73
- const skillDescriptions = this.agentRobot.getToolDescriptions()
73
+ const skillDescriptions = this.agentRobot.toolManager.descriptions
74
74
  this.emit(BrainEvent.THINK_BEFORE, messages)
75
75
  while (maxIterations-- > 0) {
76
76
  try {
77
+ // 更新系统提示词
78
+ if (messages[0].role === 'system') {
79
+ messages[0] = {
80
+ role: 'system',
81
+ content: this.agentRobot.systemPrompt,
82
+ }
83
+ }
77
84
  // 压缩上下文
78
85
  await this.messageCompresser.compress(messages)
79
86
  const { message, content, tool_calls } = await thinkByTool(
@@ -180,13 +187,6 @@ class Brain extends EventEmitterSuper {
180
187
  }
181
188
 
182
189
  _initMessages(messages) {
183
- let firstMessage = messages[0]
184
- if (firstMessage.role === 'system') {
185
- messages[0] = {
186
- role: 'system',
187
- content: this.agentRobot.systemPrompt,
188
- }
189
- }
190
190
  let lastMessage = messages[messages.length - 1]
191
191
  while (
192
192
  messages.length > 1 &&
@@ -12,7 +12,7 @@ class Hand extends EventEmitterSuper {
12
12
  super()
13
13
  this.agentRobot = agentRobot
14
14
  this.maxBlockFileSize = agentRobot.opt.maxBlockFileSize || 20 // KB
15
- this.tools = agentRobot.getTools()
15
+ this.tools = agentRobot.toolManager.functions
16
16
  }
17
17
 
18
18
  _parseToolCalls(tool_call) {
@@ -57,7 +57,7 @@ class Hand extends EventEmitterSuper {
57
57
  }
58
58
 
59
59
  _getRequiredParamNames(funcName) {
60
- const descriptions = this.agentRobot.getToolDescriptions()
60
+ const descriptions = this.agentRobot.toolManager.descriptions
61
61
  const current = descriptions.find((item) => item?.function?.name === funcName)
62
62
  return current?.function?.parameters?.required || []
63
63
  }
@@ -89,7 +89,7 @@ class Hand extends EventEmitterSuper {
89
89
  toolContent = {
90
90
  truncated: true,
91
91
  message:
92
- '文件内容过大,请使用executeJSCode工具编写脚本分块读取和处理文件,避免一次性读取整个文件内容到对话中。',
92
+ '文件内容过大,请使用executeJSCode工具编写脚本分块读取和处理文件,避免一次性读取整个文件内容到对话中。如果不是本地文件,建议创建或下载成本地文件后再进行分块读取。',
93
93
  preview: toolContent.substring(0, 1000) + '...',
94
94
  }
95
95
  } else {
@@ -1,25 +1,10 @@
1
1
  const path = require('path')
2
2
  const os = require('os')
3
- const fs = require('fs-extra')
4
- const FileTools = require('./tools/FileTools.js')
5
- const InquirerTools = require('./tools/InquirerTools.js')
6
- const SystemTools = require('./tools/SystemTools.js')
7
- const CreateAgentTools = require('./tools/CreateAgentTools.js')
8
- const lodash = require('lodash')
9
3
  const { Brain } = require('./Brain.js')
10
4
  const BrainEvent = require('./BrainEvent.js')
11
5
  const ScreenPrinter = require('./ScreenPrinter.js')
12
6
  const { HandEvent, Hand } = require('./Hand.js')
13
- const dayjs = require('dayjs')
14
- const Logger = require('./Logger.js')
15
- const GenerateTools = require('./tools/GenerateTools.js')
16
- const TaskTools = require('./tools/TaskTools.js')
17
- const TestTools = require('./tools/TestTools.js')
18
- const axios = require('axios')
19
- const echarts = require('echarts')
20
- const canvas = require('canvas')
21
- const cheerio = require('cheerio')
22
- const puppeteer = require('puppeteer')
7
+ const AIToolManager = require('./utils/AIToolManager.js')
23
8
 
24
9
  class BaseAgentRobot {
25
10
  id = '' // Agentid
@@ -27,8 +12,7 @@ class BaseAgentRobot {
27
12
 
28
13
  brain = null // 大脑,负责思考、记忆、决策
29
14
  hand = null // 手,负责使用工具
30
- originalTools = null // 原装工具
31
- attachTools = null // 附加工具, Agent后续安装的工具函数
15
+
32
16
  heart = null // 心脏,负责心跳、连接
33
17
  sender = null // 发送器,负责发送消息
34
18
  receiver = null // 接收器,负责接收消息
@@ -42,12 +26,14 @@ class BaseAgentRobot {
42
26
 
43
27
  workspace = null
44
28
  basespace = null
29
+ userspace = null
45
30
  memorySpace = null
46
31
  agentRecordFilePath = null
47
32
  agentSpace = null
48
33
  agentTree = null
49
34
  memoryFilePath = null
50
35
  logDirPath = null
36
+ toolManager = null
51
37
 
52
38
  constructor(
53
39
  opt = {
@@ -62,6 +48,7 @@ class BaseAgentRobot {
62
48
  maxBlockFileSize: 20, // 大文件分块阈值,单位KB;超过该大小的文件需要分块处理
63
49
  systemPrompt: '', // 系统提示语
64
50
  encoding: 'auto', // 命令行编码格式, 可设置为utf-8、gbk等, 也可以设置成auto或空值自动判断
51
+ isThinkPrint: true, // 是否打印思考过程
65
52
  aiConfig: {
66
53
  name: 'deepseek',
67
54
  type: 'deepseek',
@@ -81,9 +68,7 @@ class BaseAgentRobot {
81
68
  this.name = opt.name || 'AgentRobot'
82
69
  this.screenPrinter = new ScreenPrinter() // 屏幕打印机
83
70
  this._initFiles(opt) // 初始化文件
84
-
85
- this.originalTools = this._getOriginalTools() // 天赋技能
86
- this.attachTools = opt.attachTools || [] // 附加工具, Agent后续安装的工具函数
71
+ this.toolManager = new AIToolManager(this)
87
72
  this.systemPrompt = opt.systemPrompt || this._getDefaultSystemPrompt(opt) // 系统提示语
88
73
  this.brain = new Brain(this) // 初始化大脑
89
74
  this.hand = new Hand(this) // 初始化手
@@ -95,10 +80,11 @@ class BaseAgentRobot {
95
80
 
96
81
  _initEvents() {
97
82
  const aiConfig = this.opt.aiConfig
83
+ const isThinkPrint = this.opt.isThinkPrint
98
84
  let stopLoading = null
99
85
  this.brain.on(BrainEvent.THINK_BEFORE, () => {})
100
86
  this.brain.on(BrainEvent.SUB_THINK_BEFORE, (messages) => {
101
- if (!aiConfig.stream) {
87
+ if (!aiConfig.stream || !isThinkPrint) {
102
88
  if (stopLoading) {
103
89
  stopLoading('I have finished thinking.')
104
90
  }
@@ -106,27 +92,27 @@ class BaseAgentRobot {
106
92
  }
107
93
  })
108
94
  this.brain.on(BrainEvent.SUB_THINK_AFTER, (messages) => {
109
- if (!aiConfig.stream && stopLoading) {
95
+ if ((!aiConfig.stream && stopLoading) || !isThinkPrint) {
110
96
  stopLoading('I have finished thinking.')
111
97
  stopLoading = null
112
- const lastMessage = messages[messages.length - 1]
113
- this.screenPrinter.logInfo(lastMessage?.content)
98
+ // const lastMessage = messages[messages.length - 1]
99
+ // this.screenPrinter.logInfo(lastMessage?.content)
114
100
  }
115
101
  })
116
102
  this.brain.on(BrainEvent.SUB_STREAM_THINK_OUTPUT, (messages, output) => {
117
- this.screenPrinter.streamOutput(output, '#47854a')
103
+ isThinkPrint && this.screenPrinter.streamOutput(output, '#47854a')
118
104
  })
119
105
  this.brain.on(BrainEvent.SUB_STREAM_CONTENT_OUTPUT, (messages, content) => {
120
- this.screenPrinter.streamOutput(content, '#c2a654')
106
+ isThinkPrint && this.screenPrinter.streamOutput(content, '#c2a654')
121
107
  })
122
108
  this.brain.on(
123
109
  BrainEvent.SUB_STREAM_TOOL_CALLS_OUTPUT,
124
110
  (messages, toolCalls) => {
125
- this.screenPrinter.streamOutput(toolCalls, '#47854a')
111
+ isThinkPrint && this.screenPrinter.streamOutput(toolCalls, '#47854a')
126
112
  },
127
113
  )
128
114
  this.brain.on(BrainEvent.SUB_STREAM_END, () => {
129
- this.screenPrinter.streamLineBreak()
115
+ isThinkPrint && this.screenPrinter.streamLineBreak()
130
116
  })
131
117
  this.brain.on(BrainEvent.SUB_USE_TOOL, async (toolCalls) => {
132
118
  await this.hand.useTools(toolCalls)
@@ -189,71 +175,6 @@ class BaseAgentRobot {
189
175
  })
190
176
  }
191
177
 
192
- loadAttachTool(toolName) {
193
- let tool = this.attachTools.find((t) => t.name === toolName)
194
- if (!tool) {
195
- tool = this.toolCollection.find((t) => t.name === toolName)
196
- this.attachTools.push(tool)
197
- }
198
- }
199
-
200
- getTools() {
201
- const tools = [...this.originalTools, ...this.attachTools]
202
- const toolFunctions = {
203
- fs,
204
- axios,
205
- dayjs,
206
- lodash,
207
- canvas,
208
- echarts,
209
- cheerio,
210
- puppeteer
211
- }
212
-
213
- tools.forEach((tool) => {
214
- Object.assign(toolFunctions, tool.functions)
215
- })
216
- toolFunctions.agentRobot = this
217
- toolFunctions.Tools = toolFunctions
218
- // 兼容老版本
219
- toolFunctions.aiCli = {
220
- Tools: toolFunctions,
221
- }
222
- return toolFunctions
223
- }
224
-
225
- getToolDescriptions() {
226
- const tools = [...this.originalTools, ...this.attachTools]
227
- const toolDescriptions = []
228
- tools.forEach((tool) => {
229
- const descriptions = tool.descriptions.map((item) => {
230
- if (!item.type) {
231
- return {
232
- type: 'function',
233
- function: item,
234
- }
235
- } else {
236
- return item
237
- }
238
- })
239
- toolDescriptions.push(...descriptions)
240
- })
241
- return toolDescriptions
242
- }
243
-
244
- // 获取原装工具
245
- _getOriginalTools() {
246
- return [
247
- FileTools,
248
- InquirerTools,
249
- SystemTools,
250
- CreateAgentTools,
251
- GenerateTools,
252
- TaskTools,
253
- TestTools,
254
- ]
255
- }
256
-
257
178
  _getDefaultSystemPrompt(opt) {
258
179
  const osType = process.platform
259
180
  const workspace = this.workspace
@@ -268,7 +189,7 @@ class BaseAgentRobot {
268
189
  语言类型: 与用户输入语言一致
269
190
 
270
191
  ### 工具使用
271
- 执行任务前,应仔细阅读工具描述以及可以使用的Skills的描述内容,选择最合适的工具或技能来完成任务。
192
+ 执行任务前,应仔细阅读工具描述以及可以使用的Skills的描述内容,优先使用匹配到的工具或技能,避免自己发挥。
272
193
 
273
194
  ### 大文本文件处理规则(分步执行)
274
195
  处理长文档等大文件(单文件>${maxBlockFileSize}KB)时,必须按以下步骤分块处理:
@@ -283,6 +204,15 @@ class BaseAgentRobot {
283
204
  3. 结果校验:任务完成后,需简单校验结果是否符合用户目标(如文件是否生成、内容是否完整),并向用户反馈校验结果。
284
205
  4. 如果执行任务过程中需要安装依赖、软件或工具,必须通过调用用户交互函数与用户交互,等待用户确认后再执行安装,除非用户明确说明执行过程中使用静默模式。
285
206
  5. 任务执行过程中,产生的所有临时文件(如分块文件、测试文件等)必须以"tmp_"为前缀命名,如"tmp_block_filename.txt、tmp_test_filename.txt、tmp_bak_filename.txt",并在任务完成后删除这些临时文件,确保工作目录整洁。
207
+
208
+ ### 用户信息
209
+ #### 用户信息记录规则
210
+ 当对话中出现用户信息时,如个人基础信息(如姓名、年龄、职业、兴趣、性格特征)、操作习惯、代码习惯、阅读习惯、常用目录、文档收藏夹目录等,必须使用用户信息读写函数进行记录。
211
+
212
+ #### 当前用户信息
213
+ ----user info start----
214
+ ${this.toolManager.functions.readUserInfo()}
215
+ ----user info end----
286
216
  `
287
217
  }
288
218
 
@@ -0,0 +1,204 @@
1
+ /**
2
+ * @Author: Roman 306863030@qq.com
3
+ * @Description: 文档格式转换工具集(Markdown / HTML / PDF 互转)
4
+ * 依赖:puppeteer / fs-extra
5
+ * Word 相关转换(wordToPdf / wordToHtml / wordToMarkdown / markdownToWord / htmlToWord)已集成在 DocxTool 中
6
+ */
7
+ const path = require('path')
8
+ const fs = require('fs-extra')
9
+
10
+ // ─── 统一返回结构 ─────────────────────────────────────────────────────────────
11
+
12
+ function ok(data = null) {
13
+ return { success: true, data }
14
+ }
15
+
16
+ function fail(error, data = null) {
17
+ return { success: false, error: error?.message || String(error), data }
18
+ }
19
+
20
+ function resolvePath(filePath) {
21
+ return path.resolve(process.cwd(), filePath)
22
+ }
23
+
24
+ // ─── 内部辅助 ─────────────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * 使用 puppeteer 将 HTML 字符串渲染为 PDF 文件
28
+ */
29
+ async function htmlStringToPdf(html, outputPath) {
30
+ let puppeteer
31
+ try {
32
+ puppeteer = require('puppeteer')
33
+ } catch {
34
+ throw new Error('puppeteer 未安装,请先执行 npm install puppeteer')
35
+ }
36
+ const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] })
37
+ try {
38
+ const page = await browser.newPage()
39
+ await page.setContent(html, { waitUntil: 'networkidle0' })
40
+ fs.ensureDirSync(path.dirname(outputPath))
41
+ await page.pdf({ path: outputPath, format: 'A4', printBackground: true })
42
+ } finally {
43
+ await browser.close()
44
+ }
45
+ }
46
+
47
+ function escapeHtml(str) {
48
+ return str
49
+ .replace(/&/g, '&')
50
+ .replace(/</g, '&lt;')
51
+ .replace(/>/g, '&gt;')
52
+ }
53
+
54
+ /**
55
+ * 将 Markdown 文本转换为 HTML 字符串(基础实现,无需额外依赖)
56
+ */
57
+ function markdownToHtmlString(md) {
58
+ let html = md
59
+ // 代码块
60
+ .replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) =>
61
+ `<pre><code class="language-${lang}">${escapeHtml(code.trimEnd())}</code></pre>`)
62
+ // 行内代码
63
+ .replace(/`([^`]+)`/g, (_, c) => `<code>${escapeHtml(c)}</code>`)
64
+ // 标题
65
+ .replace(/^###### (.+)$/gm, '<h6>$1</h6>')
66
+ .replace(/^##### (.+)$/gm, '<h5>$1</h5>')
67
+ .replace(/^#### (.+)$/gm, '<h4>$1</h4>')
68
+ .replace(/^### (.+)$/gm, '<h3>$1</h3>')
69
+ .replace(/^## (.+)$/gm, '<h2>$1</h2>')
70
+ .replace(/^# (.+)$/gm, '<h1>$1</h1>')
71
+ // 水平线
72
+ .replace(/^[-*_]{3,}$/gm, '<hr>')
73
+ // 粗斜体
74
+ .replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>')
75
+ // 粗体
76
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
77
+ .replace(/__(.+?)__/g, '<strong>$1</strong>')
78
+ // 斜体
79
+ .replace(/\*(.+?)\*/g, '<em>$1</em>')
80
+ .replace(/_(.+?)_/g, '<em>$1</em>')
81
+ // 删除线
82
+ .replace(/~~(.+?)~~/g, '<del>$1</del>')
83
+ // 图片
84
+ .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img alt="$1" src="$2">')
85
+ // 链接
86
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
87
+ // 无序列表
88
+ .replace(/^[ \t]*[-*+] (.+)$/gm, '<li>$1</li>')
89
+ // 有序列表
90
+ .replace(/^[ \t]*\d+\. (.+)$/gm, '<li>$1</li>')
91
+ // 引用
92
+ .replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
93
+
94
+ // 将连续 <li> 包裹进 <ul>
95
+ html = html.replace(/(<li>[\s\S]+?<\/li>)(\n(?!<li>)|$)/g, (_, items) => `<ul>${items}</ul>`)
96
+
97
+ // 段落:连续非标签行
98
+ html = html.replace(/^(?!<[a-z]|$)(.+)$/gm, '<p>$1</p>')
99
+
100
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
101
+ body{font-family:sans-serif;line-height:1.7;max-width:900px;margin:40px auto;padding:0 20px;color:#333}
102
+ h1,h2,h3,h4,h5,h6{margin-top:1.2em}
103
+ pre{background:#f5f5f5;padding:12px;border-radius:4px;overflow:auto}
104
+ code{background:#f0f0f0;padding:2px 4px;border-radius:3px}
105
+ blockquote{border-left:4px solid #ddd;margin:0;padding-left:1em;color:#666}
106
+ table{border-collapse:collapse;width:100%}td,th{border:1px solid #ddd;padding:6px 10px}
107
+ </style></head><body>${html}</body></html>`
108
+ }
109
+
110
+ // ─── 工具函数 ─────────────────────────────────────────────────────────────────
111
+
112
+ /**
113
+ * Markdown 转 PDF
114
+ */
115
+ async function markdownToPdf(inputPath, outputPath) {
116
+ try {
117
+ const fullInput = resolvePath(inputPath)
118
+ const fullOutput = resolvePath(outputPath)
119
+ if (!fs.existsSync(fullInput)) {
120
+ return fail(`File does not exist: ${fullInput}`, { inputPath: fullInput })
121
+ }
122
+ const md = fs.readFileSync(fullInput, 'utf8')
123
+ const html = markdownToHtmlString(md)
124
+ await htmlStringToPdf(html, fullOutput)
125
+ return ok({ inputPath: fullInput, outputPath: fullOutput })
126
+ } catch (error) {
127
+ return fail(error, { inputPath, outputPath })
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Markdown 转 HTML
133
+ */
134
+ async function markdownToHtml(inputPath, outputPath) {
135
+ try {
136
+ const fullInput = resolvePath(inputPath)
137
+ const fullOutput = resolvePath(outputPath)
138
+ if (!fs.existsSync(fullInput)) {
139
+ return fail(`File does not exist: ${fullInput}`, { inputPath: fullInput })
140
+ }
141
+ const md = fs.readFileSync(fullInput, 'utf8')
142
+ const html = markdownToHtmlString(md)
143
+ fs.ensureDirSync(path.dirname(fullOutput))
144
+ fs.writeFileSync(fullOutput, html, 'utf8')
145
+ return ok({ inputPath: fullInput, outputPath: fullOutput })
146
+ } catch (error) {
147
+ return fail(error, { inputPath, outputPath })
148
+ }
149
+ }
150
+
151
+ // ─── 工具描述 ─────────────────────────────────────────────────────────────────
152
+
153
+ const descriptions = [
154
+ {
155
+ type: 'function',
156
+ function: {
157
+ name: 'markdownToPdf',
158
+ description:
159
+ '将 Markdown 文件(.md)转换为 PDF 文件。依赖 puppeteer,转换时应用默认样式。参数:inputPath 为源 .md 路径;outputPath 为输出 .pdf 路径。返回值:对象,包含 success、data(含 inputPath、outputPath)、error。',
160
+ parameters: {
161
+ type: 'object',
162
+ properties: {
163
+ inputPath: { type: 'string', description: '源 .md 文件路径。' },
164
+ outputPath: { type: 'string', description: '输出 .pdf 文件路径。' },
165
+ },
166
+ required: ['inputPath', 'outputPath'],
167
+ },
168
+ },
169
+ },
170
+ {
171
+ type: 'function',
172
+ function: {
173
+ name: 'markdownToHtml',
174
+ description:
175
+ '将 Markdown 文件(.md)转换为 HTML 文件,包含内联样式,可直接在浏览器中打开。参数:inputPath 为源 .md 路径;outputPath 为输出 .html 路径。返回值:对象,包含 success、data(含 inputPath、outputPath)、error。',
176
+ parameters: {
177
+ type: 'object',
178
+ properties: {
179
+ inputPath: { type: 'string', description: '源 .md 文件路径。' },
180
+ outputPath: { type: 'string', description: '输出 .html 文件路径。' },
181
+ },
182
+ required: ['inputPath', 'outputPath'],
183
+ },
184
+ },
185
+ },
186
+ ]
187
+
188
+ // ─── 导出 ──────────────────────────────────────────────────────────────────────
189
+
190
+ const functions = {
191
+ markdownToPdf,
192
+ markdownToHtml,
193
+ }
194
+
195
+ const DocTransformTool = {
196
+ name: 'DocTransformTool',
197
+ description: '提供 Markdown 与 HTML/PDF 之间的格式互转能力,支持 markdown转pdf、markdown转html。Word 相关转换请使用 DocxTool。',
198
+ platform: 'all',
199
+ descriptions,
200
+ functions,
201
+ isSystem: true
202
+ }
203
+
204
+ module.exports = DocTransformTool