deepfish-ai 1.0.27 → 1.0.29
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 +83 -81
- 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 +2 -2
- package/src/AgentRobot/BaseAgentRobot/tools/InquirerTools.js +8 -0
- package/src/AgentRobot/BaseAgentRobot/tools/SystemTools.js +424 -263
- 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/SkillConfigManager.js +11 -0
- package/src/cli/ai-auth.js +231 -0
- package/src/cli/ai-config.js +21 -3
- package/src/cli/index.js +3 -1
|
@@ -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
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @Description: Skill configuration manager
|
|
8
8
|
*/
|
|
9
9
|
const path = require('path')
|
|
10
|
+
const { spawnSync } = require('child_process')
|
|
10
11
|
const fs = require('fs-extra')
|
|
11
12
|
const axios = require('axios')
|
|
12
13
|
const cheerio = require('cheerio')
|
|
@@ -156,6 +157,16 @@ class SkillConfigManager {
|
|
|
156
157
|
aiConsole.logError('Invalid skill URL. Please provide a valid ClawHub URL.')
|
|
157
158
|
return
|
|
158
159
|
}
|
|
160
|
+
// 判断url是否是github的url,如果是github的url则使用git命令拉取、执行add函数、删除目录
|
|
161
|
+
if (skillUrl.endsWith('.git')) {
|
|
162
|
+
spawnSync('git', ['clone', skillUrl], {
|
|
163
|
+
stdio: 'inherit',
|
|
164
|
+
})
|
|
165
|
+
const repoName = path.basename(skillUrl, '.git')
|
|
166
|
+
await this.add(repoName)
|
|
167
|
+
fs.removeSync(path.join(process.cwd(), repoName))
|
|
168
|
+
return
|
|
169
|
+
}
|
|
159
170
|
|
|
160
171
|
let parsedUrl
|
|
161
172
|
try {
|
|
@@ -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
|
@@ -5,6 +5,7 @@ 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
|