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.
@@ -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
  }
@@ -130,6 +130,29 @@ class ConfigManager {
130
130
  return this.config.ai.find((item) => item.name === aiName)
131
131
  }
132
132
 
133
+ getAiConfig(aiName) {
134
+ return this._getAiConfig(aiName)
135
+ }
136
+
137
+ updateAiConfigByName(aiName, updater) {
138
+ const existingIndex = this.config.ai.findIndex(
139
+ (item) => item.name === aiName,
140
+ )
141
+ if (existingIndex === -1) {
142
+ aiConsole.logError(`Configuration with name "${aiName}" not found.`)
143
+ return null
144
+ }
145
+
146
+ const current = this.config.ai[existingIndex]
147
+ const next = typeof updater === 'function'
148
+ ? updater(lodash.cloneDeep(current))
149
+ : lodash.merge(lodash.cloneDeep(current), updater)
150
+
151
+ this.config.ai[existingIndex] = next
152
+ this.writeConfig(this.config)
153
+ return next
154
+ }
155
+
133
156
  // 获取Ai列表
134
157
  getAiList() {
135
158
  console.log('AI Configurations')
@@ -188,7 +211,8 @@ class ConfigManager {
188
211
  aiConsole.logInfo(`API Base URL: ${aiConfig.baseUrl}`)
189
212
  aiConsole.logInfo(`Model: ${aiConfig.model}`)
190
213
  if (aiConfig.apiKey) {
191
- aiConsole.logInfo(`API Key: ${aiConfig.apiKey}`)
214
+ const maskPrefix = String(aiConfig.apiKey).slice(0, 6)
215
+ aiConsole.logInfo(`API Key: ${maskPrefix}... (masked)`)
192
216
  }
193
217
  aiConsole.logInfo(`Temperature: ${aiConfig.temperature}`)
194
218
  aiConsole.logInfo(`Max Tokens: ${aiConfig.maxTokens}`)
@@ -15,13 +15,13 @@ const aiCliConfig = {
15
15
  DeepSeek: {
16
16
  baseUrl: 'https://api.deepseek.com',
17
17
  model: {
18
- list: ['deepseek-chat', 'deepseek-reasoner', 'other'],
18
+ list: ['deepseek-chat', 'deepseek-reasoner', 'deepseek-v4-flash', 'deepseek-v4-pro', 'other'],
19
19
  defaultValue: '',
20
20
  },
21
21
  type: 'deepseek',
22
22
  apiKey: '',
23
23
  temperature: 0.7,
24
- maxTokens: 8, // 单位KB
24
+ maxTokens: -1, // 单位KB
25
25
  maxContextLength: 64, // 单位KB
26
26
  stream: true,
27
27
  },
@@ -34,7 +34,7 @@ const aiCliConfig = {
34
34
  type: 'minimax',
35
35
  apiKey: '',
36
36
  temperature: 0.7,
37
- maxTokens: 8, // 单位KB
37
+ maxTokens: -1, // 单位KB
38
38
  maxContextLength: 64, // 单位KB
39
39
  stream: true,
40
40
  },
@@ -47,7 +47,7 @@ const aiCliConfig = {
47
47
  type: 'qwen',
48
48
  apiKey: '',
49
49
  temperature: 0.7,
50
- maxTokens: 8, // 单位KB
50
+ maxTokens: -1, // 单位KB
51
51
  maxContextLength: 64, // 单位KB
52
52
  stream: true,
53
53
  },
@@ -60,7 +60,7 @@ const aiCliConfig = {
60
60
  type: 'ollama',
61
61
  apiKey: 'ollama',
62
62
  temperature: 0.7,
63
- maxTokens: 8, // 单位KB
63
+ maxTokens: -1, // 单位KB
64
64
  maxContextLength: 64, // 单位KB
65
65
  stream: true,
66
66
  },
@@ -73,9 +73,25 @@ const aiCliConfig = {
73
73
  type: 'openai',
74
74
  apiKey: '',
75
75
  temperature: 0.7,
76
- maxTokens: 8, // 单位KB
76
+ maxTokens: -1, // 单位KB
77
77
  maxContextLength: 64, // 单位KB
78
78
  stream: true,
79
+ },
80
+ Copilot: {
81
+ baseUrl: 'https://api.githubcopilot.com',
82
+ model: {
83
+ list: [
84
+ 'gpt-4o',
85
+ 'gpt-4o-mini',
86
+ ],
87
+ defaultValue: 'gpt-4o',
88
+ },
89
+ type: 'copilot',
90
+ apiKey: '',
91
+ temperature: 0.7,
92
+ maxTokens: -1, // 单位KB
93
+ maxContextLength: 400, // 单位KB
94
+ stream: true,
79
95
  }
80
96
  }
81
97
 
@@ -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-04-17 09:38:33
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
- return fs.statSync(path.join(this.skillDir, file)).isDirectory()
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
@@ -0,0 +1,231 @@
1
+ const axios = require('axios')
2
+ const { program } = require('commander')
3
+ const aiInquirer = require('../AgentRobot/BaseAgentRobot/utils/aiInquirer.js')
4
+ const aiConsole = require('../AgentRobot/BaseAgentRobot/utils/aiConsole.js')
5
+ const ConfigManager = require('./ConfigManager.js')
6
+ const { GlobalVariable } = require('./GlobalVariable.js')
7
+
8
+ function sleep(ms) {
9
+ return new Promise((resolve) => setTimeout(resolve, ms))
10
+ }
11
+
12
+ function secondsToISO(seconds) {
13
+ if (!seconds || Number(seconds) <= 0) {
14
+ return ''
15
+ }
16
+ return new Date(Date.now() + Number(seconds) * 1000).toISOString()
17
+ }
18
+
19
+ function getConfigManager() {
20
+ return GlobalVariable.configManager || new ConfigManager()
21
+ }
22
+
23
+ async function githubDeviceLogin({
24
+ configManager,
25
+ targetName,
26
+ currentAuth = {},
27
+ saveToConfig = true,
28
+ } = {}) {
29
+ const manager = configManager || getConfigManager()
30
+ let aiConfig = null
31
+
32
+ if (saveToConfig) {
33
+ if (!targetName) {
34
+ aiConsole.logError('No target AI configuration. Please pass a config name or set current AI first.')
35
+ return null
36
+ }
37
+ aiConfig = manager.getAiConfig(targetName)
38
+ if (!aiConfig) {
39
+ aiConsole.logError(`Configuration with name "${targetName}" not found.`)
40
+ return null
41
+ }
42
+ if (aiConfig.type !== 'copilot') {
43
+ aiConsole.logError(
44
+ `Configuration "${targetName}" is type "${aiConfig.type}". GitHub login only supports "copilot" type.`,
45
+ )
46
+ return null
47
+ }
48
+ }
49
+
50
+ const authSeed = saveToConfig ? (aiConfig.githubAuth || {}) : (currentAuth || {})
51
+ const questions = [
52
+ {
53
+ type: 'input',
54
+ name: 'clientId',
55
+ message: 'Enter GitHub OAuth App Client ID:',
56
+ default: authSeed.clientId || process.env.GITHUB_OAUTH_CLIENT_ID || '',
57
+ validate: (value) => !!String(value || '').trim() || 'Client ID is required',
58
+ },
59
+ {
60
+ type: 'password',
61
+ name: 'clientSecret',
62
+ message: 'Enter GitHub OAuth App Client Secret (optional, needed for refresh in some OAuth app settings):',
63
+ mask: '*',
64
+ default: authSeed.clientSecret || process.env.GITHUB_OAUTH_CLIENT_SECRET || '',
65
+ },
66
+ {
67
+ type: 'input',
68
+ name: 'scope',
69
+ message: 'Enter OAuth scope:',
70
+ default: authSeed.scope || 'models:read',
71
+ },
72
+ ]
73
+
74
+ const answers = await aiInquirer.askAny(questions)
75
+ const clientId = String(answers.clientId || '').trim()
76
+ const clientSecret = String(answers.clientSecret || '').trim()
77
+ const scope = String(answers.scope || 'models:read').trim()
78
+
79
+ let deviceCodeResponse
80
+ try {
81
+ const form = new URLSearchParams()
82
+ form.set('client_id', clientId)
83
+ form.set('scope', scope)
84
+
85
+ const res = await axios.post(
86
+ 'https://github.com/login/device/code',
87
+ form.toString(),
88
+ {
89
+ headers: {
90
+ Accept: 'application/json',
91
+ 'Content-Type': 'application/x-www-form-urlencoded',
92
+ },
93
+ timeout: 20000,
94
+ },
95
+ )
96
+ deviceCodeResponse = res.data
97
+ } catch (error) {
98
+ const message = error?.response?.data?.error_description || error.message
99
+ aiConsole.logError(`Failed to request GitHub device code: ${message}`)
100
+ return null
101
+ }
102
+
103
+ const deviceCode = deviceCodeResponse.device_code
104
+ const userCode = deviceCodeResponse.user_code
105
+ const verificationUri = deviceCodeResponse.verification_uri || 'https://github.com/login/device'
106
+ const expiresIn = Number(deviceCodeResponse.expires_in || 900)
107
+ let intervalSec = Number(deviceCodeResponse.interval || 5)
108
+
109
+ aiConsole.logSuccess('Please complete GitHub login in browser:')
110
+ aiConsole.logInfo(`URL: ${verificationUri}`)
111
+ aiConsole.logInfo(`Code: ${userCode}`)
112
+
113
+ const deadline = Date.now() + expiresIn * 1000
114
+
115
+ while (Date.now() < deadline) {
116
+ await sleep(intervalSec * 1000)
117
+ try {
118
+ const form = new URLSearchParams()
119
+ form.set('client_id', clientId)
120
+ form.set('device_code', deviceCode)
121
+ form.set('grant_type', 'urn:ietf:params:oauth:grant-type:device_code')
122
+
123
+ const res = await axios.post(
124
+ 'https://github.com/login/oauth/access_token',
125
+ form.toString(),
126
+ {
127
+ headers: {
128
+ Accept: 'application/json',
129
+ 'Content-Type': 'application/x-www-form-urlencoded',
130
+ },
131
+ timeout: 20000,
132
+ },
133
+ )
134
+ const tokenData = res.data || {}
135
+
136
+ if (tokenData.error) {
137
+ if (tokenData.error === 'authorization_pending') {
138
+ continue
139
+ }
140
+ if (tokenData.error === 'slow_down') {
141
+ intervalSec = Number(tokenData.interval || intervalSec + 5)
142
+ continue
143
+ }
144
+ if (tokenData.error === 'expired_token') {
145
+ aiConsole.logError('Device code expired. Please run the login command again.')
146
+ return null
147
+ }
148
+ if (tokenData.error === 'access_denied') {
149
+ aiConsole.logError('GitHub authorization denied by user.')
150
+ return null
151
+ }
152
+ aiConsole.logError(tokenData.error_description || tokenData.error)
153
+ return null
154
+ }
155
+
156
+ if (!tokenData.access_token) {
157
+ aiConsole.logError('GitHub login failed: no access_token returned.')
158
+ return null
159
+ }
160
+
161
+ const githubAuthPatch = {
162
+ provider: 'github-oauth-device',
163
+ clientId,
164
+ clientSecret,
165
+ scope,
166
+ tokenType: tokenData.token_type || 'bearer',
167
+ accessTokenExpiresAt: secondsToISO(tokenData.expires_in),
168
+ refreshToken: tokenData.refresh_token || '',
169
+ refreshTokenExpiresAt: secondsToISO(tokenData.refresh_token_expires_in),
170
+ lastUpdatedAt: new Date().toISOString(),
171
+ }
172
+
173
+ if (saveToConfig) {
174
+ manager.updateAiConfigByName(targetName, (current) => {
175
+ return {
176
+ ...current,
177
+ apiKey: tokenData.access_token,
178
+ githubAuth: {
179
+ ...(current.githubAuth || {}),
180
+ ...githubAuthPatch,
181
+ },
182
+ }
183
+ })
184
+ }
185
+
186
+ aiConsole.logSuccess('GitHub login success.')
187
+ if (githubAuthPatch.accessTokenExpiresAt) {
188
+ aiConsole.logInfo(`Access token expires at: ${githubAuthPatch.accessTokenExpiresAt}`)
189
+ }
190
+ if (!githubAuthPatch.refreshToken) {
191
+ aiConsole.logInfo('No refresh token returned. Re-login may be required after access token expires.')
192
+ }
193
+
194
+ return {
195
+ accessToken: tokenData.access_token,
196
+ githubAuth: githubAuthPatch,
197
+ }
198
+ } catch (error) {
199
+ const message = error?.response?.data?.error_description || error.message
200
+ aiConsole.logError(`Polling GitHub token failed: ${message}`)
201
+ return null
202
+ }
203
+ }
204
+
205
+ aiConsole.logError('GitHub login timeout. Please run the command again.')
206
+ return null
207
+ }
208
+
209
+ const authCommand = program
210
+ .command('auth')
211
+ .description('Authenticate third-party model providers')
212
+
213
+ authCommand
214
+ .command('github-login [name]')
215
+ .description('Login with GitHub account (Device Flow) and store short-lived token into a copilot config')
216
+ .action(async (name) => {
217
+ const configManager = getConfigManager()
218
+ const targetName = name || configManager.getCurrentAi()
219
+ const res = await githubDeviceLogin({
220
+ configManager,
221
+ targetName,
222
+ saveToConfig: true,
223
+ })
224
+ if (res) {
225
+ aiConsole.logSuccess(`Token has been saved to AI config "${targetName}".`)
226
+ }
227
+ })
228
+
229
+ module.exports = {
230
+ githubDeviceLogin,
231
+ }