grok-project / grok2api.js
kevin
fix 支持流式
8a5090c
import * as dotenv from 'dotenv';
dotenv.config();
import express from 'express';
import crypto from 'crypto';
const app = express();
const PORT = 3000;
const baseUrl = "https://api.x.com";
const xai_api_key = process.env['API_KEY'];//header
const auth_token = process.env['AUTH_TOKEN'];//cookie
const ct0 = process.env['CT0'];//cookie
const grok_headers = {
'accept': '*/*',
'accept-language': 'zh-CN,zh;q=0.9',
'accept-encoding': 'gzip, deflate, br, zstd',
'authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
'content-type': 'application/json',
'origin': 'https://x.com',
'priority': 'u=1, i',
'sec-ch-ua': '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36',
}
function getRandomIDPro(size) {
let random = '';
const customDict = '0123456789';
for (; size--;) random += customDict[(Math.random() * customDict.length) | 0];
return random;
}
class Xgrok2Worker {
constructor(modelId) {
this.modelId = modelId;
if (this.modelId !== "grok-2a") {
throw new Error(`模型: ${this.modelId} 不支持`);
}
}
constructRequestBody(messages,conversationId) {
return {
"responses": messages,
"systemPromptName": "",
"grokModelOptionId": "grok-2a",
"conversationId": conversationId,
"returnSearchResults": false,
"returnCitations": false,
"promptMetadata": {
"promptSource": "NATURAL",
"action": "INPUT"
},
"imageGenerationCount": 1,
"requestFeatures": {
"eagerTweets": false,
"serverHistory": false
}
}
}
// 转换消息内容格式
transformMessages(request) {
const transformed = request.messages.map(msg => {
switch (msg.role) {
case 'system':
return { message: msg.content, sender: 1, fileAttachments: [] };
case 'user':
return { message: msg.content, sender: 1, fileAttachments: [] };
case 'assistant':
return { message: msg.content, sender: 2 };
}
});
return this.mergeUserMessages(transformed);
}
// 合并用户消息
mergeUserMessages(messages) {
return messages.reduce((merged, current) => {
const prev = merged[merged.length - 1];
if (prev && prev.sender === 1 && current.sender === 1) {
prev.message += "\n" + current.message;
return merged;
}
merged.push(current);
return merged;
}, []);
}
async sendChatRequest(request) {
const conversationId = `18758${getRandomIDPro(14)}`;
const transformedMessages = this.transformMessages(request);
const requestBody = this.constructRequestBody(transformedMessages,conversationId);
let fullResponse = "";
const response = await fetch(`${baseUrl}/2/grok/add_response.json`, {
method: 'POST',
headers: {
...grok_headers,
'x-csrf-token': ct0,
'cookie': `auth_token=${auth_token};ct0=${ct0}`
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new Error(`上游服务请求失败! status: ${response.status}`);
}
// 创建响应流读取器
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
fullResponse += decoder.decode(value);
}
//提取合并消息
const combinedMessage = await this.extractMessages(fullResponse);
// 删除历史会话
await fetch(`https://x.com/i/api/graphql/TlKHSWVMVeaa-i7dqQqFQA/ConversationItem_DeleteConversationMutation`, {
method: 'POST',
headers: {
...grok_headers,
'x-csrf-token': ct0,
'cookie': `auth_token=${auth_token};ct0=${ct0}`
},
body: JSON.stringify({
"variables": {
"conversationId": conversationId
},
"queryId": "TlKHSWVMVeaa-i7dqQqFQA"
})
});
// 转换响应格式为 OpenAI 格式
return {
id: `chatcmpl-${crypto.randomUUID()}`,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model: request.model,
choices: [{
index: 0,
message: {
role: "assistant",
content: combinedMessage
},
finish_reason: "stop"
}],
usage: null
};
}
async extractMessages(data) {
const lines = data.trim().split('\n');
let messages = [];
for (const line of lines) {
const json = JSON.parse(line);
if (json.result && json.result.message) {
messages.push(json.result.message);
}
}
return messages.join('');
}
}
// 中间件配置
app.use(express.json({ limit: '5mb' }));
app.use(express.urlencoded({ extended: true, limit: '5mb' }));
// 路由处理
app.get('/api/v1/models', (req, res) => {
res.json({
object: "list",
data: {
id: "grok-2a",
object: "model",
created: 1706745937,
owned_by: "xai",
},
});
});
app.post('/api/v1/chat/completions', async (req, res) => {
let authToken = req.headers.authorization?.replace('Bearer ', '');
if (authToken !== xai_api_key) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
try {
const reqBody = req.body;
const xgrok2Worker = new Xgrok2Worker(reqBody.model);
const result = await xgrok2Worker.sendChatRequest(reqBody);
if (reqBody.stream) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const content = result.choices[0].message.content;
const chunks = content.split(/(?<=[\.\?\!。?!\n])\s*/g).filter(chunk => chunk.trim());
for (const chunkText of chunks) {
const streamPayload = {
id: result.id,
object: 'chat.completion.chunk',
created: Math.floor(Date.now() / 1000),
model: result.model,
choices: [{
index: 0,
delta: {
content: chunkText.includes('\n') ? chunkText : chunkText + ' '
},
finish_reason: null
}]
};
res.write(`data: ${JSON.stringify(streamPayload)}\n\n`);
await new Promise(resolve => setTimeout(resolve, 50));
}
const endPayload = {
id: result.id,
object: 'chat.completion.chunk',
created: Math.floor(Date.now() / 1000),
model: result.model,
choices: [{
index: 0,
delta: {},
finish_reason: 'stop'
}]
};
res.write(`data: ${JSON.stringify(endPayload)}\n\n`);
res.end();
} else {
res.json(result);
}
} catch (error) {
console.error('Error:', error);
res.status(500).json({
error: {
message: error.message,
type: 'server_error',
param: null,
code: error.code || null
}
});
}
});
// 处理 404 路由
app.use((req, res) => {
res.status(404).send('请使用正确请求路径');
});
// 启动服务器
app.listen(PORT, () => {
console.log(`服务器运行在端口 ${PORT}`);
});