wq / src /chat.js
pigracing's picture
Upload 12 files
60a8358 verified
Raw History Blame Contribute Delete
6.23 kB
import { chatHeaders } from './headers.js';
import { reportTokenError, reportTokenSuccess } from './auth.js';
const BASE_URL = 'https://chat.qwen.ai';
export async function createChat(token, model, chatMode = 't2t') {
const res = await fetch(`${BASE_URL}/api/v2/chats/new`, {
method: 'POST',
headers: {
'authorization': `Bearer ${token}`,
'content-type': 'application/json',
'source': 'web',
'version': '0.2.57',
'bx-v': '2.5.36',
'origin': 'https://chat.qwen.ai',
'referer': 'https://chat.qwen.ai/c/new-chat',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36',
},
body: JSON.stringify({
title: '新建对话',
models: [model],
chat_mode: chatMode,
chat_type: chatMode,
timestamp: Date.now(),
project_id: '',
}),
});
const json = await res.json();
const chatId = json.data?.id;
if (!chatId) throw new Error(`Failed to create chat: ${JSON.stringify(json)}`);
return chatId;
}
export async function completion({ token, model, messages, chatMode = 't2t', thinkingEnabled = true, searchEnabled = true }) {
const chatId = await createChat(token, model, chatMode);
const timestamp = Math.floor(Date.now() / 1000);
const isSpecialMode = chatMode !== 't2t';
const isImageMode = chatMode === 't2i';
const isVideoMode = chatMode === 't2v';
const isDeepResearch = chatMode === 'deep_research';
const featureConfig = {
thinking_enabled: isImageMode || isVideoMode ? false : thinkingEnabled,
output_schema: 'phase',
research_mode: isDeepResearch ? 'deep' : 'normal',
auto_thinking: isImageMode || isVideoMode ? false : thinkingEnabled,
thinking_mode: (isImageMode || isVideoMode || !thinkingEnabled) ? 'Disabled' : 'Auto',
thinking_format: 'summary',
auto_search: isImageMode || isVideoMode ? false : searchEnabled,
};
const body = {
stream: true,
version: '2.1',
incremental_output: true,
chat_id: chatId,
chat_mode: chatMode,
model,
parent_id: null,
messages: messages.map(msg => ({
fid: crypto.randomUUID(),
parentId: null,
childrenIds: [crypto.randomUUID()],
role: msg.role,
content: msg.content,
user_action: 'chat',
files: [],
timestamp,
models: [model],
chat_type: chatMode,
feature_config: featureConfig,
extra: { meta: { subChatType: chatMode } },
sub_chat_type: chatMode,
parent_id: null,
})),
timestamp,
};
const res = await fetch(`${BASE_URL}/api/v2/chat/completions?chat_id=${chatId}`, {
method: 'POST',
headers: chatHeaders(token, chatId),
body: JSON.stringify(body),
});
if (!res.ok) {
reportTokenError(token);
const text = await res.text();
throw new Error(`Completion failed: ${res.status} ${text}`);
}
reportTokenSuccess(token);
return { body: res.body };
}
export async function* parseSSEStream(body) {
const reader = body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let lastPhaseStatus = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const data = line.slice(6).trim();
if (!data) continue;
try {
const parsed = JSON.parse(data);
if (parsed['response.created'] || parsed['response.info']) continue;
if (parsed.choices) {
for (const choice of parsed.choices) {
const delta = choice.delta;
if (!delta) continue;
const phase = delta.phase;
const status = delta.status;
const content = delta.content || '';
const usage = parsed.usage;
// Deduplicate: skip same phase+status combo
const key = `${phase}:${status}`;
if (key === lastPhaseStatus && status === 'typing' && !content) continue;
lastPhaseStatus = key;
if (status === 'finished' && phase === 'answer') {
yield { type: 'done', usage };
return;
}
// Image generation: content is the CDN URL
if (phase === 'image_gen') {
if (status === 'finished') continue;
if (content) {
yield { type: 'image', content, usage };
}
continue;
}
// Deep research phases
const researchPhases = ['ResearchNotice', 'ResearchPlanning', 'ResearchSearching', 'ResearchReading', 'Writing'];
if (researchPhases.includes(phase)) {
if (status === 'finished' && !content) continue;
const extra = delta.extra || {};
const drInfo = extra.deep_research || {};
const stage = drInfo.stage || phase;
if (content) {
yield { type: 'research', content, stage, usage };
}
continue;
}
// Thinking summary
if (phase === 'thinking_summary') {
if (status === 'finished') continue;
const extra = delta.extra || {};
const summaryTitle = extra.summary_title?.content?.join('') || '';
const summaryThought = extra.summary_thought?.content?.join('') || '';
const thinkingContent = summaryThought || summaryTitle || content;
if (thinkingContent) {
yield { type: 'thinking', content: thinkingContent, usage };
}
continue;
}
// Regular answer
if (phase === 'answer' && content) {
yield { type: 'content', content, usage };
}
}
}
} catch {
// skip unparseable lines
}
}
}
} finally {
reader.releaseLock();
}
}