diff --git a/VERSION b/VERSION index c6436a8..2a81da1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.10.2 +2.10.2.1 diff --git a/src/converters/strategies/GrokConverter.js b/src/converters/strategies/GrokConverter.js index 53dd1a9..84f6a76 100644 --- a/src/converters/strategies/GrokConverter.js +++ b/src/converters/strategies/GrokConverter.js @@ -13,9 +13,9 @@ import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; * 实现Grok协议到其他协议的转换 */ export class GrokConverter extends BaseConverter { - // 静态属性,确保所有实例共享最新的认证和基础 URL 配置 - static sharedSsoToken = null; + // 静态属性,确保所有实例共享最新的基础 URL 和 UUID 配置 static sharedRequestBaseUrl = ""; + static sharedUuid = null; constructor() { super('grok'); @@ -23,20 +23,6 @@ export class GrokConverter extends BaseConverter { this.requestStates = new Map(); } - /** - * 设置 Grok SSO token - */ - setSsoToken(token) { - if (!token) return; - - // 如果 token 包含 sso= 前缀,则去掉它 - let processedToken = token; - if (processedToken.startsWith("sso=")) { - processedToken = processedToken.substring(4); - } - GrokConverter.sharedSsoToken = processedToken; - } - /** * 设置请求的基础 URL */ @@ -47,13 +33,22 @@ export class GrokConverter extends BaseConverter { } /** - * 为 assets.grok.com 域名的资源 URL 添加 sso 参数,并转换为本地代理 URL + * 设置账号的 UUID + */ + setUuid(uuid) { + if (uuid) { + GrokConverter.sharedUuid = uuid; + } + } + + /** + * 为 assets.grok.com 域名的资源 URL 添加 uuid 参数,并转换为本地代理 URL */ _appendSsoToken(url, state = null) { - const ssoToken = state?.ssoToken || GrokConverter.sharedSsoToken; const requestBaseUrl = state?.requestBaseUrl || GrokConverter.sharedRequestBaseUrl; + const uuid = state?.uuid || GrokConverter.sharedUuid; - if (!url || !ssoToken) return url; + if (!url || !uuid) return url; // 检查是否为 assets.grok.com 域名或相对路径 const isGrokAsset = url.includes('assets.grok.com') || (!url.startsWith('http') && !url.startsWith('data:')); @@ -67,7 +62,10 @@ export class GrokConverter extends BaseConverter { } // 返回本地代理接口 URL - const proxyPath = `/api/grok/assets?url=${encodeURIComponent(originalUrl)}&sso=${encodeURIComponent(ssoToken)}`; + // 使用 uuid 以提高安全性,防止 token 泄露在链接中 + const authParam = `uuid=${encodeURIComponent(uuid)}`; + + const proxyPath = `/api/grok/assets?url=${encodeURIComponent(originalUrl)}&${authParam}`; if (requestBaseUrl) { return `${requestBaseUrl}${proxyPath}`; } @@ -78,8 +76,8 @@ export class GrokConverter extends BaseConverter { * 在文本中查找并替换所有 assets.grok.com 的资源链接为绝对代理链接 */ _processGrokAssetsInText(text, state = null) { - const ssoToken = state?.ssoToken || GrokConverter.sharedSsoToken; - if (!text || !ssoToken) return text; + const uuid = state?.uuid || GrokConverter.sharedUuid; + if (!text || !uuid) return text; // 更宽松的正则匹配 assets.grok.com 的 URL const grokUrlRegex = /https?:\/\/assets\.grok\.com\/[^\s\)\"\'\>]+/g; @@ -106,8 +104,8 @@ export class GrokConverter extends BaseConverter { has_tool_call: false, rollout_id: "", in_tool_call: false, // 是否处于 块内 - ssoToken: null, requestBaseUrl: "", + uuid: null, pending_text_buffer: "" // 用于处理流式输出中被截断的 URL }); } @@ -345,32 +343,32 @@ export class GrokConverter extends BaseConverter { /** * 渲染图片为 Markdown */ - _renderImage(url, imageId = "image") { + _renderImage(url, imageId = "image", state = null) { let finalUrl = url; if (!url.startsWith('http')) { finalUrl = `https://assets.grok.com${url.startsWith('/') ? '' : '/'}${url}`; } - finalUrl = this._appendSsoToken(finalUrl); + finalUrl = this._appendSsoToken(finalUrl, state); return `![${imageId}](${finalUrl})`; } /** * 渲染视频为 Markdown/HTML (render_video) */ - _renderVideo(videoUrl, thumbnailImageUrl = "") { + _renderVideo(videoUrl, thumbnailImageUrl = "", state = null) { let finalVideoUrl = videoUrl; if (!videoUrl.startsWith('http')) { finalVideoUrl = `https://assets.grok.com${videoUrl.startsWith('/') ? '' : '/'}${videoUrl}`; } - finalVideoUrl = this._appendSsoToken(finalVideoUrl); + finalVideoUrl = this._appendSsoToken(finalVideoUrl, state); let finalThumbUrl = thumbnailImageUrl; if (thumbnailImageUrl && !thumbnailImageUrl.startsWith('http')) { finalThumbUrl = `https://assets.grok.com${thumbnailImageUrl.startsWith('/') ? '' : '/'}${thumbnailImageUrl}`; } - finalThumbUrl = this._appendSsoToken(finalThumbUrl); + finalThumbUrl = this._appendSsoToken(finalThumbUrl, state); - const defaultThumb = this._appendSsoToken('https://assets.grok.com/favicon.ico'); + const defaultThumb = this._appendSsoToken('https://assets.grok.com/favicon.ico', state); return `\n[![video](${finalThumbUrl || defaultThumb})](${finalVideoUrl})\n[Play Video](${finalVideoUrl})\n`; } @@ -455,16 +453,12 @@ export class GrokConverter extends BaseConverter { const modelHash = grokResponse.llmInfo?.modelHash || ""; const state = this._getState(this._formatResponseId(responseId)); - if (grokResponse._ssoToken) { - let processedToken = grokResponse._ssoToken; - if (processedToken.startsWith("sso=")) { - processedToken = processedToken.substring(4); - } - state.ssoToken = processedToken; - } if (grokResponse._requestBaseUrl) { state.requestBaseUrl = grokResponse._requestBaseUrl; } + if (grokResponse._uuid) { + state.uuid = grokResponse._uuid; + } // 过滤内容并处理其中的 Grok 资源链接 content = this._filterToken(content, responseId); @@ -534,17 +528,13 @@ export class GrokConverter extends BaseConverter { const responseId = this._formatResponseId(rawResponseId); const state = this._getState(responseId); - // 从响应块中同步 token 和基础 URL - if (resp._ssoToken) { - let processedToken = resp._ssoToken; - if (processedToken.startsWith("sso=")) { - processedToken = processedToken.substring(4); - } - state.ssoToken = processedToken; - } + // 从响应块中同步 uuid 和基础 URL if (resp._requestBaseUrl) { state.requestBaseUrl = resp._requestBaseUrl; } + if (resp._uuid) { + state.uuid = resp._uuid; + } if (resp.llmInfo?.modelHash && !state.fingerprint) { state.fingerprint = resp.llmInfo.modelHash; diff --git a/src/handlers/request-handler.js b/src/handlers/request-handler.js index b9664df..dd18b2b 100644 --- a/src/handlers/request-handler.js +++ b/src/handlers/request-handler.js @@ -115,7 +115,7 @@ export function createRequestHandler(config, providerPoolManager) { // Grok assets proxy endpoint if (method === 'GET' && path === '/api/grok/assets') { - await handleGrokAssetsProxy(req, res, currentConfig); + await handleGrokAssetsProxy(req, res, currentConfig, providerPoolManager); return true; } diff --git a/src/providers/grok/grok-core.js b/src/providers/grok/grok-core.js index 4f24adb..268af45 100644 --- a/src/providers/grok/grok-core.js +++ b/src/providers/grok/grok-core.js @@ -108,10 +108,10 @@ export class GrokApiService { this.chatApi = `${this.baseUrl}/rest/app-chat/conversations/new`; this.isInitialized = false; - // 使用全局转换器实例,确保与适配器层使用的是同一个实例,从而共享 SSO token 和基础 URL 配置 + // 使用全局转换器实例,确保与适配器层使用的是同一个实例 this.converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GROK); - if (this.converter) { - this.converter.setSsoToken(this.token); + if (this.converter && this.uuid) { + this.converter.setUuid(this.uuid); } this.lastSyncAt = null; @@ -520,8 +520,8 @@ export class GrokApiService { if (resp.rolloutId) collected.rolloutId = resp.rolloutId; // 同步私有字段到最终结果 - if (resp._ssoToken) collected._ssoToken = resp._ssoToken; if (resp._requestBaseUrl) collected._requestBaseUrl = resp._requestBaseUrl; + if (resp._uuid) collected._uuid = resp._uuid; if (resp.modelResponse) collected.modelResponse = resp.modelResponse; if (resp.cardAttachment) collected.cardAttachment = resp.cardAttachment; @@ -595,9 +595,11 @@ export class GrokApiService { } async * generateContentStream(model, requestBody) { - // 确保全局转换器拥有最新的 SSO token 和基础 URL + // 确保全局转换器拥有最新的基础 URL 和 UUID if (this.converter) { - this.converter.setSsoToken(this.token); + if (this.uuid) { + this.converter.setUuid(this.uuid); + } if (requestBody._requestBaseUrl) { this.converter.setRequestBaseUrl(requestBody._requestBaseUrl); } @@ -705,9 +707,9 @@ export class GrokApiService { try { const json = JSON.parse(dataStr); if (json.result?.response) { - // 注入 SSO token 和基础 URL,以便全局转换器能获取到 - json.result.response._ssoToken = this.token; + // 注入基础 URL 和 UUID,以便全局转换器能获取到 json.result.response._requestBaseUrl = requestBody._requestBaseUrl; + json.result.response._uuid = this.uuid; if (json.result.response.responseId) { lastResponseId = json.result.response.responseId; @@ -730,8 +732,8 @@ export class GrokApiService { response: { isDone: true, responseId: lastResponseId, - _ssoToken: this.token, - _requestBaseUrl: requestBody._requestBaseUrl + _requestBaseUrl: requestBody._requestBaseUrl, + _uuid: this.uuid } } }; diff --git a/src/providers/provider-pool-manager.js b/src/providers/provider-pool-manager.js index 02d1b14..fd6ad7c 100644 --- a/src/providers/provider-pool-manager.js +++ b/src/providers/provider-pool-manager.js @@ -583,6 +583,20 @@ export class ProviderPoolManager { return pool?.find(p => p.uuid === uuid) || null; } + /** + * 根据 UUID 在所有池中查找提供商配置 + * @param {string} uuid - 提供商 UUID + * @returns {object|null} 提供商配置对象或 null + */ + findProviderByUuid(uuid) { + if (!uuid) return null; + for (const type in this.providerStatus) { + const provider = this.providerStatus[type].find(p => p.uuid === uuid); + if (provider) return provider.config; + } + return null; + } + /** * Initializes the status for each provider in the pools. * Initially, all providers are considered healthy and have zero usage. diff --git a/src/utils/grok-assets-proxy.js b/src/utils/grok-assets-proxy.js index 9330679..80f6f13 100644 --- a/src/utils/grok-assets-proxy.js +++ b/src/utils/grok-assets-proxy.js @@ -8,12 +8,14 @@ import { MODEL_PROVIDER } from './common.js'; * @param {http.IncomingMessage} req 原始请求 * @param {http.ServerResponse} res 原始响应 * @param {Object} config 全局配置 + * @param {Object} providerPoolManager 提供商号池管理器 */ -export async function handleGrokAssetsProxy(req, res, config) { +export async function handleGrokAssetsProxy(req, res, config, providerPoolManager) { try { const requestUrl = new URL(req.url, `http://${req.headers.host}`); const targetUrl = requestUrl.searchParams.get('url'); let ssoToken = requestUrl.searchParams.get('sso'); + const uuid = requestUrl.searchParams.get('uuid'); if (!targetUrl) { res.writeHead(400, { 'Content-Type': 'application/json' }); @@ -21,9 +23,20 @@ export async function handleGrokAssetsProxy(req, res, config) { return; } + // 优先尝试从 uuid 换取 token,提高安全性 + if (!ssoToken && uuid && providerPoolManager) { + const providerConfig = providerPoolManager.findProviderByUuid(uuid); + if (providerConfig) { + ssoToken = providerConfig.GROK_COOKIE_TOKEN; + logger.debug(`[Grok Proxy] Resolved SSO token from uuid: ${uuid}`); + } else { + logger.warn(`[Grok Proxy] Could not find provider configuration for uuid: ${uuid}`); + } + } + if (!ssoToken) { res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Missing sso parameter' })); + res.end(JSON.stringify({ error: 'Missing sso parameter or valid uuid' })); return; }