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 };