Spaces:
Sleeping
Sleeping
| import axios from 'axios'; | |
| import tokenManager from '../auth/token_manager.js'; | |
| import config from '../config/config.js'; | |
| import { generateToolCallId } from '../utils/idGenerator.js'; | |
| import AntigravityRequester from '../AntigravityRequester.js'; | |
| import { saveBase64Image } from '../utils/imageStorage.js'; | |
| // 请求客户端:优先使用 AntigravityRequester,失败则降级到 axios | |
| let requester = null; | |
| let useAxios = false; | |
| if (config.useNativeAxios === true) { | |
| useAxios = true; | |
| } else { | |
| try { | |
| requester = new AntigravityRequester(); | |
| } catch (error) { | |
| console.warn('AntigravityRequester 初始化失败,降级使用 axios:', error.message); | |
| useAxios = true; | |
| } | |
| } | |
| // ==================== 辅助函数 ==================== | |
| function buildHeaders(token) { | |
| return { | |
| 'Host': config.api.host, | |
| 'User-Agent': config.api.userAgent, | |
| 'Authorization': `Bearer ${token.access_token}`, | |
| 'Content-Type': 'application/json', | |
| 'Accept-Encoding': 'gzip' | |
| }; | |
| } | |
| function buildAxiosConfig(url, headers, body = null) { | |
| const axiosConfig = { | |
| method: 'POST', | |
| url, | |
| headers, | |
| timeout: config.timeout, | |
| proxy: config.proxy ? (() => { | |
| const proxyUrl = new URL(config.proxy); | |
| return { protocol: proxyUrl.protocol.replace(':', ''), host: proxyUrl.hostname, port: parseInt(proxyUrl.port) }; | |
| })() : false | |
| }; | |
| if (body !== null) axiosConfig.data = body; | |
| return axiosConfig; | |
| } | |
| function buildRequesterConfig(headers, body = null) { | |
| const reqConfig = { | |
| method: 'POST', | |
| headers, | |
| timeout_ms: config.timeout, | |
| proxy: config.proxy | |
| }; | |
| if (body !== null) reqConfig.body = JSON.stringify(body); | |
| return reqConfig; | |
| } | |
| // 统一错误处理 | |
| async function handleApiError(error, token) { | |
| const status = error.response?.status || error.status || 'Unknown'; | |
| let errorBody = error.message; | |
| if (error.response?.data?.readable) { | |
| const chunks = []; | |
| for await (const chunk of error.response.data) { | |
| chunks.push(chunk); | |
| } | |
| errorBody = Buffer.concat(chunks).toString(); | |
| } else if (typeof error.response?.data === 'object') { | |
| errorBody = JSON.stringify(error.response.data, null, 2); | |
| } else if (error.response?.data) { | |
| errorBody = error.response.data; | |
| } | |
| if (status === 403) { | |
| tokenManager.disableCurrentToken(token); | |
| throw new Error(`该账号没有使用权限,已自动禁用。错误详情: ${errorBody}`); | |
| } | |
| throw new Error(`API请求失败 (${status}): ${errorBody}`); | |
| } | |
| // 转换 functionCall 为 OpenAI 格式 | |
| function convertToToolCall(functionCall) { | |
| return { | |
| id: functionCall.id || generateToolCallId(), | |
| type: 'function', | |
| function: { | |
| name: functionCall.name, | |
| arguments: JSON.stringify(functionCall.args) | |
| } | |
| }; | |
| } | |
| // 解析并发送流式响应片段(会修改 state 并触发 callback) | |
| function parseAndEmitStreamChunk(line, state, callback) { | |
| if (!line.startsWith('data: ')) return; | |
| try { | |
| const data = JSON.parse(line.slice(6)); | |
| const parts = data.response?.candidates?.[0]?.content?.parts; | |
| if (parts) { | |
| for (const part of parts) { | |
| if (part.thought === true) { | |
| // 思维链内容 | |
| if (!state.thinkingStarted) { | |
| callback({ type: 'thinking', content: '<think>\n' }); | |
| state.thinkingStarted = true; | |
| } | |
| callback({ type: 'thinking', content: part.text || '' }); | |
| } else if (part.text !== undefined) { | |
| // 普通文本内容 | |
| if (state.thinkingStarted) { | |
| callback({ type: 'thinking', content: '\n</think>\n' }); | |
| state.thinkingStarted = false; | |
| } | |
| callback({ type: 'text', content: part.text }); | |
| } else if (part.functionCall) { | |
| // 工具调用 | |
| state.toolCalls.push(convertToToolCall(part.functionCall)); | |
| } | |
| } | |
| } | |
| // 响应结束时发送工具调用 | |
| if (data.response?.candidates?.[0]?.finishReason && state.toolCalls.length > 0) { | |
| if (state.thinkingStarted) { | |
| callback({ type: 'thinking', content: '\n</think>\n' }); | |
| state.thinkingStarted = false; | |
| } | |
| callback({ type: 'tool_calls', tool_calls: state.toolCalls }); | |
| state.toolCalls = []; | |
| } | |
| } catch (e) { | |
| // 忽略 JSON 解析错误 | |
| } | |
| } | |
| // ==================== 导出函数 ==================== | |
| export async function generateAssistantResponse(requestBody, token, callback) { | |
| const headers = buildHeaders(token); | |
| const state = { thinkingStarted: false, toolCalls: [] }; | |
| let buffer = ''; // 缓冲区:处理跨 chunk 的不完整行 | |
| const processChunk = (chunk) => { | |
| buffer += chunk; | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop(); // 保留最后一行(可能不完整) | |
| lines.forEach(line => parseAndEmitStreamChunk(line, state, callback)); | |
| }; | |
| if (useAxios) { | |
| try { | |
| const axiosConfig = { ...buildAxiosConfig(config.api.url, headers, requestBody), responseType: 'stream' }; | |
| const response = await axios(axiosConfig); | |
| response.data.on('data', chunk => processChunk(chunk.toString())); | |
| await new Promise((resolve, reject) => { | |
| response.data.on('end', resolve); | |
| response.data.on('error', reject); | |
| }); | |
| } catch (error) { | |
| await handleApiError(error, token); | |
| } | |
| } else { | |
| try { | |
| const streamResponse = requester.antigravity_fetchStream(config.api.url, buildRequesterConfig(headers, requestBody)); | |
| let errorBody = ''; | |
| let statusCode = null; | |
| await new Promise((resolve, reject) => { | |
| streamResponse | |
| .onStart(({ status }) => { statusCode = status; }) | |
| .onData((chunk) => statusCode !== 200 ? errorBody += chunk : processChunk(chunk)) | |
| .onEnd(() => statusCode !== 200 ? reject({ status: statusCode, message: errorBody }) : resolve()) | |
| .onError(reject); | |
| }); | |
| } catch (error) { | |
| await handleApiError(error, token); | |
| } | |
| } | |
| } | |
| export async function getAvailableModels() { | |
| const token = await tokenManager.getToken(); | |
| if (!token) throw new Error('没有可用的token,请运行 npm run login 获取token'); | |
| const headers = buildHeaders(token); | |
| try { | |
| let data; | |
| if (useAxios) { | |
| data = (await axios(buildAxiosConfig(config.api.modelsUrl, headers, {}))).data; | |
| } else { | |
| const response = await requester.antigravity_fetch(config.api.modelsUrl, buildRequesterConfig(headers, {})); | |
| if (response.status !== 200) { | |
| const errorBody = await response.text(); | |
| throw { status: response.status, message: errorBody }; | |
| } | |
| data = await response.json(); | |
| } | |
| const modelList = Object.keys(data.models).map(id => ({ | |
| id, | |
| object: 'model', | |
| created: Math.floor(Date.now() / 1000), | |
| owned_by: 'google' | |
| })); | |
| modelList.push({ | |
| id: "claude-opus-4-5", | |
| object: 'model', | |
| created: Math.floor(Date.now() / 1000), | |
| owned_by: 'google' | |
| }) | |
| return { | |
| object: 'list', | |
| data: modelList | |
| }; | |
| } catch (error) { | |
| await handleApiError(error, token); | |
| } | |
| } | |
| export async function generateAssistantResponseNoStream(requestBody, token) { | |
| const headers = buildHeaders(token); | |
| let data; | |
| try { | |
| if (useAxios) { | |
| data = (await axios(buildAxiosConfig(config.api.noStreamUrl, headers, requestBody))).data; | |
| } else { | |
| const response = await requester.antigravity_fetch(config.api.noStreamUrl, buildRequesterConfig(headers, requestBody)); | |
| if (response.status !== 200) { | |
| const errorBody = await response.text(); | |
| throw { status: response.status, message: errorBody }; | |
| } | |
| data = await response.json(); | |
| } | |
| } catch (error) { | |
| await handleApiError(error, token); | |
| } | |
| // 解析响应内容 | |
| const parts = data.response?.candidates?.[0]?.content?.parts || []; | |
| let content = ''; | |
| let thinkingContent = ''; | |
| const toolCalls = []; | |
| const imageUrls = []; | |
| for (const part of parts) { | |
| if (part.thought === true) { | |
| thinkingContent += part.text || ''; | |
| } else if (part.text !== undefined) { | |
| content += part.text; | |
| } else if (part.functionCall) { | |
| toolCalls.push(convertToToolCall(part.functionCall)); | |
| } else if (part.inlineData) { | |
| // 保存图片到本地并获取 URL | |
| const imageUrl = saveBase64Image(part.inlineData.data, part.inlineData.mimeType); | |
| imageUrls.push(imageUrl); | |
| } | |
| } | |
| // 拼接思维链标签 | |
| if (thinkingContent) { | |
| content = `<think>\n${thinkingContent}\n</think>\n${content}`; | |
| } | |
| // 生图模型:转换为 markdown 格式 | |
| if (imageUrls.length > 0) { | |
| let markdown = content ? content + '\n\n' : ''; | |
| markdown += imageUrls.map(url => ``).join('\n\n'); | |
| return { content: markdown, toolCalls }; | |
| } | |
| return { content, toolCalls }; | |
| } | |
| export function closeRequester() { | |
| if (requester) requester.close(); | |
| } | |