import { DOMUtils, httpx, log, showdown, pops, utils } from "@/env"; import Qmsg from "qmsg"; const YiYanChat = { dialogInst: null as unknown as ReturnType<(typeof pops)["alert"]>, /** 是否正在进行初始化参数 */ isIniting: false, /** 是否已初始化参数 */ isInitParams: false, aisearch_id: null as unknown as string | undefined, pvId: null as unknown as string | undefined, sessionId: null as unknown as string | undefined, question: [] as { questionText: string; answerText?: string | undefined; markdownText?: string | undefined; }[], async init() { if (!this.isInitParams) { this.isIniting = true; Qmsg.info("初始化参数中..."); this.isInitParams = Boolean(await this.initParams()); this.isIniting = false; if (this.isInitParams) { Qmsg.success("初始化成功!"); this.init(); } else { Qmsg.error("初始化参数失败"); } } else if (!this.isIniting) { this.showChatGPTDialog(); } }, /** * 初始化参数 * @param queryText 需要提问的问题 */ async initParams(queryText = "") { let getResp = await httpx.get( `https://chat.baidu.com/?pcasync=pc&asyncRenderUrl=&passportStaticPage=https%3A%2F%2Fwww.baidu.com%2Fcache%2Fuser%2Fhtml%2Fv3Jump.html&from=pc_tab&word=${encodeURI( queryText )}&source=pd_ic`, { fetch: true, headers: { Accept: "*/*", Origin: "https://www.baidu.com", Referer: `https://www.baidu.com/`, }, data: JSON.stringify({ data: {}, }), } ); if (!getResp.status) { log.error(getResp); return false; } try { let aisearch_id = /"aisearch_id":"(.*?)"/i.exec(getResp.data.responseText); if (!aisearch_id?.[1]) { throw new TypeError("获取aisearch_id失败"); } let pvId = /"pvId":"(.*?)"/i.exec(getResp.data.responseText); if (!pvId?.[1]) { throw new TypeError("获取pvId失败"); } let sessionId = /"sessionId":"(.*?)"/i.exec(getResp.data.responseText); if (!sessionId?.[1]) { throw new TypeError("获取sessionId失败"); } YiYanChat.aisearch_id = aisearch_id[1]; YiYanChat.pvId = pvId[1]; YiYanChat.sessionId = sessionId[1]; log.success("获取一言参数aisearch_id:" + YiYanChat.aisearch_id); log.success("获取一言参数pvId:" + YiYanChat.pvId); log.success("获取一言参数sessionId:" + YiYanChat.sessionId); return true; } catch (error) { log.error(error); return false; } }, /** * 显示ChatGPT回答弹窗 */ showChatGPTDialog() { if (YiYanChat.dialogInst != null) { if (!YiYanChat.dialogInst.$pops.getClientRects().length) { YiYanChat.dialogInst.show(); } else { log.info("请勿重复打开"); } return; } YiYanChat.dialogInst = pops.alert({ title: { text: "

文心一言

", position: "center", html: true, }, content: { text: "", }, mask: { enable: true, clickEvent: { toHide: true, }, }, btn: { close: { enable: true, callback(event) { event.hide(); }, }, }, drag: true, dragLimit: true, width: "95vw", height: "90vh", style: /*css*/ ` .pops{ --container-title-height: 45px; --container-bottom-btn-height: 100px; --gpt-bg-color: #ffffff; --gpt-border-radius: 4px; } .pops-alert-content{ background: #ECEAF7; } .pops-alert-btn .ask-question{ width: 100%; height: 100%; display: flex; align-items: center; } .pops-alert-btn .ask-question textarea{ width: inherit; height: inherit; } .pops-alert-btn .ask-question textarea { vertical-align: bottom; position: relative; display: block; resize: none; padding: 5px 11px; line-height: 1.5; box-sizing: border-box; font-size: 16px; font-family: inherit; background-color: var(--gpt-bg-color); background-image: none; -webkit-appearance: none; appearance: none; box-shadow: 0 0 0 1px #dcdfe6 inset; border-radius: 0; transition: box-shadow .2s cubic-bezier(.645, .045, .355, 1); border: none; } .pops-alert-btn .ask-question textarea:hover{box-shadow:0 0 0 1px #c0c4cc inset} .pops-alert-btn .ask-question textarea:focus{outline:0;box-shadow:0 0 0 1px #409eff inset} .ask-container{ } .ask-container .user-question, .ask-container .gpt-answer{ display: flex; margin: 10px 10px; } .ask-container .user-question{ } .ask-container .gpt-answer{ } .ask-container .avatar-img{ } .ask-container .avatar-img img{ width: 30px; height: 30px; border-radius: 6px; background: var(--gpt-bg-color); } .ask-container .ask-text, .ask-container .answer-text{ background: var(--gpt-bg-color); border-radius: var(--gpt-border-radius); padding: 10px; margin-left: 10px; text-align: left; } .ask-container .ask-text{ width: auto; } .ask-container .answer-text{ } .ask-container .answer-text *{ text-wrap: wrap; } .gpt-btn-control{ display: flex; flex-direction: column; } .gpt-btn-control .pops-alert-btn-clear-history{ margin-bottom: 5px; } .gpt-btn-control .pops-alert-btn-ok{ margin-top: 5px; } .markdown-body .code-header{ align-items: center; background: #e3e8f6; border-radius: 7px 7px 0 0; display: flex; height: 34px; } .markdown-body .code-header+pre{ border-top-left-radius: 0px; border-top-right-radius: 0px; } .markdown-body span.code-lang{ color: #120649; flex: 1 0 auto; font-family: PingFangSC-Semibold; font-size: 16px; font-weight: 600; letter-spacing: 0; padding-left: 14px; text-align: justify; display: flex; } .markdown-body span.code-copy{ align-items: center; color: #7886a4; display: flex; font-family: PingFangSC-Regular; font-size: 13px; font-weight: 400; letter-spacing: 0; line-height: 14px; text-align: justify; user-select: none; } .markdown-body span.code-copy-text{ margin-left: 7px; margin-right: 20px; } .typing::after { content: '▌'; } .typing::after { animation: blinker 1s step-end infinite; } @keyframes blinker { 0% { visibility: visible; } 50% { visibility: hidden; } 100% { visibility: visible; } } `, }); YiYanChat.loadCSS("https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.1/github-markdown.min.css"); let $alertBtn = YiYanChat.dialogInst.$pops.querySelector(".pops-alert-btn") as HTMLDivElement; $alertBtn.innerHTML = /*html*/ `
`; let $textArea = $alertBtn.querySelector("textarea") as HTMLTextAreaElement; let $enterBtn = $alertBtn.querySelector(".pops-alert-btn-ok") as HTMLElement; let $clearHistoryBtn = $alertBtn.querySelector(".pops-alert-btn-clear-history") as HTMLElement; let $content = YiYanChat.dialogInst.$pops.querySelector(".pops-alert-content") as HTMLDivElement; $content.innerHTML = ""; /** * 查询事件 */ function sendEvent(event: Event) { let queryText = $textArea.value; if (queryText.trim() === "") { Qmsg.error("你没有输入内容哦", { timeout: 1500, }); return; } $textArea.value = ""; let askElement = YiYanChat.getAskElement(queryText); let answerElement = YiYanChat.getAnswerElement(); let answerTextElement = answerElement.querySelector(".answer-text") as HTMLElement; let askContainer = DOMUtils.createElement("div", { className: "ask-container", }); let newQueryText = ""; YiYanChat.question.forEach((item) => { /* 合并之前的提问和回答 */ if (item.questionText) { newQueryText += "\n\n" + item.questionText; if (item.answerText) { newQueryText += "\n\n" + item.answerText; } } }); newQueryText += "\n\n" + queryText; YiYanChat.question.push({ questionText: queryText, answerText: void 0, markdownText: void 0, }); YiYanChat.conversation(newQueryText).then(async (stream) => { if (!stream) { YiYanChat.question.pop(); return; } try { let latestQuestion = YiYanChat.question[YiYanChat.question.length - 1]; let answer = await YiYanChat.getAnswerStream(stream, (itemText) => { latestQuestion.answerText += itemText; answerTextElement.innerText += itemText; YiYanChat.scrollToContentContainerEnd(); }); answerTextElement.classList.remove("typing"); if (!answer) { YiYanChat.question.pop(); return; } latestQuestion.answerText = answer; /* 把text转换成markdown元素 */ let parseData = YiYanChat.conversionTextToMarkdown(answer); log.info("转换为markdown", parseData); if (parseData.status) { latestQuestion.markdownText = parseData.text; answerTextElement.innerHTML = parseData.text; YiYanChat.handleMarkdown(answerTextElement); } else { Qmsg.error("转换为Markdown失败"); } YiYanChat.scrollToContentContainerEnd(); } catch (error) { answerTextElement.classList.remove("typing"); YiYanChat.question.pop(); log.error(error); Qmsg.error(error as string); } }); askContainer.appendChild(askElement); askContainer.appendChild(answerElement); $content.appendChild(askContainer); YiYanChat.scrollToContentContainerEnd(); } DOMUtils.onKeyboard($textArea, "keydown", function (keyName, keyValue, otherCodeList) { if (otherCodeList.includes("ctrl") && keyName === "Enter") { $enterBtn.click(); } }); DOMUtils.on($enterBtn, "click", sendEvent); DOMUtils.on($clearHistoryBtn, "click", function () { YiYanChat.clearHistoryQuestion(); }); }, /** * 获取回答流 * @param stream * @param callback 每次的流读取的回调 */ async getAnswerStream(stream: ReadableStream, callback: (text: string) => void) { const reader = stream.getReader(); async function parseStreamText(): Promise { /** * 所有回答 **/ let answerChunkList: string[] = []; /** 前一记录 */ let preResponseItem = ""; /** 合并 */ let combineItem: string[] = []; /** 引用 */ let referenceList; return new Promise((resolve, reject) => { reader .read() .then( //@ts-ignore function processText({ done, value }) { try { if (done) { log.success("=====读取结束,转换内容====="); /* 所有回答合数组并成字符串 */ let result = answerChunkList.join(""); resolve(result); return; } let responseItem = new TextDecoder("utf-8").decode(value as unknown as AllowSharedBufferSource); /* 去除空格 */ responseItem = responseItem.trim(); if (!responseItem.includes("event:ping") && !responseItem.startsWith("event:messag")) { combineItem.push(preResponseItem); combineItem.push(responseItem); /* 重置 */ preResponseItem = ""; /* 合并 */ responseItem = combineItem.join(""); /* 清空 */ combineItem.length = 0; } else if (!responseItem.includes("event:ping")) { preResponseItem = responseItem; } let responseItemSplit = responseItem.split("\n").filter((item) => item.trim().startsWith("data:")); for (let item of responseItemSplit) { item = item.trim(); /* 解析出数据 */ let streamDataStr = item.replace(/^data:/gi, "").trim(); if (utils.isNull(streamDataStr)) { continue; } log.info(streamDataStr); let streamData = utils.toJSON(streamDataStr); if (utils.isNull(streamData)) { continue; } /** 回答的文字块 @type {string} */ let answerChunk = streamData?.data?.message?.content?.generator?.text; if (!answerChunk) { /* 不存在回答内容 */ continue; } callback(answerChunk); /* 添加到数组中 */ answerChunkList.push(answerChunk); if (streamData?.data?.message?.content?.generator?.referenceList) { referenceList = streamData?.data.message.content.generator.referenceList; } } } catch (error) { log.error(error); } return reader.read().then(processText); } ) .catch((error) => { reject(error); }); }); } return parseStreamText(); }, /** * 添加CSS链接 * @param url */ loadCSS(url: string) { YiYanChat.dialogInst.$shadowRoot.insertBefore( DOMUtils.createElement("link", { rel: "stylesheet", href: url, type: "text/css", crossOrigin: "anonymous", }), YiYanChat.dialogInst.$shadowRoot.childNodes[0] ); }, /** * 获取提问的元素 * @param queryText 提问的问题 */ getAskElement(queryText = "") { let element = DOMUtils.createElement("div", { className: "user-question", innerHTML: /*html*/ `
${queryText}
`, }); return element; }, /** * 获取回答的元素 */ getAnswerElement() { let element = DOMUtils.createElement("div", { className: "gpt-answer", innerHTML: /*html*/ `
`, }); return element; }, /** * 获取AI的回答 */ async conversation(queryText = "") { let response = await httpx.post("https://chat-ws.baidu.com/aichat/api/conversation", { headers: { Accept: "text/event-stream", "Content-Type": "application/json", Origin: "https://www.baidu.com", Referer: `https://www.baidu.com/`, }, fetch: true, responseType: "stream", data: JSON.stringify({ message: { inputMethod: "keyboard", isRebuild: false, content: { query: queryText, qtype: 0, }, }, sessionId: YiYanChat.sessionId, aisearchId: YiYanChat.aisearch_id, pvId: YiYanChat.pvId, }), }); if (!response.status) { return; } let stream = response.data.response; return stream; }, /** * 转换文本为markdown格式 * @param text */ conversionTextToMarkdown(text: string) { let converter = new showdown.Converter(); /* 启用表格选项。从showdown 1.2.0版开始,表支持已作为可选功能移入核心拓展,showdown.table.min.js扩展已被弃用 */ converter.setOption("tables", true); /* 链接在新窗口打开 */ converter.setOption("openLinksInNewWindow", true); /* 删除线 */ converter.setOption("strikethrough", true); /* 开启emoji */ converter.setOption("emoji", true); /*** * original: John Gruber 规范中的原始 Markdown 风格 * vanilla:对决基础风味(v1.3.1 起) * github: GitHub 风格的 Markdown,或 GFM */ showdown.setFlavor("github"); try { let markHTML = converter.makeHtml(text); return { status: true, text: markHTML, }; } catch (error) { return { status: false, error: error, }; } }, /** * 对内部的markdown元素进行处理 * @param element */ handleMarkdown(element: HTMLElement) { element.querySelectorAll("pre").forEach((ele) => { let codeElement = ele.querySelector("code") as HTMLElement; let language = ""; if (codeElement.classList.length >= 2) { language = codeElement.classList[0]; } let copyText = codeElement.innerText || codeElement.textContent; let codeHeader = DOMUtils.createElement("div", { className: "code-header", innerHTML: /*html*/ ` ${language} 复制代码 `, }); let codeCopyText = codeHeader.querySelector(".code-copy-text") as HTMLElement; DOMUtils.on(codeCopyText, "click", function () { try { utils.copy(copyText); Qmsg.success("复制成功"); } catch (error) { Qmsg.error("复制失败," + error); } }); DOMUtils.before(ele, codeHeader); }); }, /** * 清除提问历史 */ clearHistoryQuestion() { YiYanChat.question.length = 0; let $content = YiYanChat.dialogInst.$shadowRoot.querySelector(".pops-alert-content") as HTMLElement; $content.innerHTML = ""; }, /** * 滚动到内容容器的底部 */ scrollToContentContainerEnd() { let $contentElement = YiYanChat.dialogInst.$pops.querySelector(".pops-alert-content") as HTMLElement; $contentElement.scrollTo(0, $contentElement.scrollHeight); }, }; export { YiYanChat };