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.
@@ -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
- const newGoal = `
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: '提供扩展工具与Skill工具包生成规则能力,用于辅助AI构建标准化扩展项目模板',
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: roman_123 306863030@qq.com
5
- * @LastEditTime: 2026-04-24 00:07:22
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
- // 检测code最后一行代码包含return,如果不包含,则返回一个错误信息,提示agent需要使用return返回结果
86
- const codeLines = code.trim().split('\n')
87
- const lastLine = codeLines[codeLines.length - 1].trim()
88
- if (!lastLine.startsWith('return')) {
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
- extra_body: {"reasoning_split": true},
34
- ...aiConfig,
76
+ ...requestConfig,
35
77
  }
36
78
  await thinkBefore()
37
- const response = await openAiClient.chat.completions.create(opt)
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 response = await openAiClient.chat.completions.create({
78
- messages: messages,
79
- tools: functionDescriptions,
80
- tool_choice: 'auto',
81
- extra_body: {"reasoning_split": true},
82
- ...aiConfig,
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
+ }