deepfish-ai 1.0.26 → 1.0.27
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/package.json +1 -1
- package/src/AgentRobot/AgentRobotFactory/MainAgentRobot.js +1 -0
- package/src/AgentRobot/BaseAgentRobot/Logger.js +9 -3
- package/src/AgentRobot/BaseAgentRobot/tools/GenerateTools.js +99 -4
- package/src/AgentRobot/BaseAgentRobot/tools/SystemTools.js +8 -8
- package/src/AgentRobot/BaseAgentRobot/utils/AIToolManager.js +1 -0
- package/src/AgentRobot/BaseAgentRobot/utils/AttachmentToolScanner.js +6 -4
- package/src/AgentRobot/BaseAgentRobot/utils/analyzeReturn.test.js +104 -0
- package/src/AgentRobot/BaseAgentRobot/utils/normal.js +288 -0
- package/src/cli/SkillConfigManager.js +3 -2
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/embedding.js +0 -459
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepfish-ai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.27",
|
|
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",
|
|
@@ -16,6 +16,7 @@ class MainAgentRobot extends BaseAgentRobot {
|
|
|
16
16
|
|
|
17
17
|
// 初始化文件
|
|
18
18
|
_initFiles(opt) {
|
|
19
|
+
this.logId = Date.now()
|
|
19
20
|
this.workspace = opt.workspace || process.cwd() // 工作空间,目录
|
|
20
21
|
this.basespace = opt.basespace || path.join(os.homedir(), '.deepfish-ai') // 记忆空间,目录
|
|
21
22
|
this.userspace = path.join(this.basespace, 'user-info') // 用户空间,目录
|
|
@@ -9,6 +9,7 @@ class Logger {
|
|
|
9
9
|
this.maxLogExpireTime = agentRobot.config.maxLogExpireTime
|
|
10
10
|
this.memorySpace = agentRobot.memorySpace
|
|
11
11
|
this.logTimeMap = new Map()
|
|
12
|
+
this.logId = agentRobot.logId || agentRobot.root.logId
|
|
12
13
|
}
|
|
13
14
|
clearAllLogs() {
|
|
14
15
|
const fileNames = fs.readdirSync(this.memorySpace)
|
|
@@ -55,11 +56,16 @@ class Logger {
|
|
|
55
56
|
}
|
|
56
57
|
const logFile = path.join(
|
|
57
58
|
this.logDirPath,
|
|
58
|
-
`log-${dayjs().format('YYYY-MM-DD HH')}.txt`,
|
|
59
|
+
`log-${this.logId}-${dayjs().format('YYYY-MM-DD HH')}.txt`,
|
|
60
|
+
)
|
|
61
|
+
const logFile2 = path.join(
|
|
62
|
+
this.logDirPath,
|
|
63
|
+
`log-messeage-${this.logId}.txt`,
|
|
59
64
|
)
|
|
60
65
|
try {
|
|
61
66
|
let logEntry = `[${new Date().toISOString()}][${message.role}] ${message.content}\n`
|
|
62
67
|
fs.appendFileSync(logFile, logEntry)
|
|
68
|
+
fs.appendFileSync(logFile2, logEntry)
|
|
63
69
|
return true
|
|
64
70
|
} catch (error) {
|
|
65
71
|
console.error('Failed to log message:', error.message)
|
|
@@ -72,7 +78,7 @@ class Logger {
|
|
|
72
78
|
}
|
|
73
79
|
const logFile = path.join(
|
|
74
80
|
this.logDirPath,
|
|
75
|
-
`log-${dayjs().format('YYYY-MM-DD HH')}.txt`,
|
|
81
|
+
`log-${this.logId}-${dayjs().format('YYYY-MM-DD HH')}.txt`,
|
|
76
82
|
)
|
|
77
83
|
try {
|
|
78
84
|
let logEntry = `[${new Date().toISOString()}][***COMPRESS START***]
|
|
@@ -92,7 +98,7 @@ class Logger {
|
|
|
92
98
|
}
|
|
93
99
|
const logFile = path.join(
|
|
94
100
|
this.logDirPath,
|
|
95
|
-
`log-${dayjs().format('YYYY-MM-DD HH')}.txt`,
|
|
101
|
+
`log-${this.logId}-${dayjs().format('YYYY-MM-DD HH')}.txt`,
|
|
96
102
|
)
|
|
97
103
|
try {
|
|
98
104
|
let logEntry = `[${new Date().toISOString()}][###############] ${message}\n`
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
const fs = require('fs-extra')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
|
|
1
4
|
const descriptions = [
|
|
2
5
|
{
|
|
3
6
|
type: 'function',
|
|
@@ -59,6 +62,35 @@ const descriptions = [
|
|
|
59
62
|
},
|
|
60
63
|
},
|
|
61
64
|
},
|
|
65
|
+
{
|
|
66
|
+
type: 'function',
|
|
67
|
+
function: {
|
|
68
|
+
name: 'generateClawSkillByHistory',
|
|
69
|
+
description:
|
|
70
|
+
'根据用户目标与对话历史日志或指定日志文件自动生成 OpenClaw Skill 工具包。logfile参数为日志文件名称,默认从日志目录中查找,如果不存在则从当前目录查找;若未设置该参数,则使用最新日志文件;该参数也可设置为绝对路径。',
|
|
71
|
+
parameters: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
goal: { type: 'string' },
|
|
75
|
+
logfile: { type: 'string' },
|
|
76
|
+
},
|
|
77
|
+
required: ['goal'],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: 'function',
|
|
83
|
+
function: {
|
|
84
|
+
name: 'getSessionHistoryFile',
|
|
85
|
+
description:
|
|
86
|
+
'获取最后一次会话历史日志文件路径,用于生成Skill时引用会话历史。',
|
|
87
|
+
parameters: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {},
|
|
90
|
+
required: [],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
62
94
|
]
|
|
63
95
|
|
|
64
96
|
async function getGenerateSkillRules(goal) {
|
|
@@ -77,6 +109,7 @@ async function getGenerateSkillRules(goal) {
|
|
|
77
109
|
- git仓库地址:固定为 https://github.com/qq306863030/deepfish-extensions.git
|
|
78
110
|
- author设置为"DeepFish AI"
|
|
79
111
|
- type字段设置为"commonjs",确保模块系统兼容
|
|
112
|
+
- main字段设置为"index.js",指定入口文件
|
|
80
113
|
3. 文件结构
|
|
81
114
|
- 主文件:项目入口文件必须命名为index.js
|
|
82
115
|
- 子文件:复杂的逻辑可以拆分到其他.js文件中;将descriptions、functions拆分到子文件;
|
|
@@ -224,7 +257,7 @@ async function generateSkill(rules) {
|
|
|
224
257
|
}
|
|
225
258
|
|
|
226
259
|
async function getGenerateClawSkillRules(goal) {
|
|
227
|
-
|
|
260
|
+
const newGoal = `
|
|
228
261
|
## 任务目标
|
|
229
262
|
基于OpenClaw Skill规范创建一个标准化的Skill工具包,实现用户目标:${goal},最终输出可被你直接加载使用。
|
|
230
263
|
|
|
@@ -328,19 +361,81 @@ async function generateClawSkill(rules) {
|
|
|
328
361
|
return this.Tools.executeTaskList(rules)
|
|
329
362
|
}
|
|
330
363
|
|
|
364
|
+
// 根据对话历史生成一个skill工具包
|
|
365
|
+
async function generateClawSkillByHistory(goal, logfile) {
|
|
366
|
+
const logDirPath = this.agentRobot.logDirPath
|
|
367
|
+
const workspace = this.agentRobot.workspace
|
|
368
|
+
let logFilePath
|
|
369
|
+
if (!logfile) {
|
|
370
|
+
// 如果没有明确提出对话历史文件,则从最新的对话日志中获取skill生成所需的对话历史内容
|
|
371
|
+
let logFiles = fs.readdirSync(logDirPath)
|
|
372
|
+
logFiles = logFiles.filter((file) => file.startsWith(`log-messeage-`))
|
|
373
|
+
if (logFiles.length === 0) {
|
|
374
|
+
throw new Error('No log file found for generating skill by history')
|
|
375
|
+
}
|
|
376
|
+
// 根据文件名称排序,获取最新的日志文件 log-messeage-{logId}.txt
|
|
377
|
+
let latestLogFile = logFiles[0]
|
|
378
|
+
if (logFiles.length > 1) {
|
|
379
|
+
latestLogFile = logFiles.sort((a, b) => {
|
|
380
|
+
const aTime = parseInt(a.slice(12, -4))
|
|
381
|
+
const bTime = parseInt(b.slice(12, -4))
|
|
382
|
+
return bTime - aTime
|
|
383
|
+
})[1]
|
|
384
|
+
}
|
|
385
|
+
logFilePath = path.join(logDirPath, latestLogFile)
|
|
386
|
+
} else {
|
|
387
|
+
logFilePath = path.join(logDirPath, logfile)
|
|
388
|
+
if (!fs.existsSync(logFilePath)) {
|
|
389
|
+
// 如果在日志目录中没有找到,则尝试在当前目录中查找
|
|
390
|
+
logFilePath = path.resolve(workspace, logfile)
|
|
391
|
+
if (!fs.existsSync(logFilePath)) {
|
|
392
|
+
throw new Error(`Log file not found: ${logfile}`)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const rules = await this.Tools.getGenerateClawSkillRules(
|
|
397
|
+
`基于用户的任务目标和会话历史日志中的有效信息和,生成一个OpenClaw兼容的Skill工具包。用户目标: ${goal}, 会话历史路径: ${logFilePath}`,
|
|
398
|
+
)
|
|
399
|
+
return await this.Tools.generateClawSkill(rules)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 获取会话历史文件
|
|
403
|
+
function getSessionHistoryFile() {
|
|
404
|
+
const logDirPath = this.agentRobot.logDirPath
|
|
405
|
+
let logFiles = fs.readdirSync(logDirPath)
|
|
406
|
+
logFiles = logFiles.filter((file) => file.startsWith(`log-messeage-`))
|
|
407
|
+
if (logFiles.length === 0) {
|
|
408
|
+
throw new Error('No log file found for generating skill by history')
|
|
409
|
+
}
|
|
410
|
+
// 根据文件名称排序,获取最新的日志文件 log-messeage-{logId}.txt
|
|
411
|
+
let latestLogFile = logFiles[0]
|
|
412
|
+
if (logFiles.length > 1) {
|
|
413
|
+
latestLogFile = logFiles.sort((a, b) => {
|
|
414
|
+
const aTime = parseInt(a.slice(12, -4))
|
|
415
|
+
const bTime = parseInt(b.slice(12, -4))
|
|
416
|
+
return bTime - aTime
|
|
417
|
+
})[1]
|
|
418
|
+
}
|
|
419
|
+
const logFilePath = path.join(logDirPath, latestLogFile)
|
|
420
|
+
return logFilePath
|
|
421
|
+
}
|
|
422
|
+
|
|
331
423
|
const functions = {
|
|
332
424
|
getGenerateClawSkillRules,
|
|
333
425
|
getGenerateSkillRules,
|
|
334
426
|
generateClawSkill,
|
|
335
427
|
generateSkill,
|
|
428
|
+
generateClawSkillByHistory,
|
|
429
|
+
getSessionHistoryFile,
|
|
336
430
|
}
|
|
337
431
|
|
|
338
432
|
const GenerateTools = {
|
|
339
433
|
name: 'GenerateTools',
|
|
340
|
-
description:
|
|
434
|
+
description:
|
|
435
|
+
'提供扩展工具与Skill工具包生成规则能力,用于辅助AI构建标准化扩展项目模板',
|
|
341
436
|
descriptions,
|
|
342
437
|
functions,
|
|
343
|
-
isSystem: true
|
|
438
|
+
isSystem: true,
|
|
344
439
|
}
|
|
345
440
|
|
|
346
|
-
module.exports = GenerateTools
|
|
441
|
+
module.exports = GenerateTools
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @Author: Roman 306863030@qq.com
|
|
3
3
|
* @Date: 2026-03-17 11:59:19
|
|
4
|
-
* @LastEditors:
|
|
5
|
-
* @LastEditTime: 2026-
|
|
4
|
+
* @LastEditors: Roman 306863030@qq.com
|
|
5
|
+
* @LastEditTime: 2026-05-07 14:17:50
|
|
6
6
|
* @FilePath: \deepfish\src\AgentRobot\BaseAgentRobot\tools\SystemTools.js
|
|
7
7
|
* @Description: 默认扩展函数
|
|
8
8
|
* @
|
|
@@ -12,7 +12,7 @@ const fs = require('fs-extra')
|
|
|
12
12
|
const dayjs = require('dayjs')
|
|
13
13
|
const iconv = require('iconv-lite')
|
|
14
14
|
const { spawnSync } = require('child_process')
|
|
15
|
-
const { detectEncoding } = require('../utils/normal.js')
|
|
15
|
+
const { detectEncoding, analyzeReturn } = require('../utils/normal.js')
|
|
16
16
|
const aiConsole = require('../utils/aiConsole.js')
|
|
17
17
|
|
|
18
18
|
// 执行系统命令
|
|
@@ -82,11 +82,10 @@ async function requestAI(
|
|
|
82
82
|
async function executeJSCode(code) {
|
|
83
83
|
aiConsole.logSuccess('Executing JavaScript code: ')
|
|
84
84
|
aiConsole.logSuccess(code)
|
|
85
|
-
//
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const error = new Error('The last line of the code must contain a return statement.')
|
|
85
|
+
// 校验代码片段中是否存在顶层 return,避免仅按最后一行判断导致误判。
|
|
86
|
+
const { hasReturnValue } = analyzeReturn(code)
|
|
87
|
+
if (!hasReturnValue) {
|
|
88
|
+
const error = new Error('The code must contain a return value.')
|
|
90
89
|
throw error
|
|
91
90
|
}
|
|
92
91
|
try {
|
|
@@ -96,6 +95,7 @@ async function executeJSCode(code) {
|
|
|
96
95
|
'require',
|
|
97
96
|
`return (async () => {
|
|
98
97
|
this.logMessages = []
|
|
98
|
+
this.Tools = Tools
|
|
99
99
|
const originalLog = console.log
|
|
100
100
|
const newLog = function () {
|
|
101
101
|
originalLog.apply(console, arguments)
|
|
@@ -49,6 +49,7 @@ class AIToolManager {
|
|
|
49
49
|
// 外部工具扫描
|
|
50
50
|
this.toolCollection = AttachmentToolScanner.getToolCollection(
|
|
51
51
|
this.agentRobot.workspace,
|
|
52
|
+
this.agentRobot.basespace,
|
|
52
53
|
) // 加载工具集合
|
|
53
54
|
this.clawSkillCollection = AttachmentToolScanner.getClawSkillCollection(
|
|
54
55
|
this.agentRobot.basespace,
|
|
@@ -9,7 +9,7 @@ class AttachmentToolType {
|
|
|
9
9
|
|
|
10
10
|
class AttachmentToolScanner {
|
|
11
11
|
// 获取附加工具
|
|
12
|
-
static getToolCollection(workspace) {
|
|
12
|
+
static getToolCollection(workspace, basespace) {
|
|
13
13
|
// 从文件中加载附加技能
|
|
14
14
|
// 动态加载这些文件,获取工具对象
|
|
15
15
|
const attachTools = []
|
|
@@ -51,12 +51,14 @@ class AttachmentToolScanner {
|
|
|
51
51
|
*/
|
|
52
52
|
// 1. 子agent创建时,不能拥有其他附加能力
|
|
53
53
|
// 2. 使用platform过滤
|
|
54
|
-
const dir1 = path.resolve(__dirname, '
|
|
54
|
+
const dir1 = path.resolve(__dirname, '../../../../../') // 程序所在目录
|
|
55
55
|
const dir2 = path.resolve(workspace, './node_modules') // 工作目录下node_modules目录
|
|
56
56
|
const dir3 = path.resolve(workspace, './') // 工作目录
|
|
57
57
|
const dir4 = getGlobalNodeModulesPath()
|
|
58
|
+
const dir5 = path.resolve(basespace, 'skills') // 工作目录的父目录
|
|
59
|
+
const dir6 = path.resolve(basespace, 'clawSkills') // 工作目录的父目录
|
|
58
60
|
const result = []
|
|
59
|
-
const searchDirs = [...new Set([dir1, dir2, dir3, dir4])]
|
|
61
|
+
const searchDirs = [...new Set([dir1, dir2, dir3, dir4, dir5, dir6])]
|
|
60
62
|
for (const dirPath of searchDirs) {
|
|
61
63
|
if (!fs.existsSync(dirPath)) {
|
|
62
64
|
continue
|
|
@@ -172,7 +174,7 @@ ${table}
|
|
|
172
174
|
- 使用用户请求匹配 skill description,
|
|
173
175
|
- 一次只加载一个Skill,优先匹配最具体的Skill
|
|
174
176
|
- 当用户请求不匹配任何Skill描述时,不加载任何Skill
|
|
175
|
-
- 使用Skill前先使用readFile函数读取SKILL.md文件获取调用说明,通过仔细阅读说明文件学习Skill
|
|
177
|
+
- 使用Skill前先使用readFile函数读取SKILL.md文件获取调用说明,通过仔细阅读说明文件学习Skill的使用方法,直接完成任务,无需创建子Agent来完成任务
|
|
176
178
|
## Available Skills
|
|
177
179
|
|
|
178
180
|
| Skill | Type | Description | Location | SkillFilePath |
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const assert = require('assert')
|
|
2
|
+
const { analyzeReturn } = require('./normal')
|
|
3
|
+
|
|
4
|
+
function runCase(name, code, expected) {
|
|
5
|
+
const actual = analyzeReturn(code)
|
|
6
|
+
assert.deepStrictEqual(
|
|
7
|
+
actual,
|
|
8
|
+
expected,
|
|
9
|
+
`${name} failed\nexpected: ${JSON.stringify(expected)}\nactual: ${JSON.stringify(actual)}\ncode:\n${code}`,
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function run() {
|
|
14
|
+
const cases = [
|
|
15
|
+
{
|
|
16
|
+
name: 'empty input',
|
|
17
|
+
code: '',
|
|
18
|
+
expected: { hasReturn: false, hasReturnValue: false },
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'non-string input',
|
|
22
|
+
code: null,
|
|
23
|
+
expected: { hasReturn: false, hasReturnValue: false },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'top-level return with number',
|
|
27
|
+
code: 'const x = 1\nreturn x + 1',
|
|
28
|
+
expected: { hasReturn: true, hasReturnValue: true },
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'top-level bare return',
|
|
32
|
+
code: 'if (ok) {\n return\n}\nreturn;',
|
|
33
|
+
expected: { hasReturn: true, hasReturnValue: false },
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'ASI after return newline',
|
|
37
|
+
code: "return\n'hello'",
|
|
38
|
+
expected: { hasReturn: true, hasReturnValue: false },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'return with inline comment and value',
|
|
42
|
+
code: 'return /* explain */ 42',
|
|
43
|
+
expected: { hasReturn: true, hasReturnValue: true },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'return with line comment then newline',
|
|
47
|
+
code: 'return // explain\n42',
|
|
48
|
+
expected: { hasReturn: true, hasReturnValue: false },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'return object literal',
|
|
52
|
+
code: 'return { ok: true }',
|
|
53
|
+
expected: { hasReturn: true, hasReturnValue: true },
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'return template literal',
|
|
57
|
+
code: 'return `done:${1}`',
|
|
58
|
+
expected: { hasReturn: true, hasReturnValue: true },
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'ignore return in string/comment',
|
|
62
|
+
code: "const s = 'return 1'\n// return 2\n/* return 3 */\nconst n = 4",
|
|
63
|
+
expected: { hasReturn: false, hasReturnValue: false },
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'ignore return in nested function declaration',
|
|
67
|
+
code: 'function inner() { return 1 }\nconst n = 1',
|
|
68
|
+
expected: { hasReturn: false, hasReturnValue: false },
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'ignore return in nested arrow block',
|
|
72
|
+
code: 'const fn = () => { return 1 }\nconst n = 1',
|
|
73
|
+
expected: { hasReturn: false, hasReturnValue: false },
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'ignore return in class method',
|
|
77
|
+
code: 'class A { m() { return 1 } }\nconst n = 1',
|
|
78
|
+
expected: { hasReturn: false, hasReturnValue: false },
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'ignore return in object method',
|
|
82
|
+
code: 'const obj = { m() { return 1 } }\nconst n = 1',
|
|
83
|
+
expected: { hasReturn: false, hasReturnValue: false },
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'top-level return exists even with nested returns',
|
|
87
|
+
code: 'const fn = () => { return 1 }\nif (ok) { return 2 }',
|
|
88
|
+
expected: { hasReturn: true, hasReturnValue: true },
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'real-world code snippet from code.txt shape',
|
|
92
|
+
code: "const fs = require('fs')\nconst content = 'x'\nreturn 'File updated successfully.'",
|
|
93
|
+
expected: { hasReturn: true, hasReturnValue: true },
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
for (const testCase of cases) {
|
|
98
|
+
runCase(testCase.name, testCase.code, testCase.expected)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(`analyzeReturn tests passed: ${cases.length} cases`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
run()
|
|
@@ -101,6 +101,293 @@ function sleep(ms) {
|
|
|
101
101
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// 判断代码是否有返回值
|
|
105
|
+
function analyzeReturn(code) {
|
|
106
|
+
if (typeof code !== 'string' || !code.trim()) {
|
|
107
|
+
return {
|
|
108
|
+
hasReturn: false,
|
|
109
|
+
hasReturnValue: false,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 移除字符串和注释,避免把文本中的 return 误判为代码关键字。
|
|
114
|
+
function stripStringsAndComments(input) {
|
|
115
|
+
const chars = input.split('')
|
|
116
|
+
let i = 0
|
|
117
|
+
let state = 'normal'
|
|
118
|
+
|
|
119
|
+
while (i < chars.length) {
|
|
120
|
+
const ch = chars[i]
|
|
121
|
+
const next = chars[i + 1]
|
|
122
|
+
|
|
123
|
+
if (state === 'normal') {
|
|
124
|
+
if (ch === '/' && next === '/') {
|
|
125
|
+
state = 'line-comment'
|
|
126
|
+
chars[i] = ' '
|
|
127
|
+
chars[i + 1] = ' '
|
|
128
|
+
i += 2
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
if (ch === '/' && next === '*') {
|
|
132
|
+
state = 'block-comment'
|
|
133
|
+
chars[i] = ' '
|
|
134
|
+
chars[i + 1] = ' '
|
|
135
|
+
i += 2
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
if (ch === "'") {
|
|
139
|
+
state = 'single-quote'
|
|
140
|
+
chars[i] = ' '
|
|
141
|
+
i += 1
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
if (ch === '"') {
|
|
145
|
+
state = 'double-quote'
|
|
146
|
+
chars[i] = ' '
|
|
147
|
+
i += 1
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
if (ch === '`') {
|
|
151
|
+
state = 'template'
|
|
152
|
+
chars[i] = ' '
|
|
153
|
+
i += 1
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
i += 1
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (state === 'line-comment') {
|
|
161
|
+
if (ch === '\n') {
|
|
162
|
+
state = 'normal'
|
|
163
|
+
} else {
|
|
164
|
+
chars[i] = ' '
|
|
165
|
+
}
|
|
166
|
+
i += 1
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (state === 'block-comment') {
|
|
171
|
+
if (ch === '*' && next === '/') {
|
|
172
|
+
chars[i] = ' '
|
|
173
|
+
chars[i + 1] = ' '
|
|
174
|
+
state = 'normal'
|
|
175
|
+
i += 2
|
|
176
|
+
} else {
|
|
177
|
+
if (ch !== '\n') {
|
|
178
|
+
chars[i] = ' '
|
|
179
|
+
}
|
|
180
|
+
i += 1
|
|
181
|
+
}
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (state === 'single-quote') {
|
|
186
|
+
if (ch === '\\') {
|
|
187
|
+
chars[i] = ' '
|
|
188
|
+
if (i + 1 < chars.length) {
|
|
189
|
+
chars[i + 1] = ' '
|
|
190
|
+
}
|
|
191
|
+
i += 2
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
chars[i] = ch === '\n' ? '\n' : ' '
|
|
195
|
+
if (ch === "'") {
|
|
196
|
+
state = 'normal'
|
|
197
|
+
}
|
|
198
|
+
i += 1
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (state === 'double-quote') {
|
|
203
|
+
if (ch === '\\') {
|
|
204
|
+
chars[i] = ' '
|
|
205
|
+
if (i + 1 < chars.length) {
|
|
206
|
+
chars[i + 1] = ' '
|
|
207
|
+
}
|
|
208
|
+
i += 2
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
chars[i] = ch === '\n' ? '\n' : ' '
|
|
212
|
+
if (ch === '"') {
|
|
213
|
+
state = 'normal'
|
|
214
|
+
}
|
|
215
|
+
i += 1
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (state === 'template') {
|
|
220
|
+
if (ch === '\\') {
|
|
221
|
+
chars[i] = ' '
|
|
222
|
+
if (i + 1 < chars.length) {
|
|
223
|
+
chars[i + 1] = ' '
|
|
224
|
+
}
|
|
225
|
+
i += 2
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
chars[i] = ch === '\n' ? '\n' : ' '
|
|
229
|
+
if (ch === '`') {
|
|
230
|
+
state = 'normal'
|
|
231
|
+
}
|
|
232
|
+
i += 1
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return chars.join('')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isReturnWithValue(input, startIndex) {
|
|
240
|
+
let i = startIndex
|
|
241
|
+
while (i < input.length) {
|
|
242
|
+
const ch = input[i]
|
|
243
|
+
const next = input[i + 1]
|
|
244
|
+
if (ch === ' ' || ch === '\t' || ch === '\r') {
|
|
245
|
+
i += 1
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (ch === '/' && next === '/') {
|
|
250
|
+
i += 2
|
|
251
|
+
while (i < input.length && input[i] !== '\n') {
|
|
252
|
+
i += 1
|
|
253
|
+
}
|
|
254
|
+
continue
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (ch === '/' && next === '*') {
|
|
258
|
+
i += 2
|
|
259
|
+
while (i < input.length) {
|
|
260
|
+
if (input[i] === '*' && input[i + 1] === '/') {
|
|
261
|
+
i += 2
|
|
262
|
+
break
|
|
263
|
+
}
|
|
264
|
+
i += 1
|
|
265
|
+
}
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (ch === '\n' || ch === ';' || ch === '}' || ch === ')') {
|
|
270
|
+
return false
|
|
271
|
+
}
|
|
272
|
+
return true
|
|
273
|
+
}
|
|
274
|
+
return false
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function isControlKeyword(token) {
|
|
278
|
+
return ['if', 'for', 'while', 'switch', 'catch', 'with'].includes(token)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const cleaned = stripStringsAndComments(code)
|
|
282
|
+
const tokenRegex = /[A-Za-z_$][\w$]*|=>|[{}()\[\]]/g
|
|
283
|
+
const blockStack = []
|
|
284
|
+
const parenStack = []
|
|
285
|
+
let functionDepth = 0
|
|
286
|
+
let pendingFunctionBlock = 0
|
|
287
|
+
let pendingArrow = false
|
|
288
|
+
let hasReturn = false
|
|
289
|
+
let hasReturnValue = false
|
|
290
|
+
let lastToken = ''
|
|
291
|
+
let recentClosedParen = null
|
|
292
|
+
let match
|
|
293
|
+
|
|
294
|
+
while ((match = tokenRegex.exec(cleaned)) !== null) {
|
|
295
|
+
const token = match[0]
|
|
296
|
+
const index = match.index
|
|
297
|
+
|
|
298
|
+
if (token === '(') {
|
|
299
|
+
parenStack.push({
|
|
300
|
+
beforeToken: lastToken,
|
|
301
|
+
})
|
|
302
|
+
lastToken = token
|
|
303
|
+
continue
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (token === ')') {
|
|
307
|
+
recentClosedParen = parenStack.pop() || null
|
|
308
|
+
lastToken = token
|
|
309
|
+
continue
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (token === 'function') {
|
|
313
|
+
pendingFunctionBlock += 1
|
|
314
|
+
pendingArrow = false
|
|
315
|
+
lastToken = token
|
|
316
|
+
recentClosedParen = null
|
|
317
|
+
continue
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (token === '=>') {
|
|
321
|
+
pendingArrow = true
|
|
322
|
+
lastToken = token
|
|
323
|
+
recentClosedParen = null
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (token === '{') {
|
|
328
|
+
let isFunctionBlock = false
|
|
329
|
+
if (pendingFunctionBlock > 0 || pendingArrow) {
|
|
330
|
+
isFunctionBlock = true
|
|
331
|
+
} else if (lastToken === ')' && recentClosedParen) {
|
|
332
|
+
const beforeToken = recentClosedParen.beforeToken
|
|
333
|
+
if (beforeToken && !isControlKeyword(beforeToken)) {
|
|
334
|
+
// 识别 class/object method 这类无 function 关键字的方法体。
|
|
335
|
+
isFunctionBlock = true
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (isFunctionBlock) {
|
|
340
|
+
blockStack.push('function')
|
|
341
|
+
functionDepth += 1
|
|
342
|
+
if (pendingFunctionBlock > 0) {
|
|
343
|
+
pendingFunctionBlock -= 1
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
blockStack.push('block')
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
pendingArrow = false
|
|
350
|
+
lastToken = token
|
|
351
|
+
recentClosedParen = null
|
|
352
|
+
continue
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (token === '}') {
|
|
356
|
+
const top = blockStack.pop()
|
|
357
|
+
if (top === 'function' && functionDepth > 0) {
|
|
358
|
+
functionDepth -= 1
|
|
359
|
+
}
|
|
360
|
+
pendingArrow = false
|
|
361
|
+
lastToken = token
|
|
362
|
+
recentClosedParen = null
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (token === 'return' && functionDepth === 0) {
|
|
367
|
+
hasReturn = true
|
|
368
|
+
if (isReturnWithValue(code, index + token.length)) {
|
|
369
|
+
hasReturnValue = true
|
|
370
|
+
}
|
|
371
|
+
if (hasReturn && hasReturnValue) {
|
|
372
|
+
break
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (pendingArrow) {
|
|
377
|
+
// 箭头函数表达式体不带 {} 时,不会有 return 关键字参与判断。
|
|
378
|
+
pendingArrow = false
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
lastToken = token
|
|
382
|
+
recentClosedParen = null
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
hasReturn,
|
|
387
|
+
hasReturnValue,
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
104
391
|
module.exports = {
|
|
105
392
|
objStrToObj,
|
|
106
393
|
delay,
|
|
@@ -108,4 +395,5 @@ module.exports = {
|
|
|
108
395
|
openDirectory,
|
|
109
396
|
detectEncoding,
|
|
110
397
|
sleep,
|
|
398
|
+
analyzeReturn,
|
|
111
399
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @Author: Roman 306863030@qq.com
|
|
3
3
|
* @Date: 2026-03-23 15:23:42
|
|
4
4
|
* @LastEditors: Roman 306863030@qq.com
|
|
5
|
-
* @LastEditTime: 2026-
|
|
5
|
+
* @LastEditTime: 2026-05-06 14:53:07
|
|
6
6
|
* @FilePath: \deepfish\src\cli\SkillConfigManager.js
|
|
7
7
|
* @Description: Skill configuration manager
|
|
8
8
|
*/
|
|
@@ -89,7 +89,8 @@ class SkillConfigManager {
|
|
|
89
89
|
// 如果数组的数量与目录中的数量不一致,则自动同步
|
|
90
90
|
const skills = this.readSkills()
|
|
91
91
|
const skillDirs = fs.readdirSync(this.skillDir).filter((file) => {
|
|
92
|
-
|
|
92
|
+
const fullPath = path.join(this.skillDir, file)
|
|
93
|
+
return fs.statSync(fullPath).isDirectory() && fs.existsSync(path.join(fullPath, 'SKILL.md'))
|
|
93
94
|
})
|
|
94
95
|
if (skills.length === skillDirs.length) {
|
|
95
96
|
return
|
|
@@ -1,459 +0,0 @@
|
|
|
1
|
-
const path = require('path')
|
|
2
|
-
const fs = require('fs-extra')
|
|
3
|
-
const crypto = require('crypto')
|
|
4
|
-
const mammoth = require('mammoth')
|
|
5
|
-
const pdfParse = require('pdf-parse')
|
|
6
|
-
const XLSX = require('xlsx')
|
|
7
|
-
const aiInquirer = require('../utils/aiInquirer')
|
|
8
|
-
|
|
9
|
-
function ok(data = null) {
|
|
10
|
-
return { success: true, data }
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function fail(error, data = null) {
|
|
14
|
-
return { success: false, error: error?.message || String(error), data }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function _getKbRootPath() {
|
|
18
|
-
return path.resolve(process.cwd(), '.deepfish-rag')
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function _getKbIndexPath(kbRootPath) {
|
|
22
|
-
return path.join(kbRootPath, 'index.json')
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function _sha256(content) {
|
|
26
|
-
return crypto.createHash('sha256').update(content).digest('hex')
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function _isSupportedFile(filePath) {
|
|
30
|
-
const ext = path.extname(filePath).toLowerCase()
|
|
31
|
-
const supportedExts = new Set([
|
|
32
|
-
'.md', '.txt', '.json', '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.html', '.htm', '.css', '.scss', '.less', '.xml', '.yaml', '.yml', '.csv', '.log', '.sql', '.py', '.java', '.go', '.rs', '.cpp', '.c', '.h', '.docx', '.pdf', '.xlsx', '.xls',
|
|
33
|
-
])
|
|
34
|
-
return supportedExts.has(ext)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function _collectSourceFiles(sourcePath) {
|
|
38
|
-
const stat = fs.statSync(sourcePath)
|
|
39
|
-
if (stat.isFile()) {
|
|
40
|
-
return [sourcePath]
|
|
41
|
-
}
|
|
42
|
-
const files = []
|
|
43
|
-
const walk = (current) => {
|
|
44
|
-
const children = fs.readdirSync(current)
|
|
45
|
-
for (const child of children) {
|
|
46
|
-
const fullPath = path.join(current, child)
|
|
47
|
-
const childStat = fs.statSync(fullPath)
|
|
48
|
-
if (childStat.isDirectory()) {
|
|
49
|
-
walk(fullPath)
|
|
50
|
-
} else if (childStat.isFile()) {
|
|
51
|
-
files.push(fullPath)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
walk(sourcePath)
|
|
56
|
-
return files
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function _readDocumentContent(filePath) {
|
|
60
|
-
const ext = path.extname(filePath).toLowerCase()
|
|
61
|
-
if (ext === '.docx') {
|
|
62
|
-
const result = await mammoth.extractRawText({ path: filePath })
|
|
63
|
-
return result.value || ''
|
|
64
|
-
}
|
|
65
|
-
if (ext === '.pdf') {
|
|
66
|
-
const buffer = fs.readFileSync(filePath)
|
|
67
|
-
const result = await pdfParse(buffer)
|
|
68
|
-
return result.text || ''
|
|
69
|
-
}
|
|
70
|
-
if (ext === '.xlsx' || ext === '.xls') {
|
|
71
|
-
const workbook = XLSX.readFile(filePath)
|
|
72
|
-
return workbook.SheetNames.map((sheetName) => {
|
|
73
|
-
const rows = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName], { header: 1 })
|
|
74
|
-
return [`# ${sheetName}`, ...rows.map((row) => row.join(' | '))].join('\n')
|
|
75
|
-
}).join('\n\n')
|
|
76
|
-
}
|
|
77
|
-
return fs.readFileSync(filePath, 'utf8')
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function _normalizeText(content = '') {
|
|
81
|
-
return String(content || '').replace(/\s+/g, ' ').trim()
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function _buildSummary(content = '', maxLen = 320) {
|
|
85
|
-
const normalized = _normalizeText(content)
|
|
86
|
-
if (!normalized) return ''
|
|
87
|
-
if (normalized.length <= maxLen) return normalized
|
|
88
|
-
return `${normalized.slice(0, maxLen)}...`
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function _extractSummary(content = '', maxLen = 320, absolutePath = '') {
|
|
92
|
-
const fallbackSummary = _buildSummary(content, maxLen)
|
|
93
|
-
if (!fallbackSummary) return ''
|
|
94
|
-
|
|
95
|
-
if (!this?.Tools?.requestAI) {
|
|
96
|
-
return fallbackSummary
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const systemDescription = '你是文档摘要助手,只输出简洁摘要正文,不要解释。'
|
|
100
|
-
const prompt = `请为下面文档提取摘要:\n\n文档路径:${absolutePath || '未知'}\n文档内容:\n${content}\n\n要求:\n1. 输出中文摘要,保留关键事实与结论。\n2. 摘要长度不超过${maxLen}个字符。\n3. 不要输出标题、前后缀或解释,只输出摘要正文。`
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
const aiSummary = await this.Tools.requestAI(systemDescription, prompt, 0.2)
|
|
104
|
-
if (typeof aiSummary !== 'string') {
|
|
105
|
-
return fallbackSummary
|
|
106
|
-
}
|
|
107
|
-
const normalizedSummary = _normalizeText(aiSummary)
|
|
108
|
-
if (!normalizedSummary) {
|
|
109
|
-
return fallbackSummary
|
|
110
|
-
}
|
|
111
|
-
return normalizedSummary.length > maxLen
|
|
112
|
-
? `${normalizedSummary.slice(0, maxLen)}...`
|
|
113
|
-
: normalizedSummary
|
|
114
|
-
} catch {
|
|
115
|
-
return fallbackSummary
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function _getEmptyKnowledgeBase(kbRootPath) {
|
|
120
|
-
const now = new Date().toISOString()
|
|
121
|
-
return {
|
|
122
|
-
version: 2,
|
|
123
|
-
name: 'deepfish-rag',
|
|
124
|
-
kbRootPath,
|
|
125
|
-
createdAt: now,
|
|
126
|
-
updatedAt: now,
|
|
127
|
-
sourceHistory: [],
|
|
128
|
-
documents: [],
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function _loadKnowledgeBase(kbRootPath) {
|
|
133
|
-
const indexPath = _getKbIndexPath(kbRootPath)
|
|
134
|
-
fs.ensureDirSync(kbRootPath)
|
|
135
|
-
if (!fs.existsSync(indexPath)) {
|
|
136
|
-
const emptyKb = _getEmptyKnowledgeBase(kbRootPath)
|
|
137
|
-
fs.writeFileSync(indexPath, JSON.stringify(emptyKb, null, 2), 'utf8')
|
|
138
|
-
return emptyKb
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const content = fs.readFileSync(indexPath, 'utf8')
|
|
142
|
-
const parsed = JSON.parse(content)
|
|
143
|
-
const base = {
|
|
144
|
-
..._getEmptyKnowledgeBase(kbRootPath),
|
|
145
|
-
...parsed,
|
|
146
|
-
kbRootPath,
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// 向后兼容旧结构:若旧数据里有content,则在加载时迁移为summary。
|
|
150
|
-
base.documents = (base.documents || []).map((doc) => {
|
|
151
|
-
const absolutePath = path.resolve(doc.absolutePath || doc.sourcePath || '')
|
|
152
|
-
return {
|
|
153
|
-
id: doc.id || _sha256(absolutePath).slice(0, 16),
|
|
154
|
-
absolutePath,
|
|
155
|
-
sourceHash: doc.sourceHash || '',
|
|
156
|
-
size: doc.size || 0,
|
|
157
|
-
summary: doc.summary || _buildSummary(doc.content || ''),
|
|
158
|
-
createdAt: doc.createdAt || base.createdAt,
|
|
159
|
-
updatedAt: doc.updatedAt || base.updatedAt,
|
|
160
|
-
}
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
return base
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function _saveKnowledgeBase(kbRootPath, knowledgeBase) {
|
|
167
|
-
const indexPath = _getKbIndexPath(kbRootPath)
|
|
168
|
-
knowledgeBase.updatedAt = new Date().toISOString()
|
|
169
|
-
fs.writeFileSync(indexPath, JSON.stringify(knowledgeBase, null, 2), 'utf8')
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async function _upsertKnowledgeBase(sourcePath = '', knowledgeBasePath = '', reset = false) {
|
|
173
|
-
const inputSourcePath = sourcePath || (await aiInquirer.askInput('请输入源文件目录或文件路径', '', {}))
|
|
174
|
-
if (!inputSourcePath) {
|
|
175
|
-
return fail('未提供源文件目录或文件路径')
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const resolvedSourcePath = path.resolve(process.cwd(), inputSourcePath)
|
|
179
|
-
if (!fs.existsSync(resolvedSourcePath)) {
|
|
180
|
-
return fail(`Source path does not exist: ${resolvedSourcePath}`, {
|
|
181
|
-
sourcePath: resolvedSourcePath,
|
|
182
|
-
})
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const kbRootPath = _getKbRootPath(knowledgeBasePath)
|
|
186
|
-
if (reset && fs.existsSync(kbRootPath)) {
|
|
187
|
-
fs.removeSync(kbRootPath)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const knowledgeBase = _loadKnowledgeBase(kbRootPath)
|
|
191
|
-
const sourceFiles = _collectSourceFiles(resolvedSourcePath)
|
|
192
|
-
const supportedFiles = sourceFiles.filter((filePath) => _isSupportedFile(filePath))
|
|
193
|
-
|
|
194
|
-
let addedCount = 0
|
|
195
|
-
let updatedCount = 0
|
|
196
|
-
let skippedCount = 0
|
|
197
|
-
|
|
198
|
-
for (const filePath of supportedFiles) {
|
|
199
|
-
try {
|
|
200
|
-
const absolutePath = path.resolve(filePath)
|
|
201
|
-
const content = await _readDocumentContent(absolutePath)
|
|
202
|
-
if (!content || !content.trim()) {
|
|
203
|
-
skippedCount += 1
|
|
204
|
-
continue
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const sourceHash = _sha256(content)
|
|
208
|
-
const existingIndex = knowledgeBase.documents.findIndex((item) => item.absolutePath === absolutePath)
|
|
209
|
-
|
|
210
|
-
if (existingIndex >= 0) {
|
|
211
|
-
if (knowledgeBase.documents[existingIndex].sourceHash === sourceHash) {
|
|
212
|
-
skippedCount += 1
|
|
213
|
-
continue
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const summary = await _extractSummary.call(this, content, 320, absolutePath)
|
|
217
|
-
|
|
218
|
-
knowledgeBase.documents[existingIndex] = {
|
|
219
|
-
...knowledgeBase.documents[existingIndex],
|
|
220
|
-
sourceHash,
|
|
221
|
-
size: Buffer.byteLength(content, 'utf8'),
|
|
222
|
-
summary,
|
|
223
|
-
updatedAt: new Date().toISOString(),
|
|
224
|
-
}
|
|
225
|
-
_saveKnowledgeBase(kbRootPath, knowledgeBase)
|
|
226
|
-
updatedCount += 1
|
|
227
|
-
continue
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const summary = await _extractSummary.call(this, content, 320, absolutePath)
|
|
231
|
-
|
|
232
|
-
knowledgeBase.documents.push({
|
|
233
|
-
id: _sha256(absolutePath).slice(0, 16),
|
|
234
|
-
absolutePath,
|
|
235
|
-
sourceHash,
|
|
236
|
-
size: Buffer.byteLength(content, 'utf8'),
|
|
237
|
-
summary,
|
|
238
|
-
createdAt: new Date().toISOString(),
|
|
239
|
-
updatedAt: new Date().toISOString(),
|
|
240
|
-
})
|
|
241
|
-
_saveKnowledgeBase(kbRootPath, knowledgeBase)
|
|
242
|
-
addedCount += 1
|
|
243
|
-
} catch {
|
|
244
|
-
skippedCount += 1
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
knowledgeBase.sourceHistory.push({
|
|
249
|
-
sourcePath: resolvedSourcePath,
|
|
250
|
-
loadedAt: new Date().toISOString(),
|
|
251
|
-
scannedFiles: sourceFiles.length,
|
|
252
|
-
supportedFiles: supportedFiles.length,
|
|
253
|
-
addedCount,
|
|
254
|
-
updatedCount,
|
|
255
|
-
skippedCount,
|
|
256
|
-
mode: reset ? 'create' : 'append',
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
_saveKnowledgeBase(kbRootPath, knowledgeBase)
|
|
260
|
-
|
|
261
|
-
return ok({
|
|
262
|
-
knowledgeBasePath: kbRootPath,
|
|
263
|
-
sourcePath: resolvedSourcePath,
|
|
264
|
-
scannedFiles: sourceFiles.length,
|
|
265
|
-
supportedFiles: supportedFiles.length,
|
|
266
|
-
addedCount,
|
|
267
|
-
updatedCount,
|
|
268
|
-
skippedCount,
|
|
269
|
-
totalDocuments: knowledgeBase.documents.length,
|
|
270
|
-
})
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
async function createKnowledgeBase(sourcePath = '', knowledgeBasePath = '') {
|
|
274
|
-
try {
|
|
275
|
-
return await _upsertKnowledgeBase.call(this, sourcePath, knowledgeBasePath, true)
|
|
276
|
-
} catch (error) {
|
|
277
|
-
return fail(error, { sourcePath, knowledgeBasePath })
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
async function appendKnowledgeBase(sourcePath = '', knowledgeBasePath = '') {
|
|
282
|
-
try {
|
|
283
|
-
return await _upsertKnowledgeBase.call(this, sourcePath, knowledgeBasePath, false)
|
|
284
|
-
} catch (error) {
|
|
285
|
-
return fail(error, { sourcePath, knowledgeBasePath })
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function _matchSummary(knowledgeBase, normalizedKeyword = '', maxResult = 10) {
|
|
290
|
-
const filtered = knowledgeBase.documents.filter((item) => {
|
|
291
|
-
if (!normalizedKeyword) return true
|
|
292
|
-
const haystack = `${item.absolutePath || ''} ${item.summary || ''}`.toLowerCase()
|
|
293
|
-
return haystack.includes(normalizedKeyword)
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
return filtered.slice(0, maxResult).map((item) => ({
|
|
297
|
-
id: item.id,
|
|
298
|
-
absolutePath: item.absolutePath,
|
|
299
|
-
size: item.size,
|
|
300
|
-
updatedAt: item.updatedAt,
|
|
301
|
-
summary: item.summary,
|
|
302
|
-
}))
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async function _queryBySubAgent(keyword, summaryMatches, includeFullDocument = false) {
|
|
306
|
-
if (!this?.Tools?.createSubAgent) {
|
|
307
|
-
return {
|
|
308
|
-
success: false,
|
|
309
|
-
skipped: true,
|
|
310
|
-
reason: 'createSubAgent tool is unavailable in current context',
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const prompt = `你是知识库检索子agent,请完成文档检索。
|
|
315
|
-
|
|
316
|
-
用户关键词:${keyword}
|
|
317
|
-
是否允许读取全文:${includeFullDocument ? '是' : '否(仅在必要时)'}
|
|
318
|
-
候选文档(按摘要初筛后)如下:
|
|
319
|
-
${JSON.stringify(summaryMatches, null, 2)}
|
|
320
|
-
|
|
321
|
-
执行要求:
|
|
322
|
-
1. 先使用候选文档的summary进行关键词匹配和排序。
|
|
323
|
-
2. 当summary不足以回答问题时,再读取对应absolutePath的完整文档内容进行补充。
|
|
324
|
-
3. 输出结构化结果,必须包含:
|
|
325
|
-
- 命中文档列表(id、absolutePath、匹配原因)
|
|
326
|
-
- 最终结论
|
|
327
|
-
- 若读取全文,列出已读取的absolutePath。
|
|
328
|
-
`
|
|
329
|
-
|
|
330
|
-
return this.Tools.createSubAgent(prompt)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
async function queryKnowledgeBase(keyword = '', knowledgeBasePath = '', limit = 10, includeFullDocument = false) {
|
|
334
|
-
try {
|
|
335
|
-
const normalizedKeyword = String(keyword || '').trim().toLowerCase()
|
|
336
|
-
if (!normalizedKeyword) {
|
|
337
|
-
return fail('keyword is required')
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const kbRootPath = _getKbRootPath(knowledgeBasePath)
|
|
341
|
-
const knowledgeBase = _loadKnowledgeBase(kbRootPath)
|
|
342
|
-
const maxResult = Number(limit) > 0 ? Number(limit) : 10
|
|
343
|
-
const summaryMatches = _matchSummary(knowledgeBase, normalizedKeyword, maxResult)
|
|
344
|
-
|
|
345
|
-
let subAgentResult = null
|
|
346
|
-
if (summaryMatches.length > 0) {
|
|
347
|
-
subAgentResult = await _queryBySubAgent.call(this, keyword, summaryMatches, includeFullDocument)
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return ok({
|
|
351
|
-
knowledgeBasePath: kbRootPath,
|
|
352
|
-
keyword,
|
|
353
|
-
totalDocuments: knowledgeBase.documents.length,
|
|
354
|
-
matchedDocuments: summaryMatches.length,
|
|
355
|
-
items: summaryMatches,
|
|
356
|
-
subAgentResult,
|
|
357
|
-
})
|
|
358
|
-
} catch (error) {
|
|
359
|
-
return fail(error, { keyword, knowledgeBasePath, limit, includeFullDocument })
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function deleteKnowledgeBase(knowledgeBasePath = '') {
|
|
364
|
-
try {
|
|
365
|
-
const kbRootPath = _getKbRootPath(knowledgeBasePath)
|
|
366
|
-
if (!fs.existsSync(kbRootPath)) {
|
|
367
|
-
return ok({
|
|
368
|
-
knowledgeBasePath: kbRootPath,
|
|
369
|
-
deleted: false,
|
|
370
|
-
message: 'knowledge base path not found',
|
|
371
|
-
})
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
fs.removeSync(kbRootPath)
|
|
375
|
-
return ok({
|
|
376
|
-
knowledgeBasePath: kbRootPath,
|
|
377
|
-
deleted: true,
|
|
378
|
-
})
|
|
379
|
-
} catch (error) {
|
|
380
|
-
return fail(error, { knowledgeBasePath })
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const descriptions = [
|
|
385
|
-
{
|
|
386
|
-
type: 'function',
|
|
387
|
-
function: {
|
|
388
|
-
name: 'createKnowledgeBase',
|
|
389
|
-
description: '创建知识库(先删除旧库再重建),存储文档绝对路径和约300字摘要。',
|
|
390
|
-
parameters: {
|
|
391
|
-
type: 'object',
|
|
392
|
-
properties: {
|
|
393
|
-
sourcePath: { type: 'string', description: '源文件目录或文件路径。为空时会交互输入。' },
|
|
394
|
-
},
|
|
395
|
-
required: [],
|
|
396
|
-
},
|
|
397
|
-
},
|
|
398
|
-
},
|
|
399
|
-
{
|
|
400
|
-
type: 'function',
|
|
401
|
-
function: {
|
|
402
|
-
name: 'appendKnowledgeBase',
|
|
403
|
-
description: '续加知识库(增量导入),存储文档绝对路径和约300字摘要。',
|
|
404
|
-
parameters: {
|
|
405
|
-
type: 'object',
|
|
406
|
-
properties: {
|
|
407
|
-
sourcePath: { type: 'string', description: '源文件目录或文件路径。为空时会交互输入。' },
|
|
408
|
-
},
|
|
409
|
-
required: [],
|
|
410
|
-
},
|
|
411
|
-
},
|
|
412
|
-
},
|
|
413
|
-
{
|
|
414
|
-
type: 'function',
|
|
415
|
-
function: {
|
|
416
|
-
name: 'queryKnowledgeBase',
|
|
417
|
-
description: '查询知识库:先按摘要匹配关键词,再由子agent在必要时读取命中文档的完整内容。',
|
|
418
|
-
parameters: {
|
|
419
|
-
type: 'object',
|
|
420
|
-
properties: {
|
|
421
|
-
keyword: { type: 'string', description: '检索关键词。' },
|
|
422
|
-
limit: { type: 'number', description: '返回数量上限,默认 10。' },
|
|
423
|
-
includeFullDocument: { type: 'boolean', description: '是否允许子agent读取全文,默认 false。' },
|
|
424
|
-
},
|
|
425
|
-
required: ['keyword'],
|
|
426
|
-
},
|
|
427
|
-
},
|
|
428
|
-
},
|
|
429
|
-
{
|
|
430
|
-
type: 'function',
|
|
431
|
-
function: {
|
|
432
|
-
name: 'deleteKnowledgeBase',
|
|
433
|
-
description: '删除知识库目录(默认删除命令执行目录下的 .deepfish-rag)。',
|
|
434
|
-
parameters: {
|
|
435
|
-
type: 'object',
|
|
436
|
-
properties: {},
|
|
437
|
-
required: [],
|
|
438
|
-
},
|
|
439
|
-
},
|
|
440
|
-
},
|
|
441
|
-
]
|
|
442
|
-
|
|
443
|
-
const functions = {
|
|
444
|
-
createKnowledgeBase,
|
|
445
|
-
appendKnowledgeBase,
|
|
446
|
-
queryKnowledgeBase,
|
|
447
|
-
deleteKnowledgeBase,
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const EmbeddingTool = {
|
|
451
|
-
name: 'EmbeddingTool',
|
|
452
|
-
description: '提供本地知识库创建、续加、查询、删除能力(索引仅存摘要和绝对路径)',
|
|
453
|
-
platform: 'all',
|
|
454
|
-
descriptions,
|
|
455
|
-
functions,
|
|
456
|
-
isSystem: true
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
module.exports = EmbeddingTool
|