import axios, { AxiosResponse, CancelTokenSource } from "axios"; import { logger } from "../util/logger"; import { getFileContent } from "../util/commonUtil"; import { devchatSocket, startSocketConn, closeSocketConn, } from "./socketClient"; import { UiUtilWrapper } from "../util/uiUtil"; import { workspace } from "vscode"; import { deleteTopic } from "@/handler/topicHandler"; function timeThis( target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor ) { const originalMethod = descriptor.value; descriptor.value = async function (...args: any[]) { const start = process.hrtime.bigint(); const result = await originalMethod.apply(this, args); const end = process.hrtime.bigint(); const nanoseconds = end - start; const seconds = Number(nanoseconds) / 1e9; const className = target.constructor.name; logger .channel() ?.debug(`Exec time [${className}.${propertyKey}]: ${seconds} s`); return result; }; return descriptor; } export interface ChatRequest { content: string; model_name: string; api_key: string; api_base: string; parent?: string; context?: string[]; } export interface ChatResponse { // eslint-disable-next-line @typescript-eslint/naming-convention "prompt-hash": string; user: string; date: string; response: string; // eslint-disable-next-line @typescript-eslint/naming-convention finish_reason: string; isError: boolean; extra?: object; } export interface LogData { model: string; messages: object[]; parent?: string; timestamp: number; request_tokens: number; response_tokens: number; } export interface LogInsertRes { hash?: string; error?: string; } export async function buildRoleContextsFromFiles( files: string[] | undefined ): Promise { const contexts: object[] = []; if (!files) { return contexts; } for (const file of files) { const content = await getFileContent(file); if (!content) { continue; } contexts.push({ role: "system", content: `${content}`, }); } return contexts; } export class DevChatClient { private baseURL: string; private _cancelMessageToken: CancelTokenSource | null = null; constructor() { // TODO: tmp dev this.baseURL = "http://localhost:22222"; } async _get(path: string): Promise { try { logger.channel()?.debug(`GET request to ${this.baseURL}${path}`); const response = await axios.get(`${this.baseURL}${path}`); return response; } catch (error) { console.error(error); throw error; } } async _post(path: string, data: any = undefined): Promise { try { const response = await axios.post(`${this.baseURL}${path}`, data); return response; } catch (error) { console.error(error); throw error; } } @timeThis async message( message: ChatRequest, onData: (data: ChatResponse) => void ): Promise { this._cancelMessageToken = axios.CancelToken.source(); const workspace = UiUtilWrapper.workspaceFoldersFirstPath(); // const workspace = undefined; const data = { ...message, workspace: workspace, }; return new Promise(async (resolve, reject) => { try { const response = await axios.post( `${this.baseURL}/message/msg`, data, { responseType: "stream", cancelToken: this._cancelMessageToken!.token, } ); const chatRes: ChatResponse = { "prompt-hash": "", // TODO: prompt-hash is not in chatting response user: "", date: "", response: "", finish_reason: "", isError: false, }; response.data.on("data", (chunk) => { const chunkData = JSON.parse(chunk.toString()); if (chatRes.user === "") { chatRes.user = chunkData["user"]; } if (chatRes.date === "") { chatRes.date = chunkData["date"]; } chatRes.finish_reason = chunkData["finish_reason"]; // TODO: tmp string literal 临时字面量 if (chatRes.finish_reason === "should_run_workflow") { chatRes.extra = chunkData["extra"]; logger .channel() ?.debug( "res on data: should_run_workflow. do nothing now." ); logger .channel() ?.debug( `chatRes.extra: ${JSON.stringify( chatRes.extra )}` ); return; } chatRes.isError = chunkData["isError"]; chatRes.response += chunkData["content"]; logger.channel()?.debug(`${chunkData["content"]}`); onData(chatRes); }); response.data.on("end", () => { logger.channel()?.debug("\nStreaming ended"); resolve(chatRes); // Resolve the promise with chatRes when the stream ends }); response.data.on("error", (error) => { logger.channel()?.error("Streaming error:", error); // TODO: handle error reject(error); // Reject the promise on error }); } catch (error) { // TODO: handle error reject(error); // Reject the promise if the request fails } }); } cancelMessage(): void { if (this._cancelMessageToken) { this._cancelMessageToken.cancel( "Message request cancelled by user" ); this._cancelMessageToken = null; } } /** * Insert a message log. * * @param logData - The log data to be inserted. * @returns A tuple of inserted hash and error message. */ @timeThis async insertLog(logData: LogData): Promise { // TODO: 处理当jsondata太大时,写入临时文件 const data = { workspace: UiUtilWrapper.workspaceFoldersFirstPath(), // workspace: undefined, jsondata: JSON.stringify(logData), }; const response = await this._post("/logs/insert", data); logger .channel() ?.debug( `insertLog response data: ${JSON.stringify( response.data )}, ${typeof response.data}}` ); // TODO: handle error const res: LogInsertRes = { hash: response.data["hash"], error: response.data["error"], }; return res; } stopAllRequest(): void { this.cancelMessage(); // add other requests here if needed } }