deepfish-ai 1.0.27 → 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/BaseAgentRobot/index.js +1 -0
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/AliBailian.js +997 -0
- package/src/AgentRobot/BaseAgentRobot/tools/GenerateTools.js +1 -1
- package/src/AgentRobot/BaseAgentRobot/tools/InquirerTools.js +8 -0
- package/src/AgentRobot/BaseAgentRobot/utils/AIRequest.js +59 -10
- package/src/AgentRobot/BaseAgentRobot/utils/copilot.js +117 -0
- package/src/cli/ConfigManager.js +25 -1
- package/src/cli/DefaultConfig.js +22 -6
- package/src/cli/ai-auth.js +231 -0
- package/src/cli/ai-config.js +21 -3
- package/src/cli/index.js +4 -2
|
@@ -269,7 +269,7 @@ async function getGenerateClawSkillRules(goal) {
|
|
|
269
269
|
3. 文档文件:目录中需新增2个说明文档:
|
|
270
270
|
- README_CN.md(中文说明文档)
|
|
271
271
|
- README.md(英文说明文档)
|
|
272
|
-
4. 辅助文件:如果Skill涉及复杂逻辑,可在目录中创建辅助脚本文件(如 .js、.sh、.py 等),并在 SKILL.md
|
|
272
|
+
4. 辅助文件:如果Skill涉及复杂逻辑,可在目录中创建辅助脚本文件(如 .js、.sh、.py 等),并在 SKILL.md 中说明其用途和调用方式;如果是js文件,可以在函数中通过this.Tools调用内置函数,如直接使用this.Tools.createSubAgent(workGoal)。
|
|
273
273
|
|
|
274
274
|
#### 标准目录结构
|
|
275
275
|
\`\`\`
|
|
@@ -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,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,
|
|
@@ -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
|
+
}
|
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
|
|
|
@@ -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
|
+
}
|
package/src/cli/ai-config.js
CHANGED
|
@@ -11,6 +11,8 @@ const { program } = require('commander')
|
|
|
11
11
|
const { aiCliConfig } = require('./DefaultConfig.js')
|
|
12
12
|
const ConfigManager = require('./ConfigManager.js')
|
|
13
13
|
const aiInquirer = require('../AgentRobot/BaseAgentRobot/utils/aiInquirer.js')
|
|
14
|
+
const aiConsole = require('../AgentRobot/BaseAgentRobot/utils/aiConsole.js')
|
|
15
|
+
const { githubDeviceLogin } = require('./ai-auth.js')
|
|
14
16
|
|
|
15
17
|
const configManager = new ConfigManager()
|
|
16
18
|
const configCommand = program
|
|
@@ -87,6 +89,7 @@ configCommand
|
|
|
87
89
|
type: 'input',
|
|
88
90
|
name: 'baseUrl',
|
|
89
91
|
message: 'Enter API base URL:',
|
|
92
|
+
when: (answers) => answers.Type !== 'Copilot',
|
|
90
93
|
default: (answers) => {
|
|
91
94
|
return aiCliConfig[answers.Type].baseUrl
|
|
92
95
|
},
|
|
@@ -122,6 +125,7 @@ configCommand
|
|
|
122
125
|
type: 'input',
|
|
123
126
|
name: 'apiKey',
|
|
124
127
|
message: 'Enter API key:',
|
|
128
|
+
when: (answers) => answers.Type !== 'Copilot',
|
|
125
129
|
default: (answers) => {
|
|
126
130
|
return aiCliConfig[answers.Type].apiKey
|
|
127
131
|
},
|
|
@@ -143,7 +147,6 @@ configCommand
|
|
|
143
147
|
default: (answers) => {
|
|
144
148
|
return aiCliConfig[answers.Type].maxTokens
|
|
145
149
|
},
|
|
146
|
-
validate: (value) => value > 0 || 'Max tokens must be greater than 0',
|
|
147
150
|
},
|
|
148
151
|
{
|
|
149
152
|
type: 'number',
|
|
@@ -167,14 +170,29 @@ configCommand
|
|
|
167
170
|
const aiConfig = {
|
|
168
171
|
name: answers.name,
|
|
169
172
|
type: aiCliConfig[answers.Type].type,
|
|
170
|
-
baseUrl: answers.baseUrl,
|
|
173
|
+
baseUrl: answers.baseUrl || aiCliConfig[answers.Type].baseUrl,
|
|
171
174
|
model: answers.model,
|
|
172
|
-
apiKey: answers.apiKey,
|
|
175
|
+
apiKey: answers.apiKey || aiCliConfig[answers.Type].apiKey,
|
|
173
176
|
temperature: answers.temperature,
|
|
174
177
|
maxTokens: answers.maxTokens,
|
|
175
178
|
maxContextLength: answers.maxContextLength,
|
|
176
179
|
stream: answers.stream,
|
|
177
180
|
}
|
|
181
|
+
|
|
182
|
+
if (answers.Type === 'Copilot') {
|
|
183
|
+
const loginRes = await githubDeviceLogin({
|
|
184
|
+
configManager,
|
|
185
|
+
targetName: answers.name,
|
|
186
|
+
saveToConfig: false,
|
|
187
|
+
})
|
|
188
|
+
if (!loginRes) {
|
|
189
|
+
aiConsole.logError('Copilot login failed. Configuration was not added.')
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
aiConfig.apiKey = loginRes.accessToken
|
|
193
|
+
aiConfig.githubAuth = loginRes.githubAuth
|
|
194
|
+
}
|
|
195
|
+
|
|
178
196
|
return configManager.addAiConfig(aiConfig)
|
|
179
197
|
})
|
|
180
198
|
|
package/src/cli/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
const { program } = require('commander')
|
|
3
3
|
const { DeepFishAI } = require('../index.js')
|
|
4
4
|
const { GlobalVariable } = require('./GlobalVariable.js')
|
|
5
5
|
require('./ai-config.js')
|
|
6
6
|
require('./ai-skill.js')
|
|
7
7
|
require('./ai-memory.js')
|
|
8
|
+
require('./ai-auth.js')
|
|
8
9
|
const aiConsole = require('../AgentRobot/BaseAgentRobot/utils/aiConsole.js')
|
|
9
10
|
program
|
|
10
11
|
.version('1.0.0')
|
|
@@ -23,7 +24,8 @@ async function main() {
|
|
|
23
24
|
if (
|
|
24
25
|
(program.args &&
|
|
25
26
|
(program.args[0] === 'config' ||
|
|
26
|
-
program.args[0] === 'skill'
|
|
27
|
+
program.args[0] === 'skill' ||
|
|
28
|
+
program.args[0] === 'auth')) ||
|
|
27
29
|
program.args[0] === 'memory'
|
|
28
30
|
) {
|
|
29
31
|
return
|