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
|
@@ -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
|
}
|
package/src/cli/ConfigManager.js
CHANGED
|
@@ -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
|
-
|
|
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}`)
|
package/src/cli/DefaultConfig.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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-
|
|
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
|
|
@@ -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
|
+
}
|