AIClient-2-API/src/providers/grok/ws-imagine.js
hex2077 77f73f0603 feat(grok): 增强图片生成功能并支持提供商置顶
- 更新 Grok 图片生成的 WebSocket 协议以适配最新服务端接口
- 扩展资源代理支持至 imagine-public.x.ai 和 grok.com 域名
- 在配置页面为预加载模型提供商添加置顶功能,可设置默认提供商
- 改进图片渲染逻辑,避免流式输出中的重复图片显示
- 更新相关界面文本以更准确描述预加载提供商功能
2026-04-06 16:08:13 +08:00

154 lines
5.6 KiB
JavaScript

import WebSocket from 'ws';
import logger from '../../utils/logger.js';
import { v4 as uuidv4 } from 'uuid';
import { getProxyConfigForProvider } from '../../utils/proxy-utils.js';
import { MODEL_PROVIDER } from '../../utils/common.js';
/**
* Grok WebSocket Imagine Service
* Handles image generation via Grok's WebSocket endpoint.
*/
export class ImagineWebSocketService {
constructor(config) {
this.config = config;
this.baseUrl = (config.GROK_BASE_URL || 'https://grok.com').replace(/\/$/, '');
this.wsUrl = this.baseUrl.replace(/^http/, 'ws') + '/ws/imagine/listen';
}
/**
* Start an image generation stream via WebSocket.
*
* @param {string} token - SSO token
* @param {string} prompt - Image prompt
* @param {string} aspectRatio - Aspect ratio (e.g. "1:1")
* @param {number} n - Number of images
* @param {boolean} enableNsfw - Enable NSFW filter
* @returns {AsyncGenerator<object>}
*/
async *stream(token, prompt, aspectRatio = '1:1', n = 1, enableNsfw = true) {
const proxyConfig = getProxyConfigForProvider(this.config, MODEL_PROVIDER.GROK_CUSTOM);
const agent = proxyConfig?.httpsAgent;
let ssoToken = token || "";
if (ssoToken.startsWith("sso=")) ssoToken = ssoToken.substring(4);
const cfClearance = this.config.GROK_CF_CLEARANCE;
const cookie = ssoToken ? `sso=${ssoToken}; sso-rw=${ssoToken}${cfClearance ? `; cf_clearance=${cfClearance}` : ""}` : "";
const headers = {
'Cookie': cookie,
'Origin': this.baseUrl,
'Host': 'grok.com',
'Connection': 'Upgrade',
'Pragma': 'no-cache',
'Cache-Control': 'no-cache',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7',
'User-Agent': this.config.GROK_USER_AGENT || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
};
logger.debug(`[Grok WS] Connecting to ${this.wsUrl} for prompt: ${prompt.substring(0, 50)}...`);
const ws = new WebSocket(this.wsUrl, {
headers,
agent,
handshakeTimeout: 30000,
rejectUnauthorized: false
});
const queue = [];
let done = false;
let resolveNext = null;
ws.on('open', () => {
logger.debug(`[Grok WS] Connected. Sending reset and imagine request.`);
// 遵循协议:首先发送重置消息
const resetPayload = {
"type": "conversation.item.create",
"timestamp": Date.now(),
"item": {
"type": "message",
"content": [{ "type": "reset" }]
}
};
ws.send(JSON.stringify(resetPayload));
// 延迟 50ms 发送实际生成请求 (模拟浏览器行为)
setTimeout(() => {
if (ws.readyState !== WebSocket.OPEN) return;
const payload = {
"type": "conversation.item.create",
"timestamp": Date.now(),
"item": {
"type": "message",
"content": [
{
"requestId": uuidv4(),
"text": prompt,
"type": "input_text",
"properties": {
"section_count": 0,
"is_kids_mode": false,
"enable_nsfw": enableNsfw,
"skip_upsampler": false,
"enable_side_by_side": true,
"is_initial": false,
"aspect_ratio": aspectRatio,
"enable_pro": false
}
}
]
}
};
ws.send(JSON.stringify(payload));
}, 50);
});
ws.on('message', (data) => {
try {
const msg = JSON.parse(data.toString());
queue.push(msg);
if (resolveNext) {
resolveNext();
resolveNext = null;
}
} catch (e) {
logger.error(`[Grok WS] Failed to parse message: ${data.toString().substring(0, 100)}`);
}
});
ws.on('close', (code, reason) => {
logger.debug(`[Grok WS] Connection closed: ${code} ${reason}`);
done = true;
if (resolveNext) {
resolveNext();
resolveNext = null;
}
});
ws.on('error', (err) => {
logger.error(`[Grok WS] WebSocket error: ${err.message}`);
queue.push({ type: 'error', error: err.message });
done = true;
if (resolveNext) {
resolveNext();
resolveNext = null;
}
});
try {
while (!done || queue.length > 0) {
if (queue.length === 0 && !done) {
await new Promise(r => resolveNext = r);
}
while (queue.length > 0) {
yield queue.shift();
}
}
} finally {
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
ws.close();
}
}
}
}