"use client"; import { REQUEST_TIMEOUT_MS } from "@/app/constant"; import { useChatStore } from "@/app/store"; import { ChatOptions, LLMApi, LLMModel, } from "../api"; import Locale from "../../locales"; import { EventStreamContentType, fetchEventSource, } from "@fortaine/fetch-event-source"; import { prettyObject } from "@/app/utils/format"; import { getMessageTextContent } from "@/app/utils"; import { bigModelApiKey, knowledgeId, template } from "../config"; export class BigModelApi implements LLMApi { public useApi: 'public' | 'private'; public publicPath: string; public privatePath: string; constructor() { this.useApi = 'private'; this.publicPath = 'https://open.bigmodel.cn/api/paas/v4/chat/completions'; // 配置私有请求地址 this.privatePath = 'https://open.bigmodel.cn/api/llm-application/open/model-api/1828613766624038913/sse-invoke'; } async chat(options: ChatOptions) { const messages = options.messages.map((item) => { return { role: item.role, content: getMessageTextContent(item), } }); const userMessages = messages.filter(item => item.content); if (userMessages.length % 2 === 0) { userMessages.unshift({ role: "user", content: "⠀", }); } // 开放大模型参数 const publicParams: any = { messages: userMessages, stream: true,// 流式回复 model: 'glm-4-0520',// 模型 temperature: 0.01,// 采样温度 top_p: 0.7,// 核取样 // 进阶配置 tools: [ { type: 'retrieval', // 工具类型为检索 retrieval: { // 知识库ID knowledge_id: knowledgeId, // 知识库模板 prompt_template: template.content, }, }, ], }; // 私有大模型参数 const privateParams: any = { prompt: userMessages, // model: 'glm-4-0520',// 模型 // temperature: 0.01,// 采样温度 // top_p: 0.7,// 核取样 // 进阶配置 request_id: 'jkec2024', returnType: undefined, knowledge_ids: undefined, document_ids: undefined, }; const controller = new AbortController(); options.onController?.(controller); try { const chatPath = this.useApi === 'public' ? this.publicPath : this.privatePath; const chatPayload = { method: "POST", body: JSON.stringify(this.useApi === 'public' ? publicParams : privateParams), signal: controller.signal, headers: { 'Content-Type': 'application/json', // APIKey Authorization: bigModelApiKey }, }; const requestTimeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); let responseText = ""; let remainText = ""; let finished = false; function animateResponseText() { if (finished || controller.signal.aborted) { responseText += remainText; if (responseText?.length === 0) { options.onError?.(new Error("empty response from server")); } return; } if (remainText.length > 0) { const fetchCount = Math.max(1, Math.round(remainText.length / 60)); const fetchText = remainText.slice(0, fetchCount); responseText += fetchText; remainText = remainText.slice(fetchCount); options.onUpdate?.(responseText, fetchText); } requestAnimationFrame(animateResponseText); } animateResponseText(); const finish = () => { if (!finished) { finished = true; options.onFinish(responseText + remainText); } }; controller.signal.onabort = finish; // 记录上次的 remainText let previousRemainText = ""; fetchEventSource(chatPath, { ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); const contentType = res.headers.get("content-type"); if (contentType?.startsWith("text/plain")) { responseText = await res.clone().text(); return finish(); } if ( !res.ok || !res.headers.get("content-type")?.startsWith(EventStreamContentType) || res.status !== 200 ) { const responseTexts = [responseText]; let extraInfo = await res.clone().text(); try { const resJson = await res.clone().json(); extraInfo = prettyObject(resJson); } catch { } if (res.status === 401) { responseTexts.push(Locale.Error.Unauthorized); } if (extraInfo) { responseTexts.push(extraInfo); } responseText = responseTexts.join("\n\n"); return finish(); } }, onmessage: (msg) => { const handlePublicMessage = () => { if (msg.data === "[DONE]" || finished) { return finish(); } const text = msg.data; try { const json = JSON.parse(text); const choices = json.choices as Array<{ delta: { content: string } }>; const delta = choices[0]?.delta?.content; if (delta) { remainText += delta; } } catch (e) { console.error("[Request] parse error", text, msg); } }; const handlePrivateMessage = () => { if (msg.event === 'finish') { return finish(); } // 获取当前的数据 const currentData = msg.data; // 计算新增的字符 const newChars = currentData.substring(previousRemainText.length); remainText += newChars; // 更新 previousRemainText previousRemainText = currentData; }; if (this.useApi === 'public') { handlePublicMessage(); } else { handlePrivateMessage(); } }, async onclose() { finish(); const session = useChatStore.getState().sessions[0]; const data = { id: session.id, messages: session.messages.map(item => ({ id: item.id, date: item.date, role: item.role, content: item.content, })), }; await fetch('/api/bigModel', { method: 'POST', body: JSON.stringify(data), }); }, onerror(e) { options.onError?.(e); throw e; }, openWhenHidden: true, }); } catch (e) { options.onError?.(e as Error); } } async usage() { return { used: 0, total: 0, }; } async models(): Promise { return []; } }