deepfish-ai 1.0.26 → 1.0.28
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/index.js +1 -0
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/AliBailian.js +997 -0
- package/src/AgentRobot/BaseAgentRobot/tools/GenerateTools.js +100 -5
- package/src/AgentRobot/BaseAgentRobot/tools/InquirerTools.js +8 -0
- package/src/AgentRobot/BaseAgentRobot/tools/SystemTools.js +8 -8
- package/src/AgentRobot/BaseAgentRobot/utils/AIRequest.js +59 -10
- 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/copilot.js +117 -0
- package/src/AgentRobot/BaseAgentRobot/utils/normal.js +288 -0
- package/src/cli/ConfigManager.js +25 -1
- package/src/cli/DefaultConfig.js +22 -6
- package/src/cli/SkillConfigManager.js +3 -2
- package/src/cli/ai-auth.js +231 -0
- package/src/cli/ai-config.js +21 -3
- package/src/cli/index.js +4 -2
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/embedding.js +0 -459
|
@@ -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
|
|
|
@@ -236,7 +269,7 @@ async function getGenerateClawSkillRules(goal) {
|
|
|
236
269
|
3. 文档文件:目录中需新增2个说明文档:
|
|
237
270
|
- README_CN.md(中文说明文档)
|
|
238
271
|
- README.md(英文说明文档)
|
|
239
|
-
4. 辅助文件:如果Skill涉及复杂逻辑,可在目录中创建辅助脚本文件(如 .js、.sh、.py 等),并在 SKILL.md
|
|
272
|
+
4. 辅助文件:如果Skill涉及复杂逻辑,可在目录中创建辅助脚本文件(如 .js、.sh、.py 等),并在 SKILL.md 中说明其用途和调用方式;如果是js文件,可以在函数中通过this.Tools调用内置函数,如直接使用this.Tools.createSubAgent(workGoal)。
|
|
240
273
|
|
|
241
274
|
#### 标准目录结构
|
|
242
275
|
\`\`\`
|
|
@@ -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
|
|
@@ -199,6 +199,14 @@ const descriptions = [
|
|
|
199
199
|
type: 'array',
|
|
200
200
|
description:
|
|
201
201
|
'选项数组,适用于list/checkbox/rawlist/expand类型',
|
|
202
|
+
items: {
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: {
|
|
205
|
+
name: { type: 'string' },
|
|
206
|
+
value: { type: 'string' },
|
|
207
|
+
short: { type: 'string' },
|
|
208
|
+
},
|
|
209
|
+
},
|
|
202
210
|
},
|
|
203
211
|
validate: {
|
|
204
212
|
type: 'string',
|
|
@@ -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)
|
|
@@ -1,9 +1,49 @@
|
|
|
1
1
|
const { OpenAI } = require('openai')
|
|
2
|
+
const {
|
|
3
|
+
refreshGithubModelsTokenIfNeeded,
|
|
4
|
+
buildDefaultHeaders,
|
|
5
|
+
} = require('./copilot.js')
|
|
6
|
+
|
|
7
|
+
function normalizeAiRequestConfig(aiConfig = {}) {
|
|
8
|
+
const max_tokens = (!aiConfig.maxTokens || aiConfig.maxTokens === -1)
|
|
9
|
+
? undefined
|
|
10
|
+
: aiConfig.maxTokens * 1024 // 兼容旧配置,按 1KB ~= 1024 tokens 估算
|
|
11
|
+
|
|
12
|
+
const requestConfig = {
|
|
13
|
+
model: aiConfig.model,
|
|
14
|
+
temperature: aiConfig.temperature,
|
|
15
|
+
stream: aiConfig.stream,
|
|
16
|
+
max_tokens,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 仅 DeepSeek 兼容端注入扩展字段,避免其它网关因未知参数报错
|
|
20
|
+
if (aiConfig.type === 'deepseek') {
|
|
21
|
+
requestConfig.extra_body = { reasoning_split: true }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return requestConfig
|
|
25
|
+
}
|
|
2
26
|
|
|
3
27
|
function creatClient(aiConfig) {
|
|
28
|
+
if (aiConfig.type === 'github-models') {
|
|
29
|
+
if (!aiConfig.apiKey || !String(aiConfig.apiKey).trim()) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'GitHub Models requires apiKey. Please set a GitHub token with models:read permission in your current AI config.',
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (aiConfig.type === 'copilot') {
|
|
36
|
+
if (!aiConfig.apiKey || !String(aiConfig.apiKey).trim()) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
'GitHub Copilot requires apiKey. Please set a GitHub Copilot access token in your current AI config.',
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
4
43
|
return new OpenAI({
|
|
5
44
|
baseURL: aiConfig.baseUrl,
|
|
6
45
|
apiKey: aiConfig.apiKey || '',
|
|
46
|
+
defaultHeaders: buildDefaultHeaders(aiConfig),
|
|
7
47
|
})
|
|
8
48
|
}
|
|
9
49
|
|
|
@@ -28,13 +68,16 @@ async function think(
|
|
|
28
68
|
streamEnd,
|
|
29
69
|
) {
|
|
30
70
|
try {
|
|
71
|
+
await refreshGithubModelsTokenIfNeeded(aiConfig)
|
|
72
|
+
const requestClient = creatClient(aiConfig)
|
|
73
|
+
const requestConfig = normalizeAiRequestConfig(aiConfig)
|
|
31
74
|
const opt = {
|
|
32
75
|
messages: messages,
|
|
33
|
-
|
|
34
|
-
...aiConfig,
|
|
76
|
+
...requestConfig,
|
|
35
77
|
}
|
|
36
78
|
await thinkBefore()
|
|
37
|
-
const
|
|
79
|
+
const extraHeaders = aiConfig.type === 'copilot' ? { 'x-initiator': 'user' } : undefined
|
|
80
|
+
const response = await requestClient.chat.completions.create(opt, extraHeaders ? { headers: extraHeaders } : undefined)
|
|
38
81
|
if (aiConfig.stream) {
|
|
39
82
|
const messageRes = await _streamToNonStream(
|
|
40
83
|
response,
|
|
@@ -73,14 +116,20 @@ async function thinkByTool(
|
|
|
73
116
|
streamEnd,
|
|
74
117
|
) {
|
|
75
118
|
try {
|
|
119
|
+
await refreshGithubModelsTokenIfNeeded(aiConfig)
|
|
120
|
+
const requestClient = creatClient(aiConfig)
|
|
121
|
+
const requestConfig = normalizeAiRequestConfig(aiConfig)
|
|
76
122
|
await thinkBefore()
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
123
|
+
const extraHeaders = aiConfig.type === 'copilot' ? { 'x-initiator': 'agent' } : undefined
|
|
124
|
+
const response = await requestClient.chat.completions.create(
|
|
125
|
+
{
|
|
126
|
+
messages: messages,
|
|
127
|
+
tools: functionDescriptions,
|
|
128
|
+
tool_choice: 'auto',
|
|
129
|
+
...requestConfig,
|
|
130
|
+
},
|
|
131
|
+
extraHeaders ? { headers: extraHeaders } : undefined,
|
|
132
|
+
)
|
|
84
133
|
if (aiConfig.stream) {
|
|
85
134
|
const messageRes = await _streamToNonStream(
|
|
86
135
|
response,
|
|
@@ -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()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const axios = require('axios')
|
|
2
|
+
const { GlobalVariable } = require('../../../cli/GlobalVariable.js')
|
|
3
|
+
|
|
4
|
+
function isExpiring(isoTime, thresholdSec = 120) {
|
|
5
|
+
if (!isoTime) {
|
|
6
|
+
return false
|
|
7
|
+
}
|
|
8
|
+
const target = new Date(isoTime).getTime()
|
|
9
|
+
if (!Number.isFinite(target)) {
|
|
10
|
+
return false
|
|
11
|
+
}
|
|
12
|
+
return target - Date.now() <= thresholdSec * 1000
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function secondsToISO(seconds) {
|
|
16
|
+
if (!seconds || Number(seconds) <= 0) {
|
|
17
|
+
return ''
|
|
18
|
+
}
|
|
19
|
+
return new Date(Date.now() + Number(seconds) * 1000).toISOString()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function refreshGithubModelsTokenIfNeeded(aiConfig = {}) {
|
|
23
|
+
if (aiConfig.type !== 'github-models' && aiConfig.type !== 'copilot') {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
const githubAuth = aiConfig.githubAuth || {}
|
|
27
|
+
if (!isExpiring(githubAuth.accessTokenExpiresAt)) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
if (!githubAuth.refreshToken) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
'GitHub access token expired and no refresh token is available. Please run "ai auth github-login" again.',
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
if (!githubAuth.clientId || !githubAuth.clientSecret) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
'GitHub token refresh requires clientId and clientSecret. Please run "ai auth github-login" and provide both values.',
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
if (isExpiring(githubAuth.refreshTokenExpiresAt, 0)) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'GitHub refresh token expired. Please run "ai auth github-login" again.',
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const form = new URLSearchParams()
|
|
47
|
+
form.set('client_id', githubAuth.clientId)
|
|
48
|
+
form.set('client_secret', githubAuth.clientSecret)
|
|
49
|
+
form.set('grant_type', 'refresh_token')
|
|
50
|
+
form.set('refresh_token', githubAuth.refreshToken)
|
|
51
|
+
|
|
52
|
+
const response = await axios.post(
|
|
53
|
+
'https://github.com/login/oauth/access_token',
|
|
54
|
+
form.toString(),
|
|
55
|
+
{
|
|
56
|
+
headers: {
|
|
57
|
+
Accept: 'application/json',
|
|
58
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
59
|
+
},
|
|
60
|
+
timeout: 20000,
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const tokenData = response.data || {}
|
|
65
|
+
if (!tokenData.access_token) {
|
|
66
|
+
const errMessage = tokenData.error_description || tokenData.error || 'Unknown token refresh error'
|
|
67
|
+
throw new Error(`GitHub token refresh failed: ${errMessage}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const newAuth = {
|
|
71
|
+
...githubAuth,
|
|
72
|
+
tokenType: tokenData.token_type || githubAuth.tokenType || 'bearer',
|
|
73
|
+
accessTokenExpiresAt: secondsToISO(tokenData.expires_in),
|
|
74
|
+
refreshToken: tokenData.refresh_token || githubAuth.refreshToken,
|
|
75
|
+
refreshTokenExpiresAt: tokenData.refresh_token_expires_in
|
|
76
|
+
? secondsToISO(tokenData.refresh_token_expires_in)
|
|
77
|
+
: githubAuth.refreshTokenExpiresAt,
|
|
78
|
+
lastUpdatedAt: new Date().toISOString(),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
aiConfig.apiKey = tokenData.access_token
|
|
82
|
+
aiConfig.githubAuth = newAuth
|
|
83
|
+
|
|
84
|
+
const configManager = GlobalVariable.configManager
|
|
85
|
+
if (configManager && aiConfig.name && configManager.updateAiConfigByName) {
|
|
86
|
+
configManager.updateAiConfigByName(aiConfig.name, (current) => {
|
|
87
|
+
return {
|
|
88
|
+
...current,
|
|
89
|
+
apiKey: tokenData.access_token,
|
|
90
|
+
githubAuth: {
|
|
91
|
+
...(current.githubAuth || {}),
|
|
92
|
+
...newAuth,
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildDefaultHeaders(aiConfig = {}) {
|
|
100
|
+
if (aiConfig.type === 'github-models') {
|
|
101
|
+
return {
|
|
102
|
+
Accept: 'application/vnd.github+json',
|
|
103
|
+
'X-GitHub-Api-Version': '2026-03-10',
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (aiConfig.type === 'copilot') {
|
|
107
|
+
return {
|
|
108
|
+
'Openai-Intent': 'conversation-edits',
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return undefined
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
refreshGithubModelsTokenIfNeeded,
|
|
116
|
+
buildDefaultHeaders,
|
|
117
|
+
}
|