feat(grok): 添加资源代理功能并优化用量显示

- 新增 Grok 资源代理接口,将 assets.grok.com 的资源通过本地代理访问
- 在请求处理中注入 requestBaseUrl 配置,供转换器生成正确的代理链接
- 统一各提供商核心服务中删除 _requestBaseUrl 字段的逻辑
- 优化 Grok 用量显示逻辑,支持按 token 或 query 显示剩余额度
- 更新 UI 管理器,允许 /api/grok/assets 接口免认证访问
- 改进 Grok 转换器,在流式输出中智能处理被截断的 URL
This commit is contained in:
hex2077 2026-03-01 23:55:07 +08:00
parent 1cf6f9092b
commit c91d2ce3ab
18 changed files with 416 additions and 46 deletions

View file

@ -1 +1 @@
2.10.1
2.10.2

View file

@ -13,12 +13,82 @@ import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
* 实现Grok协议到其他协议的转换
*/
export class GrokConverter extends BaseConverter {
// 静态属性,确保所有实例共享最新的认证和基础 URL 配置
static sharedSsoToken = null;
static sharedRequestBaseUrl = "";
constructor() {
super('grok');
// 用于跟踪每个请求的状态
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
*/
setRequestBaseUrl(baseUrl) {
if (baseUrl) {
GrokConverter.sharedRequestBaseUrl = baseUrl;
}
}
/**
* assets.grok.com 域名的资源 URL 添加 sso 参数并转换为本地代理 URL
*/
_appendSsoToken(url, state = null) {
const ssoToken = state?.ssoToken || GrokConverter.sharedSsoToken;
const requestBaseUrl = state?.requestBaseUrl || GrokConverter.sharedRequestBaseUrl;
if (!url || !ssoToken) return url;
// 检查是否为 assets.grok.com 域名或相对路径
const isGrokAsset = url.includes('assets.grok.com') || (!url.startsWith('http') && !url.startsWith('data:'));
if (!isGrokAsset) return url;
// 构造完整的原始 URL
let originalUrl = url;
if (!url.startsWith('http')) {
originalUrl = `https://assets.grok.com${url.startsWith('/') ? '' : '/'}${url}`;
}
// 返回本地代理接口 URL
const proxyPath = `/api/grok/assets?url=${encodeURIComponent(originalUrl)}&sso=${encodeURIComponent(ssoToken)}`;
if (requestBaseUrl) {
return `${requestBaseUrl}${proxyPath}`;
}
return proxyPath;
}
/**
* 在文本中查找并替换所有 assets.grok.com 的资源链接为绝对代理链接
*/
_processGrokAssetsInText(text, state = null) {
const ssoToken = state?.ssoToken || GrokConverter.sharedSsoToken;
if (!text || !ssoToken) return text;
// 更宽松的正则匹配 assets.grok.com 的 URL
const grokUrlRegex = /https?:\/\/assets\.grok\.com\/[^\s\)\"\'\>]+/g;
return text.replace(grokUrlRegex, (url) => {
return this._appendSsoToken(url, state);
});
}
/**
* 获取或初始化请求状态
*/
@ -35,7 +105,10 @@ export class GrokConverter extends BaseConverter {
content_buffer: "", // 用于缓存内容以解析工具调用
has_tool_call: false,
rollout_id: "",
in_tool_call: false // 是否处于 <tool_call> 块内
in_tool_call: false, // 是否处于 <tool_call> 块内
ssoToken: null,
requestBaseUrl: "",
pending_text_buffer: "" // 用于处理流式输出中被截断的 URL
});
}
return this.requestStates.get(requestId);
@ -277,6 +350,7 @@ export class GrokConverter extends BaseConverter {
if (!url.startsWith('http')) {
finalUrl = `https://assets.grok.com${url.startsWith('/') ? '' : '/'}${url}`;
}
finalUrl = this._appendSsoToken(finalUrl);
return `![${imageId}](${finalUrl})`;
}
@ -288,13 +362,16 @@ export class GrokConverter extends BaseConverter {
if (!videoUrl.startsWith('http')) {
finalVideoUrl = `https://assets.grok.com${videoUrl.startsWith('/') ? '' : '/'}${videoUrl}`;
}
finalVideoUrl = this._appendSsoToken(finalVideoUrl);
let finalThumbUrl = thumbnailImageUrl;
if (thumbnailImageUrl && !thumbnailImageUrl.startsWith('http')) {
finalThumbUrl = `https://assets.grok.com${thumbnailImageUrl.startsWith('/') ? '' : '/'}${thumbnailImageUrl}`;
}
finalThumbUrl = this._appendSsoToken(finalThumbUrl);
return `\n[![video](${finalThumbUrl || 'https://assets.grok.com/favicon.ico'})](${finalVideoUrl})\n[Play Video](${finalVideoUrl})\n`;
const defaultThumb = this._appendSsoToken('https://assets.grok.com/favicon.ico');
return `\n[![video](${finalThumbUrl || defaultThumb})](${finalVideoUrl})\n[Play Video](${finalVideoUrl})\n`;
}
/**
@ -376,22 +453,35 @@ export class GrokConverter extends BaseConverter {
const responseId = grokResponse.responseId || `chatcmpl-${uuidv4()}`;
let content = grokResponse.message || "";
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;
}
// 过滤内容
// 过滤内容并处理其中的 Grok 资源链接
content = this._filterToken(content, responseId);
content = this._processGrokAssetsInText(content, state);
// 收集图片并追加
const imageUrls = this._collectImages(grokResponse);
if (imageUrls.length > 0) {
content += "\n";
for (const url of imageUrls) {
content += this._renderImage(url) + "\n";
content += this._renderImage(url, "image", state) + "\n";
}
}
// 处理视频 (非流式模式)
if (grokResponse.finalVideoUrl) {
content += this._renderVideo(grokResponse.finalVideoUrl, grokResponse.finalThumbnailUrl);
content += this._renderVideo(grokResponse.finalVideoUrl, grokResponse.finalThumbnailUrl, state);
}
// 解析工具调用
@ -444,6 +534,18 @@ 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;
}
if (resp._requestBaseUrl) {
state.requestBaseUrl = resp._requestBaseUrl;
}
if (resp.llmInfo?.modelHash && !state.fingerprint) {
state.fingerprint = resp.llmInfo.modelHash;
}
@ -473,12 +575,11 @@ export class GrokConverter extends BaseConverter {
// 处理结束标志
if (resp.isDone) {
let finalContent = "";
/*
if (state.think_opened) {
finalContent += "\n</think>\n";
state.think_opened = false;
// 处理剩余的缓冲区
if (state.pending_text_buffer) {
finalContent += this._processGrokAssetsInText(state.pending_text_buffer, state);
state.pending_text_buffer = "";
}
*/
// 处理 buffer 中的工具调用
const { text, toolCalls } = this.parseToolCalls(state.content_buffer);
@ -493,7 +594,7 @@ export class GrokConverter extends BaseConverter {
choices: [{
index: 0,
delta: {
content: ((/* finalContent + */ "") + (text || "")).trim() || null,
content: (finalContent + (text || "")).trim() || null,
tool_calls: toolCalls
},
finish_reason: "tool_calls"
@ -508,7 +609,7 @@ export class GrokConverter extends BaseConverter {
system_fingerprint: state.fingerprint,
choices: [{
index: 0,
delta: { content: /* finalContent || */ null },
delta: { content: finalContent || null },
finish_reason: "stop"
}]
});
@ -558,7 +659,7 @@ export class GrokConverter extends BaseConverter {
}
*/
state.video_think_active = false;
deltaContent += this._renderVideo(vid.videoUrl, vid.thumbnailImageUrl);
deltaContent += this._renderVideo(vid.videoUrl, vid.thumbnailImageUrl, state);
}
}
@ -576,7 +677,7 @@ export class GrokConverter extends BaseConverter {
const imageUrls = this._collectImages(mr);
for (const url of imageUrls) {
deltaContent += this._renderImage(url) + "\n";
deltaContent += this._renderImage(url, "image", state) + "\n";
}
if (mr.metadata?.llm_info?.modelHash) {
@ -590,9 +691,14 @@ export class GrokConverter extends BaseConverter {
if (card.jsonData) {
try {
const cardData = JSON.parse(card.jsonData);
const original = cardData.image?.original;
let original = cardData.image?.original;
const title = cardData.image?.title || "image";
if (original) {
// 确保是绝对路径
if (!original.startsWith('http')) {
original = `https://assets.grok.com${original.startsWith('/') ? '' : '/'}${original}`;
}
original = this._appendSsoToken(original, state);
deltaContent += `![${title}](${original})\n`;
}
} catch (e) {
@ -611,25 +717,54 @@ export class GrokConverter extends BaseConverter {
if (inThink) {
deltaReasoning += filtered;
} else {
// 工具调用抑制逻辑:不向客户端输出 <tool_call> 块及其内容
let outputToken = filtered;
// 将新 token 加入待处理缓冲区,解决 URL 被截断的问题
state.pending_text_buffer += filtered;
// 简单的状态切换检测
if (outputToken.includes('<tool_call>')) {
state.in_tool_call = true;
state.has_tool_call = true;
// 移除标签之后的部分(如果有)
outputToken = outputToken.split('<tool_call>')[0];
} else if (state.in_tool_call && outputToken.includes('</tool_call>')) {
state.in_tool_call = false;
// 只保留标签之后的部分
outputToken = outputToken.split('</tool_call>')[1] || "";
} else if (state.in_tool_call) {
// 处于块内,完全抑制
outputToken = "";
let outputFromBuffer = "";
// 启发式逻辑:检查缓冲区是否包含完整的 URL
if (state.pending_text_buffer.includes("https://assets.grok.com")) {
const lastUrlIndex = state.pending_text_buffer.lastIndexOf("https://assets.grok.com");
const textAfterUrl = state.pending_text_buffer.slice(lastUrlIndex);
// 检查 URL 是否结束(空格、右括号、引号、换行、大于号等)
const terminatorMatch = textAfterUrl.match(/[\s\)\"\'\>\n]/);
if (terminatorMatch) {
// URL 已结束,可以安全地处理并输出缓冲区
outputFromBuffer = this._processGrokAssetsInText(state.pending_text_buffer, state);
state.pending_text_buffer = "";
} else if (state.pending_text_buffer.length > 1000) {
// 缓冲区过长,强制处理输出,避免过度延迟
outputFromBuffer = this._processGrokAssetsInText(state.pending_text_buffer, state);
state.pending_text_buffer = "";
}
} else {
// 不包含 Grok URL直接输出
outputFromBuffer = state.pending_text_buffer;
state.pending_text_buffer = "";
}
deltaContent += outputToken;
if (outputFromBuffer) {
// 工具调用抑制逻辑:不向客户端输出 <tool_call> 块及其内容
let outputToken = outputFromBuffer;
// 简单的状态切换检测
if (outputToken.includes('<tool_call>')) {
state.in_tool_call = true;
state.has_tool_call = true;
// 移除标签之后的部分(如果有)
outputToken = outputToken.split('<tool_call>')[0];
} else if (state.in_tool_call && outputToken.includes('</tool_call>')) {
state.in_tool_call = false;
// 只保留标签之后的部分
outputToken = outputToken.split('</tool_call>')[1] || "";
} else if (state.in_tool_call) {
// 处于块内,完全抑制
outputToken = "";
}
deltaContent += outputToken;
}
// 将内容加入 buffer 用于最终解析工具调用
state.content_buffer += filtered;

View file

@ -11,6 +11,7 @@ import { PROMPT_LOG_FILENAME } from '../core/config-manager.js';
import { handleOllamaRequest, handleOllamaShow } from './ollama-handler.js';
import { getPluginManager } from '../core/plugin-manager.js';
import { randomUUID } from 'crypto';
import { handleGrokAssetsProxy } from '../utils/grok-assets-proxy.js';
/**
* Generate a short unique request ID (8 characters)
@ -53,6 +54,12 @@ export function createRequestHandler(config, providerPoolManager) {
// Deep copy the config for each request to allow dynamic modification
const currentConfig = deepmerge({}, config);
// 计算当前请求的基础 URL
const protocol = req.socket.encrypted || req.headers['x-forwarded-proto'] === 'https' ? 'https' : 'http';
const host = req.headers.host;
currentConfig.requestBaseUrl = `${protocol}://${host}`;
const requestUrl = new URL(req.url, `http://${req.headers.host}`);
let path = requestUrl.pathname;
const method = req.method;
@ -106,6 +113,12 @@ export function createRequestHandler(config, providerPoolManager) {
return true;
}
// Grok assets proxy endpoint
if (method === 'GET' && path === '/api/grok/assets') {
await handleGrokAssetsProxy(req, res, currentConfig);
return true;
}
// providers health endpoint
// url params: provider[string], customName[string], unhealthRatioThreshold[float]
// 支持provider, customName过滤记录

View file

@ -231,6 +231,9 @@ export class ClaudeApiService {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
const response = await this.callApi('/messages', requestBody);
return response;

View file

@ -1776,6 +1776,9 @@ async saveCredentialsToFile(filePath, newData) {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
// 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) {
@ -2145,6 +2148,9 @@ async saveCredentialsToFile(filePath, newData) {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
// 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) {

View file

@ -151,6 +151,9 @@ export class ForwardApiService {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
// Transparently pass the endpoint if provided in requestBody, otherwise use default
const endpoint = requestBody.endpoint || '';
@ -163,6 +166,9 @@ export class ForwardApiService {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
const endpoint = requestBody.endpoint || '';
yield* this.streamApi(endpoint, requestBody);

View file

@ -1314,6 +1314,9 @@ export class AntigravityApiService {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
// 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) {
@ -1387,6 +1390,9 @@ export class AntigravityApiService {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
// 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) {

View file

@ -645,6 +645,9 @@ export class GeminiApiService {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
// 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) {
@ -676,6 +679,9 @@ export class GeminiApiService {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
// 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) {

View file

@ -4,7 +4,7 @@ import * as http from 'http';
import * as https from 'https';
import * as tls from 'tls';
import { v4 as uuidv4 } from 'uuid';
import { API_ACTIONS, isRetryableNetworkError } from '../../utils/common.js';
import { API_ACTIONS, isRetryableNetworkError, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
import { getProviderModels } from '../provider-models.js';
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
import { getTLSSidecar } from '../../utils/tls-sidecar.js';
@ -107,7 +107,13 @@ export class GrokApiService {
this.baseUrl = config.GROK_BASE_URL || 'https://grok.com';
this.chatApi = `${this.baseUrl}/rest/app-chat/conversations/new`;
this.isInitialized = false;
this.converter = new GrokConverter();
// 使用全局转换器实例,确保与适配器层使用的是同一个实例,从而共享 SSO token 和基础 URL 配置
this.converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GROK);
if (this.converter) {
this.converter.setSsoToken(this.token);
}
this.lastSyncAt = null;
}
@ -169,7 +175,7 @@ export class GrokApiService {
const payload = {
"requestKind": "DEFAULT",
"modelName": "grok-4-1-thinking-1129", // Default model for checking limits
"modelName": "grok-3", // Default model for checking limits
};
const axiosConfig = {
@ -188,17 +194,29 @@ export class GrokApiService {
try {
const response = await axios(axiosConfig);
const data = response.data;
logger.info('[Grok] Raw rate limits response:', JSON.stringify(data));
let remaining = data.remainingTokens;
if (remaining === undefined) {
remaining = data.remainingQueries !== undefined ? data.remainingQueries : data.totalQueries;
}
// 注入固定总量逻辑 (根据反馈:查询总数固定为 80)
if (data.remainingQueries !== undefined || data.totalQueries !== undefined) {
// 注入用量逻辑
if (data.totalQueries > 0) {
// 付费账号totalQueries > 0 时用 totalQueries - remainingQueries 计算已用量
data.totalLimit = data.totalQueries;
data.usedQueries = Math.max(0, data.totalQueries - (data.remainingQueries !== undefined ? data.remainingQueries : 0));
data.unit = 'queries';
} else if (data.totalQueries === 0) {
// 免费账号totalQueries = 0 时用 token 额度totalTokens - remainingTokens
data.totalLimit = data.totalTokens || 0;
data.usedQueries = Math.max(0, (data.totalTokens || 0) - (data.remainingTokens || 0));
data.unit = 'tokens';
} else if (data.remainingQueries !== undefined || data.totalQueries !== undefined) {
// 保底逻辑
data.totalLimit = 80;
// 计算已用次数
data.usedQueries = Math.max(0, 80 - (data.remainingQueries !== undefined ? data.remainingQueries : data.totalQueries));
data.unit = 'queries';
}
this.lastSyncAt = Date.now();
@ -501,6 +519,10 @@ export class GrokApiService {
if (resp.llmInfo) Object.assign(collected.llmInfo, resp.llmInfo);
if (resp.rolloutId) collected.rolloutId = resp.rolloutId;
// 同步私有字段到最终结果
if (resp._ssoToken) collected._ssoToken = resp._ssoToken;
if (resp._requestBaseUrl) collected._requestBaseUrl = resp._requestBaseUrl;
if (resp.modelResponse) collected.modelResponse = resp.modelResponse;
if (resp.cardAttachment) collected.cardAttachment = resp.cardAttachment;
@ -573,6 +595,23 @@ export class GrokApiService {
}
async * generateContentStream(model, requestBody) {
// 确保全局转换器拥有最新的 SSO token 和基础 URL
if (this.converter) {
this.converter.setSsoToken(this.token);
if (requestBody._requestBaseUrl) {
this.converter.setRequestBaseUrl(requestBody._requestBaseUrl);
}
}
// 临时存储 monitorRequestId
if (requestBody._monitorRequestId) {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
// 检查是否即将到期(需要同步用量),如果是则推送到刷新队列
if (this.isExpiryDateNear()) {
const poolManager = getProviderPoolManager();
@ -665,8 +704,14 @@ export class GrokApiService {
try {
const json = JSON.parse(dataStr);
if (json.result?.response?.responseId) {
lastResponseId = json.result.response.responseId;
if (json.result?.response) {
// 注入 SSO token 和基础 URL以便全局转换器能获取到
json.result.response._ssoToken = this.token;
json.result.response._requestBaseUrl = requestBody._requestBaseUrl;
if (json.result.response.responseId) {
lastResponseId = json.result.response.responseId;
}
}
yield json;
} catch (e) {
@ -684,7 +729,9 @@ export class GrokApiService {
result: {
response: {
isDone: true,
responseId: lastResponseId
responseId: lastResponseId,
_ssoToken: this.token,
_requestBaseUrl: requestBody._requestBaseUrl
}
}
};

View file

@ -134,6 +134,9 @@ export class CodexApiService {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
// 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) {
@ -205,6 +208,9 @@ export class CodexApiService {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
// 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) {

View file

@ -1042,6 +1042,9 @@ export class IFlowApiService {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
// 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) {
@ -1070,6 +1073,9 @@ export class IFlowApiService {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
// 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) {

View file

@ -194,6 +194,9 @@ export class OpenAIApiService {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
return this.callApi('/chat/completions', requestBody);
}
@ -204,6 +207,9 @@ export class OpenAIApiService {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
yield* this.streamApi('/chat/completions', requestBody);
}

View file

@ -162,6 +162,9 @@ export class OpenAIResponsesApiService {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
return this.callApi('/responses', requestBody);
}
@ -172,6 +175,9 @@ export class OpenAIResponsesApiService {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
yield* this.streamApi('/responses', requestBody);
}

View file

@ -656,6 +656,9 @@ export class QwenApiService {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
// 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) {
@ -677,6 +680,9 @@ export class QwenApiService {
this.config._monitorRequestId = requestBody._monitorRequestId;
delete requestBody._monitorRequestId;
}
if (requestBody._requestBaseUrl) {
delete requestBody._requestBaseUrl;
}
// 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) {

View file

@ -64,7 +64,7 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
}
// Handle UI management API requests (需要token验证除了登录接口、健康检查和Events接口)
if (pathParam.startsWith('/api/') && pathParam !== '/api/login' && pathParam !== '/api/health' && pathParam !== '/api/events' ) {
if (pathParam.startsWith('/api/') && pathParam !== '/api/login' && pathParam !== '/api/health' && pathParam !== '/api/events' && pathParam !== '/api/grok/assets') {
// 检查token验证
const isAuth = await auth.checkAuth(req);
if (!isAuth) {

View file

@ -566,13 +566,14 @@ export function formatGrokUsage(usageData) {
usageBreakdown: []
};
// Grok 返回的数据结构已在 core 中预处理:{ remainingTokens, remainingQueries, totalQueries, totalLimit, usedQueries, ... }
// Grok 返回的数据结构已在 core 中预处理:{ remainingTokens, remainingQueries, totalQueries, totalLimit, usedQueries, unit, ... }
if (usageData.totalLimit !== undefined && usageData.usedQueries !== undefined) {
const isTokens = usageData.unit === 'tokens';
const item = {
resourceType: 'TOKEN_USAGE',
displayName: 'Remaining Queries',
displayNamePlural: 'Remaining Queries',
unit: 'queries',
displayName: isTokens ? 'Remaining Tokens' : 'Remaining Queries',
displayNamePlural: isTokens ? 'Remaining Tokens' : 'Remaining Queries',
unit: usageData.unit || 'queries',
currency: null,
// 使用从 core 传出的计算好的值

View file

@ -912,6 +912,11 @@ export async function handleContentGenerationRequest(req, res, service, endpoint
if (CONFIG._monitorRequestId) {
processedRequestBody._monitorRequestId = CONFIG._monitorRequestId;
}
// 将 requestBaseUrl 注入到 requestBody 中,以便在转换器中使用
if (CONFIG.requestBaseUrl) {
processedRequestBody._requestBaseUrl = CONFIG.requestBaseUrl;
}
// fs.writeFile('originalRequestBody'+Date.now()+'.json', JSON.stringify(originalRequestBody));
if (getProtocolPrefix(fromProvider) !== getProtocolPrefix(toProvider)) {

View file

@ -0,0 +1,112 @@
import axios from 'axios';
import logger from './logger.js';
import { configureAxiosProxy } from './proxy-utils.js';
import { MODEL_PROVIDER } from './common.js';
/**
* 处理 Grok 资源代理请求
* @param {http.IncomingMessage} req 原始请求
* @param {http.ServerResponse} res 原始响应
* @param {Object} config 全局配置
*/
export async function handleGrokAssetsProxy(req, res, config) {
try {
const requestUrl = new URL(req.url, `http://${req.headers.host}`);
const targetUrl = requestUrl.searchParams.get('url');
let ssoToken = requestUrl.searchParams.get('sso');
if (!targetUrl) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Missing url parameter' }));
return;
}
if (!ssoToken) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Missing sso parameter' }));
return;
}
// 清理 token
if (ssoToken.startsWith('sso=')) {
ssoToken = ssoToken.substring(4);
}
// 构造完整的 assets.grok.com URL如果是相对路径
let finalTargetUrl = targetUrl;
if (!targetUrl.startsWith('http')) {
finalTargetUrl = `https://assets.grok.com${targetUrl.startsWith('/') ? '' : '/'}${targetUrl}`;
}
// 验证域名安全,只允许代理 assets.grok.com
try {
const parsedTarget = new URL(finalTargetUrl);
if (parsedTarget.hostname !== 'assets.grok.com') {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Forbidden: Only assets.grok.com is allowed' }));
return;
}
} catch (e) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid target URL' }));
return;
}
const headers = {
'User-Agent': config.GROK_USER_AGENT || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
'Cookie': `sso=${ssoToken}; sso-rw=${ssoToken}`,
'Referer': 'https://grok.com/',
'Accept': '*/*'
};
const axiosConfig = {
method: 'get',
url: finalTargetUrl,
headers: headers,
responseType: 'stream',
timeout: 30000,
validateStatus: false
};
// 配置代理
configureAxiosProxy(axiosConfig, config, MODEL_PROVIDER.GROK_CUSTOM);
logger.debug(`[Grok Proxy] Proxying request to: ${finalTargetUrl}`);
const response = await axios(axiosConfig);
// 转发响应头
const responseHeaders = {
'Content-Type': response.headers['content-type'] || 'application/octet-stream',
'Cache-Control': response.headers['cache-control'] || 'public, max-age=3600',
};
if (response.headers['content-length']) {
responseHeaders['Content-Length'] = response.headers['content-length'];
}
res.writeHead(response.status, responseHeaders);
// 管道传输数据
response.data.pipe(res);
response.data.on('error', (err) => {
logger.error(`[Grok Proxy] Stream error: ${err.message}`);
if (!res.headersSent) {
res.writeHead(500);
res.end();
} else {
res.end();
}
});
} catch (error) {
logger.error(`[Grok Proxy] Error: ${error.message}`);
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Internal Server Error', message: error.message }));
} else {
res.end();
}
}
}