diff --git a/client/assets/marked.min.js b/client/assets/marked.min.js new file mode 100644 index 0000000..b4e0d73 --- /dev/null +++ b/client/assets/marked.min.js @@ -0,0 +1,69 @@ +/** + * marked v15.0.12 - a markdown parser + * Copyright (c) 2011-2025, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ + +/** + * DO NOT EDIT THIS FILE + * The code in this file is generated from files in ./src/ + */ +(function(g,f){if(typeof exports=="object"&&typeof module<"u"){module.exports=f()}else if("function"==typeof define && define.amd){define("marked",f)}else {g["marked"]=f()}}(typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : this,function(){var exports={};var __exports=exports;var module={exports}; +"use strict";var H=Object.defineProperty;var be=Object.getOwnPropertyDescriptor;var Te=Object.getOwnPropertyNames;var we=Object.prototype.hasOwnProperty;var ye=(l,e)=>{for(var t in e)H(l,t,{get:e[t],enumerable:!0})},Re=(l,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Te(e))!we.call(l,s)&&s!==t&&H(l,s,{get:()=>e[s],enumerable:!(n=be(e,s))||n.enumerable});return l};var Se=l=>Re(H({},"__esModule",{value:!0}),l);var kt={};ye(kt,{Hooks:()=>L,Lexer:()=>x,Marked:()=>E,Parser:()=>b,Renderer:()=>$,TextRenderer:()=>_,Tokenizer:()=>S,defaults:()=>w,getDefaults:()=>z,lexer:()=>ht,marked:()=>k,options:()=>it,parse:()=>pt,parseInline:()=>ct,parser:()=>ut,setOptions:()=>ot,use:()=>lt,walkTokens:()=>at});module.exports=Se(kt);function z(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var w=z();function N(l){w=l}var I={exec:()=>null};function h(l,e=""){let t=typeof l=="string"?l:l.source,n={replace:(s,i)=>{let r=typeof i=="string"?i:i.source;return r=r.replace(m.caret,"$1"),t=t.replace(s,r),n},getRegex:()=>new RegExp(t,e)};return n}var m={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:l=>new RegExp(`^( {0,3}${l})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),hrRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}#`),htmlBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}<(?:[a-z].*>|!--)`,"i")},$e=/^(?:[ \t]*(?:\n|$))+/,_e=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Le=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,O=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,ze=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,F=/(?:[*+-]|\d{1,9}[.)])/,ie=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,oe=h(ie).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),Me=h(ie).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),Q=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,Pe=/^[^\n]+/,U=/(?!\s*\])(?:\\.|[^\[\]\\])+/,Ae=h(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",U).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),Ee=h(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,F).getRegex(),v="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",K=/|$))/,Ce=h("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",K).replace("tag",v).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),le=h(Q).replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),Ie=h(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",le).getRegex(),X={blockquote:Ie,code:_e,def:Ae,fences:Le,heading:ze,hr:O,html:Ce,lheading:oe,list:Ee,newline:$e,paragraph:le,table:I,text:Pe},re=h("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),Oe={...X,lheading:Me,table:re,paragraph:h(Q).replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",re).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex()},Be={...X,html:h(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",K).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:I,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:h(Q).replace("hr",O).replace("heading",` *#{1,6} *[^ +]`).replace("lheading",oe).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},qe=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,ve=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,ae=/^( {2,}|\\)\n(?!\s*$)/,De=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\]*?>/g,ue=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,je=h(ue,"u").replace(/punct/g,D).getRegex(),Fe=h(ue,"u").replace(/punct/g,pe).getRegex(),he="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",Qe=h(he,"gu").replace(/notPunctSpace/g,ce).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),Ue=h(he,"gu").replace(/notPunctSpace/g,He).replace(/punctSpace/g,Ge).replace(/punct/g,pe).getRegex(),Ke=h("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,ce).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),Xe=h(/\\(punct)/,"gu").replace(/punct/g,D).getRegex(),We=h(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),Je=h(K).replace("(?:-->|$)","-->").getRegex(),Ve=h("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",Je).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),q=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,Ye=h(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",q).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),ke=h(/^!?\[(label)\]\[(ref)\]/).replace("label",q).replace("ref",U).getRegex(),ge=h(/^!?\[(ref)\](?:\[\])?/).replace("ref",U).getRegex(),et=h("reflink|nolink(?!\\()","g").replace("reflink",ke).replace("nolink",ge).getRegex(),J={_backpedal:I,anyPunctuation:Xe,autolink:We,blockSkip:Ne,br:ae,code:ve,del:I,emStrongLDelim:je,emStrongRDelimAst:Qe,emStrongRDelimUnd:Ke,escape:qe,link:Ye,nolink:ge,punctuation:Ze,reflink:ke,reflinkSearch:et,tag:Ve,text:De,url:I},tt={...J,link:h(/^!?\[(label)\]\((.*?)\)/).replace("label",q).getRegex(),reflink:h(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",q).getRegex()},j={...J,emStrongRDelimAst:Ue,emStrongLDelim:Fe,url:h(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},fe=l=>st[l];function R(l,e){if(e){if(m.escapeTest.test(l))return l.replace(m.escapeReplace,fe)}else if(m.escapeTestNoEncode.test(l))return l.replace(m.escapeReplaceNoEncode,fe);return l}function V(l){try{l=encodeURI(l).replace(m.percentDecode,"%")}catch{return null}return l}function Y(l,e){let t=l.replace(m.findPipe,(i,r,o)=>{let a=!1,c=r;for(;--c>=0&&o[c]==="\\";)a=!a;return a?"|":" |"}),n=t.split(m.splitPipe),s=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length0?-2:-1}function me(l,e,t,n,s){let i=e.href,r=e.title||null,o=l[1].replace(s.other.outputLinkReplace,"$1");n.state.inLink=!0;let a={type:l[0].charAt(0)==="!"?"image":"link",raw:t,href:i,title:r,text:o,tokens:n.inlineTokens(o)};return n.state.inLink=!1,a}function rt(l,e,t){let n=l.match(t.other.indentCodeCompensation);if(n===null)return e;let s=n[1];return e.split(` +`).map(i=>{let r=i.match(t.other.beginningSpace);if(r===null)return i;let[o]=r;return o.length>=s.length?i.slice(s.length):i}).join(` +`)}var S=class{options;rules;lexer;constructor(e){this.options=e||w}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?n:A(n,` +`)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],s=rt(n,t[3]||"",this.rules);return{type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:s}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let s=A(n,"#");(this.options.pedantic||!s||this.rules.other.endingSpaceChar.test(s))&&(n=s.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:A(t[0],` +`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=A(t[0],` +`).split(` +`),s="",i="",r=[];for(;n.length>0;){let o=!1,a=[],c;for(c=0;c1,i={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");let r=this.rules.other.listItemRegex(n),o=!1;for(;e;){let c=!1,p="",u="";if(!(t=r.exec(e))||this.rules.block.hr.test(e))break;p=t[0],e=e.substring(p.length);let d=t[2].split(` +`,1)[0].replace(this.rules.other.listReplaceTabs,Z=>" ".repeat(3*Z.length)),g=e.split(` +`,1)[0],T=!d.trim(),f=0;if(this.options.pedantic?(f=2,u=d.trimStart()):T?f=t[1].length+1:(f=t[2].search(this.rules.other.nonSpaceChar),f=f>4?1:f,u=d.slice(f),f+=t[1].length),T&&this.rules.other.blankLine.test(g)&&(p+=g+` +`,e=e.substring(g.length+1),c=!0),!c){let Z=this.rules.other.nextBulletRegex(f),te=this.rules.other.hrRegex(f),ne=this.rules.other.fencesBeginRegex(f),se=this.rules.other.headingBeginRegex(f),xe=this.rules.other.htmlBeginRegex(f);for(;e;){let G=e.split(` +`,1)[0],C;if(g=G,this.options.pedantic?(g=g.replace(this.rules.other.listReplaceNesting," "),C=g):C=g.replace(this.rules.other.tabCharGlobal," "),ne.test(g)||se.test(g)||xe.test(g)||Z.test(g)||te.test(g))break;if(C.search(this.rules.other.nonSpaceChar)>=f||!g.trim())u+=` +`+C.slice(f);else{if(T||d.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||ne.test(d)||se.test(d)||te.test(d))break;u+=` +`+g}!T&&!g.trim()&&(T=!0),p+=G+` +`,e=e.substring(G.length+1),d=C.slice(f)}}i.loose||(o?i.loose=!0:this.rules.other.doubleBlankLine.test(p)&&(o=!0));let y=null,ee;this.options.gfm&&(y=this.rules.other.listIsTask.exec(u),y&&(ee=y[0]!=="[ ] ",u=u.replace(this.rules.other.listReplaceTask,""))),i.items.push({type:"list_item",raw:p,task:!!y,checked:ee,loose:!1,text:u,tokens:[]}),i.raw+=p}let a=i.items.at(-1);if(a)a.raw=a.raw.trimEnd(),a.text=a.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let c=0;cd.type==="space"),u=p.length>0&&p.some(d=>this.rules.other.anyLine.test(d.raw));i.loose=u}if(i.loose)for(let c=0;c({text:a,tokens:this.lexer.inline(a),header:!1,align:r.align[c]})));return r}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:t[2].charAt(0)==="="?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===` +`?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let r=A(n.slice(0,-1),"\\");if((n.length-r.length)%2===0)return}else{let r=de(t[2],"()");if(r===-2)return;if(r>-1){let a=(t[0].indexOf("!")===0?5:4)+t[1].length+r;t[2]=t[2].substring(0,r),t[0]=t[0].substring(0,a).trim(),t[3]=""}}let s=t[2],i="";if(this.options.pedantic){let r=this.rules.other.pedanticHrefTitle.exec(s);r&&(s=r[1],i=r[3])}else i=t[3]?t[3].slice(1,-1):"";return s=s.trim(),this.rules.other.startAngleBracket.test(s)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?s=s.slice(1):s=s.slice(1,-1)),me(t,{href:s&&s.replace(this.rules.inline.anyPunctuation,"$1"),title:i&&i.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let s=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),i=t[s.toLowerCase()];if(!i){let r=n[0].charAt(0);return{type:"text",raw:r,text:r}}return me(n,i,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s||s[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){let r=[...s[0]].length-1,o,a,c=r,p=0,u=s[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(u.lastIndex=0,t=t.slice(-1*e.length+r);(s=u.exec(t))!=null;){if(o=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!o)continue;if(a=[...o].length,s[3]||s[4]){c+=a;continue}else if((s[5]||s[6])&&r%3&&!((r+a)%3)){p+=a;continue}if(c-=a,c>0)continue;a=Math.min(a,a+c+p);let d=[...s[0]][0].length,g=e.slice(0,r+s.index+d+a);if(Math.min(r,a)%2){let f=g.slice(1,-1);return{type:"em",raw:g,text:f,tokens:this.lexer.inlineTokens(f)}}let T=g.slice(2,-2);return{type:"strong",raw:g,text:T,tokens:this.lexer.inlineTokens(T)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),s=this.rules.other.nonSpaceChar.test(n),i=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return s&&i&&(n=n.substring(1,n.length-1)),{type:"codespan",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){let t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,s;return t[2]==="@"?(n=t[1],s="mailto:"+n):(n=t[1],s=n),{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,s;if(t[2]==="@")n=t[0],s="mailto:"+n;else{let i;do i=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(i!==t[0]);n=t[0],t[1]==="www."?s="http://"+t[0]:s=t[0]}return{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:n}}}};var x=class l{tokens;options;state;tokenizer;inlineQueue;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||w,this.options.tokenizer=this.options.tokenizer||new S,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let t={other:m,block:B.normal,inline:P.normal};this.options.pedantic?(t.block=B.pedantic,t.inline=P.pedantic):this.options.gfm&&(t.block=B.gfm,this.options.breaks?t.inline=P.breaks:t.inline=P.gfm),this.tokenizer.rules=t}static get rules(){return{block:B,inline:P}}static lex(e,t){return new l(t).lex(e)}static lexInline(e,t){return new l(t).inlineTokens(e)}lex(e){e=e.replace(m.carriageReturn,` +`),this.blockTokens(e,this.tokens);for(let t=0;t(s=r.call({lexer:this},e,t))?(e=e.substring(s.raw.length),t.push(s),!0):!1))continue;if(s=this.tokenizer.space(e)){e=e.substring(s.raw.length);let r=t.at(-1);s.raw.length===1&&r!==void 0?r.raw+=` +`:t.push(s);continue}if(s=this.tokenizer.code(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="paragraph"||r?.type==="text"?(r.raw+=` +`+s.raw,r.text+=` +`+s.text,this.inlineQueue.at(-1).src=r.text):t.push(s);continue}if(s=this.tokenizer.fences(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.heading(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.hr(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.blockquote(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.list(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.html(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.def(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="paragraph"||r?.type==="text"?(r.raw+=` +`+s.raw,r.text+=` +`+s.raw,this.inlineQueue.at(-1).src=r.text):this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title});continue}if(s=this.tokenizer.table(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.lheading(e)){e=e.substring(s.raw.length),t.push(s);continue}let i=e;if(this.options.extensions?.startBlock){let r=1/0,o=e.slice(1),a;this.options.extensions.startBlock.forEach(c=>{a=c.call({lexer:this},o),typeof a=="number"&&a>=0&&(r=Math.min(r,a))}),r<1/0&&r>=0&&(i=e.substring(0,r+1))}if(this.state.top&&(s=this.tokenizer.paragraph(i))){let r=t.at(-1);n&&r?.type==="paragraph"?(r.raw+=` +`+s.raw,r.text+=` +`+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=r.text):t.push(s),n=i.length!==e.length,e=e.substring(s.raw.length);continue}if(s=this.tokenizer.text(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="text"?(r.raw+=` +`+s.raw,r.text+=` +`+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=r.text):t.push(s);continue}if(e){let r="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(r);break}else throw new Error(r)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,s=null;if(this.tokens.links){let o=Object.keys(this.tokens.links);if(o.length>0)for(;(s=this.tokenizer.rules.inline.reflinkSearch.exec(n))!=null;)o.includes(s[0].slice(s[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(s=this.tokenizer.rules.inline.anyPunctuation.exec(n))!=null;)n=n.slice(0,s.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;(s=this.tokenizer.rules.inline.blockSkip.exec(n))!=null;)n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);let i=!1,r="";for(;e;){i||(r=""),i=!1;let o;if(this.options.extensions?.inline?.some(c=>(o=c.call({lexer:this},e,t))?(e=e.substring(o.raw.length),t.push(o),!0):!1))continue;if(o=this.tokenizer.escape(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.tag(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.link(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(o.raw.length);let c=t.at(-1);o.type==="text"&&c?.type==="text"?(c.raw+=o.raw,c.text+=o.text):t.push(o);continue}if(o=this.tokenizer.emStrong(e,n,r)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.codespan(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.br(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.del(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.autolink(e)){e=e.substring(o.raw.length),t.push(o);continue}if(!this.state.inLink&&(o=this.tokenizer.url(e))){e=e.substring(o.raw.length),t.push(o);continue}let a=e;if(this.options.extensions?.startInline){let c=1/0,p=e.slice(1),u;this.options.extensions.startInline.forEach(d=>{u=d.call({lexer:this},p),typeof u=="number"&&u>=0&&(c=Math.min(c,u))}),c<1/0&&c>=0&&(a=e.substring(0,c+1))}if(o=this.tokenizer.inlineText(a)){e=e.substring(o.raw.length),o.raw.slice(-1)!=="_"&&(r=o.raw.slice(-1)),i=!0;let c=t.at(-1);c?.type==="text"?(c.raw+=o.raw,c.text+=o.text):t.push(o);continue}if(e){let c="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(c);break}else throw new Error(c)}}return t}};var $=class{options;parser;constructor(e){this.options=e||w}space(e){return""}code({text:e,lang:t,escaped:n}){let s=(t||"").match(m.notSpaceStart)?.[0],i=e.replace(m.endingNewline,"")+` +`;return s?'
'+(n?i:R(i,!0))+`
+`:"
"+(n?i:R(i,!0))+`
+`}blockquote({tokens:e}){return`
+${this.parser.parse(e)}
+`}html({text:e}){return e}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)} +`}hr(e){return`
+`}list(e){let t=e.ordered,n=e.start,s="";for(let o=0;o +`+s+" +`}listitem(e){let t="";if(e.task){let n=this.checkbox({checked:!!e.checked});e.loose?e.tokens[0]?.type==="paragraph"?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&e.tokens[0].tokens[0].type==="text"&&(e.tokens[0].tokens[0].text=n+" "+R(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:!0}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`
  • ${t}
  • +`}checkbox({checked:e}){return"'}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    +`}table(e){let t="",n="";for(let i=0;i${s}`),` + +`+t+` +`+s+`
    +`}tablerow({text:e}){return` +${e} +`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+` +`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${R(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){let s=this.parser.parseInline(n),i=V(e);if(i===null)return s;e=i;let r='
    ",r}image({href:e,title:t,text:n,tokens:s}){s&&(n=this.parser.parseInline(s,this.parser.textRenderer));let i=V(e);if(i===null)return R(n);e=i;let r=`${n}{let o=i[r].flat(1/0);n=n.concat(this.walkTokens(o,t))}):i.tokens&&(n=n.concat(this.walkTokens(i.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let s={...n};if(s.async=this.defaults.async||s.async||!1,n.extensions&&(n.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let r=t.renderers[i.name];r?t.renderers[i.name]=function(...o){let a=i.renderer.apply(this,o);return a===!1&&(a=r.apply(this,o)),a}:t.renderers[i.name]=i.renderer}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let r=t[i.level];r?r.unshift(i.tokenizer):t[i.level]=[i.tokenizer],i.start&&(i.level==="block"?t.startBlock?t.startBlock.push(i.start):t.startBlock=[i.start]:i.level==="inline"&&(t.startInline?t.startInline.push(i.start):t.startInline=[i.start]))}"childTokens"in i&&i.childTokens&&(t.childTokens[i.name]=i.childTokens)}),s.extensions=t),n.renderer){let i=this.defaults.renderer||new $(this.defaults);for(let r in n.renderer){if(!(r in i))throw new Error(`renderer '${r}' does not exist`);if(["options","parser"].includes(r))continue;let o=r,a=n.renderer[o],c=i[o];i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u||""}}s.renderer=i}if(n.tokenizer){let i=this.defaults.tokenizer||new S(this.defaults);for(let r in n.tokenizer){if(!(r in i))throw new Error(`tokenizer '${r}' does not exist`);if(["options","rules","lexer"].includes(r))continue;let o=r,a=n.tokenizer[o],c=i[o];i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u}}s.tokenizer=i}if(n.hooks){let i=this.defaults.hooks||new L;for(let r in n.hooks){if(!(r in i))throw new Error(`hook '${r}' does not exist`);if(["options","block"].includes(r))continue;let o=r,a=n.hooks[o],c=i[o];L.passThroughHooks.has(r)?i[o]=p=>{if(this.defaults.async)return Promise.resolve(a.call(i,p)).then(d=>c.call(i,d));let u=a.call(i,p);return c.call(i,u)}:i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u}}s.hooks=i}if(n.walkTokens){let i=this.defaults.walkTokens,r=n.walkTokens;s.walkTokens=function(o){let a=[];return a.push(r.call(this,o)),i&&(a=a.concat(i.call(this,o))),a}}this.defaults={...this.defaults,...s}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return x.lex(e,t??this.defaults)}parser(e,t){return b.parse(e,t??this.defaults)}parseMarkdown(e){return(n,s)=>{let i={...s},r={...this.defaults,...i},o=this.onError(!!r.silent,!!r.async);if(this.defaults.async===!0&&i.async===!1)return o(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof n>"u"||n===null)return o(new Error("marked(): input parameter is undefined or null"));if(typeof n!="string")return o(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));r.hooks&&(r.hooks.options=r,r.hooks.block=e);let a=r.hooks?r.hooks.provideLexer():e?x.lex:x.lexInline,c=r.hooks?r.hooks.provideParser():e?b.parse:b.parseInline;if(r.async)return Promise.resolve(r.hooks?r.hooks.preprocess(n):n).then(p=>a(p,r)).then(p=>r.hooks?r.hooks.processAllTokens(p):p).then(p=>r.walkTokens?Promise.all(this.walkTokens(p,r.walkTokens)).then(()=>p):p).then(p=>c(p,r)).then(p=>r.hooks?r.hooks.postprocess(p):p).catch(o);try{r.hooks&&(n=r.hooks.preprocess(n));let p=a(n,r);r.hooks&&(p=r.hooks.processAllTokens(p)),r.walkTokens&&this.walkTokens(p,r.walkTokens);let u=c(p,r);return r.hooks&&(u=r.hooks.postprocess(u)),u}catch(p){return o(p)}}}onError(e,t){return n=>{if(n.message+=` +Please report this to https://github.com/markedjs/marked.`,e){let s="

    An error occurred:

    "+R(n.message+"",!0)+"
    ";return t?Promise.resolve(s):s}if(t)return Promise.reject(n);throw n}}};var M=new E;function k(l,e){return M.parse(l,e)}k.options=k.setOptions=function(l){return M.setOptions(l),k.defaults=M.defaults,N(k.defaults),k};k.getDefaults=z;k.defaults=w;k.use=function(...l){return M.use(...l),k.defaults=M.defaults,N(k.defaults),k};k.walkTokens=function(l,e){return M.walkTokens(l,e)};k.parseInline=M.parseInline;k.Parser=b;k.parser=b.parse;k.Renderer=$;k.TextRenderer=_;k.Lexer=x;k.lexer=x.lex;k.Tokenizer=S;k.Hooks=L;k.parse=k;var it=k.options,ot=k.setOptions,lt=k.use,at=k.walkTokens,ct=k.parseInline,pt=k,ut=b.parse,ht=x.lex; + +if(__exports != exports)module.exports = exports;return module.exports})); diff --git a/client/dist/chatbot-sdk.js b/client/dist/chatbot-sdk.js deleted file mode 100644 index 5f267c0..0000000 --- a/client/dist/chatbot-sdk.js +++ /dev/null @@ -1,5700 +0,0 @@ -var ChatbotSDK = (function () { - 'use strict'; - - /** 错误回调函数(由宿主通过 onError 配置注入) */ - let errorCallback = null; - /** 设置是否开启调试日志(保留接口兼容,不再输出控制台日志) */ - function setDebug(_enabled) { - // 控制台日志已禁用,此方法保留仅为 API 兼容 - } - /** 设置错误回调(宿主 onError 配置) */ - function setErrorCallback(cb) { - errorCallback = cb || null; - } - const logger = { - /** 普通信息日志(已禁用控制台输出) */ - info(_msg, _data) { - // 控制台日志已禁用 - }, - /** 警告日志(已禁用控制台输出) */ - warn(_msg, _data) { - // 控制台日志已禁用 - }, - /** 错误日志(不输出控制台,仅触发 onError 回调) */ - error(msg, data) { - // 触发宿主的 onError 回调 - if (errorCallback) { - try { - const code = data instanceof Error ? data.type || 'error' : 'error'; - errorCallback({ message: msg, code: String(code), detail: data }); - } - catch ( /* 回调异常不影响 SDK */_a) { /* 回调异常不影响 SDK */ } - } - }, - /** 开始计时(已禁用) */ - time(_label) { - // 控制台日志已禁用 - }, - /** 结束计时(已禁用) */ - timeEnd(_label, _prefix) { - return 0; - }, - /** 生命周期日志:init */ - lifecycleInit(_integrateId, _requestDomain) { - // 控制台日志已禁用 - }, - /** 生命周期日志:destroy */ - lifecycleDestroy(_integrateId) { - // 控制台日志已禁用 - }, - /** 生命周期日志:sendMessage */ - lifecycleSend(_integrateId, _length) { - // 控制台日志已禁用 - }, - /** 生命周期日志:收到回复 */ - lifecycleReply(_integrateId, _length) { - // 控制台日志已禁用 - }, - /** 生命周期日志:请求失败 */ - lifecycleError(_integrateId, _status, _message) { - // 控制台日志已禁用 - }, - /** 生命周期日志:新对话 */ - lifecycleClear(_integrateId) { - // 控制台日志已禁用 - }, - /** 生命周期日志:流式回复完成 */ - lifecycleStreamDone(_integrateId, _length) { - // 控制台日志已禁用 - }, - /** 生命周期日志:知识库切换 */ - lifecycleCategoryChange(_categoryId) { - // 控制台日志已禁用 - }, - }; - - /** - * 解析并校验用户传入的配置,填充默认值 - */ - function parseConfig(raw) { - var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o; - // 校验必传参数:integrateId(对应后端 roleId) - if (!raw.integrateId || (typeof raw.integrateId !== 'string' && typeof raw.integrateId !== 'number') - || (typeof raw.integrateId === 'string' && raw.integrateId.trim() === '')) { - logger.error('integrateId 是必传参数(对应后端 roleId 客服角色 ID),请检查 init() 调用。示例:ChatbotSDK.init({ integrateId: 1, requestDomain: "https://api.example.com" })'); - return null; - } - // 校验必传参数:requestDomain - if (!raw.requestDomain || typeof raw.requestDomain !== 'string' || raw.requestDomain.trim() === '') { - logger.error('requestDomain 是必传参数,请检查 init() 调用。示例:ChatbotSDK.init({ integrateId: 1, requestDomain: "https://api.example.com" })'); - return null; - } - // 校验 requestDomain 是否为合法 URL 格式 - try { - new URL(raw.requestDomain); - } - catch (_p) { - logger.error(`requestDomain 不是合法的 URL 格式:${raw.requestDomain}。请提供完整的域名,如 https://api.example.com`); - return null; - } - // integrateId 统一转为字符串(后端 roleId 为 Long,但 query param 传字符串也可接收) - const integrateIdStr = String(raw.integrateId).trim(); - // 解析图标:用户传入优先,否则使用 requestDomain + /sdk/launcher-logo.png - const domain = raw.requestDomain.replace(/\/+$/, ''); - const resolvedLauncherIcon = raw.launcherIcon || ``; - const resolvedPrimaryColor = raw.primaryColor || '#4F46E5'; - // 填充默认值 - const config = { - integrateId: integrateIdStr, - requestDomain: raw.requestDomain.replace(/\/+$/, ''), // 去掉末尾斜杠 - userId: raw.userId, - categoryId: raw.categoryId, - showCategorySwitch: (_a = raw.showCategorySwitch) !== null && _a !== void 0 ? _a : false, - title: raw.title || 'AI 智能助手', - width: (_b = raw.width) !== null && _b !== void 0 ? _b : 500, - height: Math.max(400, (_c = raw.height) !== null && _c !== void 0 ? _c : 520), - position: raw.position === 'left-bottom' ? 'left-bottom' : 'right-bottom', - primaryColor: resolvedPrimaryColor, - launcherIcon: resolvedLauncherIcon, - showClear: (_d = raw.showClear) !== null && _d !== void 0 ? _d : true, - showAdminPanel: (_e = raw.showAdminPanel) !== null && _e !== void 0 ? _e : false, - quickReplies: Array.isArray(raw.quickReplies) - ? raw.quickReplies.map(s => String(s).trim()).filter(Boolean) - : [], - suggestions: (_f = raw.suggestions) !== null && _f !== void 0 ? _f : true, - theme: raw.theme === 'dark' ? 'dark' : 'light', - showTeaser: (_g = raw.showTeaser) !== null && _g !== void 0 ? _g : true, - teaserText: (typeof raw.teaserText === 'string' && raw.teaserText.trim()) || '', - resizable: (_h = raw.resizable) !== null && _h !== void 0 ? _h : true, - watermark: (typeof raw.watermark === 'string' && raw.watermark.trim()) || undefined, - streaming: (_j = raw.streaming) !== null && _j !== void 0 ? _j : true, - enableRag: (_k = raw.enableRag) !== null && _k !== void 0 ? _k : true, - rewriteStrategy: raw.rewriteStrategy || 'REWRITE', - locale: raw.locale || 'zh-CN', - debug: (_l = raw.debug) !== null && _l !== void 0 ? _l : true, - sound: (_m = raw.sound) !== null && _m !== void 0 ? _m : false, - notification: (_o = raw.notification) !== null && _o !== void 0 ? _o : false, - onError: typeof raw.onError === 'function' ? raw.onError : undefined, - onReady: typeof raw.onReady === 'function' ? raw.onReady : undefined, - onMessage: typeof raw.onMessage === 'function' ? raw.onMessage : undefined, - token: raw.token, - roles: raw.roles, - disclaimer: raw.disclaimer, // undefined = 从后端拉取,'' = 隐藏,'...' = 直接用 - chatId: '', // 初始为空,由 chatId 初始化流程填充 - }; - logger.info(`配置解析完成 integrateId(=roleId)=${config.integrateId} userId(=accountId)=${config.userId || '(未设置)'} requestDomain=${config.requestDomain}`); - return config; - } - - /** - * 多语言国际化模块 - i18n 字典 + 翻译函数 - */ - /** 语言包字典 */ - const dictionaries = { - 'zh-CN': { - // 头部 - title: 'AI 智能助手', - minimize: '最小化', - close: '关闭', - status_online: '在线', - role_selector_label: '服务角色', - // 欢迎空状态 - welcome_title: '你好,我是 AI 智能助手', - welcome_desc: '有什么可以帮你的吗?在下方输入框开始提问吧~', - // 输入区 - placeholder: '输入您的问题...', - send: '发送', - stop: '停止生成', - new_message: '新消息', - // 消息操作 - copy: '复制', - copied: '已复制', - retry: '重新生成', - retrying: '重新生成中...', - // 消息 - loading: '正在思考...', - stream_interrupted: '回复被中断', - stream_unstable: '网络不稳定,内容可能不完整', - // 知识库 - category_placeholder: '选择知识库分类', - category_all: '全部分类', - category_load_error: '加载分类失败', - source_title: '参考来源', - source_count: '{n} 条参考来源', - source_loading: '加载来源中...', - // 新对话 - clear: '新对话', - // 历史会话 - history_title: '历史会话', - history_empty: '暂无历史会话', - history_load_error: '加载会话列表失败', - history_delete_confirm: '确定删除该会话?', - history_export: '导出', - history_delete: '删除', - history_search: '搜索会话...', - history_group_today: '今天', - history_group_yesterday: '昨天', - history_group_week: '本周', - history_group_earlier: '更早', - // 消息反馈 - feedback_up: '有帮助', - feedback_down: '没帮助', - feedback_reason_title: '请选择不满意的原因:', - feedback_reason_inaccurate: '信息错误', - feedback_reason_irrelevant: '不相关', - feedback_reason_incomplete: '不完整', - feedback_reason_other: '其他', - feedback_reason_comment_placeholder: '补充说明(可选)', - feedback_reason_submit: '提交', - // 提示气泡 - teaser_text: '有什么可以帮你的吗?', - new_msg_announce: '收到新消息', - resize: '拖拽缩放窗口', - // 错误提示 - error_network: '网络连接失败,请检查网络', - error_offline: '⚠️ 网络连接异常,请检查网络后重试', - error_timeout: '请求超时,请稍后重试', - error_server: '服务器异常,请稍后重试', - error_cors: '跨域请求被拦截,请联系管理员将当前域名加入 API 白名单', - error_auth: '鉴权失败,请联系管理员', - error_forbidden: '无访问权限,请联系管理员配置', - error_not_found: '请求的资源不存在', - error_rate_limit: '请求过于频繁,请稍后重试', - error_unavailable: '服务暂不可用,请稍后重试', - error_unknown: '请求发生未知错误', - error_send: '发送失败,请稍后重试', - error_stream_unsupported: '浏览器不支持流式读取', - }, - 'en': { - // Header - title: 'AI Assistant', - minimize: 'Minimize', - close: 'Close', - status_online: 'Online', - role_selector_label: 'Service', - // Welcome - welcome_title: 'Hi, I am your AI assistant', - welcome_desc: 'How can I help you? Type your question below to get started.', - // Input - placeholder: 'Type your question...', - send: 'Send', - stop: 'Stop generating', - new_message: 'New message', - // Message actions - copy: 'Copy', - copied: 'Copied', - retry: 'Regenerate', - retrying: 'Regenerating...', - // Messages - loading: 'Thinking...', - stream_interrupted: 'Response interrupted', - stream_unstable: 'Network unstable, content may be incomplete', - // Knowledge base - category_placeholder: 'Select category', - category_all: 'All categories', - category_load_error: 'Failed to load categories', - source_title: 'Sources', - source_count: '{n} source(s)', - source_loading: 'Loading sources...', - // Clear/New chat - clear: 'New chat', - // History - history_title: 'History', - history_empty: 'No conversations yet', - history_load_error: 'Failed to load conversations', - history_delete_confirm: 'Delete this conversation?', - history_export: 'Export', - history_delete: 'Delete', - history_search: 'Search conversations...', - history_group_today: 'Today', - history_group_yesterday: 'Yesterday', - history_group_week: 'This Week', - history_group_earlier: 'Earlier', - // Feedback - feedback_up: 'Helpful', - feedback_down: 'Not helpful', - feedback_reason_title: 'Please tell us why:', - feedback_reason_inaccurate: 'Inaccurate', - feedback_reason_irrelevant: 'Irrelevant', - feedback_reason_incomplete: 'Incomplete', - feedback_reason_other: 'Other', - feedback_reason_comment_placeholder: 'Additional comments (optional)', - feedback_reason_submit: 'Submit', - // Teaser - teaser_text: 'How can I help you?', - new_msg_announce: 'New message received', - resize: 'Resize window', - // Errors - error_network: 'Network connection failed', - error_offline: '⚠️ Connection lost. Please check your network and try again.', - error_timeout: 'Request timed out, please try again', - error_server: 'Server error, please try again later', - error_cors: 'CORS request blocked. Please contact admin to whitelist your domain', - error_auth: 'Authentication failed, please contact admin', - error_forbidden: 'Access denied, please contact admin', - error_not_found: 'Resource not found', - error_rate_limit: 'Too many requests, please try again later', - error_unavailable: 'Service temporarily unavailable', - error_unknown: 'Unknown request error', - error_send: 'Failed to send, please try again', - error_stream_unsupported: 'Browser does not support streaming', - }, - }; - /** 当前语言 */ - let currentLocale = 'zh-CN'; - /** - * 设置当前语言 - */ - function setLocale(locale) { - if (dictionaries[locale]) { - currentLocale = locale; - } - else { - // 尝试匹配语言前缀(如 zh -> zh-CN) - const prefix = locale.split('-')[0]; - const matched = Object.keys(dictionaries).find(k => k.startsWith(prefix)); - if (matched) { - currentLocale = matched; - } - // 未匹配则保持默认 zh-CN - } - } - /** - * 获取翻译文本 - * @param key 翻译 key - * @param params 插值参数,如 { n: 3 } 替换 {n} - */ - function t(key, params) { - const dict = dictionaries[currentLocale] || dictionaries['zh-CN']; - let text = dict[key] || dictionaries['zh-CN'][key] || key; - // 简单插值替换:{n} → 实际值 - if (params) { - for (const [k, v] of Object.entries(params)) { - text = text.replace(`{${k}}`, String(v)); - } - } - return text; - } - - /** 请求超时时间(毫秒) */ - const REQUEST_TIMEOUT = 30000; - let currentConfig = null; - /** 运行时动态设置的角色 ID(通过角色选择器切换),优先于 currentConfig.integrateId */ - let activeIntegrateId = null; - /** 设置当前配置 */ - function setApiConfig(config) { - currentConfig = config; - } - /** 清空当前配置(销毁 SDK 时调用) */ - function clearApiConfig() { - currentConfig = null; - activeIntegrateId = null; - } - /** 设置当前活跃角色 ID(角色切换时调用),影响后续所有 API 调用的 roleId 参数 */ - function setActiveRoleId(roleId) { - activeIntegrateId = roleId; - } - /** 获取当前有效的 integrateId(优先使用运行时设置,回退到配置默认值) */ - function getActiveIntegrateId() { - var _a; - return (_a = activeIntegrateId !== null && activeIntegrateId !== void 0 ? activeIntegrateId : currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.integrateId) !== null && _a !== void 0 ? _a : ''; - } - /** 更新当前 chatId(对话 ID) */ - function updateChatId(chatId) { - if (currentConfig) { - currentConfig.chatId = chatId; - } - } - /** 获取当前 chatId */ - function getChatId() { - return (currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.chatId) || ''; - } - /** 构建完整请求 URL,自动防御双斜杠 */ - function buildUrl(path) { - if (!currentConfig) { - throw new Error('API 配置未初始化'); - } - const domain = currentConfig.requestDomain.replace(/\/+$/, ''); - const cleanPath = path.startsWith('/') ? path : `/${path}`; - return `${domain}${cleanPath}`; - } - /** - * 安全设置可选参数:仅当 value 非空时追加 - */ - function setIfPresent(params, key, value) { - if (value === undefined || value === null) - return; - if (typeof value === 'string' && value.trim() === '') - return; - params.set(key, String(value)); - } - // ==================== 对话接口 URL 构建 ==================== - /** - * 构建同步对话请求 URL - * - integrateId → roleId - * - userId → accountId - * - chatId → 自动管理的对话 ID - */ - function buildChatUrl(message) { - const params = new URLSearchParams(); - params.set('message', message); - params.set('chatId', currentConfig.chatId); - // integrateId 映射为 roleId - setIfPresent(params, 'roleId', getActiveIntegrateId()); - // userId 映射为 accountId - setIfPresent(params, 'accountId', currentConfig.userId); - return buildUrl(`/ai/assistant_app/chat/sync?${params.toString()}`); - } - /** - * 构建 SSE 流式请求 URL - */ - function buildChatSSEUrl(message, categoryId) { - const params = new URLSearchParams(); - params.set('message', message); - params.set('chatId', currentConfig.chatId); - setIfPresent(params, 'roleId', getActiveIntegrateId()); - setIfPresent(params, 'accountId', currentConfig.userId); - setIfPresent(params, 'categoryId', categoryId !== null && categoryId !== void 0 ? categoryId : currentConfig.categoryId); - return buildUrl(`/ai/assistant_app/chat/sse?${params.toString()}`); - } - /** - * 构建 RAG 增强流式请求 URL - */ - function buildChatRAGSSEUrl(message, categoryId) { - const params = new URLSearchParams(); - params.set('message', message); - params.set('chatId', currentConfig.chatId); - params.set('rewriteStrategy', currentConfig.rewriteStrategy || 'REWRITE'); - setIfPresent(params, 'roleId', getActiveIntegrateId()); - setIfPresent(params, 'accountId', currentConfig.userId); - setIfPresent(params, 'categoryId', categoryId !== null && categoryId !== void 0 ? categoryId : currentConfig.categoryId); - return buildUrl(`/ai/assistant_app/chat/rag/sse?${params.toString()}`); - } - /** - * 构建 RAG 引用来源请求 URL - */ - function buildRagSourcesUrl(message, categoryId) { - const params = new URLSearchParams(); - params.set('message', message); - params.set('chatId', currentConfig.chatId); - params.set('rewriteStrategy', currentConfig.rewriteStrategy || 'REWRITE'); - setIfPresent(params, 'roleId', getActiveIntegrateId()); - setIfPresent(params, 'accountId', currentConfig.userId); - setIfPresent(params, 'categoryId', categoryId !== null && categoryId !== void 0 ? categoryId : currentConfig.categoryId); - return buildUrl(`/ai/assistant_app/rag/sources?${params.toString()}`); - } - // ==================== HTTP 基础封装 ==================== - /** 带超时的 fetch 封装,支持外部 AbortSignal(用于流式主动中断) */ - async function safeFetch(url, options = {}, timeout = REQUEST_TIMEOUT, externalSignal) { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), timeout); - // 外部信号触发时同步中断内部请求 - if (externalSignal) { - if (externalSignal.aborted) { - controller.abort(); - } - else { - externalSignal.addEventListener('abort', () => controller.abort(), { once: true }); - } - } - try { - // 注入 SDK Token 认证头(仅对 /ai/ 路径注入,避免将 SDK Token 发送到管理接口) - const headers = {}; - if (options.headers) { - if (options.headers instanceof Headers) { - options.headers.forEach((value, key) => { headers[key] = value; }); - } - else if (Array.isArray(options.headers)) { - options.headers.forEach(([key, value]) => { headers[key] = value; }); - } - else { - Object.assign(headers, options.headers); - } - } - // 为受 SdkAuthFilter 保护的路径自动注入 Bearer Token(/ai/** 和 /feedback) - if ((currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.token) && (url.includes('/ai/') || url.endsWith('/feedback'))) { - headers['Authorization'] = `Bearer ${currentConfig.token}`; - } - const response = await fetch(url, Object.assign(Object.assign({}, options), { headers, signal: controller.signal, mode: 'cors', credentials: 'include' })); - if (response.status === 401 && (url.includes('/ai/') || url.endsWith('/feedback'))) { - logger.error('SDK 认证失败:Token 缺失或已过期,请重新调用 /open-api/auth/token 换取 Token'); - } - return response; - } - catch (err) { - // 用户主动中断(停止生成),不算错误 - if (externalSignal === null || externalSignal === void 0 ? void 0 : externalSignal.aborted) { - throw new CskError('aborted', 'aborted'); - } - if (err instanceof DOMException && err.name === 'AbortError') { - throw new CskError(t('error_timeout'), 'timeout'); - } - if (err instanceof TypeError && err.message.includes('Failed to fetch')) { - throw new CskError(t('error_cors'), 'cors'); - } - throw new CskError(t('error_network'), 'network'); - } - finally { - clearTimeout(timer); - } - } - /** 自定义错误类型 */ - class CskError extends Error { - constructor(message, type) { - super(message); - this.name = 'CskError'; - this.type = type; - } - } - /** 根据 HTTP 状态码返回对应的国际化错误消息 */ - function getHttpErrorMessage(status) { - switch (status) { - case 401: return t('error_auth'); - case 403: return t('error_forbidden'); - case 404: return t('error_not_found'); - case 429: return t('error_rate_limit'); - case 500: return t('error_server'); - case 502: - case 503: return t('error_unavailable'); - default: return `${t('error_unknown')}(${status})`; - } - } - // ==================== 对话请求 ==================== - /** - * 同步对话请求 - */ - async function chatRequest(message) { - const url = buildChatUrl(message); - logger.lifecycleSend(getActiveIntegrateId(), message.length); - try { - const response = await safeFetch(url); - if (!response.ok) { - const errorMsg = getHttpErrorMessage(response.status); - logger.lifecycleError(getActiveIntegrateId(), String(response.status), errorMsg); - throw new CskError(errorMsg, `http_${response.status}`); - } - const text = await response.text(); - logger.lifecycleReply(getActiveIntegrateId(), text.length); - return text; - } - catch (err) { - if (err instanceof CskError) - throw err; - logger.lifecycleError(getActiveIntegrateId(), 'unknown', String(err)); - throw new CskError(t('error_unknown'), 'unknown'); - } - } - /** - * SSE 流式对话请求 - * @param useRag 是否使用 RAG 增强对话 - * @param categoryId 知识库分类 ID - */ - async function chatSSERequest(message, onChunk, onDone, onError, categoryId, useRag, signal) { - var _a; - const url = useRag - ? buildChatRAGSSEUrl(message, categoryId) - : buildChatSSEUrl(message, categoryId); - let totalText = ''; - logger.lifecycleSend(getActiveIntegrateId(), message.length); - try { - const response = await safeFetch(url, {}, REQUEST_TIMEOUT * 2, signal); - if (!response.ok) { - const errorMsg = getHttpErrorMessage(response.status); - logger.lifecycleError(getActiveIntegrateId(), String(response.status), errorMsg); - onError(new CskError(errorMsg, `http_${response.status}`)); - return; - } - const reader = (_a = response.body) === null || _a === void 0 ? void 0 : _a.getReader(); - if (!reader) { - onError(new CskError(t('error_stream_unsupported'), 'stream_unsupported')); - return; - } - const decoder = new TextDecoder('utf-8', { stream: true }); - let buffer = ''; - let eventLines = []; - /** 当前 SSE 事件类型(event:xxx 行声明的类型),空=默认 message 事件 */ - let currentEventType = ''; - 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) { - const trimmed = line.trim(); - // 空行 = SSE 事件边界,刷新当前事件 - if (!trimmed) { - if (eventLines.length > 0) { - // 非内容事件(status 等)不回调 onChunk,避免污染 AI 回复渲染 - if (currentEventType !== 'status' && currentEventType !== 'tool_call_result') { - const chunk = eventLines.join('\n'); - totalText += chunk; - onChunk(chunk); - } - eventLines = []; - } - currentEventType = ''; - continue; - } - // 忽略注释行 (: ...) - if (trimmed.startsWith(':')) { - // TODO: 信号行 ":ok" 或其他 - continue; - } - // [DONE] 信号(结束整次 SSE 流) - if (trimmed === '[DONE]') { - if (eventLines.length > 0 && currentEventType !== 'status' && currentEventType !== 'tool_call_result') { - const chunk = eventLines.join('\n'); - totalText += chunk; - onChunk(chunk); - eventLines = []; - } - currentEventType = ''; - break; - } - // SSE 协议字段:event / id / retry - if (trimmed.startsWith('event:')) { - // 记录当前事件类型,用于决定是否将后续 data 行传递给 AI 渲染 - currentEventType = trimmed.substring('event:'.length).trim(); - continue; - } - if (trimmed.startsWith('id:') || trimmed.startsWith('retry:')) - continue; - // 提取 content(data: 前缀行去掉前缀,裸行为 Flux 元素内换行的续行) - let content; - if (trimmed.startsWith('data:')) { - content = trimmed.substring('data:'.length); - // 去掉 data: 后面紧跟的一个空格(SSE 标准允许) - if (content.startsWith(' ')) - content = content.substring(1); - } - else { - content = trimmed; - } - eventLines.push(content); - } - } - if (buffer.trim()) { - const trimmed = buffer.trim(); - if (!trimmed.startsWith(':')) { - if (trimmed.startsWith('data:')) { - let content = trimmed.substring('data:'.length); - if (content.startsWith(' ')) - content = content.substring(1); - eventLines.push(content); - } - else if (trimmed !== '[DONE]' && !trimmed.startsWith('event:') && !trimmed.startsWith('id:') && !trimmed.startsWith('retry:')) { - eventLines.push(trimmed); - } - } - } - // 处理缓冲区剩余 - if (eventLines.length > 0 && currentEventType !== 'status' && currentEventType !== 'tool_call_result') { - const chunk = eventLines.join('\n'); - totalText += chunk; - onChunk(chunk); - eventLines = []; - } - } - catch (readErr) { - // 用户主动中断:视为正常结束,保留已生成内容 - if (signal === null || signal === void 0 ? void 0 : signal.aborted) { - logger.info(`流式被用户中断,保留已生成内容 length=${totalText.length}`); - } - else if (totalText.length > 0) { - onChunk('\n\n' + t('stream_unstable')); - } - else { - throw readErr; - } - } - finally { - reader.releaseLock(); - } - logger.lifecycleStreamDone(getActiveIntegrateId(), totalText.length); - onDone(); - } - catch (err) { - // 用户主动中断不触发 onError,走 onDone - if ((signal === null || signal === void 0 ? void 0 : signal.aborted) || (err instanceof CskError && err.type === 'aborted')) { - onDone(); - return; - } - if (err instanceof CskError) { - onError(err); - } - else { - logger.lifecycleError(getActiveIntegrateId(), 'unknown', String(err)); - onError(new CskError(t('error_network'), 'network')); - } - } - } - // ==================== P1: 知识库分类 ==================== - /** - * 获取知识库分类树 - */ - async function fetchCategoryTree() { - const url = buildUrl('/category/tree'); - try { - const response = await safeFetch(url); - if (!response.ok) - throw new CskError(getHttpErrorMessage(response.status), `http_${response.status}`); - const json = await response.json(); - if (json.success && Array.isArray(json.data)) { - logger.info(`加载分类树成功 count=${json.data.length}`); - return json.data; - } - return []; - } - catch (err) { - if (err instanceof CskError) - logger.error(`加载分类树失败: ${err.message}`); - else - logger.error('加载分类树失败', err); - return []; - } - } - /** - * 获取 RAG 引用来源 - */ - async function fetchRagSources(message, categoryId) { - const url = buildRagSourcesUrl(message, categoryId); - try { - const response = await safeFetch(url); - if (!response.ok) - throw new CskError(getHttpErrorMessage(response.status), `http_${response.status}`); - const json = await response.json(); - if (json.success && Array.isArray(json.data)) { - logger.info(`获取引用来源 count=${json.data.length}`); - return json.data; - } - return []; - } - catch (err) { - logger.error('获取引用来源失败', err); - return []; - } - } - // ==================== P0-002: 消息反馈 ==================== - /** - * 提交消息反馈(点赞/点踩),支持点踩原因分类 - */ - async function submitFeedbackApi(messageId, feedbackType, reasonCategory, reasonComment) { - if (!currentConfig) - return false; - const url = buildUrl('/feedback'); - try { - const body = { - messageId: String(messageId), - conversationId: currentConfig.chatId, - feedbackType, - }; - if (reasonCategory) - body.reasonCategory = reasonCategory; - if (reasonComment) - body.reasonComment = reasonComment; - const response = await safeFetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - if (!response.ok) { - logger.error(`反馈提交失败 status=${response.status}`); - return false; - } - const json = await response.json(); - return json.success || false; - } - catch (err) { - logger.error('反馈提交异常', err); - return false; - } - } - /** - * 获取会话列表(SDK 安全端点) - * 使用 /ai/sdk/conversation/list,强制按 accountId + roleId 过滤 - */ - async function fetchConversationList(page = 1, size = 20, accountId, roleId) { - const params = new URLSearchParams(); - params.set('page', String(page)); - params.set('size', String(size)); - if (accountId) - params.set('accountId', accountId); - if (roleId) - params.set('roleId', roleId); - const url = buildUrl(`/ai/sdk/conversation/list?${params.toString()}`); - try { - const response = await safeFetch(url); - if (!response.ok) - throw new CskError(getHttpErrorMessage(response.status), `http_${response.status}`); - const json = await response.json(); - return { - list: json.success && Array.isArray(json.data) ? json.data : [], - total: json.total || 0, - pages: json.pages || 0, - }; - } - catch (err) { - logger.error('加载会话列表失败', err); - return { list: [], total: 0, pages: 0 }; - } - } - /** - * 获取会话消息(SDK 安全端点) - * 使用 /ai/sdk/conversation/{id}/messages,含账户归属校验 - */ - async function fetchConversationMessages(conversationId) { - const params = new URLSearchParams(); - if (currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.userId) - params.set('accountId', currentConfig.userId); - if (getActiveIntegrateId()) - params.set('roleId', getActiveIntegrateId()); - const url = buildUrl(`/ai/sdk/conversation/${conversationId}/messages?${params.toString()}`); - try { - const response = await safeFetch(url); - if (!response.ok) - throw new CskError(getHttpErrorMessage(response.status), `http_${response.status}`); - const json = await response.json(); - return { - messages: json.success && Array.isArray(json.data) ? json.data : [], - total: json.total || 0, - }; - } - catch (err) { - logger.error('加载会话消息失败', err); - return { messages: [], total: 0 }; - } - } - /** - * 删除会话(SDK 安全端点) - * 使用 DELETE /ai/sdk/conversation/{id},含账户归属校验 - */ - async function deleteConversation(conversationId) { - const params = new URLSearchParams(); - if (currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.userId) - params.set('accountId', currentConfig.userId); - if (getActiveIntegrateId()) - params.set('roleId', getActiveIntegrateId()); - const url = buildUrl(`/ai/sdk/conversation/${conversationId}?${params.toString()}`); - try { - const response = await safeFetch(url, { method: 'DELETE' }); - if (!response.ok) - throw new CskError(getHttpErrorMessage(response.status), `http_${response.status}`); - const json = await response.json(); - logger.info(`删除会话 id=${conversationId} success=${json.success}`); - return json.success || false; - } - catch (err) { - logger.error('删除会话失败', err); - return false; - } - } - /** - * 导出会话 URL(SDK 安全端点) - * 使用 /ai/sdk/conversation/{id}/export,含账户归属参数 - */ - function getConversationExportUrl(conversationId) { - const params = new URLSearchParams(); - if (currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.userId) - params.set('accountId', currentConfig.userId); - if (getActiveIntegrateId()) - params.set('roleId', getActiveIntegrateId()); - return buildUrl(`/ai/sdk/conversation/${conversationId}/export?${params.toString()}`); - } - // ==================== chatId 自动初始化 ==================== - /** - * 初始化 chatId:查询后端已有会话,找到则复用,否则生成新的 - * - * @param forceRefresh 为 true 时跳过 localStorage 缓存,强制查询后端 - */ - async function initChatId(forceRefresh = false) { - if (!currentConfig) - return ''; - const activeId = getActiveIntegrateId(); - // 1. 先从 localStorage 恢复(非强制刷新时) - if (!forceRefresh) { - const cachedChatId = loadCachedChatId(activeId, currentConfig.userId); - if (cachedChatId) { - currentConfig.chatId = cachedChatId; - return cachedChatId; - } - } - // 2. 查询后端会话列表 - try { - const result = await fetchConversationList(1, 5, currentConfig.userId, activeId); - if (result.list.length > 0) { - // 使用最新会话的 conversationId 作为 chatId - const latestConv = result.list[0]; - const chatId = latestConv.conversationId || latestConv.chatId || ''; - if (chatId) { - currentConfig.chatId = chatId; - saveCachedChatId(activeId, currentConfig.userId, chatId); - logger.info(`从后端恢复会话 chatId=${chatId} messageCount=${latestConv.messageCount}`); - return chatId; - } - } - } - catch (err) { - } - // 3. 生成新的 chatId - const newChatId = generateChatId(); - currentConfig.chatId = newChatId; - saveCachedChatId(activeId, currentConfig.userId, newChatId); - return newChatId; - } - /** 生成 chatId(格式:sdk_timestamp_random) */ - function generateChatId() { - const random = typeof crypto !== 'undefined' && crypto.randomUUID - ? crypto.randomUUID().substring(0, 8) - : Math.random().toString(36).substring(2, 10); - return `sdk_${Date.now()}_${random}`; - } - /** localStorage key 格式 */ - function chatIdStorageKey(integrateId, userId) { - return `csk_chatId_${integrateId}${userId ? '_' + userId : ''}`; - } - /** 从 localStorage 加载 chatId */ - function loadCachedChatId(integrateId, userId) { - try { - return localStorage.getItem(chatIdStorageKey(integrateId, userId)) || ''; - } - catch (_a) { - return ''; - } - } - /** 保存 chatId 到 localStorage */ - function saveCachedChatId(integrateId, userId, chatId) { - try { - if (chatId) { - localStorage.setItem(chatIdStorageKey(integrateId, userId), chatId); - } - else { - localStorage.removeItem(chatIdStorageKey(integrateId, userId)); - } - } - catch (_a) { - // localStorage 不可用则忽略 - } - } - // ==================== 系统配置(SDK 公开接口) ==================== - /** - * 获取系统配置值(供 SDK 动态拉取声明等配置) - * 路径 /ai/system-config/{key},safeFetch 自动注入 Bearer Token; - * 无 Token 的 SDK 环境将被 SdkAuthFilter 拦截返回 401,此处静默降级为空字符串。 - */ - async function fetchSystemConfig(key) { - if (!currentConfig) - return ''; - try { - const url = buildUrl(`/ai/system-config/${encodeURIComponent(key)}`); - const response = await safeFetch(url); - if (!response.ok) - return ''; - const json = await response.json(); - return json.success && json.data ? (json.data.configValue || '') : ''; - } - catch (_a) { - return ''; - } - } - /** - * 获取 AI 推荐问题列表(suggest-message-list) - * 在 AI 回复结束后调用,从后端缓存中读取本次对话生成的建议问题 - * @param chatId 当前对话 ID - * @returns 推荐问题数组(最多 3 条),接口失败或功能关闭时返回空数组 - */ - async function fetchSuggestions(chatId) { - if (!currentConfig || !chatId) - return []; - try { - const params = new URLSearchParams(); - params.set('chatId', chatId); - const url = buildUrl(`/ai/suggestions?${params.toString()}`); - const response = await safeFetch(url); - if (!response.ok) - return []; - const json = await response.json(); - if (json.success && json.data && Array.isArray(json.data.suggestions)) { - return json.data.suggestions.filter(s => typeof s === 'string' && s.trim().length > 0); - } - return []; - } - catch (_a) { - // 静默降级,不阻塞对话 - return []; - } - } - - let styleElement = null; - /** 将 hex 颜色解析为 "r, g, b" 字符串,用于 rgba() 拼接 */ - function hexToRgb(hex) { - const match = hex.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/); - if (!match) - return '79, 70, 229'; - return `${parseInt(match[1], 16)}, ${parseInt(match[2], 16)}, ${parseInt(match[3], 16)}`; - } - /** CSS 变量:将配置中的主题色转换为 CSS 自定义属性 */ - function cssVars(config) { - const darker = adjustColor(config.primaryColor, -15); - const lighter = adjustColor(config.primaryColor, 18); - const rgb = hexToRgb(config.primaryColor); - return ` - --csk-primary: ${config.primaryColor}; - --csk-primary-hover: ${darker}; - --csk-primary-light: ${lighter}; - --csk-primary-rgb: ${rgb}; - --csk-bg-user: linear-gradient(135deg, ${config.primaryColor}, ${darker}); - --csk-bg-ai: #ffffff; - --csk-text-user: #ffffff; - --csk-text-ai: #1F2937; - --csk-window-width: ${config.width}px; - --csk-window-height: ${config.height}px; - --csk-radius: 16px; - --csk-shadow-window: 0 12px 48px rgba(15, 23, 42, 0.18), 0 2px 8px rgba(15, 23, 42, 0.06); - --csk-shadow-bubble: 0 1px 2px rgba(15, 23, 42, 0.06); - --csk-border: #ECEEF2; - --csk-bg-app: #F6F7F9; - `; - } - /** 简单的颜色加深(按通道加减) */ - function adjustColor(hex, amount) { - const match = hex.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/); - if (!match) { - return hex; - } - const clamp = (v) => Math.max(0, Math.min(255, v)); - const r = clamp(parseInt(match[1], 16) + amount); - const g = clamp(parseInt(match[2], 16) + amount); - const b = clamp(parseInt(match[3], 16) + amount); - return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; - } - /** 完整 CSS 样式表 */ - function getStyles(config) { - return ` -/* ChatbotSDK 样式 - csk- 命名空间 */ -.csk-root { - ${cssVars(config)} - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif; - font-size: 14px; - line-height: 1.55; - color: #1F2937; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - box-sizing: border-box; -} -.csk-root *, .csk-root *::before, .csk-root *::after { box-sizing: border-box; } - -/* ========== 悬浮按钮 ========== */ -.csk-launcher { - position: fixed; - bottom: 24px; - z-index: 9998; - width: 60px; - height: 60px; - border-radius: 50%; - /* 白色背景,简洁克制 */ - background: #ffffff; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - color: #fff; - user-select: none; - overflow: hidden; - /* 浅灰边框 */ - border: 2px solid #E5E7EB; - /* 简洁柔和阴影 */ - box-shadow: - 0 2px 8px rgba(0, 0, 0, 0.08), - 0 4px 16px rgba(0, 0, 0, 0.06); - transition: - transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), - box-shadow 0.25s ease, - border-color 0.25s ease; - animation: - csk-launcher-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) both; -} -.csk-launcher--right { right: 24px; } -.csk-launcher--left { left: 24px; } - -.csk-launcher:hover { - transform: translateY(-3px) scale(1.08); - border-color: #D1D5DB; - box-shadow: - 0 4px 12px rgba(0, 0, 0, 0.10), - 0 8px 24px rgba(0, 0, 0, 0.08); -} -.csk-launcher:active { transform: scale(0.93); } -.csk-launcher svg { - width: 30px; - height: 30px; - position: relative; - z-index: 1; - filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.15)); -} -.csk-launcher img { - width: 100%; - height: 100%; - object-fit: cover; - position: relative; - z-index: 1; - -webkit-user-drag: none; - user-select: none; -} - -@keyframes csk-launcher-in { - 0% { opacity: 0; transform: scale(0.5) translateY(16px); } - 60% { transform: scale(1.06) translateY(-2px); } - 100% { opacity: 1; transform: scale(1) translateY(0); } -} - -/* ========== 聊天弹窗 ========== */ -.csk-window { - position: fixed; - bottom: 24px; - z-index: 9999; - width: var(--csk-window-width); - height: var(--csk-window-height); - max-height: calc(100vh - 48px); - background: #fff; - border-radius: var(--csk-radius); - box-shadow: var(--csk-shadow-window); - display: flex; - flex-direction: column; - overflow: hidden; - transform-origin: bottom right; - transition: opacity 0.24s ease, transform 0.24s cubic-bezier(0.34, 1.2, 0.64, 1), visibility 0.24s; - opacity: 1; - transform: translateY(0) scale(1); - visibility: visible; - pointer-events: auto; -} -.csk-window--right { right: 24px; transform-origin: bottom right; } -.csk-window--left { left: 24px; transform-origin: bottom left; } -.csk-window--hidden { - opacity: 0; - transform: translateY(12px) scale(0.96); - visibility: hidden; - pointer-events: none; -} - -/* ========== 水印层 ========== */ -.csk-watermark { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - pointer-events: none; - user-select: none; - z-index: 0; - background-repeat: repeat; - opacity: 0.8; -} - -/* ========== 头部 ========== */ -.csk-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 16px; - min-height: 60px; - background: var(--csk-bg-user); - color: #fff; - cursor: move; - user-select: none; - position: relative; - flex-shrink: 0; -} -.csk-header::after { - content: ''; - position: absolute; - left: 0; right: 0; bottom: 0; - height: 24px; - background: linear-gradient(to bottom, rgba(0,0,0,0.04), transparent); - pointer-events: none; -} -.csk-header__left { - display: flex; - align-items: center; - gap: 12px; - min-width: 0; -} -.csk-header__avatar { - width: 38px; - height: 38px; - border-radius: 50%; - background: rgba(255, 255, 255, 0.22); - backdrop-filter: blur(4px); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25); -} -.csk-header__avatar svg { width: 22px; height: 22px; } -.csk-header__info { - display: flex; - flex-direction: column; - min-width: 0; -} -.csk-header__title { - font-size: 15px; - font-weight: 600; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - letter-spacing: 0.2px; -} -.csk-header__status { - display: inline-flex; - align-items: center; - gap: 5px; - font-size: 11.5px; - opacity: 0.9; - margin-top: 2px; -} -.csk-status-dot { - width: 7px; - height: 7px; - border-radius: 50%; - background: #6EE7B7; - box-shadow: 0 0 0 0 rgba(110, 231, 183, 0.7); - animation: csk-status-pulse 2s infinite; -} -@keyframes csk-status-pulse { - 0% { box-shadow: 0 0 0 0 rgba(110, 231, 183, 0.7); } - 70% { box-shadow: 0 0 0 6px rgba(110, 231, 183, 0); } - 100% { box-shadow: 0 0 0 0 rgba(110, 231, 183, 0); } -} -.csk-header__actions { - display: flex; - align-items: center; - gap: 2px; - flex-shrink: 0; -} -.csk-header__btn, -.csk-history-btn { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: none; - background: transparent; - color: #fff; - cursor: pointer; - border-radius: 8px; - transition: background 0.15s, transform 0.15s; -} -.csk-header__btn:hover, -.csk-history-btn:hover { - background: rgba(255, 255, 255, 0.2); -} -.csk-header__btn:active, -.csk-history-btn:active { transform: scale(0.92); } - -/* ========== 角色选择器 ========== */ -.csk-role-select-wrap { - display: flex; - align-items: center; - flex-shrink: 0; - margin: 0 4px; -} -.csk-role-select { - appearance: none; - -webkit-appearance: none; - padding: 4px 24px 4px 10px; - border: 1px solid rgba(255, 255, 255, 0.35); - border-radius: 8px; - background: rgba(255, 255, 255, 0.15) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='white' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat right 6px center; - color: #fff; - font-size: 12px; - font-family: inherit; - cursor: pointer; - outline: none; - transition: background 0.15s, border-color 0.15s; - max-width: 120px; - text-overflow: ellipsis; - white-space: nowrap; -} -.csk-role-select:hover { - background-color: rgba(255, 255, 255, 0.25); - border-color: rgba(255, 255, 255, 0.55); -} -.csk-role-select:focus { - border-color: rgba(255, 255, 255, 0.7); - box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2); -} -.csk-role-select option { - color: #1F2937; - background: #fff; -} - -/* ========== 消息区 ========== */ -.csk-messages { - flex: 1; - overflow-y: auto; - padding: 20px 16px 12px; - background: var(--csk-bg-app); - scroll-behavior: smooth; -} -.csk-messages::-webkit-scrollbar { width: 6px; } -.csk-messages::-webkit-scrollbar-track { background: transparent; } -.csk-messages::-webkit-scrollbar-thumb { - background: #D8DCE3; - border-radius: 3px; -} -.csk-messages::-webkit-scrollbar-thumb:hover { background: #C2C7D0; } - -/* 离线提示横幅 */ -.csk-offline-banner { - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - padding: 8px 12px; - margin: 0 0 8px 0; - border-radius: 8px; - font-size: 12px; - line-height: 1.5; - background: #FEF2F2; - color: #991B1B; - border: 1px solid #FECACA; -} -.csk-dark .csk-offline-banner { - background: #450A0A; - color: #FCA5A5; - border-color: #7F1D1D; -} - -/* 欢迎空状态 */ -.csk-welcome { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - padding: 48px 24px 32px; - color: #6B7280; - animation: csk-fade-in 0.3s ease; -} -.csk-welcome__avatar { - width: 56px; - height: 56px; - border-radius: 50%; - background: var(--csk-bg-user); - color: #fff; - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 14px; - box-shadow: 0 6px 18px rgba(var(--csk-primary-rgb), 0.3); -} -.csk-welcome__avatar svg { width: 30px; height: 30px; } -.csk-welcome__title { - font-size: 15px; - font-weight: 600; - color: #1F2937; - margin-bottom: 4px; -} -.csk-welcome__desc { - font-size: 13px; - color: #9CA3AF; - line-height: 1.6; -} - -/* 消息气泡 */ -.csk-msg { - display: flex; - margin-bottom: 12px; - max-width: 100%; - word-break: break-word; - animation: csk-msg-in 0.28s cubic-bezier(0.22, 1, 0.36, 1); -} -.csk-msg--user { - flex-direction: row-reverse; - align-items: flex-end; -} -.csk-msg--ai { - flex-direction: row; - align-items: flex-start; -} -.csk-msg__avatar { - width: 30px; - height: 30px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - margin: 0 8px; - box-shadow: var(--csk-shadow-bubble); -} -.csk-msg__avatar svg { width: 18px; height: 18px; } -.csk-msg__avatar--ai { - background: #fff; - color: var(--csk-primary); - border: 1px solid var(--csk-border); -} -.csk-msg__avatar--user { - background: var(--csk-bg-user); - color: #fff; -} -.csk-msg__content { - display: flex; - flex-direction: column; - min-width: 0; - max-width: calc(100% - 46px); -} -.csk-msg--user .csk-msg__content { align-items: flex-end; } -.csk-msg--ai .csk-msg__content { align-items: flex-start; } -.csk-msg__bubble { - padding: 10px 14px; - border-radius: 16px; - font-size: 14px; - line-height: 1.6; - box-shadow: var(--csk-shadow-bubble); -} -.csk-msg--user .csk-msg__bubble { - background: var(--csk-bg-user); - color: var(--csk-text-user); - border-radius: 16px 16px 4px 16px; -} -.csk-msg--ai .csk-msg__bubble { - background: var(--csk-bg-ai); - color: var(--csk-text-ai); - border-radius: 16px 16px 16px 4px; - border: 1px solid var(--csk-border); -} -.csk-msg__time { - font-size: 11px; - color: #9CA3AF; - margin-top: 3px; - padding: 0 4px; -} - -/* 元信息行:时间戳居左,操作按钮居右,共用一行 */ -.csk-msg__meta { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 3px; - width: 100%; -} -.csk-msg__meta .csk-msg__time { margin-top: 0; } - -@keyframes csk-msg-in { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: translateY(0); } -} -@keyframes csk-fade-in { - from { opacity: 0; } - to { opacity: 1; } -} - -/* ========== Loading 动画 ========== */ -.csk-loading { - display: flex; - align-items: center; - margin-bottom: 12px; - animation: csk-msg-in 0.28s ease; -} -.csk-loading__avatar { - width: 30px; - height: 30px; - border-radius: 50%; - background: #fff; - color: var(--csk-primary); - border: 1px solid var(--csk-border); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - margin: 0 8px; - box-shadow: var(--csk-shadow-bubble); -} -.csk-loading__avatar svg { width: 18px; height: 18px; } -.csk-loading__bubble { - background: #fff; - border: 1px solid var(--csk-border); - border-radius: 16px 16px 16px 4px; - padding: 12px 16px; - display: flex; - align-items: center; - gap: 5px; -} -.csk-loading__dot { - width: 7px; - height: 7px; - border-radius: 50%; - background: #B4B9C2; - animation: csk-bounce 1.4s ease-in-out infinite both; -} -.csk-loading__dot:nth-child(1) { animation-delay: 0s; } -.csk-loading__dot:nth-child(2) { animation-delay: 0.16s; } -.csk-loading__dot:nth-child(3) { animation-delay: 0.32s; } - -@keyframes csk-bounce { - 0%, 80%, 100% { transform: scale(0.6); opacity: 0.6; } - 40% { transform: scale(1); opacity: 1; } -} - -/* ========== 输入区 ========== */ -.csk-input-area { - padding: 10px 12px 14px; - background: #fff; - border-top: 1px solid var(--csk-border); - flex-shrink: 0; -} -.csk-input-wrap { - display: flex; - align-items: flex-end; - gap: 8px; - background: var(--csk-bg-app); - border: 1px solid var(--csk-border); - border-radius: 14px; - padding: 6px 6px 6px 14px; - transition: border-color 0.2s, box-shadow 0.2s, background 0.2s; -} -.csk-input-wrap--focus { - border-color: var(--csk-primary); - background: #fff; - box-shadow: 0 0 0 3px rgba(var(--csk-primary-rgb), 0.12); -} -.csk-input { - flex: 1; - border: none; - background: transparent; - padding: 8px 0; - font-size: 14px; - outline: none; - font-family: inherit; - resize: none; - min-height: 22px; - max-height: 120px; - line-height: 1.5; - color: #1F2937; -} -.csk-input::placeholder { color: #9CA3AF; } -.csk-root .csk-input:focus-visible { outline: none; } -.csk-send-btn { - display: flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - min-width: 36px; - border: none; - border-radius: 10px; - background: var(--csk-bg-user); - color: #fff; - cursor: pointer; - transition: transform 0.18s, box-shadow 0.18s, opacity 0.18s; - box-shadow: 0 3px 10px rgba(var(--csk-primary-rgb), 0.3); -} -.csk-send-btn:hover:not(:disabled) { - transform: translateY(-1px); - box-shadow: 0 5px 14px rgba(var(--csk-primary-rgb), 0.4); -} -.csk-send-btn:active:not(:disabled) { transform: scale(0.92); } -.csk-send-btn:disabled { - background: #D1D5DB; - box-shadow: none; - cursor: not-allowed; - opacity: 0.7; -} -.csk-send-btn svg { width: 18px; height: 18px; } - -/* ========== 保密声明脚注 ========== */ -.csk-disclaimer { - margin-top: 8px; - padding: 6px 8px 4px; - font-size: 10px; - color: #9CA3AF; - line-height: 1.5; - user-select: none; -} -.csk-disclaimer__title { - font-weight: 600; - margin-bottom: 2px; - color: #9CA3AF; -} -.csk-dark .csk-disclaimer { color: #6B7280; } -.csk-dark .csk-disclaimer__title { color: #6B7280; } - -/* ========== 新对话按钮 ========== */ -.csk-clear-btn { - display: inline-flex; - align-items: center; - gap: 5px; - padding: 5px 12px; - border: 1px solid var(--csk-border); - border-radius: 999px; - background: #fff; - color: #6B7280; - font-size: 12px; - cursor: pointer; - margin: 0 auto 8px; - transition: all 0.15s; - font-family: inherit; -} -.csk-clear-btn:hover { - background: #FEF2F2; - border-color: #FCA5A5; - color: #DC2626; -} - -/* ========== P1: 知识库分类下拉 ========== */ -.csk-category-bar { - display: flex; - align-items: center; - padding: 8px 12px; - border-top: 1px solid var(--csk-border); - background: #FBFBFC; - gap: 8px; - flex-shrink: 0; -} -.csk-category-bar__label { - font-size: 13px; - white-space: nowrap; -} -.csk-category-select { - flex: 1; - padding: 6px 28px 6px 10px; - border: 1px solid var(--csk-border); - border-radius: 8px; - font-size: 12.5px; - color: #374151; - background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239CA3AF' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E") no-repeat right 9px center; - appearance: none; - -webkit-appearance: none; - outline: none; - cursor: pointer; - font-family: inherit; - transition: border-color 0.2s, box-shadow 0.2s; - max-width: 220px; -} -.csk-category-select:focus { - border-color: var(--csk-primary); - box-shadow: 0 0 0 3px rgba(var(--csk-primary-rgb), 0.12); -} - -/* ========== P1: RAG 引用来源卡片 ========== */ -.csk-sources { - margin-top: 6px; - border: 1px solid var(--csk-border); - border-radius: 12px; - overflow: hidden; - font-size: 12px; - max-width: 100%; - background: #FAFBFC; -} -.csk-sources__header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 6px 10px; - background: #F3F4F7; - cursor: pointer; - user-select: none; - transition: background 0.15s; -} -.csk-sources__header:hover { background: #ECEEF2; } -.csk-sources__title { - display: flex; - align-items: center; - gap: 5px; - font-weight: 500; - color: #374151; -} -.csk-sources__arrow { - transition: transform 0.2s; - color: #9CA3AF; - font-size: 10px; -} -.csk-sources--collapsed .csk-sources__arrow { transform: rotate(-90deg); } -.csk-sources__body { - border-top: 1px solid var(--csk-border); - padding: 0; -} -.csk-sources--collapsed .csk-sources__body { display: none; } -.csk-source-item { - padding: 6px 10px; - border-bottom: 1px solid #F0F1F4; - transition: background 0.15s; -} -.csk-source-item:last-child { border-bottom: none; } -.csk-source-item:hover { background: #fff; } -.csk-source-item__name { - font-weight: 500; - color: #1F2937; - margin-bottom: 2px; -} -.csk-source-item__snippet { - color: #6B7280; - line-height: 1.5; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -} -.csk-source-item__meta { - font-size: 11px; - color: #9CA3AF; - margin-top: 2px; -} - -/* ========== P1: Markdown 渲染样式 ========== */ -.csk-msg--ai .csk-msg__bubble .csk-md-p { margin: 0 0 8px; } -.csk-msg--ai .csk-msg__bubble .csk-md-p:last-child { margin-bottom: 0; } -.csk-msg--ai .csk-msg__bubble .csk-md-h1, -.csk-msg--ai .csk-msg__bubble .csk-md-h2, -.csk-msg--ai .csk-msg__bubble .csk-md-h3, -.csk-msg--ai .csk-msg__bubble .csk-md-h4, -.csk-msg--ai .csk-msg__bubble .csk-md-h5, -.csk-msg--ai .csk-msg__bubble .csk-md-h6 { - margin: 14px 0 6px; - font-weight: 600; - line-height: 1.3; - color: #111827; -} -.csk-msg--ai .csk-msg__bubble .csk-md-h1 { font-size: 20px; } -.csk-msg--ai .csk-msg__bubble .csk-md-h2 { font-size: 17px; } -.csk-msg--ai .csk-msg__bubble .csk-md-h3 { font-size: 15px; } -.csk-msg--ai .csk-msg__bubble .csk-md-h4 { font-size: 14px; } - -.csk-md-code-block { - position: relative; - background: #1E293B; - color: #E2E8F0; - padding: 12px 14px; - border-radius: 10px; - overflow-x: auto; - margin: 8px 0; - font-size: 13px; - line-height: 1.55; - font-family: 'SF Mono', 'Consolas', 'Menlo', 'Monaco', monospace; -} -.csk-md-code-block code { - background: none; - padding: 0; - border-radius: 0; - font-size: inherit; - color: inherit; -} -.csk-md-inline-code { - background: rgba(var(--csk-primary-rgb), 0.1); - color: var(--csk-primary); - padding: 1px 6px; - border-radius: 5px; - font-size: 13px; - font-family: 'SF Mono', 'Consolas', 'Menlo', 'Monaco', monospace; -} -.csk-msg--ai .csk-msg__bubble .csk-md-ul, -.csk-msg--ai .csk-msg__bubble .csk-md-ol { - padding-left: 22px; - margin: 6px 0; -} -.csk-msg--ai .csk-msg__bubble .csk-md-ul li, -.csk-msg--ai .csk-msg__bubble .csk-md-ol li { margin-bottom: 4px; } -.csk-md-blockquote { - border-left: 3px solid var(--csk-primary); - padding: 2px 12px; - margin: 8px 0; - color: #6B7280; - background: rgba(var(--csk-primary-rgb), 0.05); - border-radius: 0 6px 6px 0; -} -.csk-md-link { - color: var(--csk-primary); - text-decoration: none; - border-bottom: 1px solid rgba(var(--csk-primary-rgb), 0.3); - transition: border-color 0.15s; -} -.csk-md-link:hover { border-bottom-color: var(--csk-primary); } -.csk-md-hr { - border: none; - border-top: 1px solid var(--csk-border); - margin: 12px 0; -} -.csk-md-table-wrap { - max-width: 100%; - overflow-x: auto; - margin: 10px 0; - border: 1px solid var(--csk-border); - border-radius: 8px; -} -.csk-md-table { - width: 100%; - min-width: 360px; - border-collapse: collapse; - font-size: 12.5px; - line-height: 1.5; -} -.csk-md-table th, -.csk-md-table td { - padding: 7px 9px; - border-right: 1px solid var(--csk-border); - border-bottom: 1px solid var(--csk-border); - text-align: left; - vertical-align: top; -} -.csk-md-table th:last-child, -.csk-md-table td:last-child { border-right: none; } -.csk-md-table tbody tr:last-child td { border-bottom: none; } -.csk-md-table th { - background: rgba(var(--csk-primary-rgb), 0.07); - color: #374151; - font-weight: 600; -} -.csk-md-table tbody tr:nth-child(even) { - background: rgba(148, 163, 184, 0.06); -} -.csk-md-table .csk-md-align-left { text-align: left; } -.csk-md-table .csk-md-align-center { text-align: center; } -.csk-md-table .csk-md-align-right { text-align: right; } - -/* ========== P2: 会话管理面板 ========== */ -.csk-history-panel { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: #fff; - z-index: 10; - display: flex; - flex-direction: column; - overflow: hidden; - animation: csk-slide-in 0.24s ease; -} -.csk-history-panel--hidden { display: none; } -@keyframes csk-slide-in { - from { transform: translateX(100%); opacity: 0.4; } - to { transform: translateX(0); opacity: 1; } -} -.csk-history-panel__header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 14px 16px; - border-bottom: 1px solid var(--csk-border); - background: var(--csk-bg-app); - flex-shrink: 0; -} -.csk-history-panel__title { - font-size: 14px; - font-weight: 600; - color: #1F2937; -} -.csk-history-panel__back { - display: flex; - align-items: center; - gap: 4px; - padding: 5px 12px; - border: 1px solid var(--csk-border); - border-radius: 8px; - background: #fff; - color: #374151; - font-size: 12px; - cursor: pointer; - transition: all 0.15s; - font-family: inherit; -} -.csk-history-panel__back:hover { background: #F3F4F6; } -.csk-history-panel__list { - flex: 1; - overflow-y: auto; - padding: 10px; -} -.csk-history-panel__list::-webkit-scrollbar { width: 5px; } -.csk-history-panel__list::-webkit-scrollbar-thumb { background: #E5E7EB; border-radius: 2px; } -.csk-history-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 11px 12px; - border-radius: 10px; - cursor: pointer; - transition: background 0.15s; - margin-bottom: 5px; - border: 1px solid transparent; -} -.csk-history-item:hover { background: #F3F4F6; } -.csk-history-item--active { - background: rgba(var(--csk-primary-rgb), 0.08); - border-color: rgba(var(--csk-primary-rgb), 0.2); -} -.csk-history-item--active:hover { background: rgba(var(--csk-primary-rgb), 0.12); } -.csk-history-item__info { flex: 1; min-width: 0; } -.csk-history-item__id { - font-size: 13px; - font-weight: 500; - color: #1F2937; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.csk-history-item__meta { - font-size: 11px; - color: #9CA3AF; - margin-top: 3px; -} -.csk-history-item__actions { - display: flex; - gap: 4px; - margin-left: 8px; - opacity: 0; - transition: opacity 0.15s; -} -.csk-history-item:hover .csk-history-item__actions { opacity: 1; } -.csk-history-action { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border: none; - border-radius: 7px; - cursor: pointer; - font-size: 12px; - transition: all 0.15s; -} -.csk-history-action--export { background: #EFF6FF; color: #2563EB; } -.csk-history-action--export:hover { background: #DBEAFE; } -.csk-history-action--delete { background: #FEF2F2; color: #DC2626; } -.csk-history-action--delete:hover { background: #FEE2E2; } -.csk-history-panel__empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 48px 20px; - color: #9CA3AF; - font-size: 13px; - text-align: center; -} -.csk-history-panel__empty-icon { - font-size: 34px; - margin-bottom: 10px; - opacity: 0.5; -} -.csk-history-panel__loading { - display: flex; - align-items: center; - justify-content: center; - padding: 24px; - color: #9CA3AF; - font-size: 13px; -} - -/* ========== 快捷问题芯片 ========== */ -.csk-quick-replies { - display: flex; - flex-wrap: wrap; - gap: 8px; - justify-content: center; - margin-top: 18px; - max-width: 320px; -} -.csk-quick-reply { - border: 1px solid var(--csk-border); - background: #fff; - color: #374151; - font-size: 12.5px; - padding: 7px 13px; - border-radius: 999px; - cursor: pointer; - font-family: inherit; - line-height: 1.3; - transition: all 0.18s ease; - max-width: 100%; -} -.csk-quick-reply:hover { - border-color: var(--csk-primary); - color: var(--csk-primary); - background: rgba(var(--csk-primary-rgb), 0.06); - transform: translateY(-1px); -} -.csk-quick-reply:active { transform: scale(0.96); } - -/* ========== 推荐问题(suggest-message-list) ========== */ -.csk-suggestions { - margin-top: 6px; - padding: 4px 0 0 0; -} -.csk-suggestions__list { - display: flex; - flex-wrap: wrap; - gap: 4px; -} -.csk-suggestion-item { - border: 1px solid var(--csk-border); - background: #fff; - color: #374151; - font-size: 12px; - padding: 4px 10px; - border-radius: 999px; - cursor: pointer; - font-family: inherit; - line-height: 1.3; - transition: all 0.18s ease; - max-width: 100%; - text-align: left; -} -.csk-suggestion-item:hover { - border-color: var(--csk-primary); - color: var(--csk-primary); - background: rgba(var(--csk-primary-rgb), 0.06); - transform: translateY(-1px); -} -.csk-suggestion-item:active { transform: scale(0.96); } - -/* ========== AI 消息操作条 ========== */ -.csk-msg__actions { - display: flex; - gap: 2px; - padding: 0 2px; - opacity: 0; - transition: opacity 0.15s ease; -} -.csk-msg--ai:hover .csk-msg__actions { opacity: 1; } -.csk-msg--streaming .csk-msg__actions { display: none; } -.csk-action-btn { - display: flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - border: none; - background: transparent; - color: #9CA3AF; - border-radius: 6px; - cursor: pointer; - transition: all 0.15s ease; -} -.csk-action-btn:hover { - background: #F3F4F6; - color: #374151; -} -.csk-action-btn--done { color: #10B981; } -.csk-action-btn:active { transform: scale(0.9); } - -/* ========== 流式打字光标 ========== */ -.csk-caret { - display: inline-block; - width: 7px; - height: 15px; - margin-left: 2px; - vertical-align: text-bottom; - background: var(--csk-primary); - border-radius: 1px; - animation: csk-caret-blink 1s step-end infinite; -} -@keyframes csk-caret-blink { - 0%, 50% { opacity: 1; } - 51%, 100% { opacity: 0; } -} - -/* ========== 代码块复制按钮 ========== */ -.csk-md-code-copy { - position: absolute; - top: 8px; - right: 8px; - display: flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - border: none; - background: rgba(255, 255, 255, 0.12); - color: #CBD5E1; - border-radius: 6px; - cursor: pointer; - opacity: 0; - transition: all 0.15s ease; -} -.csk-md-code-block:hover .csk-md-code-copy { opacity: 1; } -.csk-md-code-copy:hover { background: rgba(255, 255, 255, 0.22); color: #fff; } -.csk-md-code-copy:active { transform: scale(0.9); } - -/* ========== 新消息提示按钮 ========== */ -.csk-newmsg { - position: absolute; - bottom: 82px; - left: 50%; - transform: translateX(-50%); - z-index: 5; - display: inline-flex; - align-items: center; - gap: 5px; - padding: 7px 14px; - border: none; - background: var(--csk-primary); - color: #fff; - font-size: 12px; - font-family: inherit; - border-radius: 999px; - cursor: pointer; - box-shadow: 0 4px 14px rgba(var(--csk-primary-rgb), 0.38); - transition: transform 0.2s ease, box-shadow 0.2s ease; - animation: csk-msg-in 0.25s ease; - white-space: nowrap; -} -.csk-newmsg:hover { transform: translateX(-50%) translateY(-1px); box-shadow: 0 6px 18px rgba(var(--csk-primary-rgb), 0.48); } -.csk-newmsg:active { transform: translateX(-50%) scale(0.95); } -.csk-newmsg--hidden { display: none; } - -/* ========== 发送按钮停止态 ========== */ -.csk-send-btn--stop { - background: #EF4444 !important; - box-shadow: 0 3px 10px rgba(239, 68, 68, 0.35) !important; -} -.csk-send-btn--stop:hover:not(:disabled) { - box-shadow: 0 5px 14px rgba(239, 68, 68, 0.45) !important; -} - -/* ========== 窗口缩放拖拽手柄 ========== */ -.csk-resize-handle { - position: absolute; - right: 4px; - bottom: 4px; - width: 20px; - height: 20px; - display: flex; - align-items: flex-end; - justify-content: flex-end; - cursor: nwse-resize; - z-index: 5; - user-select: none; - -webkit-user-select: none; - /* 圆形半透明背景,与整体圆润风格一致 */ - border-radius: 50%; - background: transparent; - transition: background 0.2s ease; -} -.csk-resize-handle:hover { - background: rgba(var(--csk-primary-rgb), 0.08); -} - -/* 三斜线抓手 — 纯 CSS 绘制,用两条间距微调的线,视觉上是三条 */ -.csk-resize-grip { - display: block; - width: 10px; - height: 10px; - background: - linear-gradient(135deg, - transparent 0%, transparent 38%, - #C8CCD2 38%, #C8CCD2 42%, - transparent 42%, transparent 54%, - #C8CCD2 54%, #C8CCD2 58%, - transparent 58%, transparent 70%, - #C8CCD2 70%, #C8CCD2 74%, - transparent 74% - ); - transition: background 0.2s ease; - border-radius: 2px; -} -/* hover 时线条变为主色 */ -.csk-resize-handle:hover .csk-resize-grip { - background: - linear-gradient(135deg, - transparent 0%, transparent 38%, - var(--csk-primary) 38%, var(--csk-primary) 42%, - transparent 42%, transparent 54%, - var(--csk-primary) 54%, var(--csk-primary) 58%, - transparent 58%, transparent 70%, - var(--csk-primary) 70%, var(--csk-primary) 74%, - transparent 74% - ); -} - -/* 暗色模式 */ -.csk-dark .csk-resize-grip { - background: - linear-gradient(135deg, - transparent 0%, transparent 38%, - #5B6070 38%, #5B6070 42%, - transparent 42%, transparent 54%, - #5B6070 54%, #5B6070 58%, - transparent 58%, transparent 70%, - #5B6070 70%, #5B6070 74%, - transparent 74% - ); -} -.csk-dark .csk-resize-handle:hover .csk-resize-grip { - background: - linear-gradient(135deg, - transparent 0%, transparent 38%, - var(--csk-primary-light) 38%, var(--csk-primary-light) 42%, - transparent 42%, transparent 54%, - var(--csk-primary-light) 54%, var(--csk-primary-light) 58%, - transparent 58%, transparent 70%, - var(--csk-primary-light) 70%, var(--csk-primary-light) 74%, - transparent 74% - ); -} - -/* 缩放拖拽中:禁用过渡动画 + 全局光标 */ -.csk-window--resizing { - transition: none !important; - user-select: none; -} -.csk-window--resizing * { cursor: nwse-resize !important; } - -/* ========== 暗色模式 ========== */ -.csk-root.csk-dark { - --csk-bg-ai: #1E1E2E; - --csk-text-ai: #E2E8F0; - --csk-border: #3B3B52; - --csk-bg-app: #141421; - color: #E2E8F0; -} -.csk-dark .csk-window { - background: #1A1A2E; - box-shadow: 0 12px 48px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.2); -} -.csk-dark .csk-watermark { - opacity: 1; -} -.csk-dark .csk-header::after { background: linear-gradient(to bottom, rgba(0,0,0,0.12), transparent); } -.csk-dark .csk-msg__avatar--ai { - background: var(--csk-bg-ai); - color: var(--csk-primary); - border-color: var(--csk-border); -} -.csk-dark .csk-input-area { background: #1A1A2E; border-top-color: var(--csk-border); } -.csk-dark .csk-input-wrap { background: var(--csk-bg-app); border-color: var(--csk-border); } -.csk-dark .csk-input-wrap--focus { background: #1A1A2E; border-color: var(--csk-primary); } -.csk-dark .csk-input { color: #E2E8F0; } -.csk-dark .csk-loading__avatar { background: var(--csk-bg-ai); border-color: var(--csk-border); } -.csk-dark .csk-loading__bubble { background: var(--csk-bg-ai); border-color: var(--csk-border); } -.csk-dark .csk-loading__dot { background: #6B7280; } -.csk-dark .csk-category-bar { background: #1A1A2E; border-top-color: var(--csk-border); } -.csk-dark .csk-category-select { background-color: var(--csk-bg-app); border-color: var(--csk-border); color: #E2E8F0; } -.csk-dark .csk-sources { background: #1E1E2E; border-color: var(--csk-border); } -.csk-dark .csk-sources__header { background: #25253A; } -.csk-dark .csk-sources__header:hover { background: #2D2D48; } -.csk-dark .csk-source-item:hover { background: var(--csk-bg-ai); } -.csk-dark .csk-source-item__name { color: #E2E8F0; } -.csk-dark .csk-source-item__meta { color: #6B7280; } -.csk-dark .csk-history-panel { background: #1A1A2E; } -.csk-dark .csk-history-panel__header { background: var(--csk-bg-app); border-bottom-color: var(--csk-border); } -.csk-dark .csk-history-panel__back { background: var(--csk-bg-app); border-color: var(--csk-border); color: #E2E8F0; } -.csk-dark .csk-history-panel__back:hover { background: #2D2D48; } -.csk-dark .csk-history-item:hover { background: #25253A; } -.csk-dark .csk-history-item__id { color: #E2E8F0; } -.csk-dark .csk-history-item__meta { color: #6B7280; } -.csk-dark .csk-history-item--active { background: rgba(var(--csk-primary-rgb), 0.15); } -.csk-dark .csk-history-item--active:hover { background: rgba(var(--csk-primary-rgb), 0.22); } -.csk-dark .csk-action-btn:hover { background: #2D2D48; color: #E2E8F0; } -.csk-dark .csk-clear-btn { background: var(--csk-bg-app); border-color: var(--csk-border); color: #9CA3AF; } -.csk-dark .csk-clear-btn:hover { background: #3B1A1A; border-color: #7F1D1D; color: #FCA5A5; } -.csk-dark .csk-md-code-block { background: #0F0F1A; } -.csk-dark .csk-md-blockquote { background: rgba(var(--csk-primary-rgb), 0.08); } -.csk-dark .csk-md-link { color: var(--csk-primary-light); } -.csk-dark .csk-md-table th { background: rgba(var(--csk-primary-rgb), 0.14); color: #E2E8F0; } -.csk-dark .csk-md-table tbody tr:nth-child(even) { background: rgba(148, 163, 184, 0.05); } -.csk-dark .csk-msg--ai .csk-msg__bubble { background: var(--csk-bg-ai); color: var(--csk-text-ai); border-color: var(--csk-border); } -.csk-dark .csk-welcome__title { color: #E2E8F0; } -.csk-dark .csk-welcome__desc { color: #6B7280; } -.csk-dark .csk-quick-reply { background: var(--csk-bg-app); border-color: var(--csk-border); color: #CBD5E1; } -.csk-dark .csk-quick-reply:hover { background: rgba(var(--csk-primary-rgb), 0.12); border-color: var(--csk-primary); color: var(--csk-primary-light); } -.csk-dark .csk-suggestion-item { background: var(--csk-bg-app); border-color: var(--csk-border); color: #CBD5E1; } -.csk-dark .csk-suggestion-item:hover { background: rgba(var(--csk-primary-rgb), 0.12); border-color: var(--csk-primary); color: var(--csk-primary-light); } -.csk-dark .csk-teaser { background: #2D2D48; color: #E2E8F0; box-shadow: 0 4px 20px rgba(0,0,0,0.35); } -.csk-dark .csk-teaser::after { border-top-color: #2D2D48; } -.csk-dark .csk-teaser__close { color: #6B7280; } -.csk-dark .csk-teaser__close:hover { color: #9CA3AF; } -.csk-dark .csk-feedback-btn { background: rgba(var(--csk-primary-rgb), 0.1); color: #9CA3AF; } -.csk-dark .csk-feedback-btn:hover { background: rgba(var(--csk-primary-rgb), 0.2); color: var(--csk-primary-light); } -.csk-dark .csk-feedback-btn--active { background: rgba(var(--csk-primary-rgb), 0.28); color: var(--csk-primary-light); } -.csk-dark .csk-history-panel__search-wrap { background: var(--csk-bg-app); border-color: var(--csk-border); } -.csk-dark .csk-history-panel__search-wrap:focus-within { border-color: var(--csk-primary); } -.csk-dark .csk-history-panel__search { color: #E2E8F0; } -.csk-dark .csk-launcher__badge { box-shadow: 0 0 0 2px #1A1A2E; } -.csk-dark .csk-welcome__avatar { box-shadow: 0 6px 18px rgba(var(--csk-primary-rgb), 0.25); } -.csk-dark .csk-header__avatar { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.1); } -.csk-dark .csk-role-select option { - background: #1A1A2E; - color: #E2E8F0; -} - -/* Launcher 拖拽态:微放大 + 阴影加深 */ -.csk-launcher--dragging { - cursor: grabbing !important; - transform: scale(1.1) !important; - box-shadow: - 0 4px 12px rgba(0, 0, 0, 0.14), - 0 12px 32px rgba(0, 0, 0, 0.18), - 0 14px 40px rgba(236, 72, 153, 0.36), - 0 0 56px rgba(236, 72, 153, 0.18) !important; -} -/* 拖拽中隐藏未读徽章 */ -.csk-launcher--dragging .csk-launcher__badge { display: none !important; } -/* 吸附过渡动画(释放时的缓动) */ -.csk-launcher--snap { - transition: left 0.3s ease-out, right 0.3s ease-out, bottom 0.3s ease-out !important; -} - -/* ========== Launcher 未读徽章 ========== */ -.csk-launcher__badge { - position: absolute; - top: -2px; - right: -2px; - width: 16px; - height: 16px; - border-radius: 50%; - background: #EF4444; - border: 2px solid #fff; - box-shadow: 0 0 0 2px #fff; - animation: csk-badge-in 0.28s cubic-bezier(0.34, 1.56, 0.64, 1); - pointer-events: none; -} -.csk-launcher__badge--hidden { display: none; } -@keyframes csk-badge-in { - from { transform: scale(0); } - to { transform: scale(1); } -} - -/* ========== Launcher 提示气泡 ========== */ -.csk-teaser { - position: fixed; - bottom: 90px; - z-index: 9997; - max-width: 240px; - padding: 10px 14px; - background: #fff; - color: #1F2937; - font-size: 13px; - line-height: 1.5; - border-radius: 12px; - box-shadow: 0 4px 20px rgba(15, 23, 42, 0.15); - animation: csk-teaser-in 0.35s cubic-bezier(0.22, 1, 0.36, 1); - cursor: pointer; - user-select: none; -} -.csk-teaser--right { right: 24px; } -.csk-teaser--left { left: 24px; } -.csk-teaser::after { - content: ''; - position: absolute; - bottom: -6px; - right: 28px; - width: 12px; - height: 12px; - background: inherit; - border-radius: 2px; - transform: rotate(45deg); - box-shadow: 2px 2px 4px rgba(15, 23, 42, 0.08); -} -.csk-teaser--left::after { right: auto; left: 28px; } -.csk-teaser__close { - position: absolute; - top: 4px; - right: 6px; - background: none; - border: none; - font-size: 16px; - color: #9CA3AF; - cursor: pointer; - padding: 2px 4px; - line-height: 1; - border-radius: 4px; -} -.csk-teaser__close:hover { color: #374151; } -.csk-teaser--hidden { display: none; } -@keyframes csk-teaser-in { - from { opacity: 0; transform: translateY(8px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -/* ========== 消息反馈按钮(👍 / 👎) ========== */ -.csk-feedback-btn { - display: flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - border: none; - background: rgba(var(--csk-primary-rgb), 0.08); - color: #9CA3AF; - border-radius: 6px; - cursor: pointer; - transition: all 0.15s ease; - padding: 0; -} -.csk-feedback-btn:hover { background: rgba(var(--csk-primary-rgb), 0.18); color: var(--csk-primary); } -.csk-feedback-btn--active { - background: rgba(var(--csk-primary-rgb), 0.28); - color: var(--csk-primary); -} -.csk-feedback-btn--active.down { background: #FEF2F2; color: #EF4444; } -.csk-feedback-btn:active { transform: scale(0.9); } -.csk-msg--streaming .csk-feedback-btn { display: none; } - -/* ========== 点踩原因选择面板 ========== */ -.csk-feedback-reason { - position: relative; - margin-top: 6px; - padding: 10px 12px; - background: var(--csk-bg); - border: 1px solid var(--csk-border); - border-radius: 8px; - animation: cskFadeIn 0.15s ease; -} -.csk-feedback-reason__title { - font-size: 12px; - color: var(--csk-text-muted); - margin-bottom: 8px; -} -.csk-feedback-reason__options { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-bottom: 8px; -} -.csk-feedback-reason__btn { - padding: 4px 10px; - font-size: 12px; - border: 1px solid var(--csk-border); - border-radius: 14px; - background: var(--csk-bg); - color: var(--csk-text); - cursor: pointer; - transition: all 0.15s ease; -} -.csk-feedback-reason__btn:hover { - border-color: var(--csk-primary); - color: var(--csk-primary); - background: rgba(var(--csk-primary-rgb), 0.08); -} -.csk-feedback-reason__comment-row { - display: flex; - gap: 6px; -} -.csk-feedback-reason__comment { - flex: 1; - padding: 4px 8px; - font-size: 12px; - border: 1px solid var(--csk-border); - border-radius: 6px; - background: var(--csk-bg); - color: var(--csk-text); - outline: none; -} -.csk-feedback-reason__comment:focus { border-color: var(--csk-primary); } -.csk-feedback-reason__submit { - padding: 4px 10px; - font-size: 12px; - border: none; - border-radius: 6px; - background: var(--csk-primary); - color: #fff; - cursor: pointer; - white-space: nowrap; -} -.csk-feedback-reason__close { - position: absolute; - top: 8px; - right: 8px; - width: 18px; - height: 18px; - border: none; - background: none; - color: var(--csk-text-muted); - cursor: pointer; - padding: 0; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; -} -.csk-feedback-reason__close:hover { color: var(--csk-text); background: var(--csk-hover); } -.csk-dark .csk-feedback-reason { background: var(--csk-bg); border-color: var(--csk-border); } -.csk-dark .csk-feedback-reason__btn { - background: rgba(255,255,255,0.06); - border-color: var(--csk-border); - color: var(--csk-text); -} -.csk-dark .csk-feedback-reason__btn:hover { - border-color: var(--csk-primary-light); - color: var(--csk-primary-light); - background: rgba(var(--csk-primary-rgb), 0.15); -} -.csk-dark .csk-feedback-reason__comment { - background: rgba(255,255,255,0.06); - border-color: var(--csk-border); - color: var(--csk-text); -} - -/* ========== 历史会话搜索框 ========== */ -.csk-history-panel__search-wrap { - padding: 8px 10px; - border-bottom: 1px solid var(--csk-border); - flex-shrink: 0; -} -.csk-history-panel__search-wrap:focus-within { - border-bottom-color: var(--csk-primary); -} -.csk-history-panel__search { - width: 100%; - padding: 7px 10px; - border: 1px solid var(--csk-border); - border-radius: 8px; - font-size: 12.5px; - font-family: inherit; - color: #1F2937; - background: var(--csk-bg-app); - outline: none; - transition: border-color 0.2s; -} -.csk-history-panel__search:focus { border-color: var(--csk-primary); } -.csk-history-panel__search::placeholder { color: #9CA3AF; } - -/* ========== a11y 焦点指示 ========== */ -.csk-root :focus-visible { - outline: 2px solid var(--csk-primary); - outline-offset: 2px; -} -.csk-root button:focus-visible { - outline: 2px solid var(--csk-primary); - outline-offset: 2px; - border-radius: inherit; -} -/* 弹窗打开时隐藏 launcher 上的焦点环 */ -.csk-launcher:focus-visible { - outline: none; - border-color: rgba(255, 255, 255, 0.75); - box-shadow: - 0 1px 2px rgba(0, 0, 0, 0.06), - 0 4px 16px rgba(0, 0, 0, 0.10), - 0 6px 24px rgba(var(--csk-primary-rgb), 0.28), - 0 0 48px rgba(var(--csk-primary-rgb), 0.12), - 0 0 0 4px rgba(var(--csk-primary-rgb), 0.3); -} - -/* ========== a11y aria-live 播报区(视觉隐藏) ========== */ -.csk-sr-only { - position: absolute !important; - width: 1px !important; - height: 1px !important; - padding: 0 !important; - margin: -1px !important; - overflow: hidden !important; - clip: rect(0,0,0,0) !important; - white-space: nowrap !important; - border: 0 !important; -} - -/* ========== 会话分组标题 ========== */ -.csk-history-group { - margin-bottom: 4px; -} -.csk-history-group__label { - font-size: 11px; - font-weight: 600; - color: #9CA3AF; - text-transform: uppercase; - letter-spacing: 0.5px; - padding: 8px 12px 4px; - user-select: none; -} -.csk-dark .csk-history-group__label { color: #6B7280; } - -/* ========== 移动端适配 ========== */ -@media (max-width: 480px) { - .csk-window { - width: 100vw !important; - height: 100vh !important; - max-height: 100vh; - bottom: 0 !important; - right: 0 !important; - left: 0 !important; - border-radius: 0; - } - .csk-resize-handle { display: none !important; } - .csk-window--hidden { transform: translateY(100%); } - /* 移动端默认位置,仅在未被 JS 拖拽覆盖时生效 */ - .csk-launcher:not([style*="bottom"]) { bottom: 20px; } - .csk-launcher--right:not([style*="bottom"]) { right: 20px; } - .csk-launcher--left:not([style*="bottom"]) { left: 20px; } -} -`; - } - /** - * 注入样式到 document.head - */ - function injectStyles(config) { - // 避免重复注入 - if (document.querySelector('style[data-csk-sdk]')) { - return; - } - styleElement = document.createElement('style'); - styleElement.setAttribute('data-csk-sdk', ''); - styleElement.textContent = getStyles(config); - document.head.appendChild(styleElement); - } - /** - * 移除注入的样式 - */ - function removeStyles() { - if (styleElement && styleElement.parentNode) { - styleElement.parentNode.removeChild(styleElement); - styleElement = null; - } - document.querySelectorAll('style[data-csk-sdk]').forEach((el) => el.remove()); - } - - /** - * 工具函数模块 - */ - /** 生成完整 UUID */ - function uuid() { - if (typeof crypto !== 'undefined' && crypto.randomUUID) { - return crypto.randomUUID(); - } - // fallback - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = (Math.random() * 16) | 0; - const v = c === 'x' ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); - } - /** XSS 转义 - 防止用户输入中的 HTML 注入 */ - function escapeHtml(text) { - const map = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - }; - return text.replace(/[&<>"']/g, (ch) => map[ch] || ch); - } - /** 防抖函数 */ - function debounce(fn, delay) { - let timer = null; - return function (...args) { - if (timer !== null) { - clearTimeout(timer); - } - timer = setTimeout(() => { - fn.apply(this, args); - timer = null; - }, delay); - }; - } - /** 格式化时间戳为 HH:mm */ - function formatTime(timestamp) { - const d = new Date(timestamp); - const hh = String(d.getHours()).padStart(2, '0'); - const mm = String(d.getMinutes()).padStart(2, '0'); - return `${hh}:${mm}`; - } - /** 获取当前时间戳(毫秒) */ - function now() { - return Date.now(); - } - - // ==================== 图标常量 ==================== - /** 机器人头像图标(用于头部、欢迎态、AI 气泡、Loading) */ - const BOT_ICON = ``; - /** 用户头像图标 */ - const USER_ICON = ``; - // ==================== 悬浮按钮 ==================== - /** 创建悬浮按钮 */ - function createLauncher(config, onClick) { - const launcher = document.createElement('div'); - launcher.id = 'csk-launcher'; - launcher.className = `csk-launcher csk-launcher--${config.position === 'left-bottom' ? 'left' : 'right'}`; - launcher.setAttribute('title', config.title); - launcher.setAttribute('aria-label', config.title); - launcher.setAttribute('role', 'button'); - launcher.setAttribute('tabindex', '0'); - // 图标内容 - launcher.innerHTML = config.launcherIcon; - // 未读徽章(初始隐藏,有未读消息时由 index.ts 显示) - const badge = document.createElement('div'); - badge.className = 'csk-launcher__badge csk-launcher__badge--hidden'; - launcher.appendChild(badge); - // 点击事件(300ms 防抖) - const debouncedClick = debounce(onClick, 300); - launcher.addEventListener('click', debouncedClick); - // 键盘支持 - launcher.addEventListener('keydown', (e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - debouncedClick(); - } - }); - return launcher; - } - // ==================== 水印生成 ==================== - /** 生成水印纹理背景图(Canvas 离屏渲染,平铺用) */ - function createWatermarkPattern(config) { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - if (!ctx) - return ''; - // 组装水印文本:配置值 + 日期 时分 - const now = new Date(); - const y = now.getFullYear(); - const m = String(now.getMonth() + 1).padStart(2, '0'); - const d = String(now.getDate()).padStart(2, '0'); - const h = String(now.getHours()).padStart(2, '0'); - const min = String(now.getMinutes()).padStart(2, '0'); - const dateStr = `${y}-${m}-${d} ${h}:${min}`; - const watermarkText = config.watermark ? `${config.watermark} ${dateStr}` : dateStr; - // 一个平铺单元的尺寸 - const cellW = 280; - const cellH = 160; - canvas.width = cellW; - canvas.height = cellH; - // 按主题选择透明度 - const isDark = config.theme === 'dark'; - ctx.fillStyle = isDark ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.06)'; - ctx.font = '13px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - // 倾斜绘制 - ctx.save(); - ctx.translate(cellW / 2, cellH / 2); - ctx.rotate(-22 * Math.PI / 180); - ctx.fillText(watermarkText, 0, 0); - ctx.restore(); - return canvas.toDataURL('image/png'); - } - // ==================== 聊天弹窗 ==================== - /** 创建聊天弹窗完整结构,返回各区域引用 */ - function createChatWindow(config) { - // 最外层容器 - const windowEl = document.createElement('div'); - windowEl.id = 'csk-window'; - windowEl.className = `csk-root csk-window csk-window--${config.position === 'left-bottom' ? 'left' : 'right'} csk-window--hidden${config.theme === 'dark' ? ' csk-dark' : ''}`; - windowEl.setAttribute('role', 'dialog'); - windowEl.setAttribute('aria-label', config.title); - windowEl.setAttribute('aria-modal', 'false'); - // === 水印层(仅当配置了 watermark 时创建,绝对定位背景,不拦截交互) === - if (config.watermark) { - const watermark = document.createElement('div'); - watermark.className = 'csk-watermark'; - const watermarkBg = createWatermarkPattern(config); - if (watermarkBg) { - watermark.style.backgroundImage = `url(${watermarkBg})`; - } - windowEl.appendChild(watermark); - } - // === 头部 === - const header = document.createElement('div'); - header.className = 'csk-header'; - const headerLeft = document.createElement('div'); - headerLeft.className = 'csk-header__left'; - const headerAvatar = document.createElement('div'); - headerAvatar.className = 'csk-header__avatar'; - headerAvatar.innerHTML = BOT_ICON; - const headerInfo = document.createElement('div'); - headerInfo.className = 'csk-header__info'; - const titleEl = document.createElement('span'); - titleEl.className = 'csk-header__title'; - titleEl.textContent = config.title; - const statusEl = document.createElement('span'); - statusEl.className = 'csk-header__status'; - statusEl.innerHTML = `${t('status_online')}`; - headerInfo.appendChild(titleEl); - headerInfo.appendChild(statusEl); - headerLeft.appendChild(headerAvatar); - headerLeft.appendChild(headerInfo); - // === 角色选择器(仅当有多个角色时显示) === - let roleSelect = null; - const roles = config.roles; - if (roles && roles.length > 1) { - roleSelect = document.createElement('select'); - roleSelect.className = 'csk-role-select'; - roleSelect.setAttribute('aria-label', t('role_selector_label')); - for (const role of roles) { - const option = document.createElement('option'); - option.value = String(role.id); - option.textContent = role.name || String(role.id); - if (String(role.id) === String(config.integrateId)) { - option.selected = true; - } - roleSelect.appendChild(option); - } - // onChange 触发自定义事件 - roleSelect.addEventListener('change', () => { - const selectedValue = roleSelect.value; - windowEl.dispatchEvent(new CustomEvent('csk:roleChange', { - detail: { roleId: selectedValue } - })); - }); - // 包装后插入到 headerLeft 之后、actions 之前 - const roleWrap = document.createElement('div'); - roleWrap.className = 'csk-role-select-wrap'; - roleWrap.appendChild(roleSelect); - header.appendChild(headerLeft); - header.appendChild(roleWrap); - } - else { - header.appendChild(headerLeft); - } - const actions = document.createElement('div'); - actions.className = 'csk-header__actions'; - // 历史会话按钮(P2) - const historyBtn = document.createElement('button'); - historyBtn.className = 'csk-history-btn'; - historyBtn.setAttribute('title', t('history_title')); - historyBtn.innerHTML = ``; - // 最小化按钮 - const minimizeBtn = document.createElement('button'); - minimizeBtn.className = 'csk-header__btn csk-header__btn--minimize'; - minimizeBtn.setAttribute('title', t('minimize')); - minimizeBtn.innerHTML = ``; - minimizeBtn.addEventListener('click', () => { - windowEl.classList.add('csk-window--hidden'); - }); - // 关闭按钮 - const closeBtn = document.createElement('button'); - closeBtn.className = 'csk-header__btn csk-header__btn--close'; - closeBtn.setAttribute('title', t('close')); - closeBtn.innerHTML = ``; - closeBtn.addEventListener('click', () => { - windowEl.classList.add('csk-window--hidden'); - }); - actions.appendChild(historyBtn); - actions.appendChild(minimizeBtn); - actions.appendChild(closeBtn); - header.appendChild(actions); - // === 消息区 === - const messagesContainer = document.createElement('div'); - messagesContainer.id = 'csk-messages'; - messagesContainer.className = 'csk-messages'; - // === 欢迎空状态 === - const welcomeEl = document.createElement('div'); - welcomeEl.className = 'csk-welcome'; - welcomeEl.innerHTML = ` -
    ${BOT_ICON}
    -
    ${t('welcome_title')}
    -
    ${t('welcome_desc')}
    - `; - // 快捷问题芯片(点击即自动发送) - if (config.quickReplies.length > 0) { - const chips = document.createElement('div'); - chips.className = 'csk-quick-replies'; - for (const reply of config.quickReplies) { - const chip = document.createElement('button'); - chip.type = 'button'; - chip.className = 'csk-quick-reply'; - chip.textContent = reply; - chip.addEventListener('click', () => { - windowEl.dispatchEvent(new CustomEvent('csk:quickReply', { detail: { text: reply } })); - }); - chips.appendChild(chip); - } - welcomeEl.appendChild(chips); - } - messagesContainer.appendChild(welcomeEl); - // === 新消息提示按钮(用户上滑时新消息到达显示) === - // 挂到 windowEl 而非 messagesContainer,绝对定位悬浮于输入区上方,避免随内容滚动 - const newMsgBtn = document.createElement('button'); - newMsgBtn.type = 'button'; - newMsgBtn.className = 'csk-newmsg csk-newmsg--hidden'; - newMsgBtn.innerHTML = ` - - ${t('new_message')} - `; - windowEl.appendChild(newMsgBtn); - // === 会话管理面板(P2,默认隐藏) === - const historyPanel = document.createElement('div'); - historyPanel.className = 'csk-history-panel csk-history-panel--hidden'; - historyPanel.innerHTML = ` -
    - ${t('history_title')} - -
    -
    - -
    -
    - `; - messagesContainer.appendChild(historyPanel); - // 历史面板返回按钮 - const backBtn = historyPanel.querySelector('#csk-history-back'); - if (backBtn) { - backBtn.addEventListener('click', () => { - historyPanel.classList.add('csk-history-panel--hidden'); - }); - } - // 历史按钮点击 - historyBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const isHidden = historyPanel.classList.contains('csk-history-panel--hidden'); - historyPanel.classList.toggle('csk-history-panel--hidden'); - if (isHidden) { - // 触发自定义事件,通知加载会话列表 - windowEl.dispatchEvent(new CustomEvent('csk:loadHistory')); - } - }); - // === 知识库分类下拉框(P1) === - let categorySelect = null; - if (config.showCategorySwitch) { - const categoryBar = document.createElement('div'); - categoryBar.className = 'csk-category-bar'; - const categoryLabel = document.createElement('span'); - categoryLabel.className = 'csk-category-bar__label'; - categoryLabel.textContent = '📚'; - categorySelect = document.createElement('select'); - categorySelect.id = 'csk-category-select'; - categorySelect.className = 'csk-category-select'; - categorySelect.innerHTML = ``; - // onChange 触发自定义事件 - categorySelect.addEventListener('change', () => { - const selectedId = categorySelect.value; - windowEl.dispatchEvent(new CustomEvent('csk:categoryChange', { - detail: { categoryId: selectedId ? Number(selectedId) : undefined } - })); - }); - categoryBar.appendChild(categoryLabel); - categoryBar.appendChild(categorySelect); - // 插入到 messages 和 inputArea 之间 - windowEl.appendChild(header); - windowEl.appendChild(messagesContainer); - windowEl.appendChild(categoryBar); - } - else { - windowEl.appendChild(header); - windowEl.appendChild(messagesContainer); - } - // === 输入区 === - const inputArea = document.createElement('div'); - inputArea.className = 'csk-input-area'; - const inputWrap = document.createElement('div'); - inputWrap.className = 'csk-input-wrap'; - const inputEl = document.createElement('textarea'); - inputEl.id = 'csk-input'; - inputEl.className = 'csk-input'; - inputEl.setAttribute('placeholder', t('placeholder')); - inputEl.setAttribute('rows', '1'); - inputEl.setAttribute('autofocus', ''); - const sendBtn = document.createElement('button'); - sendBtn.id = 'csk-send-btn'; - sendBtn.className = 'csk-send-btn'; - sendBtn.setAttribute('title', t('send')); - sendBtn.setAttribute('disabled', 'true'); - sendBtn.innerHTML = ``; - inputWrap.appendChild(inputEl); - inputWrap.appendChild(sendBtn); - inputArea.appendChild(inputWrap); - // === 保密声明脚注 === - const disclaimer = document.createElement('div'); - disclaimer.className = 'csk-disclaimer'; - if (config.disclaimer !== undefined) { - // 用户自定义或传空串隐藏 - disclaimer.innerHTML = config.disclaimer; - if (!config.disclaimer) { - disclaimer.style.display = 'none'; - } - } - else { - // 未传值,等异步从后端拉取,先隐藏 - disclaimer.style.display = 'none'; - } - inputArea.appendChild(disclaimer); - windowEl.appendChild(inputArea); - // 新对话按钮(可选) - let clearBtn = null; - if (config.showClear) { - clearBtn = document.createElement('button'); - clearBtn.className = 'csk-clear-btn'; - clearBtn.textContent = t('clear'); - clearBtn.style.display = 'none'; // 初始隐藏,有消息后才显示 - // 插入到 categoryBar/inputArea 之前 - windowEl.insertBefore(clearBtn, inputArea); - } - // === Loading 动画 === - let loadingEl = null; - function showLoading() { - if (loadingEl) { - loadingEl.style.display = 'flex'; - return loadingEl; - } - const el = document.createElement('div'); - el.className = 'csk-loading'; - el.innerHTML = ` -
    ${BOT_ICON}
    -
    -
    -
    -
    -
    - `; - messagesContainer.appendChild(el); - loadingEl = el; - return el; - } - function hideLoading() { - if (loadingEl && loadingEl.parentNode) { - loadingEl.parentNode.removeChild(loadingEl); - loadingEl = null; - } - } - // === a11y: aria-live 播报区域(视觉隐藏,供屏幕阅读器监听新消息) === - const ariaLiveEl = document.createElement('div'); - ariaLiveEl.className = 'csk-sr-only'; - ariaLiveEl.setAttribute('aria-live', 'polite'); - ariaLiveEl.setAttribute('aria-atomic', 'true'); - windowEl.appendChild(ariaLiveEl); - // === 历史搜索输入框引用 === - const searchInput = historyPanel.querySelector('#csk-history-search'); - // === Launcher 提示气泡(首访引导,初始隐藏,由 index.ts 定时控制) === - const teaserEl = document.createElement('div'); - teaserEl.className = `csk-teaser csk-teaser--${config.position === 'left-bottom' ? 'left' : 'right'} csk-teaser--hidden`; - teaserEl.setAttribute('role', 'status'); - teaserEl.innerHTML = ` - - ${config.teaserText || t('teaser_text')} - `; - // === 缩放拖拽手柄(右下角) === - const resizeHandle = document.createElement('div'); - resizeHandle.className = 'csk-resize-handle'; - resizeHandle.setAttribute('aria-label', t('resize')); - // 纯 CSS 渲染三条斜线,无需 SVG - resizeHandle.innerHTML = ''; - windowEl.appendChild(resizeHandle); - return { - window: windowEl, - messagesContainer, - inputEl, - sendBtn, - clearBtn, - categorySelect, - historyPanel, - welcomeEl, - newMsgBtn, - teaserEl, - resizeHandle, - searchInput, - ariaLiveEl, - roleSelect, - disclaimer, - showLoading, - hideLoading, - }; - } - // ==================== 拖拽支持 ==================== - /** 启用弹窗拖拽,onDragEnd 回调返回最终位置用于持久化 */ - function enableDrag(headerEl, windowEl, onDragEnd) { - let dragging = false; - let startX = 0; - let startY = 0; - let offsetX = 0; - let offsetY = 0; - const onMouseDown = (e) => { - // 忽略头部按钮点击触发的拖拽 - const target = e.target; - if (target.closest('button')) - return; - dragging = true; - startX = e.clientX; - startY = e.clientY; - const rect = windowEl.getBoundingClientRect(); - offsetX = startX - rect.left; - offsetY = startY - rect.top; - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - }; - const onMouseMove = (e) => { - if (!dragging) - return; - const x = e.clientX - offsetX; - const y = e.clientY - offsetY; - // 边界限制,防止拖出视口 - const maxX = window.innerWidth - windowEl.offsetWidth; - const maxY = window.innerHeight - windowEl.offsetHeight; - windowEl.style.right = 'auto'; - windowEl.style.bottom = 'auto'; - windowEl.style.left = `${Math.max(0, Math.min(x, maxX))}px`; - windowEl.style.top = `${Math.max(0, Math.min(y, maxY))}px`; - }; - const onMouseUp = () => { - dragging = false; - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - // 拖拽结束回调:返回当前位置供持久化 - if (onDragEnd) { - const rect = windowEl.getBoundingClientRect(); - onDragEnd({ x: rect.left, y: rect.top }); - } - }; - headerEl.addEventListener('mousedown', onMouseDown); - // 清理函数 - return () => { - headerEl.removeEventListener('mousedown', onMouseDown); - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - }; - } - /** Launcher 拖拽:垂直方向最小 bottom 值(px),避免贴底被系统导航栏遮挡 */ - const LAUNCHER_MIN_BOTTOM = 16; - /** Launcher 拖拽:计算当前视口下最大 bottom 值 */ - function launcherMaxBottom() { - return window.innerHeight - 76; // 按钮高度 60 + 16 间距 - } - /** - * 启用 Launcher 按钮的拖拽功能(边缘吸附式) - * - * 设计要点: - * - 仅允许垂直方向自由移动,水平方向释放时自动吸附到最近的左/右边缘 - * - 移动距离 < 5px 视为点击(不干扰原有的 open/close 逻辑) - * - 拖拽中按钮微放大 + 隐藏未读徽章,释放后带缓动吸附动画 - * - 支持鼠标 + 触摸事件,触摸拖拽时阻止页面滚动 - */ - function enableLauncherDrag(launcherEl, config, onDragEnd) { - let dragging = false; - let startX = 0; - let startY = 0; - let hasMoved = false; - let startBottom = 0; - let startRight = 0; - let startLeft = 0; - // 标记是否正在吸附过渡中,避免过渡期间的 mouseup 再次触发 - let isSnapping = false; - /** 获取当前 bottom 值(纯数值) */ - function getCurrentBottom() { - const val = parseFloat(getComputedStyle(launcherEl).bottom); - return isNaN(val) ? 24 : val; - } - function onPointerDown(clientX, clientY) { - if (isSnapping) - return; - dragging = true; - hasMoved = false; - startX = clientX; - startY = clientY; - startBottom = getCurrentBottom(); - // 记录水平起始位置(用于判断当前在哪一侧) - const style = launcherEl.style; - const cs = getComputedStyle(launcherEl); - startRight = style.right ? parseFloat(style.right) : parseFloat(cs.right); - startLeft = style.left ? parseFloat(style.left) : parseFloat(cs.left); - } - function onPointerMove(clientX, clientY, e) { - if (!dragging) - return; - const dx = clientX - startX; - const dy = clientY - startY; - if (!hasMoved) { - if (Math.abs(dx) < 5 && Math.abs(dy) < 5) - return; - hasMoved = true; - // 进入拖拽态 - launcherEl.classList.add('csk-launcher--dragging'); - launcherEl.classList.remove('csk-launcher--right', 'csk-launcher--left'); - launcherEl.style.transition = 'none'; - // 同步当前水平位置(从吸附态的 right/left 转换为固定像素) - if (!isNaN(startRight) && startRight >= 0 && isNaN(startLeft)) { - launcherEl.style.right = `${startRight}px`; - launcherEl.style.left = 'auto'; - } - else if (!isNaN(startLeft) && startLeft >= 0) { - launcherEl.style.left = `${startLeft}px`; - launcherEl.style.right = 'auto'; - } - } - if (hasMoved) { - e.preventDefault(); // 阻止触摸滚动 - const newBottom = Math.max(LAUNCHER_MIN_BOTTOM, Math.min(startBottom - dy, launcherMaxBottom())); - launcherEl.style.bottom = `${newBottom}px`; - } - } - function onPointerUp() { - if (!dragging) - return; - dragging = false; - if (!hasMoved) - return; // 移动距离不足 5px,视为点击,不处理 - // 隐藏拖拽态(保留当前 bottom) - const currentBottom = parseFloat(launcherEl.style.bottom) || getCurrentBottom(); - const clampedBottom = Math.max(LAUNCHER_MIN_BOTTOM, Math.min(currentBottom, launcherMaxBottom())); - // 判断吸附到哪一侧(取离哪边更近) - const rect = launcherEl.getBoundingClientRect(); - const center = rect.left + rect.width / 2; - const snapSide = center <= window.innerWidth / 2 ? 'left' : 'right'; - // 切换到吸附过渡动画 - launcherEl.classList.remove('csk-launcher--dragging'); - launcherEl.classList.add(`csk-launcher--${snapSide}`, 'csk-launcher--snap'); - launcherEl.style.left = ''; - launcherEl.style.right = ''; - launcherEl.style.bottom = `${clampedBottom}px`; - // 过渡结束后清理(保留 bottom,否则 CSS 默认 24px 会覆盖拖拽位置) - isSnapping = true; - const onTransitionEnd = () => { - launcherEl.removeEventListener('transitionend', onTransitionEnd); - launcherEl.classList.remove('csk-launcher--snap'); - launcherEl.style.transition = ''; - // 注意:不清除 style.bottom,保留用户拖拽的垂直位置 - isSnapping = false; - }; - launcherEl.addEventListener('transitionend', onTransitionEnd); - // 兜底:350ms 后强制清理(transitionend 可能不触发) - setTimeout(() => { - if (isSnapping) - onTransitionEnd(); - }, 350); - // 阻止本次拖拽触发 click 事件(mousedown → mousemove → mouseup → click 的浏览器默认链路) - const preventClick = (ev) => { - ev.stopPropagation(); - launcherEl.removeEventListener('click', preventClick, true); - }; - launcherEl.addEventListener('click', preventClick, true); - // 恢复未读徽章 - const badge = launcherEl.querySelector('.csk-launcher__badge'); - if (badge) - badge.style.display = ''; - if (onDragEnd) { - onDragEnd({ side: snapSide, bottom: clampedBottom }); - } - } - // --- 鼠标事件 --- - function onMouseDown(e) { onPointerDown(e.clientX, e.clientY); } - function onMouseMove(e) { onPointerMove(e.clientX, e.clientY, e); } - function onMouseUp() { onPointerUp(); } - // --- 触摸事件 --- - function onTouchStart(e) { - if (e.touches.length === 1) - onPointerDown(e.touches[0].clientX, e.touches[0].clientY); - } - function onTouchMove(e) { - if (e.touches.length === 1) - onPointerMove(e.touches[0].clientX, e.touches[0].clientY, e); - } - function onTouchEnd() { onPointerUp(); } - launcherEl.addEventListener('mousedown', onMouseDown); - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - launcherEl.addEventListener('touchstart', onTouchStart, { passive: true }); - document.addEventListener('touchmove', onTouchMove, { passive: false }); - document.addEventListener('touchend', onTouchEnd); - return () => { - launcherEl.removeEventListener('mousedown', onMouseDown); - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - launcherEl.removeEventListener('touchstart', onTouchStart); - document.removeEventListener('touchmove', onTouchMove); - document.removeEventListener('touchend', onTouchEnd); - }; - } - // ==================== 窗口缩放支持 ==================== - /** 窗口最小尺寸 */ - const MIN_WIDTH = 300; - const MIN_HEIGHT = 300; - /** - * 启用窗口右下角拖拽缩放 - * @param handleEl 缩放手柄元素 - * @param windowEl 聊天窗口元素 - * @param onResizeEnd 缩放结束回调,返回最终宽高用于持久化 - * @returns 清理函数 - */ - function enableResize(handleEl, windowEl, onResizeEnd) { - let resizing = false; - let startX = 0; - let startY = 0; - let startWidth = 0; - let startHeight = 0; - const onPointerDown = (clientX, clientY) => { - resizing = true; - startX = clientX; - startY = clientY; - const rect = windowEl.getBoundingClientRect(); - startWidth = rect.width; - startHeight = rect.height; - windowEl.classList.add('csk-window--resizing'); - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - document.addEventListener('touchmove', onTouchMove, { passive: false }); - document.addEventListener('touchend', onTouchEnd); - }; - const onPointerMove = (clientX, clientY, e) => { - if (!resizing) - return; - if (e) - e.preventDefault(); - const dx = clientX - startX; - const dy = clientY - startY; - const newWidth = Math.max(MIN_WIDTH, Math.min(startWidth + dx, window.innerWidth - 24)); - const newHeight = Math.max(MIN_HEIGHT, Math.min(startHeight + dy, window.innerHeight - 24)); - windowEl.style.width = `${newWidth}px`; - windowEl.style.height = `${newHeight}px`; - // 同步更新 CSS 变量(供内部布局使用) - windowEl.style.setProperty('--csk-window-width', `${newWidth}px`); - windowEl.style.setProperty('--csk-window-height', `${newHeight}px`); - // 拖拽缩放后清除 bottom/right 定位,避免与固定定位冲突 - windowEl.style.bottom = ''; - windowEl.style.right = ''; - }; - const onPointerUp = () => { - if (!resizing) - return; - resizing = false; - windowEl.classList.remove('csk-window--resizing'); - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - document.removeEventListener('touchmove', onTouchMove); - document.removeEventListener('touchend', onTouchEnd); - const rect = windowEl.getBoundingClientRect(); - if (onResizeEnd) { - onResizeEnd({ width: rect.width, height: rect.height }); - } - }; - // 鼠标事件(DOM 事件监听器包装) - function onMouseMove(e) { onPointerMove(e.clientX, e.clientY, e); } - function onMouseUp() { onPointerUp(); } - // 鼠标事件 - function onMouseDown(e) { - e.preventDefault(); - e.stopPropagation(); - onPointerDown(e.clientX, e.clientY); - } - // 触摸事件 - function onTouchStart(e) { - e.preventDefault(); - e.stopPropagation(); - if (e.touches.length === 1) - onPointerDown(e.touches[0].clientX, e.touches[0].clientY); - } - function onTouchMove(e) { - if (e.touches.length === 1) - onPointerMove(e.touches[0].clientX, e.touches[0].clientY, e); - } - function onTouchEnd() { onPointerUp(); } - handleEl.addEventListener('mousedown', onMouseDown); - handleEl.addEventListener('touchstart', onTouchStart, { passive: false }); - return () => { - handleEl.removeEventListener('mousedown', onMouseDown); - handleEl.removeEventListener('touchstart', onTouchStart); - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - document.removeEventListener('touchmove', onTouchMove); - document.removeEventListener('touchend', onTouchEnd); - }; - } - // ==================== 消息渲染 ==================== - /** 渲染用户消息气泡 */ - function renderUserBubble(container, text, timestamp) { - const wrapper = document.createElement('div'); - wrapper.className = 'csk-msg csk-msg--user'; - const avatar = document.createElement('div'); - avatar.className = 'csk-msg__avatar csk-msg__avatar--user'; - avatar.innerHTML = USER_ICON; - const content = document.createElement('div'); - content.className = 'csk-msg__content'; - const bubble = document.createElement('div'); - bubble.className = 'csk-msg__bubble'; - bubble.textContent = text; - const time = document.createElement('div'); - time.className = 'csk-msg__time'; - time.textContent = formatTime(timestamp); - content.appendChild(bubble); - content.appendChild(time); - wrapper.appendChild(avatar); - wrapper.appendChild(content); - container.appendChild(wrapper); - return wrapper; - } - /** 渲染 AI 消息气泡(支持 Markdown) */ - function renderAIBubble(container, text, timestamp, renderMd, msgId) { - const wrapper = document.createElement('div'); - wrapper.className = 'csk-msg csk-msg--ai'; - if (msgId) - wrapper.dataset.cskMsgId = msgId; - const avatar = document.createElement('div'); - avatar.className = 'csk-msg__avatar csk-msg__avatar--ai'; - avatar.innerHTML = BOT_ICON; - const content = document.createElement('div'); - content.className = 'csk-msg__content'; - const bubble = document.createElement('div'); - bubble.className = 'csk-msg__bubble'; - // 支持 Markdown 渲染,传入渲染函数则使用,否则纯文本 - if (renderMd) { - bubble.innerHTML = renderMd(text); - } - else { - bubble.textContent = text; - } - const time = document.createElement('div'); - time.className = 'csk-msg__time'; - time.textContent = formatTime(timestamp); - content.appendChild(bubble); - // 元信息行:时间戳居左,操作按钮居右 - const meta = document.createElement('div'); - meta.className = 'csk-msg__meta'; - meta.appendChild(time); - meta.appendChild(buildAIActions(bubble, wrapper)); - content.appendChild(meta); - wrapper.appendChild(avatar); - wrapper.appendChild(content); - container.appendChild(wrapper); - // 代码块复制按钮 - enhanceCodeBlocks(bubble); - return wrapper; - } - /** 创建空的 AI 气泡(流式追加用) */ - function createEmptyAIBubble(container, timestamp, msgId) { - const wrapper = document.createElement('div'); - wrapper.className = 'csk-msg csk-msg--ai csk-msg--streaming'; - if (msgId) - wrapper.dataset.cskMsgId = msgId; - const avatar = document.createElement('div'); - avatar.className = 'csk-msg__avatar csk-msg__avatar--ai'; - avatar.innerHTML = BOT_ICON; - const content = document.createElement('div'); - content.className = 'csk-msg__content'; - const bubble = document.createElement('div'); - bubble.className = 'csk-msg__bubble'; - bubble.innerHTML = ''; - const time = document.createElement('div'); - time.className = 'csk-msg__time'; - time.textContent = formatTime(timestamp); - content.appendChild(bubble); - // 元信息行:时间戳居左,操作按钮居右 - const meta = document.createElement('div'); - meta.className = 'csk-msg__meta'; - meta.appendChild(time); - meta.appendChild(buildAIActions(bubble, wrapper)); - content.appendChild(meta); - wrapper.appendChild(avatar); - wrapper.appendChild(content); - container.appendChild(wrapper); - return { wrapper, bubble }; - } - // ==================== AI 消息操作条 + 代码块复制 ==================== - /** 复制图标 */ - const COPY_ICON = ``; - /** 重试图标 */ - const RETRY_ICON = ``; - /** 构建 AI 消息操作条(复制 / 重试 / 反馈) */ - function buildAIActions(bubble, wrapper) { - const bar = document.createElement('div'); - bar.className = 'csk-msg__actions'; - // 复制按钮 - const copyBtn = document.createElement('button'); - copyBtn.type = 'button'; - copyBtn.className = 'csk-action-btn'; - copyBtn.setAttribute('title', t('copy')); - copyBtn.setAttribute('aria-label', t('copy')); - copyBtn.innerHTML = COPY_ICON; - copyBtn.addEventListener('click', async (e) => { - e.stopPropagation(); - const text = bubble.textContent || ''; - await copyToClipboard(text, copyBtn); - }); - // 重试按钮 - const retryBtn = document.createElement('button'); - retryBtn.type = 'button'; - retryBtn.className = 'csk-action-btn'; - retryBtn.setAttribute('title', t('retry')); - retryBtn.setAttribute('aria-label', t('retry')); - retryBtn.innerHTML = RETRY_ICON; - retryBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const root = wrapper.closest('.csk-window'); - if (root) { - root.dispatchEvent(new CustomEvent('csk:retry', { - detail: { msgId: wrapper.dataset.cskMsgId || '' } - })); - } - }); - // 反馈按钮(👍 / 👎) - const THUMB_UP = ``; - const THUMB_DOWN = ``; - const upBtn = document.createElement('button'); - upBtn.type = 'button'; - upBtn.className = 'csk-feedback-btn'; - upBtn.setAttribute('title', t('feedback_up')); - upBtn.setAttribute('aria-label', t('feedback_up')); - upBtn.innerHTML = THUMB_UP; - upBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const root = wrapper.closest('.csk-window'); - if (root) { - root.dispatchEvent(new CustomEvent('csk:feedback', { - detail: { msgId: wrapper.dataset.cskMsgId || '', value: 'up' } - })); - } - }); - const downBtn = document.createElement('button'); - downBtn.type = 'button'; - downBtn.className = 'csk-feedback-btn'; - downBtn.setAttribute('title', t('feedback_down')); - downBtn.setAttribute('aria-label', t('feedback_down')); - downBtn.innerHTML = THUMB_DOWN; - downBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const root = wrapper.closest('.csk-window'); - if (root) { - root.dispatchEvent(new CustomEvent('csk:feedback', { - detail: { msgId: wrapper.dataset.cskMsgId || '', value: 'down' } - })); - } - }); - bar.appendChild(copyBtn); - bar.appendChild(retryBtn); - bar.appendChild(upBtn); - bar.appendChild(downBtn); - return bar; - } - /** - * 更新反馈按钮的视觉状态(由 chat.ts 调用) - * @param wrapper 消息 wrapper 元素 - * @param value 当前反馈值:'up' | 'down' | undefined - */ - function updateFeedbackUI(wrapper, value) { - const btns = wrapper.querySelectorAll('.csk-feedback-btn'); - if (btns.length < 2) - return; - btns.forEach((btn) => btn.classList.remove('csk-feedback-btn--active', 'down')); - if (value === 'up' && btns[0]) - btns[0].classList.add('csk-feedback-btn--active'); - if (value === 'down' && btns[1]) - btns[1].classList.add('csk-feedback-btn--active', 'down'); - } - /** 复制文本到剪贴板,带"已复制"反馈 */ - async function copyToClipboard(text, btn) { - const original = btn.innerHTML; - try { - if (navigator.clipboard && window.isSecureContext) { - await navigator.clipboard.writeText(text); - } - else { - // 降级方案:execCommand - const ta = document.createElement('textarea'); - ta.value = text; - ta.style.position = 'fixed'; - ta.style.opacity = '0'; - document.body.appendChild(ta); - ta.select(); - document.execCommand('copy'); - document.body.removeChild(ta); - } - btn.innerHTML = ``; - btn.classList.add('csk-action-btn--done'); - setTimeout(() => { - btn.innerHTML = original; - btn.classList.remove('csk-action-btn--done'); - }, 1500); - } - catch (_a) { - // 复制失败静默处理 - } - } - /** 为气泡内所有代码块添加复制按钮 */ - function enhanceCodeBlocks(bubble) { - const blocks = bubble.querySelectorAll('.csk-md-code-block'); - blocks.forEach((pre) => { - if (pre.querySelector('.csk-md-code-copy')) - return; - const codeEl = pre.querySelector('code'); - const btn = document.createElement('button'); - btn.type = 'button'; - btn.className = 'csk-md-code-copy'; - btn.setAttribute('title', t('copy')); - btn.setAttribute('aria-label', t('copy')); - btn.innerHTML = COPY_ICON; - btn.addEventListener('click', async (e) => { - e.stopPropagation(); - await copyToClipboard((codeEl === null || codeEl === void 0 ? void 0 : codeEl.textContent) || '', btn); - }); - pre.appendChild(btn); - }); - } - /** 流式打字光标:追加到气泡末尾(每次 chunk 后调用) */ - function appendStreamingCaret(bubble) { - if (bubble.querySelector('.csk-caret')) - return; - const caret = document.createElement('span'); - caret.className = 'csk-caret'; - caret.setAttribute('aria-hidden', 'true'); - bubble.appendChild(caret); - } - /** 标记 AI 气泡流式结束:移除流式态、补代码块复制按钮 */ - function finalizeAIBubble(wrapper, bubble) { - wrapper.classList.remove('csk-msg--streaming'); - const caret = bubble.querySelector('.csk-caret'); - if (caret) - caret.remove(); - enhanceCodeBlocks(bubble); - } - // ==================== P1: RAG 引用来源渲染 ==================== - /** 渲染 RAG 引用来源卡片 */ - function renderSources(wrapper, sources) { - // 移除已有的来源卡片 - const existing = wrapper.querySelector('.csk-sources'); - if (existing) - existing.remove(); - if (!sources || sources.length === 0) - return; - const sourcesEl = document.createElement('div'); - sourcesEl.className = 'csk-sources csk-sources--collapsed'; - // 头部 - const header = document.createElement('div'); - header.className = 'csk-sources__header'; - const titleSpan = document.createElement('span'); - titleSpan.className = 'csk-sources__title'; - titleSpan.textContent = `📚 ${t('source_count', { n: sources.length })}`; - const arrow = document.createElement('span'); - arrow.className = 'csk-sources__arrow'; - arrow.textContent = '▼'; - header.appendChild(titleSpan); - header.appendChild(arrow); - // 点击折叠/展开 - header.addEventListener('click', () => { - sourcesEl.classList.toggle('csk-sources--collapsed'); - }); - // 内容 - const body = document.createElement('div'); - body.className = 'csk-sources__body'; - for (const src of sources) { - const item = document.createElement('div'); - item.className = 'csk-source-item'; - const name = document.createElement('div'); - name.className = 'csk-source-item__name'; - name.textContent = src.title || src.sourceName || '未知文档'; - if (src.snippet) { - const snippet = document.createElement('div'); - snippet.className = 'csk-source-item__snippet'; - snippet.textContent = src.snippet; - item.appendChild(snippet); - } - const meta = document.createElement('div'); - meta.className = 'csk-source-item__meta'; - const metaParts = []; - if (src.sourceName) - metaParts.push(src.sourceName); - if (src.chunkIndex !== undefined) - metaParts.push(`分块 #${src.chunkIndex}`); - if (src.score !== undefined) - metaParts.push(`相关度 ${(src.score * 100).toFixed(0)}%`); - meta.textContent = metaParts.join(' · '); - item.appendChild(name); - item.appendChild(meta); - body.appendChild(item); - } - sourcesEl.appendChild(header); - sourcesEl.appendChild(body); - // 插入到元信息行之后(来源卡片紧接在 meta 行下方) - const metaEl = wrapper.querySelector('.csk-msg__meta'); - if (metaEl && metaEl.parentNode) { - metaEl.parentNode.insertBefore(sourcesEl, metaEl.nextSibling); - } - else { - wrapper.appendChild(sourcesEl); - } - } - /** 按日期分组会话列表:今天 / 昨天 / 本周 / 更早 */ - function groupHistoryByDate(items) { - const now = new Date(); - const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); - const yesterdayStart = todayStart - 86400000; - const weekStart = todayStart - (now.getDay() || 7) * 86400000 + 86400000; // 本周一 - const groups = { - today: [], - yesterday: [], - week: [], - earlier: [], - }; - for (const item of items) { - // 尝试解析时间:优先 lastMessageTime,其次 createdAt - const timeStr = item.lastMessageTime || item.createdAt; - const ts = timeStr ? new Date(timeStr).getTime() : 0; - if (ts >= todayStart) { - groups.today.push(item); - } - else if (ts >= yesterdayStart) { - groups.yesterday.push(item); - } - else if (ts >= weekStart) { - groups.week.push(item); - } - else { - groups.earlier.push(item); - } - } - const result = []; - if (groups.today.length) - result.push({ label: t('history_group_today'), items: groups.today }); - if (groups.yesterday.length) - result.push({ label: t('history_group_yesterday'), items: groups.yesterday }); - if (groups.week.length) - result.push({ label: t('history_group_week'), items: groups.week }); - if (groups.earlier.length) - result.push({ label: t('history_group_earlier'), items: groups.earlier }); - return result; - } - /** 渲染会话列表(带日期分组) */ - function renderHistoryList(listEl, items, onSelect, onExport, onDelete, activeChatId, emptyText) { - listEl.innerHTML = ''; - if (items.length === 0) { - const empty = document.createElement('div'); - empty.className = 'csk-history-panel__empty'; - empty.innerHTML = ` -
    💬
    -
    ${t('history_empty')}
    - `; - listEl.appendChild(empty); - return; - } - // 按日期分组渲染 - const groups = groupHistoryByDate(items); - for (const group of groups) { - // 分组标题 - if (groups.length > 1) { - const groupEl = document.createElement('div'); - groupEl.className = 'csk-history-group'; - const labelEl = document.createElement('div'); - labelEl.className = 'csk-history-group__label'; - labelEl.textContent = group.label; - groupEl.appendChild(labelEl); - listEl.appendChild(groupEl); - } - for (const item of group.items) { - const el = document.createElement('div'); - el.className = 'csk-history-item'; - // 高亮当前活跃会话 - const convId = item.chatId || item.id; - if (activeChatId && convId === activeChatId) { - el.classList.add('csk-history-item--active'); - } - const info = document.createElement('div'); - info.className = 'csk-history-item__info'; - const idEl = document.createElement('div'); - idEl.className = 'csk-history-item__id'; - // 显示最后一条消息预览,没有则显示 chatId - if (item.lastMessagePreview) { - idEl.textContent = item.lastMessagePreview.length > 60 - ? item.lastMessagePreview.substring(0, 60) + '...' - : item.lastMessagePreview; - } - else { - idEl.textContent = convId; - } - const metaEl = document.createElement('div'); - metaEl.className = 'csk-history-item__meta'; - const metaParts = []; - if (item.roleName) - metaParts.push(item.roleName); - if (item.messageCount !== undefined) - metaParts.push(`${item.messageCount} 条消息`); - if (item.lastMessageTime) - metaParts.push(item.lastMessageTime); - else if (item.createdAt) - metaParts.push(item.createdAt); - metaEl.textContent = metaParts.join(' · '); - info.appendChild(idEl); - info.appendChild(metaEl); - const actionsEl = document.createElement('div'); - actionsEl.className = 'csk-history-item__actions'; - // 导出按钮 - const exportBtn = document.createElement('button'); - exportBtn.className = 'csk-history-action csk-history-action--export'; - exportBtn.setAttribute('title', t('history_export')); - exportBtn.innerHTML = ``; - exportBtn.addEventListener('click', (e) => { - e.stopPropagation(); - onExport(item.id); - }); - // 删除按钮 - const deleteBtn = document.createElement('button'); - deleteBtn.className = 'csk-history-action csk-history-action--delete'; - deleteBtn.setAttribute('title', t('history_delete')); - deleteBtn.innerHTML = ``; - deleteBtn.addEventListener('click', (e) => { - e.stopPropagation(); - onDelete(item.id); - }); - actionsEl.appendChild(exportBtn); - actionsEl.appendChild(deleteBtn); - el.appendChild(info); - el.appendChild(actionsEl); - // 点击整行 → 切换到该会话 - el.addEventListener('click', () => { - onSelect(convId); - }); - listEl.appendChild(el); - } - } // 关闭 group 循环 - } - /** 滚动消息区到底部 */ - function scrollToBottom(container) { - container.scrollTop = container.scrollHeight; - } - /** 在消息区顶部展示离线提示横幅 */ - function showOfflineBanner(messagesContainer) { - if (document.getElementById('csk-offline-banner')) - return; - const banner = document.createElement('div'); - banner.id = 'csk-offline-banner'; - banner.className = 'csk-offline-banner'; - banner.textContent = t('error_offline'); - messagesContainer.insertBefore(banner, messagesContainer.firstChild); - } - /** 隐藏离线提示横幅 */ - function hideOfflineBanner() { - const el = document.getElementById('csk-offline-banner'); - if (el) - el.remove(); - } - // ==================== 建议问题(suggest-message-list) ==================== - /** - * 在 AI 消息气泡下方渲染推荐问题列表 - * @param wrapper AI 消息的 wrapper 元素 - * @param suggestions 推荐问题文本数组 - * @param onClick 点击回调,传入问题文本 - */ - function renderSuggestions(wrapper, suggestions, onClick) { - // 移除已有的建议区域 - removeSuggestions(wrapper); - if (!suggestions || suggestions.length === 0) - return; - const section = document.createElement('div'); - section.className = 'csk-suggestions'; - const list = document.createElement('div'); - list.className = 'csk-suggestions__list'; - for (const text of suggestions) { - const chip = document.createElement('button'); - chip.type = 'button'; - chip.className = 'csk-suggestion-item'; - chip.textContent = text; - chip.addEventListener('click', (e) => { - e.stopPropagation(); - onClick(text); - }); - list.appendChild(chip); - } - section.appendChild(list); - // 插入到操作栏和来源卡片之间(在 content 内末尾) - const content = wrapper.querySelector('.csk-msg__content'); - if (content) { - content.appendChild(section); - } - else { - wrapper.appendChild(section); - } - } - /** - * 移除指定 wrapper 中的建议问题列表 - */ - function removeSuggestions(wrapper) { - const existing = wrapper.querySelector('.csk-suggestions'); - if (existing) - existing.remove(); - } - // ==================== 反馈原因选择面板 ==================== - /** 点踩原因选项(与后端 MessageFeedback.reasonCategory 枚举一致) */ - const REASON_OPTIONS = [ - { key: 'inaccurate', labelKey: 'feedback_reason_inaccurate' }, - { key: 'irrelevant', labelKey: 'feedback_reason_irrelevant' }, - { key: 'incomplete', labelKey: 'feedback_reason_incomplete' }, - { key: 'other', labelKey: 'feedback_reason_other' }, - ]; - /** - * 在消息气泡下方展示点踩原因选择面板 - * 用户选择原因后回调 onConfirm(reason, comment),取消时回调 undefined 参数 - */ - function showFeedbackReasonPanel(wrapper, msgId, onConfirm) { - // 移除已存在的原因面板 - const existing = wrapper.querySelector('.csk-feedback-reason'); - if (existing) - existing.remove(); - const panel = document.createElement('div'); - panel.className = 'csk-feedback-reason'; - // 标题 - const title = document.createElement('div'); - title.className = 'csk-feedback-reason__title'; - title.textContent = t('feedback_reason_title'); - panel.appendChild(title); - // 原因选项按钮 - const optionsRow = document.createElement('div'); - optionsRow.className = 'csk-feedback-reason__options'; - REASON_OPTIONS.forEach((opt) => { - const btn = document.createElement('button'); - btn.type = 'button'; - btn.className = 'csk-feedback-reason__btn'; - btn.textContent = t(opt.labelKey); - btn.addEventListener('click', (e) => { - e.stopPropagation(); - onConfirm(opt.key); - panel.remove(); - }); - optionsRow.appendChild(btn); - }); - panel.appendChild(optionsRow); - // 补充说明输入框(可选) - const commentRow = document.createElement('div'); - commentRow.className = 'csk-feedback-reason__comment-row'; - const commentInput = document.createElement('input'); - commentInput.type = 'text'; - commentInput.className = 'csk-feedback-reason__comment'; - commentInput.placeholder = t('feedback_reason_comment_placeholder'); - commentInput.maxLength = 200; - commentRow.appendChild(commentInput); - const submitBtn = document.createElement('button'); - submitBtn.type = 'button'; - submitBtn.className = 'csk-feedback-reason__submit'; - submitBtn.textContent = t('feedback_reason_submit'); - submitBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const comment = commentInput.value.trim() || undefined; - onConfirm('other', comment); - panel.remove(); - }); - commentRow.appendChild(submitBtn); - panel.appendChild(commentRow); - // 关闭按钮 - const closeBtn = document.createElement('button'); - closeBtn.type = 'button'; - closeBtn.className = 'csk-feedback-reason__close'; - closeBtn.setAttribute('aria-label', t('close')); - closeBtn.innerHTML = ``; - closeBtn.addEventListener('click', (e) => { - e.stopPropagation(); - panel.remove(); - }); - panel.appendChild(closeBtn); - wrapper.appendChild(panel); - } - - const STORAGE_PREFIX = 'csk_history_'; - const MAX_MESSAGES = 200; - const TRIM_COUNT = 50; - /** 生成存储 key */ - function storageKey(integrateId) { - return `${STORAGE_PREFIX}${integrateId}`; - } - /** - * 保存消息到 localStorage - */ - function saveMessages(integrateId, messages) { - try { - // 消息上限裁剪:保留最新 200 条,超出裁剪最早 50 条 - let trimmed = messages; - if (trimmed.length > MAX_MESSAGES) { - trimmed = trimmed.slice(TRIM_COUNT); - logger.warn(`消息数量达到上限,已裁剪最早 ${TRIM_COUNT} 条,当前 ${trimmed.length} 条`); - } - const data = { - messages: trimmed, - updatedAt: Date.now(), - }; - localStorage.setItem(storageKey(integrateId), JSON.stringify(data)); - } - catch (e) { - if (e instanceof Error && e.name === 'QuotaExceededError') { - logger.error('localStorage 空间不足,会话历史保存失败。建议清空历史记录。'); - } - else { - logger.error('保存会话历史失败', e); - } - } - } - /** - * 从 localStorage 加载消息 - */ - function loadMessages(integrateId) { - try { - const raw = localStorage.getItem(storageKey(integrateId)); - if (!raw) { - return []; - } - const data = JSON.parse(raw); - if (!data || !Array.isArray(data.messages)) { - return []; - } - logger.info(`加载历史消息 integrateId=${integrateId} count=${data.messages.length}`); - return data.messages; - } - catch (e) { - return []; - } - } - /** - * 清空指定 integrateId 的本地缓存 - */ - function clearMessages(integrateId) { - try { - localStorage.removeItem(storageKey(integrateId)); - } - catch (e) { - } - } - - /** - * 轻量级 Markdown 渲染器 - 无外部依赖,XSS 安全 - * - * 支持:代码块、行内代码、标题、粗体、斜体、列表、链接、引用、段落 - * 策略:先转义 HTML,再转 Markdown 为安全 HTML 标签 - */ - /** 代码块占位符前缀 */ - const CODE_BLOCK_PREFIX = '\x00CODEBLOCK_'; - /** 行内代码占位符前缀 */ - const INLINE_CODE_PREFIX = '\x00INLINECODE_'; - /** - * 渲染 Markdown 文本为安全 HTML - * @param text Markdown 源文本 - * @returns 安全 HTML 字符串 - */ - function renderMarkdown(text) { - if (!text || typeof text !== 'string') - return ''; - // 1. 提取代码块(防止内部 Markdown 被处理) - const codeBlocks = []; - let processed = text; - // 提取围栏代码块 ``` - processed = processed.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => { - const idx = codeBlocks.length; - const escapedCode = escapeHtml(code.trimEnd()); - const langClass = lang ? ` class="language-${escapeHtml(lang)}"` : ''; - codeBlocks.push(`
    ${escapedCode}
    `); - return `${CODE_BLOCK_PREFIX}${idx}\x00`; - }); - // 2. 提取行内代码 - const inlineCodes = []; - processed = processed.replace(/`([^`\n]+)`/g, (_match, code) => { - const idx = inlineCodes.length; - inlineCodes.push(`${escapeHtml(code)}`); - return `${INLINE_CODE_PREFIX}${idx}\x00`; - }); - // 3. 转义剩余 HTML(代码块和行内代码已安全处理) - processed = escapeHtml(processed); - // 4. 还原代码块和行内代码占位符(它们已经是安全 HTML) - processed = restorePlaceholders(processed, CODE_BLOCK_PREFIX, codeBlocks); - processed = restorePlaceholders(processed, INLINE_CODE_PREFIX, inlineCodes); - // 5. 逐行处理 Markdown 语法 - const lines = processed.split('\n'); - const result = []; - let inList = false; - let listType = ''; // 'ul' 或 'ol' - let inBlockquote = false; - let paragraphBuffer = []; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - // 代码块已在占位符还原阶段处理,直接输出 - if (line.includes(CODE_BLOCK_PREFIX) || line.includes('
    ')) {
    -                flushParagraph();
    -                closeList();
    -                closeBlockquote();
    -                result.push(line);
    -                continue;
    -            }
    -            // 标题
    -            // Accept standard headings and common no-space forms before CJK text or digits.
    -            const headingMatch = line.match(/^ {0,3}(#{1,6})(?:\s+|(?=[\u3400-\u9FFF\d]))(.+)/);
    -            if (headingMatch) {
    -                flushParagraph();
    -                closeList();
    -                closeBlockquote();
    -                const level = headingMatch[1].length;
    -                result.push(`${headingMatch[2]}`);
    -                continue;
    -            }
    -            // 引用
    -            // GFM table: require a valid separator row to avoid matching ordinary pipe text.
    -            if (i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
    -                const headers = splitTableRow(line);
    -                const separators = splitTableRow(lines[i + 1]);
    -                if (headers.length > 0 && headers.length === separators.length) {
    -                    flushParagraph();
    -                    closeList();
    -                    closeBlockquote();
    -                    const alignments = separators.map(getTableAlignment);
    -                    result.push('
    '); - result.push(''); - headers.forEach((cell, index) => { - result.push(`${inlineFormat(cell)}`); - }); - result.push(''); - i += 2; - while (i < lines.length && isTableDataRow(lines[i])) { - const cells = splitTableRow(lines[i]); - result.push(''); - headers.forEach((_header, index) => { - result.push(`${inlineFormat(cells[index] || '')}`); - }); - result.push(''); - i++; - } - result.push('
    '); - i--; - continue; - } - } - const quoteMatch = line.match(/^ {0,3}>\s?(.*)/); - if (quoteMatch) { - flushParagraph(); - closeList(); - if (!inBlockquote) { - inBlockquote = true; - result.push('
    '); - } - result.push(`

    ${inlineFormat(quoteMatch[1])}

    `); - continue; - } - else if (inBlockquote) { - closeBlockquote(); - } - // 无序列表 - const ulMatch = line.match(/^ {0,3}[\-\*]\s+(.+)/); - if (ulMatch) { - flushParagraph(); - closeBlockquote(); - if (!inList || listType !== 'ul') { - closeList(); - inList = true; - listType = 'ul'; - result.push('
    ' : ''); - inList = false; - listType = ''; - } - } - /** 关闭引用块 */ - function closeBlockquote() { - if (inBlockquote) { - result.push('
    '); - inBlockquote = false; - } - } - } - /** 还原占位符为安全 HTML */ - function restorePlaceholders(text, prefix, replacements) { - return text.replace(new RegExp(escapeRegex(prefix) + '(\\d+)\x00', 'g'), (_m, idx) => { - return replacements[parseInt(idx)] || ''; - }); - } - /** 转义正则特殊字符 */ - function escapeRegex(str) { - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - } - - let config$1 = null; - let messages = []; - let messagesContainer$1 = null; - let inputEl$1 = null; - let inputWrap = null; - let sendBtn$1 = null; - let clearBtn$1 = null; - let categorySelect$1 = null; - let roleSelect = null; - let historyPanel$1 = null; - let welcomeEl = null; - let newMsgBtn = null; - let searchInput = null; - let ariaLiveEl = null; - let showLoadingFn$1 = null; - let hideLoadingFn$1 = null; - let isSending = false; - /** 当前流式请求的中断控制器(用于"停止生成") */ - let abortController = null; - /** 用户是否停留在底部附近(用于智能滚动 + 新消息提示) */ - let isNearBottom = true; - /** 缓存的历史会话列表(供搜索过滤用) */ - let historyItems = []; - /** 当前历史搜索关键字 */ - let historySearchText = ''; - /** 当前选中的知识库分类 ID */ - let currentCategoryId; - /** 当前是否使用 RAG 对话 */ - let useRag = false; - /** - * 初始化对话模块 - */ - function initChat(cfg, dom) { - config$1 = cfg; - messagesContainer$1 = dom.messagesContainer; - inputEl$1 = dom.inputEl; - inputWrap = dom.inputEl.parentElement; - sendBtn$1 = dom.sendBtn; - clearBtn$1 = dom.clearBtn; - categorySelect$1 = dom.categorySelect; - roleSelect = dom.roleSelect; - historyPanel$1 = dom.historyPanel; - welcomeEl = dom.welcomeEl; - newMsgBtn = dom.newMsgBtn; - searchInput = dom.searchInput; - ariaLiveEl = dom.ariaLiveEl; - showLoadingFn$1 = dom.showLoading; - hideLoadingFn$1 = dom.hideLoading; - // 初始化知识库分类 - currentCategoryId = cfg.categoryId; - useRag = cfg.enableRag; - // 绑定发送事件 - bindSendEvents(); - // 绑定滚动监听(智能滚动 + 新消息提示) - bindScrollEvents(); - // 绑定历史搜索过滤 - bindHistorySearch(); - // 加载知识库分类下拉框 - if (cfg.showCategorySwitch && categorySelect$1) { - loadCategories(); - } - } - /** - * 初始化 chatId 并加载对话历史 - * 异步流程:查后端会话 → 恢复 chatId → 加载历史消息 - */ - async function initChatHistory() { - if (!config$1 || !messagesContainer$1) - return; - // 1. 初始化 chatId(从后端获取已有会话或自动生成) - await initChatId(); - // 2. 尝试从后端加载对话历史 - await loadHistoryFromBackend(); - // 3. 如果后端无历史,尝试从 localStorage 恢复 - if (messages.length === 0) { - const cached = loadMessages(config$1.integrateId); - if (cached.length > 0) { - messages = cached; - renderHistory(); - logger.info(`从本地缓存恢复 ${cached.length} 条消息`); - } - } - } - /** - * 从后端加载对话历史 - */ - async function loadHistoryFromBackend() { - if (!config$1 || !messagesContainer$1) - return; - const chatId = getChatId(); - if (!chatId) - return; - try { - const result = await fetchConversationMessages(chatId); - if (result.messages.length > 0) { - // 将后端消息转换为 ChatMessage 格式 - messages = result.messages.map((msg, idx) => ({ - id: uuid(), - role: msg.messageType === 'USER' ? 'user' : 'ai', - content: msg.content, - timestamp: new Date(msg.createTime).getTime(), - })); - renderHistory(); - logger.info(`从后端加载 ${messages.length} 条历史消息`); - // 同步到 localStorage(使用当前活跃角色 ID) - saveMessages(getActiveIntegrateId(), messages); - } - } - catch (err) { - } - } - /** 绑定发送相关事件 */ - function bindSendEvents() { - if (!inputEl$1 || !sendBtn$1) - return; - // 发送按钮:发送态点击发送,停止态点击中断流式 - sendBtn$1.addEventListener('click', () => { - if (isSending && abortController) { - abortController.abort(); - return; - } - handleSend(); - }); - inputEl$1.addEventListener('keydown', (e) => { - // IME 组合输入期间(中文/日文输入法选词回车)不触发发送 - if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { - e.preventDefault(); - handleSend(); - } - }); - inputEl$1.addEventListener('input', () => { - updateSendBtnState(); - autoResizeInput(); - }); - // 输入框聚焦/失焦高亮容器 - inputEl$1.addEventListener('focus', () => { - if (inputWrap) - inputWrap.classList.add('csk-input-wrap--focus'); - }); - inputEl$1.addEventListener('blur', () => { - if (inputWrap) - inputWrap.classList.remove('csk-input-wrap--focus'); - }); - if (clearBtn$1) { - clearBtn$1.addEventListener('click', () => handleClear()); - } - } - /** 绑定滚动监听:判断是否在底部,控制新消息提示按钮 */ - function bindScrollEvents() { - if (!messagesContainer$1) - return; - messagesContainer$1.addEventListener('scroll', () => { - if (!messagesContainer$1) - return; - const { scrollTop, scrollHeight, clientHeight } = messagesContainer$1; - // 距底部 80px 内视为"在底部" - isNearBottom = scrollHeight - scrollTop - clientHeight < 80; - if (isNearBottom) - hideNewMsgBtn(); - }); - // 新消息提示按钮点击 → 回到底部 - if (newMsgBtn) { - newMsgBtn.addEventListener('click', () => { - if (messagesContainer$1) - scrollToBottom(messagesContainer$1); - isNearBottom = true; - hideNewMsgBtn(); - }); - } - } - /** 显示新消息提示按钮 */ - function showNewMsgBtn() { - if (newMsgBtn) - newMsgBtn.classList.remove('csk-newmsg--hidden'); - } - /** 隐藏新消息提示按钮 */ - function hideNewMsgBtn() { - if (newMsgBtn) - newMsgBtn.classList.add('csk-newmsg--hidden'); - } - /** 智能滚动到底部:用户在底部时自动滚,上滑时显示新消息提示 */ - function smartScrollToBottom() { - if (!messagesContainer$1) - return; - if (isNearBottom) { - scrollToBottom(messagesContainer$1); - } - else { - showNewMsgBtn(); - } - } - /** 切换发送按钮模式:发送 / 停止生成 */ - function setSendButtonMode(mode) { - if (!sendBtn$1) - return; - if (mode === 'stop') { - sendBtn$1.classList.add('csk-send-btn--stop'); - sendBtn$1.removeAttribute('disabled'); - sendBtn$1.setAttribute('title', t('stop')); - sendBtn$1.setAttribute('aria-label', t('stop')); - sendBtn$1.innerHTML = ``; - } - else { - sendBtn$1.classList.remove('csk-send-btn--stop'); - sendBtn$1.setAttribute('title', t('send')); - sendBtn$1.setAttribute('aria-label', t('send')); - sendBtn$1.innerHTML = ``; - updateSendBtnState(); - } - } - // ==================== 历史会话搜索过滤 ==================== - /** 绑定历史搜索输入事件 */ - function bindHistorySearch() { - if (!searchInput) - return; - searchInput.addEventListener('input', () => { - historySearchText = searchInput.value.trim().toLowerCase(); - renderFilteredHistory(); - }); - } - /** 按搜索关键字过滤并重新渲染历史列表 */ - function renderFilteredHistory() { - const listEl = historyPanel$1 === null || historyPanel$1 === void 0 ? void 0 : historyPanel$1.querySelector('#csk-history-list'); - if (!listEl || !config$1) - return; - const filtered = historySearchText - ? historyItems.filter(item => { - const preview = (item.lastMessagePreview || item.chatId || item.id || '').toLowerCase(); - return preview.includes(historySearchText); - }) - : historyItems; - renderHistoryList(listEl, filtered, (conversationId) => { switchToConversation(conversationId); }, (id) => { window.open(getConversationExportUrl(id), '_blank'); }, async (id) => { - if (!confirm(t('history_delete_confirm'))) - return; - const ok = await deleteConversation(id); - if (ok) { - if (id === getChatId()) { - messages = []; - if (messagesContainer$1) { - messagesContainer$1.querySelectorAll('.csk-msg, .csk-loading').forEach(el => el.remove()); - } - if (clearBtn$1) - clearBtn$1.style.display = 'none'; - updateEmptyState(); - } - // 从缓存中移除并重新渲染 - historyItems = historyItems.filter(it => (it.chatId || it.id) !== id); - renderFilteredHistory(); - } - }, getChatId()); - } - // ==================== a11y 消息播报 ==================== - /** 通过 aria-live 区域播报新消息(供屏幕阅读器使用) */ - function announceMessage(text) { - if (!ariaLiveEl) - return; - // 截取前 120 字符,避免过长播报 - const snippet = text.length > 120 ? text.substring(0, 120) + '...' : text; - ariaLiveEl.textContent = t('new_msg_announce') + ':' + snippet; - } - // ==================== 消息反馈(👍 / 👎) ==================== - /** - * 处理消息反馈:切换 AI 消息的点赞/点踩状态 - * 前端状态持久化(存入 messages 数组 + localStorage)+ 调用后端 API 记录反馈 - * 点踩时弹出原因选择弹窗,点赞直接提交 - */ - function handleFeedback(msgId, value) { - if (!messagesContainer$1) - return; - const msg = messages.find(m => m.id === msgId && m.role === 'ai'); - if (!msg) - return; - if (value === 'up') { - // 点赞:直接切换提交,无需原因 - const newValue = msg.feedback === 'up' ? undefined : 'up'; - msg.feedback = newValue; - msg.feedbackReason = undefined; - msg.feedbackComment = undefined; - const wrapper = messagesContainer$1.querySelector(`[data-csk-msg-id="${msgId}"]`); - if (wrapper) - updateFeedbackUI(wrapper, newValue); - if (config$1) - saveMessages(config$1.integrateId, messages); - if (newValue) { - submitFeedbackApi(String(msgId), 'THUMBS_UP').then(success => { - }); - } - return; - } - // 点踩:弹出原因选择弹窗 - if (value === 'down') { - const wrapper = messagesContainer$1.querySelector(`[data-csk-msg-id="${msgId}"]`); - if (!wrapper) - return; - // 如果已点踩,取消(同 SDK 现有行为) - if (msg.feedback === 'down') { - msg.feedback = undefined; - msg.feedbackReason = undefined; - msg.feedbackComment = undefined; - if (wrapper) - updateFeedbackUI(wrapper, undefined); - if (config$1) - saveMessages(config$1.integrateId, messages); - return; - } - // 弹出原因选择面板 - showFeedbackReasonPanel(wrapper, msgId, (reason, comment) => { - msg.feedback = 'down'; - msg.feedbackReason = reason; - msg.feedbackComment = comment; - if (wrapper) - updateFeedbackUI(wrapper, 'down'); - if (config$1) - saveMessages(config$1.integrateId, messages); - submitFeedbackApi(String(msgId), 'THUMBS_DOWN', reason, comment).then(success => { - }); - }); - } - } - function autoResizeInput() { - if (!inputEl$1) - return; - inputEl$1.style.height = 'auto'; - inputEl$1.style.height = `${Math.min(inputEl$1.scrollHeight, 120)}px`; - } - /** 根据消息数量切换欢迎空状态显隐 */ - function updateEmptyState() { - if (!welcomeEl) - return; - const hasMessages = messages.length > 0 || - (messagesContainer$1 && messagesContainer$1.querySelector('.csk-msg')); - welcomeEl.style.display = hasMessages ? 'none' : ''; - } - /** 更新发送按钮状态 */ - function updateSendBtnState() { - if (!sendBtn$1 || !inputEl$1) - return; - const hasText = inputEl$1.value.trim().length > 0; - if (hasText && !isSending) { - sendBtn$1.removeAttribute('disabled'); - } - else { - sendBtn$1.setAttribute('disabled', 'true'); - } - } - /** 处理发送消息 */ - async function handleSend() { - if (!inputEl$1 || !config$1 || isSending) - return; - const text = inputEl$1.value.trim(); - if (text === '') - return; - inputEl$1.value = ''; - updateSendBtnState(); - inputEl$1.style.height = 'auto'; - // 1. 渲染用户气泡 - const userTimestamp = now(); - const userMsg = { id: uuid(), role: 'user', content: text, timestamp: userTimestamp }; - if (messagesContainer$1) - renderUserBubble(messagesContainer$1, text, userTimestamp); - messages.push(userMsg); - updateEmptyState(); - if (clearBtn$1 && messages.length > 0) - clearBtn$1.style.display = 'inline-flex'; - if (messagesContainer$1) - smartScrollToBottom(); - // 2. 生成 AI 回复 - await produceAIReply(text); - } - /** - * 生成 AI 回复(发送请求 + 渲染气泡 + 持久化) - * 复用于:正常发送、重试。调用方负责先渲染用户气泡。 - */ - async function produceAIReply(userText) { - if (!config$1 || !messagesContainer$1) - return; - isSending = true; - setSendButtonMode('stop'); - // 确保 chatId 已初始化 - if (!config$1.chatId) { - await initChatId(); - } - const aiTimestamp = now(); - // RAG 启用条件:由 enableRag 控制 - const shouldUseRag = useRag; - // 显示 loading - if (showLoadingFn$1) - showLoadingFn$1(); - if (messagesContainer$1) - smartScrollToBottom(); - const aiMsgId = uuid(); - let aiContent = ''; - try { - if (config$1.streaming) { - aiContent = await sendStreamMessage(userText, aiTimestamp, shouldUseRag, aiMsgId); - } - else { - aiContent = await chatRequest(userText); - if (hideLoadingFn$1) - hideLoadingFn$1(); - if (messagesContainer$1) { - renderAIBubble(messagesContainer$1, aiContent, aiTimestamp, renderMarkdown, aiMsgId); - } - } - const aiMsg = { id: aiMsgId, role: 'ai', content: aiContent, timestamp: aiTimestamp }; - messages.push(aiMsg); - saveMessages(config$1.integrateId, messages); - if (messagesContainer$1) - smartScrollToBottom(); - // a11y 播报新 AI 消息 - announceMessage(aiContent); - // 通知 launcher 显示未读徽章(弹窗关闭时生效,由 index.ts 监听) - if (messagesContainer$1) { - messagesContainer$1.dispatchEvent(new CustomEvent('csk:newMessage', { bubbles: true, detail: { msg: aiMsg } })); - } - // RAG 引用来源 - if (shouldUseRag) - fetchAndRenderSources(userText, aiMsg); - // 拉取并展示推荐问题(suggest-message-list) - if (config$1.suggestions) { - fetchAndShowSuggestions(aiMsgId); - } - // 发送成功后清除离线横幅(网络已恢复) - hideOfflineBanner(); - } - catch (err) { - if (hideLoadingFn$1) - hideLoadingFn$1(); - const errMsg = err instanceof CskError ? err.message : t('error_send'); - if (messagesContainer$1) { - const errorBubble = document.createElement('div'); - errorBubble.className = 'csk-msg csk-msg--ai'; - const bubble = document.createElement('div'); - bubble.className = 'csk-msg__bubble'; - bubble.style.color = '#DC2626'; - bubble.textContent = `⚠ ${errMsg}`; - errorBubble.appendChild(bubble); - messagesContainer$1.appendChild(errorBubble); - } - logger.error(`发送失败 integrateId=${config$1.integrateId}`, err); - } - finally { - isSending = false; - abortController = null; - setSendButtonMode('send'); - updateSendBtnState(); - } - } - /** 流式发送消息 */ - async function sendStreamMessage(text, aiTimestamp, shouldUseRag, aiMsgId) { - // 创建中断控制器,供"停止生成"使用 - abortController = new AbortController(); - const signal = abortController.signal; - return new Promise((resolve, reject) => { - let bubbleEl = null; - let wrapperEl = null; - let accumulated = ''; - let streamStarted = false; - chatSSERequest(text, (chunk) => { - // 直接拼接:后端每个 SSE 事件是模型一个原始 token, - // token 内的换行已由 api.ts 的 eventLines.join('\n') 还原, - // chunk 之间不能再加 \n,否则会把单词/短句拆成多行、破坏 Markdown 结构 - accumulated += chunk; - if (!streamStarted && messagesContainer$1) { - if (hideLoadingFn$1) - hideLoadingFn$1(); - const { wrapper, bubble } = createEmptyAIBubble(messagesContainer$1, aiTimestamp, aiMsgId); - wrapperEl = wrapper; - bubbleEl = bubble; - streamStarted = true; - } - if (bubbleEl) { - bubbleEl.innerHTML = renderMarkdown(accumulated); - appendStreamingCaret(bubbleEl); - } - if (messagesContainer$1) - smartScrollToBottom(); - }, () => { - // 流结束 - if (wrapperEl && bubbleEl) { - // 无流内容降级为同步请求 - if (!streamStarted && accumulated === '') { - chatRequest(text).then(resolve).catch(reject); - return; - } - if (accumulated) - bubbleEl.innerHTML = renderMarkdown(accumulated); - finalizeAIBubble(wrapperEl, bubbleEl); - } - resolve(accumulated); - }, (error) => { - // 离线检测:网络不可达时展示横幅提示,下次发送成功自动清除 - if (error.type === 'network' || error.type === 'cors') { - if (messagesContainer$1) - showOfflineBanner(messagesContainer$1); - } - if (accumulated.length > 0) { - if (bubbleEl) { - bubbleEl.innerHTML = renderMarkdown(accumulated + '\n\n' + t('stream_interrupted')); - if (wrapperEl) - finalizeAIBubble(wrapperEl, bubbleEl); - } - resolve(accumulated); - } - else { - reject(error); - } - }, currentCategoryId, shouldUseRag, signal); - }); - } - /** - * 快捷问题发送:欢迎态点击芯片,直接以该文本发送 - */ - function sendQuickReply(text) { - if (!inputEl$1 || isSending) - return Promise.resolve(); - inputEl$1.value = text; - updateSendBtnState(); - return handleSend(); - } - /** - * 重试:根据 AI 消息 ID 重新生成该回复 - * 找到该 AI 消息及其上一条用户消息,移除该 AI 消息(及之后的所有消息)后重新请求 - */ - async function retryFromMessage(msgId) { - if (!config$1 || !messagesContainer$1 || isSending) - return; - const aiIndex = messages.findIndex(m => m.id === msgId && m.role === 'ai'); - if (aiIndex < 0) - return; - // 找到上一条用户消息文本 - let userText = ''; - for (let i = aiIndex - 1; i >= 0; i--) { - if (messages[i].role === 'user') { - userText = messages[i].content; - break; - } - } - if (!userText) - return; - // 移除该 AI 消息及其后所有消息(本地 + DOM) - const removed = messages.splice(aiIndex); - const removedIds = new Set(removed.map(m => m.id)); - const nodes = messagesContainer$1.querySelectorAll('.csk-msg'); - nodes.forEach(node => { - const id = node.dataset.cskMsgId; - // 无 id 的(如错误气泡、被中断的)按顺序兜底:此处仅按 id 精确移除 - if (id && removedIds.has(id)) - node.remove(); - }); - saveMessages(config$1.integrateId, messages); - updateEmptyState(); - // 重新生成(不重复渲染用户气泡) - await produceAIReply(userText); - } - /** 获取并渲染 RAG 引用来源 */ - async function fetchAndRenderSources(message, aiMsg) { - try { - const sources = await fetchRagSources(message, currentCategoryId); - if (sources.length > 0) { - const ragSources = sources.map(s => { - var _a, _b; - return ({ - documentId: s.documentId || '', - title: s.title || '', - sourceName: s.sourceName || '', - chunkIndex: (_a = s.chunkIndex) !== null && _a !== void 0 ? _a : 0, - score: (_b = s.score) !== null && _b !== void 0 ? _b : 0, - snippet: s.snippet || '', - }); - }); - aiMsg.sources = ragSources; - if (messagesContainer$1) { - const lastAiMsg = messagesContainer$1.querySelector('.csk-msg--ai:last-of-type'); - if (lastAiMsg) - renderSources(lastAiMsg, ragSources); - } - if (config$1) - saveMessages(config$1.integrateId, messages); - } - } - catch (err) { - } - } - /** 加载知识库分类到下拉框 */ - async function loadCategories() { - if (!categorySelect$1) - return; - try { - const tree = await fetchCategoryTree(); - if (tree.length === 0) - return; - categorySelect$1.innerHTML = ``; - const addOptions = (nodes, indent = 0) => { - for (const node of nodes) { - const option = document.createElement('option'); - option.value = String(node.id); - option.textContent = `${' '.repeat(indent)}${node.name}`; - if (currentCategoryId !== undefined && String(node.id) === String(currentCategoryId)) - option.selected = true; - categorySelect$1.appendChild(option); - if (node.children && node.children.length > 0) - addOptions(node.children, indent + 1); - } - }; - addOptions(tree); - logger.info(`知识库分类加载成功 count=${tree.length}`); - } - catch (err) { - logger.error(t('category_load_error'), err); - } - } - /** 渲染历史消息 */ - function renderHistory() { - if (!messagesContainer$1) - return; - const historyPanelEl = messagesContainer$1.querySelector('.csk-history-panel'); - const msgs = messagesContainer$1.querySelectorAll('.csk-msg, .csk-loading'); - msgs.forEach(el => el.remove()); - for (const msg of messages) { - if (msg.role === 'user') { - renderUserBubble(messagesContainer$1, msg.content, msg.timestamp); - } - else { - const wrapper = renderAIBubble(messagesContainer$1, msg.content, msg.timestamp, renderMarkdown, msg.id); - if (msg.sources && msg.sources.length > 0) - renderSources(wrapper, msg.sources); - if (msg.feedback) - updateFeedbackUI(wrapper, msg.feedback); - } - } - isNearBottom = true; - scrollToBottom(messagesContainer$1); - if (clearBtn$1 && messages.length > 0) - clearBtn$1.style.display = 'inline-flex'; - updateEmptyState(); - if (historyPanelEl && !messagesContainer$1.contains(historyPanelEl)) { - messagesContainer$1.appendChild(historyPanelEl); - } - } - /** 开启新对话(生成新 chatId) */ - function handleClear() { - if (!config$1) - return; - messages = []; - if (messagesContainer$1) { - const msgs = messagesContainer$1.querySelectorAll('.csk-msg, .csk-loading'); - msgs.forEach(el => el.remove()); - } - if (clearBtn$1) - clearBtn$1.style.display = 'none'; - updateEmptyState(); - clearMessages(config$1.integrateId); - // 生成新的 chatId,开始新会话 - const newId = generateNewChatId(); - updateChatId(newId); - saveCachedChatId(config$1.integrateId, config$1.userId, newId); - logger.lifecycleClear(config$1.integrateId); - } - /** 生成新 chatId */ - function generateNewChatId() { - const random = typeof crypto !== 'undefined' && crypto.randomUUID - ? crypto.randomUUID().substring(0, 8) - : Math.random().toString(36).substring(2, 10); - return `sdk_${Date.now()}_${random}`; - } - /** 设置当前知识库分类 */ - function setCategory(categoryId) { - var _a; - currentCategoryId = categoryId; - useRag = (_a = config$1 === null || config$1 === void 0 ? void 0 : config$1.enableRag) !== null && _a !== void 0 ? _a : true; - } - // ==================== 角色切换 ==================== - /** - * 切换客服角色 - * - 清理当前对话 + 生成新 chatId - * - 更新 API 层的 roleId 参数 - * - 重新加载新角色的对话历史 - */ - async function switchRole(newRoleId) { - if (!config$1 || !messagesContainer$1) - return; - const oldRoleId = getActiveIntegrateId(); - // 1. 判断是否与当前角色相同(防重复切换) - if (newRoleId === oldRoleId) - return; - // 2. 保存当前消息到 localStorage(与旧 integrateId 关联) - if (messages.length > 0) { - saveMessages(oldRoleId, messages); - } - // 3. 中断正在进行的流式请求 - if (abortController) { - abortController.abort(); - abortController = null; - } - isSending = false; - setSendButtonMode('send'); - // 4. 清空消息数组和 DOM - messages = []; - const msgNodes = messagesContainer$1.querySelectorAll('.csk-msg, .csk-loading'); - msgNodes.forEach(el => el.remove()); - // 5. 清空新旧角色的 localStorage 缓存(消息 + chatId),必须在 setActiveRoleId 之前 - clearMessages(oldRoleId); - saveCachedChatId(oldRoleId, config$1.userId, undefined); - clearMessages(newRoleId); - saveCachedChatId(newRoleId, config$1.userId, undefined); - // 6. 更新 API 层的 integrateId(必须在 initChatId 之前) - setActiveRoleId(newRoleId); - // 7. 更新角色下拉框高亮项 - if (roleSelect) { - roleSelect.value = newRoleId; - } - // 8. 强制查询后端会话列表(跳过 localStorage 缓存) - const foundChatId = await initChatId(true); - // 9. 从后端加载新角色的对话历史 - if (foundChatId) { - try { - await loadHistoryFromBackend(); - } - catch (err) { - } - } - // 10. 隐藏新对话按钮 + 更新欢迎态 - if (clearBtn$1 && messages.length === 0) - clearBtn$1.style.display = 'none'; - updateEmptyState(); - // 11. 重置滚动状态 - isNearBottom = true; - if (messagesContainer$1) - scrollToBottom(messagesContainer$1); - } - // ==================== 会话管理面板 ==================== - /** 加载会话列表并渲染 */ - async function loadHistoryConversations() { - if (!historyPanel$1 || !config$1) - return; - const listEl = historyPanel$1.querySelector('#csk-history-list'); - if (!listEl) - return; - listEl.innerHTML = `
    加载中...
    `; - try { - const result = await fetchConversationList(1, 50, config$1.userId, getActiveIntegrateId()); - historyItems = result.list.map(c => ({ - id: c.conversationId || '', - chatId: c.conversationId || '', - roleId: c.roleId, - roleName: c.roleName, - messageCount: c.messageCount, - lastMessageTime: c.lastMessageTime, - lastMessagePreview: c.lastMessagePreview, - createdAt: c.firstMessageTime || c.createdAt, - })); - // 重置搜索(新数据到达时清空过滤) - historySearchText = ''; - if (searchInput) - searchInput.value = ''; - renderHistoryList(listEl, historyItems, (conversationId) => { switchToConversation(conversationId); }, (id) => { window.open(getConversationExportUrl(id), '_blank'); }, async (id) => { - if (!confirm(t('history_delete_confirm'))) - return; - const ok = await deleteConversation(id); - if (ok) { - if (id === getChatId()) { - messages = []; - if (messagesContainer$1) { - messagesContainer$1.querySelectorAll('.csk-msg, .csk-loading').forEach(el => el.remove()); - } - if (clearBtn$1) - clearBtn$1.style.display = 'none'; - updateEmptyState(); - } - // 从缓存中移除并重新渲染(不再重新请求后端) - historyItems = historyItems.filter(it => (it.chatId || it.id) !== id); - renderFilteredHistory(); - } - }, getChatId()); - } - catch (err) { - logger.error(t('history_load_error'), err); - listEl.innerHTML = `
    ${t('history_load_error')}
    `; - } - } - /** - * 切换到指定会话:加载上下文并继续对话 - * 如果会话所属角色与当前角色不同,自动同步切换角色 - * @param conversationId 会话 ID(即 chatId) - */ - async function switchToConversation(conversationId) { - if (!config$1 || !messagesContainer$1) - return; - // 找到该会话对应的历史条目,获取其所属角色 - const historyItem = historyItems.find(it => (it.chatId || it.id) === conversationId); - // 如果会话所属角色与当前活跃角色不同,先静默切换角色 - if (historyItem && historyItem.roleId !== undefined) { - const convRoleId = String(historyItem.roleId); - const currentRoleId = getActiveIntegrateId(); - if (convRoleId && convRoleId !== currentRoleId) { - setActiveRoleId(convRoleId); - // 更新角色下拉框高亮项 - if (roleSelect) { - roleSelect.value = convRoleId; - } - } - } - // 1. 更新 chatId - updateChatId(conversationId); - saveCachedChatId(getActiveIntegrateId(), config$1.userId, conversationId); - // 2. 关闭历史面板 - if (historyPanel$1) { - historyPanel$1.classList.add('csk-history-panel--hidden'); - } - // 3. 清空当前消息 - messages = []; - const msgs = messagesContainer$1.querySelectorAll('.csk-msg, .csk-loading'); - msgs.forEach(el => el.remove()); - // 4. 从后端加载该会话的消息 - try { - const result = await fetchConversationMessages(conversationId); - if (result.messages.length > 0) { - messages = result.messages.map((msg) => ({ - id: uuid(), - role: msg.messageType === 'USER' ? 'user' : 'ai', - content: msg.content, - timestamp: new Date(msg.createTime).getTime(), - })); - renderHistory(); - logger.info(`加载会话 ${conversationId} 的 ${messages.length} 条消息`); - // 同步到 localStorage - saveMessages(config$1.integrateId, messages); - } - } - catch (err) { - } - // 5. 显示新对话按钮 - if (clearBtn$1 && messages.length > 0) { - clearBtn$1.style.display = 'inline-flex'; - } - updateEmptyState(); - } - // ==================== 建议问题(suggest-message-list) ==================== - /** - * 拉取并展示 AI 推荐问题 - * 在 AI 回复成功后调用,从后端缓存中读取本次对话生成的建议问题 - */ - async function fetchAndShowSuggestions(aiMsgId) { - const chatId = getChatId(); - if (!chatId || !messagesContainer$1) - return; - try { - const suggestions = await fetchSuggestions(chatId); - if (suggestions.length > 0 && messagesContainer$1) { - const wrapper = messagesContainer$1.querySelector(`[data-csk-msg-id="${aiMsgId}"]`); - if (wrapper) - renderSuggestions(wrapper, suggestions, sendQuickReply); - } - } - catch (_a) { - // 静默降级 - } - } - - // ==================== 单例状态 ==================== - let config = null; - let isInitialized = false; - let launcherEl = null; - let windowEl = null; - let messagesContainer = null; - let inputEl = null; - let sendBtn = null; - let clearBtn = null; - let categorySelect = null; - let historyPanel = null; - let showLoadingFn = null; - let hideLoadingFn = null; - let dragCleanup = null; - let launcherDragCleanup = null; - let resizeCleanup = null; - /** Launcher 位置记忆状态(拖拽后更新) */ - let launcherPos = null; - /** 提示气泡元素 */ - let teaserEl = null; - /** 提示气泡定时器 */ - let teaserTimer = null; - /** 未读徽章元素(挂在 launcher 内部) */ - let badgeEl = null; - /** 音频上下文(提示音用) */ - let audioCtx = null; - /** 保密声明 DOM 引用 */ - let disclaimerEl = null; - // ==================== 公开 API ==================== - /** 初始化 SDK */ - function init(rawConfig) { - if (isInitialized) { - return; - } - // 1. 配置解析与校验 - const parsed = parseConfig(rawConfig); - if (!parsed) - return; - config = parsed; - // 2. 设置国际化语言 - setLocale(config.locale); - // 3. 设置日志级别 - setDebug(config.debug); - // 3.1 设置错误回调 - setErrorCallback(config.onError); - // 4. 设置 API 配置 - setApiConfig(config); - // 5. 注入样式 - injectStyles(config); - // 6. 创建悬浮按钮 - launcherEl = createLauncher(config, toggle); - document.body.appendChild(launcherEl); - // 获取未读徽章引用(由 createLauncher 创建) - badgeEl = launcherEl.querySelector('.csk-launcher__badge'); - // 6.1 恢复 Launcher 位置(必须在挂载到 DOM 之后、拖拽绑定之前) - restoreLauncherPosition(config.integrateId, launcherEl); - // 6.2 启用 Launcher 拖拽(边缘吸附式,支持触摸 + 鼠标) - launcherDragCleanup = enableLauncherDrag(launcherEl, config, (pos) => { - const prevSide = launcherPos === null || launcherPos === void 0 ? void 0 : launcherPos.side; - launcherPos = pos; - saveLauncherPosition(config.integrateId, pos); - // 仅在换边时同步弹窗位置,同侧拖动不影响弹窗 - if (prevSide && prevSide !== pos.side) { - resetWindowPosition(); - } - syncWindowAlignment(pos.side); - // 重新定位 teaser 气泡(若正在显示) - if (teaserEl && !teaserEl.classList.contains('csk-teaser--hidden')) { - positionTeaser(); - } - }); - // 6.3 恢复弹窗对齐侧(与 Launcher 同侧) - if (launcherPos) { - syncWindowAlignment(launcherPos.side); - } - // 7. 创建聊天弹窗 - const dom = createChatWindow(config); - windowEl = dom.window; - messagesContainer = dom.messagesContainer; - inputEl = dom.inputEl; - sendBtn = dom.sendBtn; - clearBtn = dom.clearBtn; - categorySelect = dom.categorySelect; - historyPanel = dom.historyPanel; - showLoadingFn = dom.showLoading; - hideLoadingFn = dom.hideLoading; - disclaimerEl = dom.disclaimer; - document.body.appendChild(windowEl); - // 8. 安置提示气泡(挂在 launcher 同层,由配置控制显示) - teaserEl = dom.teaserEl; - document.body.appendChild(teaserEl); - positionTeaser(); - // 9. 启用拖拽(含位置持久化) - const headerEl = windowEl.querySelector('.csk-header'); - if (headerEl) { - dragCleanup = enableDrag(headerEl, windowEl, (pos) => { - saveWindowPosition(config.integrateId, pos); - }); - } - // 9.1 恢复上次拖拽位置 - restoreWindowPosition(config.integrateId, windowEl); - // 9.2 启用缩放(resizable 配置控制) - if (config.resizable) { - const resizeHandle = dom.resizeHandle; - resizeCleanup = enableResize(resizeHandle, windowEl, (size) => { - saveWindowSize(config.integrateId, size); - }); - // 恢复上次缩放的尺寸 - restoreWindowSize(config.integrateId, windowEl); - } - // 10. 初始化对话模块 - initChat(config, { - messagesContainer, - inputEl, - sendBtn, - clearBtn, - categorySelect, - historyPanel, - welcomeEl: dom.welcomeEl, - newMsgBtn: dom.newMsgBtn, - searchInput: dom.searchInput, - ariaLiveEl: dom.ariaLiveEl, - roleSelect: dom.roleSelect, - showLoading: showLoadingFn, - hideLoading: hideLoadingFn, - }); - // 11. 监听知识库分类切换事件 - windowEl.addEventListener('csk:categoryChange', ((e) => { - setCategory(e.detail.categoryId); - })); - // 12. 监听会话管理面板加载事件 - windowEl.addEventListener('csk:loadHistory', () => { - loadHistoryConversations(); - }); - // 13. 监听快捷问题芯片点击 - windowEl.addEventListener('csk:quickReply', ((e) => { - sendQuickReply(e.detail.text); - })); - // 14. 监听 AI 消息重试事件 - windowEl.addEventListener('csk:retry', ((e) => { - retryFromMessage(e.detail.msgId); - })); - // 15. 监听消息反馈事件(👍 / 👎) - windowEl.addEventListener('csk:feedback', ((e) => { - handleFeedback(e.detail.msgId, e.detail.value); - })); - // 16. 新消息到达通知(用于未读徽章 + 提示音 + 桌面通知) - windowEl.addEventListener('csk:newMessage', ((e) => { - var _a; - notifyNewMessage(); - // 触发 onMessage 回调 - if ((config === null || config === void 0 ? void 0 : config.onMessage) && ((_a = e.detail) === null || _a === void 0 ? void 0 : _a.msg)) { - try { - config.onMessage(e.detail.msg); - } - catch ( /* 回调异常不影响 SDK */_b) { /* 回调异常不影响 SDK */ } - } - })); - // 16.5 监听角色切换事件 - windowEl.addEventListener('csk:roleChange', ((e) => { - if (e.detail.roleId) - switchRole(String(e.detail.roleId)); - })); - // 17. ESC 键关闭弹窗 - document.addEventListener('keydown', onKeyDown); - // 17. 窗口 resize 时重新定位 teaser - window.addEventListener('resize', onResize); - isInitialized = true; - logger.lifecycleInit(config.integrateId, config.requestDomain); - // 触发 onReady 回调 - if (config.onReady) { - try { - config.onReady(); - } - catch ( /* 回调异常不影响 SDK */_a) { /* 回调异常不影响 SDK */ } - } - // 18. 首访提示气泡(延迟显示,弹窗未打开时生效) - if (config.showTeaser) { - teaserTimer = setTimeout(() => { - // 弹窗关闭时才显示 - if (windowEl && windowEl.classList.contains('csk-window--hidden')) { - showTeaser(); - } - }, 1500); - } - // 19. 异步初始化 chatId 和对话历史(不阻塞 UI) - initChatHistory().catch(err => { - }); - // 20. 异步拉取保密声明(config.disclaimer undefined = 从后端拉取,有值/空串已在 dom.ts 处理) - if (config.disclaimer === undefined) { - fetchSystemConfig('disclaimer').then(html => { - if (html && disclaimerEl) { - disclaimerEl.innerHTML = html; - disclaimerEl.style.display = ''; - } - }).catch(() => { - // 拉取失败静默降级,声明保持隐藏状态 - }); - } - } - // ==================== 提示气泡控制 ==================== - /** 根据 launcher 位置定位 teaser 气泡 */ - function positionTeaser() { - if (!teaserEl || !launcherEl) - return; - // launcher 被拖拽过则用实际位置,否则用 DOM 实时计算 - const rect = launcherEl.getBoundingClientRect(); - const isLeft = launcherPos ? launcherPos.side === 'left' : (config === null || config === void 0 ? void 0 : config.position) === 'left-bottom'; - if (isLeft) { - teaserEl.style.left = `${rect.left}px`; - teaserEl.style.right = 'auto'; - } - else { - teaserEl.style.right = `${window.innerWidth - rect.right}px`; - teaserEl.style.left = 'auto'; - } - teaserEl.style.bottom = `${window.innerHeight - rect.top + 8}px`; - // 同步 teaser 类名(箭头方向) - teaserEl.classList.toggle('csk-teaser--left', !!isLeft); - teaserEl.classList.toggle('csk-teaser--right', !isLeft); - } - function showTeaser() { - if (!teaserEl) - return; - positionTeaser(); - teaserEl.classList.remove('csk-teaser--hidden'); - // 点击气泡本身 → 打开弹窗 - teaserEl.addEventListener('click', onTeaserClick); - // 关闭按钮(阻止冒泡,不打开弹窗) - const closeBtn = teaserEl.querySelector('.csk-teaser__close'); - if (closeBtn) { - closeBtn.addEventListener('click', onTeaserClose); - } - } - function hideTeaser() { - if (!teaserEl) - return; - teaserEl.classList.add('csk-teaser--hidden'); - teaserEl.removeEventListener('click', onTeaserClick); - const closeBtn = teaserEl.querySelector('.csk-teaser__close'); - if (closeBtn) - closeBtn.removeEventListener('click', onTeaserClose); - } - function onTeaserClick(e) { - // 关闭按钮点击不打开弹窗 - if (e.target.closest('.csk-teaser__close')) - return; - hideTeaser(); - open(); - } - function onTeaserClose(e) { - e.stopPropagation(); - hideTeaser(); - } - function onResize() { - if (teaserEl && !teaserEl.classList.contains('csk-teaser--hidden')) { - positionTeaser(); - } - } - // ==================== 未读徽章 ==================== - function showBadge() { - if (badgeEl) - badgeEl.classList.remove('csk-launcher__badge--hidden'); - } - function hideBadge() { - if (badgeEl) - badgeEl.classList.add('csk-launcher__badge--hidden'); - } - /** 新消息到达时弹窗关闭,则显示徽章 + 播放提示音 + 桌面通知 */ - function notifyNewMessage() { - if (windowEl && windowEl.classList.contains('csk-window--hidden')) { - showBadge(); - // 提示音 - if (config === null || config === void 0 ? void 0 : config.sound) - playNotificationSound(); - // 桌面通知 - if (config === null || config === void 0 ? void 0 : config.notification) - sendDesktopNotification(); - } - } - // ==================== a11y 焦点陷阱 ==================== - /** 弹窗内可聚焦元素选择器 */ - const FOCUSABLE = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; - function trapFocus(e) { - if (!windowEl || windowEl.classList.contains('csk-window--hidden')) - return; - if (e.key !== 'Tab') - return; - const focusables = Array.from(windowEl.querySelectorAll(FOCUSABLE)); - if (focusables.length === 0) - return; - const first = focusables[0]; - const last = focusables[focusables.length - 1]; - if (e.shiftKey) { - // Shift+Tab:在第一个元素上按 → 跳到最后一个 - if (document.activeElement === first) { - e.preventDefault(); - last.focus(); - } - } - else { - // Tab:在最后一个元素上按 → 跳到第一个 - if (document.activeElement === last) { - e.preventDefault(); - first.focus(); - } - } - } - // ==================== 全局键盘事件 ==================== - function onKeyDown(e) { - if (e.key === 'Escape' && windowEl && !windowEl.classList.contains('csk-window--hidden')) { - close(); - } - trapFocus(e); - } - /** 销毁 SDK 实例 */ - function destroy() { - if (!isInitialized) - return; - if (teaserTimer) { - clearTimeout(teaserTimer); - teaserTimer = null; - } - hideTeaser(); - if (teaserEl && teaserEl.parentNode) { - teaserEl.parentNode.removeChild(teaserEl); - teaserEl = null; - } - if (launcherEl && launcherEl.parentNode) { - launcherEl.parentNode.removeChild(launcherEl); - launcherEl = null; - } - if (windowEl && windowEl.parentNode) { - windowEl.parentNode.removeChild(windowEl); - windowEl = null; - } - if (dragCleanup) { - dragCleanup(); - dragCleanup = null; - } - if (launcherDragCleanup) { - launcherDragCleanup(); - launcherDragCleanup = null; - } - if (resizeCleanup) { - resizeCleanup(); - resizeCleanup = null; - } - launcherPos = null; - removeStyles(); - document.removeEventListener('keydown', onKeyDown); - window.removeEventListener('resize', onResize); - setErrorCallback(undefined); - config === null || config === void 0 ? void 0 : config.integrateId; - config = null; - isInitialized = false; - messagesContainer = null; - inputEl = null; - sendBtn = null; - clearBtn = null; - categorySelect = null; - historyPanel = null; - showLoadingFn = null; - hideLoadingFn = null; - badgeEl = null; - audioCtx = null; - disclaimerEl = null; - clearApiConfig(); - } - function open() { - if (!windowEl) - return; - windowEl.classList.remove('csk-window--hidden'); - hideTeaser(); - hideBadge(); - setTimeout(() => { if (inputEl) - inputEl.focus(); }, 80); - } - function close() { - if (!windowEl) - return; - windowEl.classList.add('csk-window--hidden'); - } - function toggle() { - if (!windowEl) - return; - if (windowEl.classList.contains('csk-window--hidden')) { - open(); - } - else { - close(); - } - } - function clearHistory() { - if (!config) - return; - if (clearBtn) { - clearBtn.click(); - } - else { - clearMessages(config.integrateId); - } - } - // ==================== 窗口尺寸记忆 ==================== - /** localStorage key:窗口尺寸 */ - function sizeKey(integrateId) { - return `csk_size_${integrateId}`; - } - /** 保存窗口缩放的尺寸到 localStorage */ - function saveWindowSize(integrateId, size) { - try { - localStorage.setItem(sizeKey(integrateId), JSON.stringify(size)); - } - catch ( /* 忽略 */_a) { /* 忽略 */ } - } - /** 恢复窗口缩放尺寸 */ - function restoreWindowSize(integrateId, winEl) { - try { - const raw = localStorage.getItem(sizeKey(integrateId)); - if (!raw) - return; - const size = JSON.parse(raw); - if (typeof size.width !== 'number' || typeof size.height !== 'number') - return; - const w = Math.max(300, Math.min(size.width, window.innerWidth - 24)); - const h = Math.max(300, Math.min(size.height, window.innerHeight - 24)); - winEl.style.width = `${w}px`; - winEl.style.height = `${h}px`; - winEl.style.setProperty('--csk-window-width', `${w}px`); - winEl.style.setProperty('--csk-window-height', `${h}px`); - } - catch ( /* 忽略 */_a) { /* 忽略 */ } - } - // ==================== 窗口位置记忆 ==================== - /** localStorage key:窗口位置 */ - function positionKey(integrateId) { - return `csk_position_${integrateId}`; - } - /** 保存窗口拖拽位置到 localStorage */ - function saveWindowPosition(integrateId, pos) { - try { - localStorage.setItem(positionKey(integrateId), JSON.stringify(pos)); - } - catch ( /* 忽略 */_a) { /* 忽略 */ } - } - /** 恢复窗口拖拽位置 */ - function restoreWindowPosition(integrateId, winEl) { - try { - const raw = localStorage.getItem(positionKey(integrateId)); - if (!raw) - return; - const pos = JSON.parse(raw); - if (typeof pos.x !== 'number' || typeof pos.y !== 'number') - return; - // 校验位置在当前视口内 - const maxX = window.innerWidth - winEl.offsetWidth; - const maxY = window.innerHeight - winEl.offsetHeight; - const x = Math.max(0, Math.min(pos.x, maxX)); - const y = Math.max(0, Math.min(pos.y, maxY)); - winEl.style.right = 'auto'; - winEl.style.bottom = 'auto'; - winEl.style.left = `${x}px`; - winEl.style.top = `${y}px`; - } - catch ( /* 忽略 */_a) { /* 忽略 */ } - } - // ==================== Launcher 位置记忆 ==================== - /** localStorage key:Launcher 位置 */ - function launcherPosKey(integrateId) { - return `csk_launcher_pos_${integrateId}`; - } - /** 保存 Launcher 拖拽位置到 localStorage */ - function saveLauncherPosition(integrateId, pos) { - try { - localStorage.setItem(launcherPosKey(integrateId), JSON.stringify(pos)); - } - catch ( /* 忽略 */_a) { /* 忽略 */ } - } - /** 恢复 Launcher 位置(初始化时调用) */ - function restoreLauncherPosition(integrateId, el) { - try { - const raw = localStorage.getItem(launcherPosKey(integrateId)); - if (!raw) - return; - const pos = JSON.parse(raw); - if ((pos.side !== 'left' && pos.side !== 'right') || typeof pos.bottom !== 'number') - return; - const maxBottom = window.innerHeight - 76; - const clampedBottom = Math.max(16, Math.min(pos.bottom, maxBottom)); - // 覆盖默认对齐方向 - el.classList.remove('csk-launcher--right', 'csk-launcher--left'); - el.classList.add(`csk-launcher--${pos.side}`); - el.style.bottom = `${clampedBottom}px`; - // 保存到模块状态,供后续 positionTeaser / 弹窗对齐使用 - launcherPos = { side: pos.side, bottom: clampedBottom }; - } - catch ( /* 忽略 */_a) { /* 忽略 */ } - } - /** 清除弹窗的内联定位样式,使其回到 CSS 类控制的默认位置 */ - function resetWindowPosition() { - if (!windowEl) - return; - windowEl.style.left = ''; - windowEl.style.top = ''; - windowEl.style.right = ''; - windowEl.style.bottom = ''; - } - /** 同步聊天弹窗对齐侧(Launcher 拖拽到左/右侧时跟随) */ - function syncWindowAlignment(side) { - if (!windowEl) - return; - windowEl.classList.remove('csk-window--right', 'csk-window--left'); - windowEl.classList.add(`csk-window--${side}`); - // 同步 teaser 箭头方向 - if (teaserEl) { - teaserEl.classList.remove('csk-teaser--right', 'csk-teaser--left'); - teaserEl.classList.add(`csk-teaser--${side}`); - } - } - // ==================== 提示音 + 桌面通知 ==================== - /** 播放短促提示音(Web Audio API 合成,无需音频文件) */ - function playNotificationSound() { - try { - if (!audioCtx) { - audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - } - const ctx = audioCtx; - if (ctx.state === 'suspended') { - ctx.resume(); - return; - } - // 合成两声短促"叮" - const now = ctx.currentTime; - [0, 0.15].forEach((offset, i) => { - const osc = ctx.createOscillator(); - const gain = ctx.createGain(); - osc.type = 'sine'; - osc.frequency.value = i === 0 ? 880 : 1100; - gain.gain.setValueAtTime(0.15, now + offset); - gain.gain.exponentialRampToValueAtTime(0.001, now + offset + 0.12); - osc.connect(gain); - gain.connect(ctx.destination); - osc.start(now + offset); - osc.stop(now + offset + 0.12); - }); - } - catch ( /* 音频 API 不可用则忽略 */_a) { /* 音频 API 不可用则忽略 */ } - } - /** 发送桌面通知(需用户授权) */ - function sendDesktopNotification() { - try { - if (!('Notification' in window)) - return; - if (Notification.permission === 'granted') { - new Notification((config === null || config === void 0 ? void 0 : config.title) || 'AI 智能助手', { - body: '收到新消息', - icon: undefined, - silent: true, - }); - } - else if (Notification.permission !== 'denied') { - // 首次请求权限 - Notification.requestPermission(); - } - } - catch ( /* 通知 API 不可用则忽略 */_a) { /* 通知 API 不可用则忽略 */ } - } - // ==================== 挂载到全局 ==================== - const ChatbotSDK = { - init, - destroy, - open, - close, - toggle, - clearHistory, - }; - if (typeof window !== 'undefined') { - window.ChatbotSDK = ChatbotSDK; - } - - return ChatbotSDK; - -})(); diff --git a/client/dist/chatbot-sdk.min.js b/client/dist/chatbot-sdk.min.js deleted file mode 100644 index 6116bd1..0000000 --- a/client/dist/chatbot-sdk.min.js +++ /dev/null @@ -1 +0,0 @@ -var ChatbotSDK=function(){"use strict";let e=null;function n(n){e=n||null}const t={info(e,n){},warn(e,n){},error(n,t){if(e)try{const r=t instanceof Error&&t.type||"error";e({message:n,code:String(r),detail:t})}catch(e){}},time(e){},timeEnd:(e,n)=>0,lifecycleInit(e,n){},lifecycleDestroy(e){},lifecycleSend(e,n){},lifecycleReply(e,n){},lifecycleError(e,n,t){},lifecycleClear(e){},lifecycleStreamDone(e,n){},lifecycleCategoryChange(e){}};const r={"zh-CN":{title:"AI 智能助手",minimize:"最小化",close:"关闭",status_online:"在线",role_selector_label:"服务角色",welcome_title:"你好,我是 AI 智能助手",welcome_desc:"有什么可以帮你的吗?在下方输入框开始提问吧~",placeholder:"输入您的问题...",send:"发送",stop:"停止生成",new_message:"新消息",copy:"复制",copied:"已复制",retry:"重新生成",retrying:"重新生成中...",loading:"正在思考...",stream_interrupted:"回复被中断",stream_unstable:"网络不稳定,内容可能不完整",category_placeholder:"选择知识库分类",category_all:"全部分类",category_load_error:"加载分类失败",source_title:"参考来源",source_count:"{n} 条参考来源",source_loading:"加载来源中...",clear:"新对话",history_title:"历史会话",history_empty:"暂无历史会话",history_load_error:"加载会话列表失败",history_delete_confirm:"确定删除该会话?",history_export:"导出",history_delete:"删除",history_search:"搜索会话...",history_group_today:"今天",history_group_yesterday:"昨天",history_group_week:"本周",history_group_earlier:"更早",feedback_up:"有帮助",feedback_down:"没帮助",feedback_reason_title:"请选择不满意的原因:",feedback_reason_inaccurate:"信息错误",feedback_reason_irrelevant:"不相关",feedback_reason_incomplete:"不完整",feedback_reason_other:"其他",feedback_reason_comment_placeholder:"补充说明(可选)",feedback_reason_submit:"提交",teaser_text:"有什么可以帮你的吗?",new_msg_announce:"收到新消息",resize:"拖拽缩放窗口",error_network:"网络连接失败,请检查网络",error_offline:"⚠️ 网络连接异常,请检查网络后重试",error_timeout:"请求超时,请稍后重试",error_server:"服务器异常,请稍后重试",error_cors:"跨域请求被拦截,请联系管理员将当前域名加入 API 白名单",error_auth:"鉴权失败,请联系管理员",error_forbidden:"无访问权限,请联系管理员配置",error_not_found:"请求的资源不存在",error_rate_limit:"请求过于频繁,请稍后重试",error_unavailable:"服务暂不可用,请稍后重试",error_unknown:"请求发生未知错误",error_send:"发送失败,请稍后重试",error_stream_unsupported:"浏览器不支持流式读取"},en:{title:"AI Assistant",minimize:"Minimize",close:"Close",status_online:"Online",role_selector_label:"Service",welcome_title:"Hi, I am your AI assistant",welcome_desc:"How can I help you? Type your question below to get started.",placeholder:"Type your question...",send:"Send",stop:"Stop generating",new_message:"New message",copy:"Copy",copied:"Copied",retry:"Regenerate",retrying:"Regenerating...",loading:"Thinking...",stream_interrupted:"Response interrupted",stream_unstable:"Network unstable, content may be incomplete",category_placeholder:"Select category",category_all:"All categories",category_load_error:"Failed to load categories",source_title:"Sources",source_count:"{n} source(s)",source_loading:"Loading sources...",clear:"New chat",history_title:"History",history_empty:"No conversations yet",history_load_error:"Failed to load conversations",history_delete_confirm:"Delete this conversation?",history_export:"Export",history_delete:"Delete",history_search:"Search conversations...",history_group_today:"Today",history_group_yesterday:"Yesterday",history_group_week:"This Week",history_group_earlier:"Earlier",feedback_up:"Helpful",feedback_down:"Not helpful",feedback_reason_title:"Please tell us why:",feedback_reason_inaccurate:"Inaccurate",feedback_reason_irrelevant:"Irrelevant",feedback_reason_incomplete:"Incomplete",feedback_reason_other:"Other",feedback_reason_comment_placeholder:"Additional comments (optional)",feedback_reason_submit:"Submit",teaser_text:"How can I help you?",new_msg_announce:"New message received",resize:"Resize window",error_network:"Network connection failed",error_offline:"⚠️ Connection lost. Please check your network and try again.",error_timeout:"Request timed out, please try again",error_server:"Server error, please try again later",error_cors:"CORS request blocked. Please contact admin to whitelist your domain",error_auth:"Authentication failed, please contact admin",error_forbidden:"Access denied, please contact admin",error_not_found:"Resource not found",error_rate_limit:"Too many requests, please try again later",error_unavailable:"Service temporarily unavailable",error_unknown:"Unknown request error",error_send:"Failed to send, please try again",error_stream_unsupported:"Browser does not support streaming"}};let s="zh-CN";function o(e,n){let t=(r[s]||r["zh-CN"])[e]||r["zh-CN"][e]||e;if(n)for(const[e,r]of Object.entries(n))t=t.replace(`{${e}}`,String(r));return t}let a=null,i=null;function c(e){i=e}function l(){var e;return null!==(e=null!=i?i:null==a?void 0:a.integrateId)&&void 0!==e?e:""}function d(e){a&&(a.chatId=e)}function p(){return(null==a?void 0:a.chatId)||""}function u(e){if(!a)throw new Error("API 配置未初始化");return`${a.requestDomain.replace(/\/+$/,"")}${e.startsWith("/")?e:`/${e}`}`}function m(e,n,t){null!=t&&("string"==typeof t&&""===t.trim()||e.set(n,String(t)))}async function k(e,n={},r=3e4,s){const i=new AbortController,c=setTimeout(()=>i.abort(),r);s&&(s.aborted?i.abort():s.addEventListener("abort",()=>i.abort(),{once:!0}));try{const r={};n.headers&&(n.headers instanceof Headers?n.headers.forEach((e,n)=>{r[n]=e}):Array.isArray(n.headers)?n.headers.forEach(([e,n])=>{r[e]=n}):Object.assign(r,n.headers)),(null==a?void 0:a.token)&&(e.includes("/ai/")||e.endsWith("/feedback"))&&(r.Authorization=`Bearer ${a.token}`);const s=await fetch(e,Object.assign(Object.assign({},n),{headers:r,signal:i.signal,mode:"cors",credentials:"include"}));return 401===s.status&&(e.includes("/ai/")||e.endsWith("/feedback"))&&t.error("SDK 认证失败:Token 缺失或已过期,请重新调用 /open-api/auth/token 换取 Token"),s}catch(e){if(null==s?void 0:s.aborted)throw new g("aborted","aborted");if(e instanceof DOMException&&"AbortError"===e.name)throw new g(o("error_timeout"),"timeout");if(e instanceof TypeError&&e.message.includes("Failed to fetch"))throw new g(o("error_cors"),"cors");throw new g(o("error_network"),"network")}finally{clearTimeout(c)}}class g extends Error{constructor(e,n){super(e),this.name="CskError",this.type=n}}function h(e){switch(e){case 401:return o("error_auth");case 403:return o("error_forbidden");case 404:return o("error_not_found");case 429:return o("error_rate_limit");case 500:return o("error_server");case 502:case 503:return o("error_unavailable");default:return`${o("error_unknown")}(${e})`}}async function b(e){const n=function(e){const n=new URLSearchParams;return n.set("message",e),n.set("chatId",a.chatId),m(n,"roleId",l()),m(n,"accountId",a.userId),u(`/ai/assistant_app/chat/sync?${n.toString()}`)}(e);t.lifecycleSend(l(),e.length);try{const e=await k(n);if(!e.ok){const n=h(e.status);throw t.lifecycleError(l(),String(e.status),n),new g(n,`http_${e.status}`)}const r=await e.text();return t.lifecycleReply(l(),r.length),r}catch(e){if(e instanceof g)throw e;throw t.lifecycleError(l(),"unknown",String(e)),new g(o("error_unknown"),"unknown")}}async function f(e,n,r,s,i,c,d){var p;const b=c?function(e,n){const t=new URLSearchParams;return t.set("message",e),t.set("chatId",a.chatId),t.set("rewriteStrategy",a.rewriteStrategy||"REWRITE"),m(t,"roleId",l()),m(t,"accountId",a.userId),m(t,"categoryId",null!=n?n:a.categoryId),u(`/ai/assistant_app/chat/rag/sse?${t.toString()}`)}(e,i):function(e,n){const t=new URLSearchParams;return t.set("message",e),t.set("chatId",a.chatId),m(t,"roleId",l()),m(t,"accountId",a.userId),m(t,"categoryId",null!=n?n:a.categoryId),u(`/ai/assistant_app/chat/sse?${t.toString()}`)}(e,i);let f="";t.lifecycleSend(l(),e.length);try{const e=await k(b,{},6e4,d);if(!e.ok){const n=h(e.status);return t.lifecycleError(l(),String(e.status),n),void s(new g(n,`http_${e.status}`))}const a=null===(p=e.body)||void 0===p?void 0:p.getReader();if(!a)return void s(new g(o("error_stream_unsupported"),"stream_unsupported"));const i=new TextDecoder("utf-8",{stream:!0});let c="",u=[],m="";try{for(;;){const{done:e,value:t}=await a.read();if(e)break;c+=i.decode(t,{stream:!0});const r=c.split("\n");c=r.pop()||"";for(const e of r){const t=e.trim();if(!t){if(u.length>0){if("status"!==m&&"tool_call_result"!==m){const e=u.join("\n");f+=e,n(e)}u=[]}m="";continue}if(t.startsWith(":"))continue;if("[DONE]"===t){if(u.length>0&&"status"!==m&&"tool_call_result"!==m){const e=u.join("\n");f+=e,n(e),u=[]}m="";break}if(t.startsWith("event:")){m=t.substring(6).trim();continue}if(t.startsWith("id:")||t.startsWith("retry:"))continue;let r;t.startsWith("data:")?(r=t.substring(5),r.startsWith(" ")&&(r=r.substring(1))):r=t,u.push(r)}}if(c.trim()){const e=c.trim();if(!e.startsWith(":"))if(e.startsWith("data:")){let n=e.substring(5);n.startsWith(" ")&&(n=n.substring(1)),u.push(n)}else"[DONE]"===e||e.startsWith("event:")||e.startsWith("id:")||e.startsWith("retry:")||u.push(e)}if(u.length>0&&"status"!==m&&"tool_call_result"!==m){const e=u.join("\n");f+=e,n(e),u=[]}}catch(e){if(null==d?void 0:d.aborted)t.info(`流式被用户中断,保留已生成内容 length=${f.length}`);else{if(!(f.length>0))throw e;n("\n\n"+o("stream_unstable"))}}finally{a.releaseLock()}t.lifecycleStreamDone(l(),f.length),r()}catch(e){if((null==d?void 0:d.aborted)||e instanceof g&&"aborted"===e.type)return void r();e instanceof g?s(e):(t.lifecycleError(l(),"unknown",String(e)),s(new g(o("error_network"),"network")))}}async function y(e,n){const r=function(e,n){const t=new URLSearchParams;return t.set("message",e),t.set("chatId",a.chatId),t.set("rewriteStrategy",a.rewriteStrategy||"REWRITE"),m(t,"roleId",l()),m(t,"accountId",a.userId),m(t,"categoryId",null!=n?n:a.categoryId),u(`/ai/assistant_app/rag/sources?${t.toString()}`)}(e,n);try{const e=await k(r);if(!e.ok)throw new g(h(e.status),`http_${e.status}`);const n=await e.json();return n.success&&Array.isArray(n.data)?(t.info(`获取引用来源 count=${n.data.length}`),n.data):[]}catch(e){return t.error("获取引用来源失败",e),[]}}async function v(e,n,r,s){if(!a)return!1;const o=u("/feedback");try{const i={messageId:String(e),conversationId:a.chatId,feedbackType:n};r&&(i.reasonCategory=r),s&&(i.reasonComment=s);const c=await k(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});if(!c.ok)return t.error(`反馈提交失败 status=${c.status}`),!1;return(await c.json()).success||!1}catch(e){return t.error("反馈提交异常",e),!1}}async function x(e=1,n=20,r,s){const o=new URLSearchParams;o.set("page",String(e)),o.set("size",String(n)),r&&o.set("accountId",r),s&&o.set("roleId",s);const a=u(`/ai/sdk/conversation/list?${o.toString()}`);try{const e=await k(a);if(!e.ok)throw new g(h(e.status),`http_${e.status}`);const n=await e.json();return{list:n.success&&Array.isArray(n.data)?n.data:[],total:n.total||0,pages:n.pages||0}}catch(e){return t.error("加载会话列表失败",e),{list:[],total:0,pages:0}}}async function w(e){const n=new URLSearchParams;(null==a?void 0:a.userId)&&n.set("accountId",a.userId),l()&&n.set("roleId",l());const r=u(`/ai/sdk/conversation/${e}/messages?${n.toString()}`);try{const e=await k(r);if(!e.ok)throw new g(h(e.status),`http_${e.status}`);const n=await e.json();return{messages:n.success&&Array.isArray(n.data)?n.data:[],total:n.total||0}}catch(e){return t.error("加载会话消息失败",e),{messages:[],total:0}}}async function _(e){const n=new URLSearchParams;(null==a?void 0:a.userId)&&n.set("accountId",a.userId),l()&&n.set("roleId",l());const r=u(`/ai/sdk/conversation/${e}?${n.toString()}`);try{const n=await k(r,{method:"DELETE"});if(!n.ok)throw new g(h(n.status),`http_${n.status}`);const s=await n.json();return t.info(`删除会话 id=${e} success=${s.success}`),s.success||!1}catch(e){return t.error("删除会话失败",e),!1}}function E(e){const n=new URLSearchParams;return(null==a?void 0:a.userId)&&n.set("accountId",a.userId),l()&&n.set("roleId",l()),u(`/ai/sdk/conversation/${e}/export?${n.toString()}`)}async function C(e=!1){if(!a)return"";const n=l();if(!e){const e=function(e,n){try{return localStorage.getItem(L(e,n))||""}catch(e){return""}}(n,a.userId);if(e)return a.chatId=e,e}try{const e=await x(1,5,a.userId,n);if(e.list.length>0){const r=e.list[0],s=r.conversationId||r.chatId||"";if(s)return a.chatId=s,I(n,a.userId,s),t.info(`从后端恢复会话 chatId=${s} messageCount=${r.messageCount}`),s}}catch(e){}const r=function(){const e="undefined"!=typeof crypto&&crypto.randomUUID?crypto.randomUUID().substring(0,8):Math.random().toString(36).substring(2,10);return`sdk_${Date.now()}_${e}`}();return a.chatId=r,I(n,a.userId,r),r}function L(e,n){return`csk_chatId_${e}${n?"_"+n:""}`}function I(e,n,t){try{t?localStorage.setItem(L(e,n),t):localStorage.removeItem(L(e,n))}catch(e){}}let S=null;function A(e){const n=$(e.primaryColor,-15),t=$(e.primaryColor,18),r=function(e){const n=e.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);return n?`${parseInt(n[1],16)}, ${parseInt(n[2],16)}, ${parseInt(n[3],16)}`:"79, 70, 229"}(e.primaryColor);return`\n --csk-primary: ${e.primaryColor};\n --csk-primary-hover: ${n};\n --csk-primary-light: ${t};\n --csk-primary-rgb: ${r};\n --csk-bg-user: linear-gradient(135deg, ${e.primaryColor}, ${n});\n --csk-bg-ai: #ffffff;\n --csk-text-user: #ffffff;\n --csk-text-ai: #1F2937;\n --csk-window-width: ${e.width}px;\n --csk-window-height: ${e.height}px;\n --csk-radius: 16px;\n --csk-shadow-window: 0 12px 48px rgba(15, 23, 42, 0.18), 0 2px 8px rgba(15, 23, 42, 0.06);\n --csk-shadow-bubble: 0 1px 2px rgba(15, 23, 42, 0.06);\n --csk-border: #ECEEF2;\n --csk-bg-app: #F6F7F9;\n `}function $(e,n){const t=e.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);if(!t)return e;const r=e=>Math.max(0,Math.min(255,e)),s=r(parseInt(t[1],16)+n),o=r(parseInt(t[2],16)+n),a=r(parseInt(t[3],16)+n);return`#${s.toString(16).padStart(2,"0")}${o.toString(16).padStart(2,"0")}${a.toString(16).padStart(2,"0")}`}function N(e){document.querySelector("style[data-csk-sdk]")||(S=document.createElement("style"),S.setAttribute("data-csk-sdk",""),S.textContent=function(e){return`\n/* ChatbotSDK 样式 - csk- 命名空间 */\n.csk-root {\n ${A(e)}\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;\n font-size: 14px;\n line-height: 1.55;\n color: #1F2937;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n box-sizing: border-box;\n}\n.csk-root *, .csk-root *::before, .csk-root *::after { box-sizing: border-box; }\n\n/* ========== 悬浮按钮 ========== */\n.csk-launcher {\n position: fixed;\n bottom: 24px;\n z-index: 9998;\n width: 60px;\n height: 60px;\n border-radius: 50%;\n /* 白色背景,简洁克制 */\n background: #ffffff;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n color: #fff;\n user-select: none;\n overflow: hidden;\n /* 浅灰边框 */\n border: 2px solid #E5E7EB;\n /* 简洁柔和阴影 */\n box-shadow:\n 0 2px 8px rgba(0, 0, 0, 0.08),\n 0 4px 16px rgba(0, 0, 0, 0.06);\n transition:\n transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1),\n box-shadow 0.25s ease,\n border-color 0.25s ease;\n animation:\n csk-launcher-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;\n}\n.csk-launcher--right { right: 24px; }\n.csk-launcher--left { left: 24px; }\n\n.csk-launcher:hover {\n transform: translateY(-3px) scale(1.08);\n border-color: #D1D5DB;\n box-shadow:\n 0 4px 12px rgba(0, 0, 0, 0.10),\n 0 8px 24px rgba(0, 0, 0, 0.08);\n}\n.csk-launcher:active { transform: scale(0.93); }\n.csk-launcher svg {\n width: 30px;\n height: 30px;\n position: relative;\n z-index: 1;\n filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.15));\n}\n.csk-launcher img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n position: relative;\n z-index: 1;\n -webkit-user-drag: none;\n user-select: none;\n}\n\n@keyframes csk-launcher-in {\n 0% { opacity: 0; transform: scale(0.5) translateY(16px); }\n 60% { transform: scale(1.06) translateY(-2px); }\n 100% { opacity: 1; transform: scale(1) translateY(0); }\n}\n\n/* ========== 聊天弹窗 ========== */\n.csk-window {\n position: fixed;\n bottom: 24px;\n z-index: 9999;\n width: var(--csk-window-width);\n height: var(--csk-window-height);\n max-height: calc(100vh - 48px);\n background: #fff;\n border-radius: var(--csk-radius);\n box-shadow: var(--csk-shadow-window);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n transform-origin: bottom right;\n transition: opacity 0.24s ease, transform 0.24s cubic-bezier(0.34, 1.2, 0.64, 1), visibility 0.24s;\n opacity: 1;\n transform: translateY(0) scale(1);\n visibility: visible;\n pointer-events: auto;\n}\n.csk-window--right { right: 24px; transform-origin: bottom right; }\n.csk-window--left { left: 24px; transform-origin: bottom left; }\n.csk-window--hidden {\n opacity: 0;\n transform: translateY(12px) scale(0.96);\n visibility: hidden;\n pointer-events: none;\n}\n\n/* ========== 水印层 ========== */\n.csk-watermark {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n pointer-events: none;\n user-select: none;\n z-index: 0;\n background-repeat: repeat;\n opacity: 0.8;\n}\n\n/* ========== 头部 ========== */\n.csk-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 16px;\n min-height: 60px;\n background: var(--csk-bg-user);\n color: #fff;\n cursor: move;\n user-select: none;\n position: relative;\n flex-shrink: 0;\n}\n.csk-header::after {\n content: '';\n position: absolute;\n left: 0; right: 0; bottom: 0;\n height: 24px;\n background: linear-gradient(to bottom, rgba(0,0,0,0.04), transparent);\n pointer-events: none;\n}\n.csk-header__left {\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 0;\n}\n.csk-header__avatar {\n width: 38px;\n height: 38px;\n border-radius: 50%;\n background: rgba(255, 255, 255, 0.22);\n backdrop-filter: blur(4px);\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25);\n}\n.csk-header__avatar svg { width: 22px; height: 22px; }\n.csk-header__info {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n.csk-header__title {\n font-size: 15px;\n font-weight: 600;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n letter-spacing: 0.2px;\n}\n.csk-header__status {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font-size: 11.5px;\n opacity: 0.9;\n margin-top: 2px;\n}\n.csk-status-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: #6EE7B7;\n box-shadow: 0 0 0 0 rgba(110, 231, 183, 0.7);\n animation: csk-status-pulse 2s infinite;\n}\n@keyframes csk-status-pulse {\n 0% { box-shadow: 0 0 0 0 rgba(110, 231, 183, 0.7); }\n 70% { box-shadow: 0 0 0 6px rgba(110, 231, 183, 0); }\n 100% { box-shadow: 0 0 0 0 rgba(110, 231, 183, 0); }\n}\n.csk-header__actions {\n display: flex;\n align-items: center;\n gap: 2px;\n flex-shrink: 0;\n}\n.csk-header__btn,\n.csk-history-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n border: none;\n background: transparent;\n color: #fff;\n cursor: pointer;\n border-radius: 8px;\n transition: background 0.15s, transform 0.15s;\n}\n.csk-header__btn:hover,\n.csk-history-btn:hover {\n background: rgba(255, 255, 255, 0.2);\n}\n.csk-header__btn:active,\n.csk-history-btn:active { transform: scale(0.92); }\n\n/* ========== 角色选择器 ========== */\n.csk-role-select-wrap {\n display: flex;\n align-items: center;\n flex-shrink: 0;\n margin: 0 4px;\n}\n.csk-role-select {\n appearance: none;\n -webkit-appearance: none;\n padding: 4px 24px 4px 10px;\n border: 1px solid rgba(255, 255, 255, 0.35);\n border-radius: 8px;\n background: rgba(255, 255, 255, 0.15) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='white' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat right 6px center;\n color: #fff;\n font-size: 12px;\n font-family: inherit;\n cursor: pointer;\n outline: none;\n transition: background 0.15s, border-color 0.15s;\n max-width: 120px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.csk-role-select:hover {\n background-color: rgba(255, 255, 255, 0.25);\n border-color: rgba(255, 255, 255, 0.55);\n}\n.csk-role-select:focus {\n border-color: rgba(255, 255, 255, 0.7);\n box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2);\n}\n.csk-role-select option {\n color: #1F2937;\n background: #fff;\n}\n\n/* ========== 消息区 ========== */\n.csk-messages {\n flex: 1;\n overflow-y: auto;\n padding: 20px 16px 12px;\n background: var(--csk-bg-app);\n scroll-behavior: smooth;\n}\n.csk-messages::-webkit-scrollbar { width: 6px; }\n.csk-messages::-webkit-scrollbar-track { background: transparent; }\n.csk-messages::-webkit-scrollbar-thumb {\n background: #D8DCE3;\n border-radius: 3px;\n}\n.csk-messages::-webkit-scrollbar-thumb:hover { background: #C2C7D0; }\n\n/* 离线提示横幅 */\n.csk-offline-banner {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 8px 12px;\n margin: 0 0 8px 0;\n border-radius: 8px;\n font-size: 12px;\n line-height: 1.5;\n background: #FEF2F2;\n color: #991B1B;\n border: 1px solid #FECACA;\n}\n.csk-dark .csk-offline-banner {\n background: #450A0A;\n color: #FCA5A5;\n border-color: #7F1D1D;\n}\n\n/* 欢迎空状态 */\n.csk-welcome {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n padding: 48px 24px 32px;\n color: #6B7280;\n animation: csk-fade-in 0.3s ease;\n}\n.csk-welcome__avatar {\n width: 56px;\n height: 56px;\n border-radius: 50%;\n background: var(--csk-bg-user);\n color: #fff;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 14px;\n box-shadow: 0 6px 18px rgba(var(--csk-primary-rgb), 0.3);\n}\n.csk-welcome__avatar svg { width: 30px; height: 30px; }\n.csk-welcome__title {\n font-size: 15px;\n font-weight: 600;\n color: #1F2937;\n margin-bottom: 4px;\n}\n.csk-welcome__desc {\n font-size: 13px;\n color: #9CA3AF;\n line-height: 1.6;\n}\n\n/* 消息气泡 */\n.csk-msg {\n display: flex;\n margin-bottom: 12px;\n max-width: 100%;\n word-break: break-word;\n animation: csk-msg-in 0.28s cubic-bezier(0.22, 1, 0.36, 1);\n}\n.csk-msg--user {\n flex-direction: row-reverse;\n align-items: flex-end;\n}\n.csk-msg--ai {\n flex-direction: row;\n align-items: flex-start;\n}\n.csk-msg__avatar {\n width: 30px;\n height: 30px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n margin: 0 8px;\n box-shadow: var(--csk-shadow-bubble);\n}\n.csk-msg__avatar svg { width: 18px; height: 18px; }\n.csk-msg__avatar--ai {\n background: #fff;\n color: var(--csk-primary);\n border: 1px solid var(--csk-border);\n}\n.csk-msg__avatar--user {\n background: var(--csk-bg-user);\n color: #fff;\n}\n.csk-msg__content {\n display: flex;\n flex-direction: column;\n min-width: 0;\n max-width: calc(100% - 46px);\n}\n.csk-msg--user .csk-msg__content { align-items: flex-end; }\n.csk-msg--ai .csk-msg__content { align-items: flex-start; }\n.csk-msg__bubble {\n padding: 10px 14px;\n border-radius: 16px;\n font-size: 14px;\n line-height: 1.6;\n box-shadow: var(--csk-shadow-bubble);\n}\n.csk-msg--user .csk-msg__bubble {\n background: var(--csk-bg-user);\n color: var(--csk-text-user);\n border-radius: 16px 16px 4px 16px;\n}\n.csk-msg--ai .csk-msg__bubble {\n background: var(--csk-bg-ai);\n color: var(--csk-text-ai);\n border-radius: 16px 16px 16px 4px;\n border: 1px solid var(--csk-border);\n}\n.csk-msg__time {\n font-size: 11px;\n color: #9CA3AF;\n margin-top: 3px;\n padding: 0 4px;\n}\n\n/* 元信息行:时间戳居左,操作按钮居右,共用一行 */\n.csk-msg__meta {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-top: 3px;\n width: 100%;\n}\n.csk-msg__meta .csk-msg__time { margin-top: 0; }\n\n@keyframes csk-msg-in {\n from { opacity: 0; transform: translateY(8px); }\n to { opacity: 1; transform: translateY(0); }\n}\n@keyframes csk-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n/* ========== Loading 动画 ========== */\n.csk-loading {\n display: flex;\n align-items: center;\n margin-bottom: 12px;\n animation: csk-msg-in 0.28s ease;\n}\n.csk-loading__avatar {\n width: 30px;\n height: 30px;\n border-radius: 50%;\n background: #fff;\n color: var(--csk-primary);\n border: 1px solid var(--csk-border);\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n margin: 0 8px;\n box-shadow: var(--csk-shadow-bubble);\n}\n.csk-loading__avatar svg { width: 18px; height: 18px; }\n.csk-loading__bubble {\n background: #fff;\n border: 1px solid var(--csk-border);\n border-radius: 16px 16px 16px 4px;\n padding: 12px 16px;\n display: flex;\n align-items: center;\n gap: 5px;\n}\n.csk-loading__dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: #B4B9C2;\n animation: csk-bounce 1.4s ease-in-out infinite both;\n}\n.csk-loading__dot:nth-child(1) { animation-delay: 0s; }\n.csk-loading__dot:nth-child(2) { animation-delay: 0.16s; }\n.csk-loading__dot:nth-child(3) { animation-delay: 0.32s; }\n\n@keyframes csk-bounce {\n 0%, 80%, 100% { transform: scale(0.6); opacity: 0.6; }\n 40% { transform: scale(1); opacity: 1; }\n}\n\n/* ========== 输入区 ========== */\n.csk-input-area {\n padding: 10px 12px 14px;\n background: #fff;\n border-top: 1px solid var(--csk-border);\n flex-shrink: 0;\n}\n.csk-input-wrap {\n display: flex;\n align-items: flex-end;\n gap: 8px;\n background: var(--csk-bg-app);\n border: 1px solid var(--csk-border);\n border-radius: 14px;\n padding: 6px 6px 6px 14px;\n transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;\n}\n.csk-input-wrap--focus {\n border-color: var(--csk-primary);\n background: #fff;\n box-shadow: 0 0 0 3px rgba(var(--csk-primary-rgb), 0.12);\n}\n.csk-input {\n flex: 1;\n border: none;\n background: transparent;\n padding: 8px 0;\n font-size: 14px;\n outline: none;\n font-family: inherit;\n resize: none;\n min-height: 22px;\n max-height: 120px;\n line-height: 1.5;\n color: #1F2937;\n}\n.csk-input::placeholder { color: #9CA3AF; }\n.csk-root .csk-input:focus-visible { outline: none; }\n.csk-send-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n min-width: 36px;\n border: none;\n border-radius: 10px;\n background: var(--csk-bg-user);\n color: #fff;\n cursor: pointer;\n transition: transform 0.18s, box-shadow 0.18s, opacity 0.18s;\n box-shadow: 0 3px 10px rgba(var(--csk-primary-rgb), 0.3);\n}\n.csk-send-btn:hover:not(:disabled) {\n transform: translateY(-1px);\n box-shadow: 0 5px 14px rgba(var(--csk-primary-rgb), 0.4);\n}\n.csk-send-btn:active:not(:disabled) { transform: scale(0.92); }\n.csk-send-btn:disabled {\n background: #D1D5DB;\n box-shadow: none;\n cursor: not-allowed;\n opacity: 0.7;\n}\n.csk-send-btn svg { width: 18px; height: 18px; }\n\n/* ========== 保密声明脚注 ========== */\n.csk-disclaimer {\n margin-top: 8px;\n padding: 6px 8px 4px;\n font-size: 10px;\n color: #9CA3AF;\n line-height: 1.5;\n user-select: none;\n}\n.csk-disclaimer__title {\n font-weight: 600;\n margin-bottom: 2px;\n color: #9CA3AF;\n}\n.csk-dark .csk-disclaimer { color: #6B7280; }\n.csk-dark .csk-disclaimer__title { color: #6B7280; }\n\n/* ========== 新对话按钮 ========== */\n.csk-clear-btn {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 5px 12px;\n border: 1px solid var(--csk-border);\n border-radius: 999px;\n background: #fff;\n color: #6B7280;\n font-size: 12px;\n cursor: pointer;\n margin: 0 auto 8px;\n transition: all 0.15s;\n font-family: inherit;\n}\n.csk-clear-btn:hover {\n background: #FEF2F2;\n border-color: #FCA5A5;\n color: #DC2626;\n}\n\n/* ========== P1: 知识库分类下拉 ========== */\n.csk-category-bar {\n display: flex;\n align-items: center;\n padding: 8px 12px;\n border-top: 1px solid var(--csk-border);\n background: #FBFBFC;\n gap: 8px;\n flex-shrink: 0;\n}\n.csk-category-bar__label {\n font-size: 13px;\n white-space: nowrap;\n}\n.csk-category-select {\n flex: 1;\n padding: 6px 28px 6px 10px;\n border: 1px solid var(--csk-border);\n border-radius: 8px;\n font-size: 12.5px;\n color: #374151;\n background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239CA3AF' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E") no-repeat right 9px center;\n appearance: none;\n -webkit-appearance: none;\n outline: none;\n cursor: pointer;\n font-family: inherit;\n transition: border-color 0.2s, box-shadow 0.2s;\n max-width: 220px;\n}\n.csk-category-select:focus {\n border-color: var(--csk-primary);\n box-shadow: 0 0 0 3px rgba(var(--csk-primary-rgb), 0.12);\n}\n\n/* ========== P1: RAG 引用来源卡片 ========== */\n.csk-sources {\n margin-top: 6px;\n border: 1px solid var(--csk-border);\n border-radius: 12px;\n overflow: hidden;\n font-size: 12px;\n max-width: 100%;\n background: #FAFBFC;\n}\n.csk-sources__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 10px;\n background: #F3F4F7;\n cursor: pointer;\n user-select: none;\n transition: background 0.15s;\n}\n.csk-sources__header:hover { background: #ECEEF2; }\n.csk-sources__title {\n display: flex;\n align-items: center;\n gap: 5px;\n font-weight: 500;\n color: #374151;\n}\n.csk-sources__arrow {\n transition: transform 0.2s;\n color: #9CA3AF;\n font-size: 10px;\n}\n.csk-sources--collapsed .csk-sources__arrow { transform: rotate(-90deg); }\n.csk-sources__body {\n border-top: 1px solid var(--csk-border);\n padding: 0;\n}\n.csk-sources--collapsed .csk-sources__body { display: none; }\n.csk-source-item {\n padding: 6px 10px;\n border-bottom: 1px solid #F0F1F4;\n transition: background 0.15s;\n}\n.csk-source-item:last-child { border-bottom: none; }\n.csk-source-item:hover { background: #fff; }\n.csk-source-item__name {\n font-weight: 500;\n color: #1F2937;\n margin-bottom: 2px;\n}\n.csk-source-item__snippet {\n color: #6B7280;\n line-height: 1.5;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n.csk-source-item__meta {\n font-size: 11px;\n color: #9CA3AF;\n margin-top: 2px;\n}\n\n/* ========== P1: Markdown 渲染样式 ========== */\n.csk-msg--ai .csk-msg__bubble .csk-md-p { margin: 0 0 8px; }\n.csk-msg--ai .csk-msg__bubble .csk-md-p:last-child { margin-bottom: 0; }\n.csk-msg--ai .csk-msg__bubble .csk-md-h1,\n.csk-msg--ai .csk-msg__bubble .csk-md-h2,\n.csk-msg--ai .csk-msg__bubble .csk-md-h3,\n.csk-msg--ai .csk-msg__bubble .csk-md-h4,\n.csk-msg--ai .csk-msg__bubble .csk-md-h5,\n.csk-msg--ai .csk-msg__bubble .csk-md-h6 {\n margin: 14px 0 6px;\n font-weight: 600;\n line-height: 1.3;\n color: #111827;\n}\n.csk-msg--ai .csk-msg__bubble .csk-md-h1 { font-size: 20px; }\n.csk-msg--ai .csk-msg__bubble .csk-md-h2 { font-size: 17px; }\n.csk-msg--ai .csk-msg__bubble .csk-md-h3 { font-size: 15px; }\n.csk-msg--ai .csk-msg__bubble .csk-md-h4 { font-size: 14px; }\n\n.csk-md-code-block {\n position: relative;\n background: #1E293B;\n color: #E2E8F0;\n padding: 12px 14px;\n border-radius: 10px;\n overflow-x: auto;\n margin: 8px 0;\n font-size: 13px;\n line-height: 1.55;\n font-family: 'SF Mono', 'Consolas', 'Menlo', 'Monaco', monospace;\n}\n.csk-md-code-block code {\n background: none;\n padding: 0;\n border-radius: 0;\n font-size: inherit;\n color: inherit;\n}\n.csk-md-inline-code {\n background: rgba(var(--csk-primary-rgb), 0.1);\n color: var(--csk-primary);\n padding: 1px 6px;\n border-radius: 5px;\n font-size: 13px;\n font-family: 'SF Mono', 'Consolas', 'Menlo', 'Monaco', monospace;\n}\n.csk-msg--ai .csk-msg__bubble .csk-md-ul,\n.csk-msg--ai .csk-msg__bubble .csk-md-ol {\n padding-left: 22px;\n margin: 6px 0;\n}\n.csk-msg--ai .csk-msg__bubble .csk-md-ul li,\n.csk-msg--ai .csk-msg__bubble .csk-md-ol li { margin-bottom: 4px; }\n.csk-md-blockquote {\n border-left: 3px solid var(--csk-primary);\n padding: 2px 12px;\n margin: 8px 0;\n color: #6B7280;\n background: rgba(var(--csk-primary-rgb), 0.05);\n border-radius: 0 6px 6px 0;\n}\n.csk-md-link {\n color: var(--csk-primary);\n text-decoration: none;\n border-bottom: 1px solid rgba(var(--csk-primary-rgb), 0.3);\n transition: border-color 0.15s;\n}\n.csk-md-link:hover { border-bottom-color: var(--csk-primary); }\n.csk-md-hr {\n border: none;\n border-top: 1px solid var(--csk-border);\n margin: 12px 0;\n}\n.csk-md-table-wrap {\n max-width: 100%;\n overflow-x: auto;\n margin: 10px 0;\n border: 1px solid var(--csk-border);\n border-radius: 8px;\n}\n.csk-md-table {\n width: 100%;\n min-width: 360px;\n border-collapse: collapse;\n font-size: 12.5px;\n line-height: 1.5;\n}\n.csk-md-table th,\n.csk-md-table td {\n padding: 7px 9px;\n border-right: 1px solid var(--csk-border);\n border-bottom: 1px solid var(--csk-border);\n text-align: left;\n vertical-align: top;\n}\n.csk-md-table th:last-child,\n.csk-md-table td:last-child { border-right: none; }\n.csk-md-table tbody tr:last-child td { border-bottom: none; }\n.csk-md-table th {\n background: rgba(var(--csk-primary-rgb), 0.07);\n color: #374151;\n font-weight: 600;\n}\n.csk-md-table tbody tr:nth-child(even) {\n background: rgba(148, 163, 184, 0.06);\n}\n.csk-md-table .csk-md-align-left { text-align: left; }\n.csk-md-table .csk-md-align-center { text-align: center; }\n.csk-md-table .csk-md-align-right { text-align: right; }\n\n/* ========== P2: 会话管理面板 ========== */\n.csk-history-panel {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: #fff;\n z-index: 10;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n animation: csk-slide-in 0.24s ease;\n}\n.csk-history-panel--hidden { display: none; }\n@keyframes csk-slide-in {\n from { transform: translateX(100%); opacity: 0.4; }\n to { transform: translateX(0); opacity: 1; }\n}\n.csk-history-panel__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 14px 16px;\n border-bottom: 1px solid var(--csk-border);\n background: var(--csk-bg-app);\n flex-shrink: 0;\n}\n.csk-history-panel__title {\n font-size: 14px;\n font-weight: 600;\n color: #1F2937;\n}\n.csk-history-panel__back {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 5px 12px;\n border: 1px solid var(--csk-border);\n border-radius: 8px;\n background: #fff;\n color: #374151;\n font-size: 12px;\n cursor: pointer;\n transition: all 0.15s;\n font-family: inherit;\n}\n.csk-history-panel__back:hover { background: #F3F4F6; }\n.csk-history-panel__list {\n flex: 1;\n overflow-y: auto;\n padding: 10px;\n}\n.csk-history-panel__list::-webkit-scrollbar { width: 5px; }\n.csk-history-panel__list::-webkit-scrollbar-thumb { background: #E5E7EB; border-radius: 2px; }\n.csk-history-item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 11px 12px;\n border-radius: 10px;\n cursor: pointer;\n transition: background 0.15s;\n margin-bottom: 5px;\n border: 1px solid transparent;\n}\n.csk-history-item:hover { background: #F3F4F6; }\n.csk-history-item--active {\n background: rgba(var(--csk-primary-rgb), 0.08);\n border-color: rgba(var(--csk-primary-rgb), 0.2);\n}\n.csk-history-item--active:hover { background: rgba(var(--csk-primary-rgb), 0.12); }\n.csk-history-item__info { flex: 1; min-width: 0; }\n.csk-history-item__id {\n font-size: 13px;\n font-weight: 500;\n color: #1F2937;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.csk-history-item__meta {\n font-size: 11px;\n color: #9CA3AF;\n margin-top: 3px;\n}\n.csk-history-item__actions {\n display: flex;\n gap: 4px;\n margin-left: 8px;\n opacity: 0;\n transition: opacity 0.15s;\n}\n.csk-history-item:hover .csk-history-item__actions { opacity: 1; }\n.csk-history-action {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n border-radius: 7px;\n cursor: pointer;\n font-size: 12px;\n transition: all 0.15s;\n}\n.csk-history-action--export { background: #EFF6FF; color: #2563EB; }\n.csk-history-action--export:hover { background: #DBEAFE; }\n.csk-history-action--delete { background: #FEF2F2; color: #DC2626; }\n.csk-history-action--delete:hover { background: #FEE2E2; }\n.csk-history-panel__empty {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 48px 20px;\n color: #9CA3AF;\n font-size: 13px;\n text-align: center;\n}\n.csk-history-panel__empty-icon {\n font-size: 34px;\n margin-bottom: 10px;\n opacity: 0.5;\n}\n.csk-history-panel__loading {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n color: #9CA3AF;\n font-size: 13px;\n}\n\n/* ========== 快捷问题芯片 ========== */\n.csk-quick-replies {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n justify-content: center;\n margin-top: 18px;\n max-width: 320px;\n}\n.csk-quick-reply {\n border: 1px solid var(--csk-border);\n background: #fff;\n color: #374151;\n font-size: 12.5px;\n padding: 7px 13px;\n border-radius: 999px;\n cursor: pointer;\n font-family: inherit;\n line-height: 1.3;\n transition: all 0.18s ease;\n max-width: 100%;\n}\n.csk-quick-reply:hover {\n border-color: var(--csk-primary);\n color: var(--csk-primary);\n background: rgba(var(--csk-primary-rgb), 0.06);\n transform: translateY(-1px);\n}\n.csk-quick-reply:active { transform: scale(0.96); }\n\n/* ========== 推荐问题(suggest-message-list) ========== */\n.csk-suggestions {\n margin-top: 6px;\n padding: 4px 0 0 0;\n}\n.csk-suggestions__list {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n}\n.csk-suggestion-item {\n border: 1px solid var(--csk-border);\n background: #fff;\n color: #374151;\n font-size: 12px;\n padding: 4px 10px;\n border-radius: 999px;\n cursor: pointer;\n font-family: inherit;\n line-height: 1.3;\n transition: all 0.18s ease;\n max-width: 100%;\n text-align: left;\n}\n.csk-suggestion-item:hover {\n border-color: var(--csk-primary);\n color: var(--csk-primary);\n background: rgba(var(--csk-primary-rgb), 0.06);\n transform: translateY(-1px);\n}\n.csk-suggestion-item:active { transform: scale(0.96); }\n\n/* ========== AI 消息操作条 ========== */\n.csk-msg__actions {\n display: flex;\n gap: 2px;\n padding: 0 2px;\n opacity: 0;\n transition: opacity 0.15s ease;\n}\n.csk-msg--ai:hover .csk-msg__actions { opacity: 1; }\n.csk-msg--streaming .csk-msg__actions { display: none; }\n.csk-action-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n border: none;\n background: transparent;\n color: #9CA3AF;\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n.csk-action-btn:hover {\n background: #F3F4F6;\n color: #374151;\n}\n.csk-action-btn--done { color: #10B981; }\n.csk-action-btn:active { transform: scale(0.9); }\n\n/* ========== 流式打字光标 ========== */\n.csk-caret {\n display: inline-block;\n width: 7px;\n height: 15px;\n margin-left: 2px;\n vertical-align: text-bottom;\n background: var(--csk-primary);\n border-radius: 1px;\n animation: csk-caret-blink 1s step-end infinite;\n}\n@keyframes csk-caret-blink {\n 0%, 50% { opacity: 1; }\n 51%, 100% { opacity: 0; }\n}\n\n/* ========== 代码块复制按钮 ========== */\n.csk-md-code-copy {\n position: absolute;\n top: 8px;\n right: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n border: none;\n background: rgba(255, 255, 255, 0.12);\n color: #CBD5E1;\n border-radius: 6px;\n cursor: pointer;\n opacity: 0;\n transition: all 0.15s ease;\n}\n.csk-md-code-block:hover .csk-md-code-copy { opacity: 1; }\n.csk-md-code-copy:hover { background: rgba(255, 255, 255, 0.22); color: #fff; }\n.csk-md-code-copy:active { transform: scale(0.9); }\n\n/* ========== 新消息提示按钮 ========== */\n.csk-newmsg {\n position: absolute;\n bottom: 82px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 5;\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 7px 14px;\n border: none;\n background: var(--csk-primary);\n color: #fff;\n font-size: 12px;\n font-family: inherit;\n border-radius: 999px;\n cursor: pointer;\n box-shadow: 0 4px 14px rgba(var(--csk-primary-rgb), 0.38);\n transition: transform 0.2s ease, box-shadow 0.2s ease;\n animation: csk-msg-in 0.25s ease;\n white-space: nowrap;\n}\n.csk-newmsg:hover { transform: translateX(-50%) translateY(-1px); box-shadow: 0 6px 18px rgba(var(--csk-primary-rgb), 0.48); }\n.csk-newmsg:active { transform: translateX(-50%) scale(0.95); }\n.csk-newmsg--hidden { display: none; }\n\n/* ========== 发送按钮停止态 ========== */\n.csk-send-btn--stop {\n background: #EF4444 !important;\n box-shadow: 0 3px 10px rgba(239, 68, 68, 0.35) !important;\n}\n.csk-send-btn--stop:hover:not(:disabled) {\n box-shadow: 0 5px 14px rgba(239, 68, 68, 0.45) !important;\n}\n\n/* ========== 窗口缩放拖拽手柄 ========== */\n.csk-resize-handle {\n position: absolute;\n right: 4px;\n bottom: 4px;\n width: 20px;\n height: 20px;\n display: flex;\n align-items: flex-end;\n justify-content: flex-end;\n cursor: nwse-resize;\n z-index: 5;\n user-select: none;\n -webkit-user-select: none;\n /* 圆形半透明背景,与整体圆润风格一致 */\n border-radius: 50%;\n background: transparent;\n transition: background 0.2s ease;\n}\n.csk-resize-handle:hover {\n background: rgba(var(--csk-primary-rgb), 0.08);\n}\n\n/* 三斜线抓手 — 纯 CSS 绘制,用两条间距微调的线,视觉上是三条 */\n.csk-resize-grip {\n display: block;\n width: 10px;\n height: 10px;\n background:\n linear-gradient(135deg,\n transparent 0%, transparent 38%,\n #C8CCD2 38%, #C8CCD2 42%,\n transparent 42%, transparent 54%,\n #C8CCD2 54%, #C8CCD2 58%,\n transparent 58%, transparent 70%,\n #C8CCD2 70%, #C8CCD2 74%,\n transparent 74%\n );\n transition: background 0.2s ease;\n border-radius: 2px;\n}\n/* hover 时线条变为主色 */\n.csk-resize-handle:hover .csk-resize-grip {\n background:\n linear-gradient(135deg,\n transparent 0%, transparent 38%,\n var(--csk-primary) 38%, var(--csk-primary) 42%,\n transparent 42%, transparent 54%,\n var(--csk-primary) 54%, var(--csk-primary) 58%,\n transparent 58%, transparent 70%,\n var(--csk-primary) 70%, var(--csk-primary) 74%,\n transparent 74%\n );\n}\n\n/* 暗色模式 */\n.csk-dark .csk-resize-grip {\n background:\n linear-gradient(135deg,\n transparent 0%, transparent 38%,\n #5B6070 38%, #5B6070 42%,\n transparent 42%, transparent 54%,\n #5B6070 54%, #5B6070 58%,\n transparent 58%, transparent 70%,\n #5B6070 70%, #5B6070 74%,\n transparent 74%\n );\n}\n.csk-dark .csk-resize-handle:hover .csk-resize-grip {\n background:\n linear-gradient(135deg,\n transparent 0%, transparent 38%,\n var(--csk-primary-light) 38%, var(--csk-primary-light) 42%,\n transparent 42%, transparent 54%,\n var(--csk-primary-light) 54%, var(--csk-primary-light) 58%,\n transparent 58%, transparent 70%,\n var(--csk-primary-light) 70%, var(--csk-primary-light) 74%,\n transparent 74%\n );\n}\n\n/* 缩放拖拽中:禁用过渡动画 + 全局光标 */\n.csk-window--resizing {\n transition: none !important;\n user-select: none;\n}\n.csk-window--resizing * { cursor: nwse-resize !important; }\n\n/* ========== 暗色模式 ========== */\n.csk-root.csk-dark {\n --csk-bg-ai: #1E1E2E;\n --csk-text-ai: #E2E8F0;\n --csk-border: #3B3B52;\n --csk-bg-app: #141421;\n color: #E2E8F0;\n}\n.csk-dark .csk-window {\n background: #1A1A2E;\n box-shadow: 0 12px 48px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.2);\n}\n.csk-dark .csk-watermark {\n opacity: 1;\n}\n.csk-dark .csk-header::after { background: linear-gradient(to bottom, rgba(0,0,0,0.12), transparent); }\n.csk-dark .csk-msg__avatar--ai {\n background: var(--csk-bg-ai);\n color: var(--csk-primary);\n border-color: var(--csk-border);\n}\n.csk-dark .csk-input-area { background: #1A1A2E; border-top-color: var(--csk-border); }\n.csk-dark .csk-input-wrap { background: var(--csk-bg-app); border-color: var(--csk-border); }\n.csk-dark .csk-input-wrap--focus { background: #1A1A2E; border-color: var(--csk-primary); }\n.csk-dark .csk-input { color: #E2E8F0; }\n.csk-dark .csk-loading__avatar { background: var(--csk-bg-ai); border-color: var(--csk-border); }\n.csk-dark .csk-loading__bubble { background: var(--csk-bg-ai); border-color: var(--csk-border); }\n.csk-dark .csk-loading__dot { background: #6B7280; }\n.csk-dark .csk-category-bar { background: #1A1A2E; border-top-color: var(--csk-border); }\n.csk-dark .csk-category-select { background-color: var(--csk-bg-app); border-color: var(--csk-border); color: #E2E8F0; }\n.csk-dark .csk-sources { background: #1E1E2E; border-color: var(--csk-border); }\n.csk-dark .csk-sources__header { background: #25253A; }\n.csk-dark .csk-sources__header:hover { background: #2D2D48; }\n.csk-dark .csk-source-item:hover { background: var(--csk-bg-ai); }\n.csk-dark .csk-source-item__name { color: #E2E8F0; }\n.csk-dark .csk-source-item__meta { color: #6B7280; }\n.csk-dark .csk-history-panel { background: #1A1A2E; }\n.csk-dark .csk-history-panel__header { background: var(--csk-bg-app); border-bottom-color: var(--csk-border); }\n.csk-dark .csk-history-panel__back { background: var(--csk-bg-app); border-color: var(--csk-border); color: #E2E8F0; }\n.csk-dark .csk-history-panel__back:hover { background: #2D2D48; }\n.csk-dark .csk-history-item:hover { background: #25253A; }\n.csk-dark .csk-history-item__id { color: #E2E8F0; }\n.csk-dark .csk-history-item__meta { color: #6B7280; }\n.csk-dark .csk-history-item--active { background: rgba(var(--csk-primary-rgb), 0.15); }\n.csk-dark .csk-history-item--active:hover { background: rgba(var(--csk-primary-rgb), 0.22); }\n.csk-dark .csk-action-btn:hover { background: #2D2D48; color: #E2E8F0; }\n.csk-dark .csk-clear-btn { background: var(--csk-bg-app); border-color: var(--csk-border); color: #9CA3AF; }\n.csk-dark .csk-clear-btn:hover { background: #3B1A1A; border-color: #7F1D1D; color: #FCA5A5; }\n.csk-dark .csk-md-code-block { background: #0F0F1A; }\n.csk-dark .csk-md-blockquote { background: rgba(var(--csk-primary-rgb), 0.08); }\n.csk-dark .csk-md-link { color: var(--csk-primary-light); }\n.csk-dark .csk-md-table th { background: rgba(var(--csk-primary-rgb), 0.14); color: #E2E8F0; }\n.csk-dark .csk-md-table tbody tr:nth-child(even) { background: rgba(148, 163, 184, 0.05); }\n.csk-dark .csk-msg--ai .csk-msg__bubble { background: var(--csk-bg-ai); color: var(--csk-text-ai); border-color: var(--csk-border); }\n.csk-dark .csk-welcome__title { color: #E2E8F0; }\n.csk-dark .csk-welcome__desc { color: #6B7280; }\n.csk-dark .csk-quick-reply { background: var(--csk-bg-app); border-color: var(--csk-border); color: #CBD5E1; }\n.csk-dark .csk-quick-reply:hover { background: rgba(var(--csk-primary-rgb), 0.12); border-color: var(--csk-primary); color: var(--csk-primary-light); }\n.csk-dark .csk-suggestion-item { background: var(--csk-bg-app); border-color: var(--csk-border); color: #CBD5E1; }\n.csk-dark .csk-suggestion-item:hover { background: rgba(var(--csk-primary-rgb), 0.12); border-color: var(--csk-primary); color: var(--csk-primary-light); }\n.csk-dark .csk-teaser { background: #2D2D48; color: #E2E8F0; box-shadow: 0 4px 20px rgba(0,0,0,0.35); }\n.csk-dark .csk-teaser::after { border-top-color: #2D2D48; }\n.csk-dark .csk-teaser__close { color: #6B7280; }\n.csk-dark .csk-teaser__close:hover { color: #9CA3AF; }\n.csk-dark .csk-feedback-btn { background: rgba(var(--csk-primary-rgb), 0.1); color: #9CA3AF; }\n.csk-dark .csk-feedback-btn:hover { background: rgba(var(--csk-primary-rgb), 0.2); color: var(--csk-primary-light); }\n.csk-dark .csk-feedback-btn--active { background: rgba(var(--csk-primary-rgb), 0.28); color: var(--csk-primary-light); }\n.csk-dark .csk-history-panel__search-wrap { background: var(--csk-bg-app); border-color: var(--csk-border); }\n.csk-dark .csk-history-panel__search-wrap:focus-within { border-color: var(--csk-primary); }\n.csk-dark .csk-history-panel__search { color: #E2E8F0; }\n.csk-dark .csk-launcher__badge { box-shadow: 0 0 0 2px #1A1A2E; }\n.csk-dark .csk-welcome__avatar { box-shadow: 0 6px 18px rgba(var(--csk-primary-rgb), 0.25); }\n.csk-dark .csk-header__avatar { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.1); }\n.csk-dark .csk-role-select option {\n background: #1A1A2E;\n color: #E2E8F0;\n}\n\n/* Launcher 拖拽态:微放大 + 阴影加深 */\n.csk-launcher--dragging {\n cursor: grabbing !important;\n transform: scale(1.1) !important;\n box-shadow:\n 0 4px 12px rgba(0, 0, 0, 0.14),\n 0 12px 32px rgba(0, 0, 0, 0.18),\n 0 14px 40px rgba(236, 72, 153, 0.36),\n 0 0 56px rgba(236, 72, 153, 0.18) !important;\n}\n/* 拖拽中隐藏未读徽章 */\n.csk-launcher--dragging .csk-launcher__badge { display: none !important; }\n/* 吸附过渡动画(释放时的缓动) */\n.csk-launcher--snap {\n transition: left 0.3s ease-out, right 0.3s ease-out, bottom 0.3s ease-out !important;\n}\n\n/* ========== Launcher 未读徽章 ========== */\n.csk-launcher__badge {\n position: absolute;\n top: -2px;\n right: -2px;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: #EF4444;\n border: 2px solid #fff;\n box-shadow: 0 0 0 2px #fff;\n animation: csk-badge-in 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);\n pointer-events: none;\n}\n.csk-launcher__badge--hidden { display: none; }\n@keyframes csk-badge-in {\n from { transform: scale(0); }\n to { transform: scale(1); }\n}\n\n/* ========== Launcher 提示气泡 ========== */\n.csk-teaser {\n position: fixed;\n bottom: 90px;\n z-index: 9997;\n max-width: 240px;\n padding: 10px 14px;\n background: #fff;\n color: #1F2937;\n font-size: 13px;\n line-height: 1.5;\n border-radius: 12px;\n box-shadow: 0 4px 20px rgba(15, 23, 42, 0.15);\n animation: csk-teaser-in 0.35s cubic-bezier(0.22, 1, 0.36, 1);\n cursor: pointer;\n user-select: none;\n}\n.csk-teaser--right { right: 24px; }\n.csk-teaser--left { left: 24px; }\n.csk-teaser::after {\n content: '';\n position: absolute;\n bottom: -6px;\n right: 28px;\n width: 12px;\n height: 12px;\n background: inherit;\n border-radius: 2px;\n transform: rotate(45deg);\n box-shadow: 2px 2px 4px rgba(15, 23, 42, 0.08);\n}\n.csk-teaser--left::after { right: auto; left: 28px; }\n.csk-teaser__close {\n position: absolute;\n top: 4px;\n right: 6px;\n background: none;\n border: none;\n font-size: 16px;\n color: #9CA3AF;\n cursor: pointer;\n padding: 2px 4px;\n line-height: 1;\n border-radius: 4px;\n}\n.csk-teaser__close:hover { color: #374151; }\n.csk-teaser--hidden { display: none; }\n@keyframes csk-teaser-in {\n from { opacity: 0; transform: translateY(8px) scale(0.95); }\n to { opacity: 1; transform: translateY(0) scale(1); }\n}\n\n/* ========== 消息反馈按钮(👍 / 👎) ========== */\n.csk-feedback-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n border: none;\n background: rgba(var(--csk-primary-rgb), 0.08);\n color: #9CA3AF;\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.15s ease;\n padding: 0;\n}\n.csk-feedback-btn:hover { background: rgba(var(--csk-primary-rgb), 0.18); color: var(--csk-primary); }\n.csk-feedback-btn--active {\n background: rgba(var(--csk-primary-rgb), 0.28);\n color: var(--csk-primary);\n}\n.csk-feedback-btn--active.down { background: #FEF2F2; color: #EF4444; }\n.csk-feedback-btn:active { transform: scale(0.9); }\n.csk-msg--streaming .csk-feedback-btn { display: none; }\n\n/* ========== 点踩原因选择面板 ========== */\n.csk-feedback-reason {\n position: relative;\n margin-top: 6px;\n padding: 10px 12px;\n background: var(--csk-bg);\n border: 1px solid var(--csk-border);\n border-radius: 8px;\n animation: cskFadeIn 0.15s ease;\n}\n.csk-feedback-reason__title {\n font-size: 12px;\n color: var(--csk-text-muted);\n margin-bottom: 8px;\n}\n.csk-feedback-reason__options {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-bottom: 8px;\n}\n.csk-feedback-reason__btn {\n padding: 4px 10px;\n font-size: 12px;\n border: 1px solid var(--csk-border);\n border-radius: 14px;\n background: var(--csk-bg);\n color: var(--csk-text);\n cursor: pointer;\n transition: all 0.15s ease;\n}\n.csk-feedback-reason__btn:hover {\n border-color: var(--csk-primary);\n color: var(--csk-primary);\n background: rgba(var(--csk-primary-rgb), 0.08);\n}\n.csk-feedback-reason__comment-row {\n display: flex;\n gap: 6px;\n}\n.csk-feedback-reason__comment {\n flex: 1;\n padding: 4px 8px;\n font-size: 12px;\n border: 1px solid var(--csk-border);\n border-radius: 6px;\n background: var(--csk-bg);\n color: var(--csk-text);\n outline: none;\n}\n.csk-feedback-reason__comment:focus { border-color: var(--csk-primary); }\n.csk-feedback-reason__submit {\n padding: 4px 10px;\n font-size: 12px;\n border: none;\n border-radius: 6px;\n background: var(--csk-primary);\n color: #fff;\n cursor: pointer;\n white-space: nowrap;\n}\n.csk-feedback-reason__close {\n position: absolute;\n top: 8px;\n right: 8px;\n width: 18px;\n height: 18px;\n border: none;\n background: none;\n color: var(--csk-text-muted);\n cursor: pointer;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n}\n.csk-feedback-reason__close:hover { color: var(--csk-text); background: var(--csk-hover); }\n.csk-dark .csk-feedback-reason { background: var(--csk-bg); border-color: var(--csk-border); }\n.csk-dark .csk-feedback-reason__btn {\n background: rgba(255,255,255,0.06);\n border-color: var(--csk-border);\n color: var(--csk-text);\n}\n.csk-dark .csk-feedback-reason__btn:hover {\n border-color: var(--csk-primary-light);\n color: var(--csk-primary-light);\n background: rgba(var(--csk-primary-rgb), 0.15);\n}\n.csk-dark .csk-feedback-reason__comment {\n background: rgba(255,255,255,0.06);\n border-color: var(--csk-border);\n color: var(--csk-text);\n}\n\n/* ========== 历史会话搜索框 ========== */\n.csk-history-panel__search-wrap {\n padding: 8px 10px;\n border-bottom: 1px solid var(--csk-border);\n flex-shrink: 0;\n}\n.csk-history-panel__search-wrap:focus-within {\n border-bottom-color: var(--csk-primary);\n}\n.csk-history-panel__search {\n width: 100%;\n padding: 7px 10px;\n border: 1px solid var(--csk-border);\n border-radius: 8px;\n font-size: 12.5px;\n font-family: inherit;\n color: #1F2937;\n background: var(--csk-bg-app);\n outline: none;\n transition: border-color 0.2s;\n}\n.csk-history-panel__search:focus { border-color: var(--csk-primary); }\n.csk-history-panel__search::placeholder { color: #9CA3AF; }\n\n/* ========== a11y 焦点指示 ========== */\n.csk-root :focus-visible {\n outline: 2px solid var(--csk-primary);\n outline-offset: 2px;\n}\n.csk-root button:focus-visible {\n outline: 2px solid var(--csk-primary);\n outline-offset: 2px;\n border-radius: inherit;\n}\n/* 弹窗打开时隐藏 launcher 上的焦点环 */\n.csk-launcher:focus-visible {\n outline: none;\n border-color: rgba(255, 255, 255, 0.75);\n box-shadow:\n 0 1px 2px rgba(0, 0, 0, 0.06),\n 0 4px 16px rgba(0, 0, 0, 0.10),\n 0 6px 24px rgba(var(--csk-primary-rgb), 0.28),\n 0 0 48px rgba(var(--csk-primary-rgb), 0.12),\n 0 0 0 4px rgba(var(--csk-primary-rgb), 0.3);\n}\n\n/* ========== a11y aria-live 播报区(视觉隐藏) ========== */\n.csk-sr-only {\n position: absolute !important;\n width: 1px !important;\n height: 1px !important;\n padding: 0 !important;\n margin: -1px !important;\n overflow: hidden !important;\n clip: rect(0,0,0,0) !important;\n white-space: nowrap !important;\n border: 0 !important;\n}\n\n/* ========== 会话分组标题 ========== */\n.csk-history-group {\n margin-bottom: 4px;\n}\n.csk-history-group__label {\n font-size: 11px;\n font-weight: 600;\n color: #9CA3AF;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n padding: 8px 12px 4px;\n user-select: none;\n}\n.csk-dark .csk-history-group__label { color: #6B7280; }\n\n/* ========== 移动端适配 ========== */\n@media (max-width: 480px) {\n .csk-window {\n width: 100vw !important;\n height: 100vh !important;\n max-height: 100vh;\n bottom: 0 !important;\n right: 0 !important;\n left: 0 !important;\n border-radius: 0;\n }\n .csk-resize-handle { display: none !important; }\n .csk-window--hidden { transform: translateY(100%); }\n /* 移动端默认位置,仅在未被 JS 拖拽覆盖时生效 */\n .csk-launcher:not([style*="bottom"]) { bottom: 20px; }\n .csk-launcher--right:not([style*="bottom"]) { right: 20px; }\n .csk-launcher--left:not([style*="bottom"]) { left: 20px; }\n}\n`}(e),document.head.appendChild(S))}function M(){return"undefined"!=typeof crypto&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{const n=16*Math.random()|0;return("x"===e?n:3&n|8).toString(16)})}function F(e){const n={"&":"&","<":"<",">":">",'"':""","'":"'"};return e.replace(/[&<>"']/g,e=>n[e]||e)}function z(e){const n=new Date(e);return`${String(n.getHours()).padStart(2,"0")}:${String(n.getMinutes()).padStart(2,"0")}`}function D(){return Date.now()}const T='';function B(e,n){const t=document.createElement("div");t.id="csk-launcher",t.className="csk-launcher csk-launcher--"+("left-bottom"===e.position?"left":"right"),t.setAttribute("title",e.title),t.setAttribute("aria-label",e.title),t.setAttribute("role","button"),t.setAttribute("tabindex","0"),t.innerHTML=e.launcherIcon;const r=document.createElement("div");r.className="csk-launcher__badge csk-launcher__badge--hidden",t.appendChild(r);const s=function(e,n){let t=null;return function(...r){null!==t&&clearTimeout(t),t=setTimeout(()=>{e.apply(this,r),t=null},n)}}(n,300);return t.addEventListener("click",s),t.addEventListener("keydown",e=>{"Enter"!==e.key&&" "!==e.key||(e.preventDefault(),s())}),t}function q(e){const n=document.createElement("div");if(n.id="csk-window",n.className=`csk-root csk-window csk-window--${"left-bottom"===e.position?"left":"right"} csk-window--hidden${"dark"===e.theme?" csk-dark":""}`,n.setAttribute("role","dialog"),n.setAttribute("aria-label",e.title),n.setAttribute("aria-modal","false"),e.watermark){const t=document.createElement("div");t.className="csk-watermark";const r=function(e){const n=document.createElement("canvas"),t=n.getContext("2d");if(!t)return"";const r=new Date,s=`${r.getFullYear()}-${String(r.getMonth()+1).padStart(2,"0")}-${String(r.getDate()).padStart(2,"0")} ${String(r.getHours()).padStart(2,"0")}:${String(r.getMinutes()).padStart(2,"0")}`,o=e.watermark?`${e.watermark} ${s}`:s;n.width=280,n.height=160;const a="dark"===e.theme;return t.fillStyle=a?"rgba(255, 255, 255, 0.05)":"rgba(0, 0, 0, 0.06)",t.font='13px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif',t.textAlign="center",t.textBaseline="middle",t.save(),t.translate(140,80),t.rotate(-22*Math.PI/180),t.fillText(o,0,0),t.restore(),n.toDataURL("image/png")}(e);r&&(t.style.backgroundImage=`url(${r})`),n.appendChild(t)}const t=document.createElement("div");t.className="csk-header";const r=document.createElement("div");r.className="csk-header__left";const s=document.createElement("div");s.className="csk-header__avatar",s.innerHTML=T;const a=document.createElement("div");a.className="csk-header__info";const i=document.createElement("span");i.className="csk-header__title",i.textContent=e.title;const c=document.createElement("span");c.className="csk-header__status",c.innerHTML=`${o("status_online")}`,a.appendChild(i),a.appendChild(c),r.appendChild(s),r.appendChild(a);let l=null;const d=e.roles;if(d&&d.length>1){l=document.createElement("select"),l.className="csk-role-select",l.setAttribute("aria-label",o("role_selector_label"));for(const n of d){const t=document.createElement("option");t.value=String(n.id),t.textContent=n.name||String(n.id),String(n.id)===String(e.integrateId)&&(t.selected=!0),l.appendChild(t)}l.addEventListener("change",()=>{const e=l.value;n.dispatchEvent(new CustomEvent("csk:roleChange",{detail:{roleId:e}}))});const s=document.createElement("div");s.className="csk-role-select-wrap",s.appendChild(l),t.appendChild(r),t.appendChild(s)}else t.appendChild(r);const p=document.createElement("div");p.className="csk-header__actions";const u=document.createElement("button");u.className="csk-history-btn",u.setAttribute("title",o("history_title")),u.innerHTML='';const m=document.createElement("button");m.className="csk-header__btn csk-header__btn--minimize",m.setAttribute("title",o("minimize")),m.innerHTML='',m.addEventListener("click",()=>{n.classList.add("csk-window--hidden")});const k=document.createElement("button");k.className="csk-header__btn csk-header__btn--close",k.setAttribute("title",o("close")),k.innerHTML='',k.addEventListener("click",()=>{n.classList.add("csk-window--hidden")}),p.appendChild(u),p.appendChild(m),p.appendChild(k),t.appendChild(p);const g=document.createElement("div");g.id="csk-messages",g.className="csk-messages";const h=document.createElement("div");if(h.className="csk-welcome",h.innerHTML=`\n
    ${T}
    \n
    ${o("welcome_title")}
    \n
    ${o("welcome_desc")}
    \n `,e.quickReplies.length>0){const t=document.createElement("div");t.className="csk-quick-replies";for(const r of e.quickReplies){const e=document.createElement("button");e.type="button",e.className="csk-quick-reply",e.textContent=r,e.addEventListener("click",()=>{n.dispatchEvent(new CustomEvent("csk:quickReply",{detail:{text:r}}))}),t.appendChild(e)}h.appendChild(t)}g.appendChild(h);const b=document.createElement("button");b.type="button",b.className="csk-newmsg csk-newmsg--hidden",b.innerHTML=`\n \n ${o("new_message")}\n `,n.appendChild(b);const f=document.createElement("div");f.className="csk-history-panel csk-history-panel--hidden",f.innerHTML=`\n
    \n ${o("history_title")}\n \n
    \n
    \n \n
    \n
    \n `,g.appendChild(f);const y=f.querySelector("#csk-history-back");y&&y.addEventListener("click",()=>{f.classList.add("csk-history-panel--hidden")}),u.addEventListener("click",e=>{e.stopPropagation();const t=f.classList.contains("csk-history-panel--hidden");f.classList.toggle("csk-history-panel--hidden"),t&&n.dispatchEvent(new CustomEvent("csk:loadHistory"))});let v=null;if(e.showCategorySwitch){const e=document.createElement("div");e.className="csk-category-bar";const r=document.createElement("span");r.className="csk-category-bar__label",r.textContent="📚",v=document.createElement("select"),v.id="csk-category-select",v.className="csk-category-select",v.innerHTML=``,v.addEventListener("change",()=>{const e=v.value;n.dispatchEvent(new CustomEvent("csk:categoryChange",{detail:{categoryId:e?Number(e):void 0}}))}),e.appendChild(r),e.appendChild(v),n.appendChild(t),n.appendChild(g),n.appendChild(e)}else n.appendChild(t),n.appendChild(g);const x=document.createElement("div");x.className="csk-input-area";const w=document.createElement("div");w.className="csk-input-wrap";const _=document.createElement("textarea");_.id="csk-input",_.className="csk-input",_.setAttribute("placeholder",o("placeholder")),_.setAttribute("rows","1"),_.setAttribute("autofocus","");const E=document.createElement("button");E.id="csk-send-btn",E.className="csk-send-btn",E.setAttribute("title",o("send")),E.setAttribute("disabled","true"),E.innerHTML='',w.appendChild(_),w.appendChild(E),x.appendChild(w);const C=document.createElement("div");C.className="csk-disclaimer",void 0!==e.disclaimer?(C.innerHTML=e.disclaimer,e.disclaimer||(C.style.display="none")):C.style.display="none",x.appendChild(C),n.appendChild(x);let L=null;e.showClear&&(L=document.createElement("button"),L.className="csk-clear-btn",L.textContent=o("clear"),L.style.display="none",n.insertBefore(L,x));let I=null;const S=document.createElement("div");S.className="csk-sr-only",S.setAttribute("aria-live","polite"),S.setAttribute("aria-atomic","true"),n.appendChild(S);const A=f.querySelector("#csk-history-search"),$=document.createElement("div");$.className=`csk-teaser csk-teaser--${"left-bottom"===e.position?"left":"right"} csk-teaser--hidden`,$.setAttribute("role","status"),$.innerHTML=`\n \n ${e.teaserText||o("teaser_text")}\n `;const N=document.createElement("div");return N.className="csk-resize-handle",N.setAttribute("aria-label",o("resize")),N.innerHTML='',n.appendChild(N),{window:n,messagesContainer:g,inputEl:_,sendBtn:E,clearBtn:L,categorySelect:v,historyPanel:f,welcomeEl:h,newMsgBtn:b,teaserEl:$,resizeHandle:N,searchInput:A,ariaLiveEl:S,roleSelect:l,disclaimer:C,showLoading:function(){if(I)return I.style.display="flex",I;const e=document.createElement("div");return e.className="csk-loading",e.innerHTML=`\n
    ${T}
    \n
    \n
    \n
    \n
    \n
    \n `,g.appendChild(e),I=e,e},hideLoading:function(){I&&I.parentNode&&(I.parentNode.removeChild(I),I=null)}}}function H(){return window.innerHeight-76}function j(e,n,t){const r=document.createElement("div");r.className="csk-msg csk-msg--user";const s=document.createElement("div");s.className="csk-msg__avatar csk-msg__avatar--user",s.innerHTML='';const o=document.createElement("div");o.className="csk-msg__content";const a=document.createElement("div");a.className="csk-msg__bubble",a.textContent=n;const i=document.createElement("div");return i.className="csk-msg__time",i.textContent=z(t),o.appendChild(a),o.appendChild(i),r.appendChild(s),r.appendChild(o),e.appendChild(r),r}function R(e,n,t,r,s){const o=document.createElement("div");o.className="csk-msg csk-msg--ai",s&&(o.dataset.cskMsgId=s);const a=document.createElement("div");a.className="csk-msg__avatar csk-msg__avatar--ai",a.innerHTML=T;const i=document.createElement("div");i.className="csk-msg__content";const c=document.createElement("div");c.className="csk-msg__bubble",r?c.innerHTML=r(n):c.textContent=n;const l=document.createElement("div");l.className="csk-msg__time",l.textContent=z(t),i.appendChild(c);const d=document.createElement("div");return d.className="csk-msg__meta",d.appendChild(l),d.appendChild(W(c,o)),i.appendChild(d),o.appendChild(a),o.appendChild(i),e.appendChild(o),X(c),o}const P='',U='';function W(e,n){const t=document.createElement("div");t.className="csk-msg__actions";const r=document.createElement("button");r.type="button",r.className="csk-action-btn",r.setAttribute("title",o("copy")),r.setAttribute("aria-label",o("copy")),r.innerHTML=P,r.addEventListener("click",async n=>{n.stopPropagation();const t=e.textContent||"";await O(t,r)});const s=document.createElement("button");s.type="button",s.className="csk-action-btn",s.setAttribute("title",o("retry")),s.setAttribute("aria-label",o("retry")),s.innerHTML=U,s.addEventListener("click",e=>{e.stopPropagation();const t=n.closest(".csk-window");t&&t.dispatchEvent(new CustomEvent("csk:retry",{detail:{msgId:n.dataset.cskMsgId||""}}))});const a=document.createElement("button");a.type="button",a.className="csk-feedback-btn",a.setAttribute("title",o("feedback_up")),a.setAttribute("aria-label",o("feedback_up")),a.innerHTML='',a.addEventListener("click",e=>{e.stopPropagation();const t=n.closest(".csk-window");t&&t.dispatchEvent(new CustomEvent("csk:feedback",{detail:{msgId:n.dataset.cskMsgId||"",value:"up"}}))});const i=document.createElement("button");return i.type="button",i.className="csk-feedback-btn",i.setAttribute("title",o("feedback_down")),i.setAttribute("aria-label",o("feedback_down")),i.innerHTML='',i.addEventListener("click",e=>{e.stopPropagation();const t=n.closest(".csk-window");t&&t.dispatchEvent(new CustomEvent("csk:feedback",{detail:{msgId:n.dataset.cskMsgId||"",value:"down"}}))}),t.appendChild(r),t.appendChild(s),t.appendChild(a),t.appendChild(i),t}function Y(e,n){const t=e.querySelectorAll(".csk-feedback-btn");t.length<2||(t.forEach(e=>e.classList.remove("csk-feedback-btn--active","down")),"up"===n&&t[0]&&t[0].classList.add("csk-feedback-btn--active"),"down"===n&&t[1]&&t[1].classList.add("csk-feedback-btn--active","down"))}async function O(e,n){const t=n.innerHTML;try{if(navigator.clipboard&&window.isSecureContext)await navigator.clipboard.writeText(e);else{const n=document.createElement("textarea");n.value=e,n.style.position="fixed",n.style.opacity="0",document.body.appendChild(n),n.select(),document.execCommand("copy"),document.body.removeChild(n)}n.innerHTML='',n.classList.add("csk-action-btn--done"),setTimeout(()=>{n.innerHTML=t,n.classList.remove("csk-action-btn--done")},1500)}catch(e){}}function X(e){e.querySelectorAll(".csk-md-code-block").forEach(e=>{if(e.querySelector(".csk-md-code-copy"))return;const n=e.querySelector("code"),t=document.createElement("button");t.type="button",t.className="csk-md-code-copy",t.setAttribute("title",o("copy")),t.setAttribute("aria-label",o("copy")),t.innerHTML=P,t.addEventListener("click",async e=>{e.stopPropagation(),await O((null==n?void 0:n.textContent)||"",t)}),e.appendChild(t)})}function K(e,n){e.classList.remove("csk-msg--streaming");const t=n.querySelector(".csk-caret");t&&t.remove(),X(n)}function J(e,n){const t=e.querySelector(".csk-sources");if(t&&t.remove(),!n||0===n.length)return;const r=document.createElement("div");r.className="csk-sources csk-sources--collapsed";const s=document.createElement("div");s.className="csk-sources__header";const a=document.createElement("span");a.className="csk-sources__title",a.textContent=`📚 ${o("source_count",{n:n.length})}`;const i=document.createElement("span");i.className="csk-sources__arrow",i.textContent="▼",s.appendChild(a),s.appendChild(i),s.addEventListener("click",()=>{r.classList.toggle("csk-sources--collapsed")});const c=document.createElement("div");c.className="csk-sources__body";for(const e of n){const n=document.createElement("div");n.className="csk-source-item";const t=document.createElement("div");if(t.className="csk-source-item__name",t.textContent=e.title||e.sourceName||"未知文档",e.snippet){const t=document.createElement("div");t.className="csk-source-item__snippet",t.textContent=e.snippet,n.appendChild(t)}const r=document.createElement("div");r.className="csk-source-item__meta";const s=[];e.sourceName&&s.push(e.sourceName),void 0!==e.chunkIndex&&s.push(`分块 #${e.chunkIndex}`),void 0!==e.score&&s.push(`相关度 ${(100*e.score).toFixed(0)}%`),r.textContent=s.join(" · "),n.appendChild(t),n.appendChild(r),c.appendChild(n)}r.appendChild(s),r.appendChild(c);const l=e.querySelector(".csk-msg__meta");l&&l.parentNode?l.parentNode.insertBefore(r,l.nextSibling):e.appendChild(r)}function V(e,n,t,r,s,a,i){if(e.innerHTML="",0===n.length){const n=document.createElement("div");return n.className="csk-history-panel__empty",n.innerHTML=`\n
    💬
    \n
    ${o("history_empty")}
    \n `,void e.appendChild(n)}const c=function(e){const n=new Date,t=new Date(n.getFullYear(),n.getMonth(),n.getDate()).getTime(),r=t-864e5,s=t-864e5*(n.getDay()||7)+864e5,a={today:[],yesterday:[],week:[],earlier:[]};for(const n of e){const e=n.lastMessageTime||n.createdAt,o=e?new Date(e).getTime():0;o>=t?a.today.push(n):o>=r?a.yesterday.push(n):o>=s?a.week.push(n):a.earlier.push(n)}const i=[];return a.today.length&&i.push({label:o("history_group_today"),items:a.today}),a.yesterday.length&&i.push({label:o("history_group_yesterday"),items:a.yesterday}),a.week.length&&i.push({label:o("history_group_week"),items:a.week}),a.earlier.length&&i.push({label:o("history_group_earlier"),items:a.earlier}),i}(n);for(const n of c){if(c.length>1){const t=document.createElement("div");t.className="csk-history-group";const r=document.createElement("div");r.className="csk-history-group__label",r.textContent=n.label,t.appendChild(r),e.appendChild(t)}for(const i of n.items){const n=document.createElement("div");n.className="csk-history-item";const c=i.chatId||i.id;a&&c===a&&n.classList.add("csk-history-item--active");const l=document.createElement("div");l.className="csk-history-item__info";const d=document.createElement("div");d.className="csk-history-item__id",i.lastMessagePreview?d.textContent=i.lastMessagePreview.length>60?i.lastMessagePreview.substring(0,60)+"...":i.lastMessagePreview:d.textContent=c;const p=document.createElement("div");p.className="csk-history-item__meta";const u=[];i.roleName&&u.push(i.roleName),void 0!==i.messageCount&&u.push(`${i.messageCount} 条消息`),i.lastMessageTime?u.push(i.lastMessageTime):i.createdAt&&u.push(i.createdAt),p.textContent=u.join(" · "),l.appendChild(d),l.appendChild(p);const m=document.createElement("div");m.className="csk-history-item__actions";const k=document.createElement("button");k.className="csk-history-action csk-history-action--export",k.setAttribute("title",o("history_export")),k.innerHTML='',k.addEventListener("click",e=>{e.stopPropagation(),r(i.id)});const g=document.createElement("button");g.className="csk-history-action csk-history-action--delete",g.setAttribute("title",o("history_delete")),g.innerHTML='',g.addEventListener("click",e=>{e.stopPropagation(),s(i.id)}),m.appendChild(k),m.appendChild(g),n.appendChild(l),n.appendChild(m),n.addEventListener("click",()=>{t(c)}),e.appendChild(n)}}}function G(e){e.scrollTop=e.scrollHeight}const Q=[{key:"inaccurate",labelKey:"feedback_reason_inaccurate"},{key:"irrelevant",labelKey:"feedback_reason_irrelevant"},{key:"incomplete",labelKey:"feedback_reason_incomplete"},{key:"other",labelKey:"feedback_reason_other"}];function Z(e){return`csk_history_${e}`}function ee(e,n){try{let r=n;r.length>200&&(r=r.slice(50),t.warn(`消息数量达到上限,已裁剪最早 50 条,当前 ${r.length} 条`));const s={messages:r,updatedAt:Date.now()};localStorage.setItem(Z(e),JSON.stringify(s))}catch(e){e instanceof Error&&"QuotaExceededError"===e.name?t.error("localStorage 空间不足,会话历史保存失败。建议清空历史记录。"):t.error("保存会话历史失败",e)}}function ne(e){try{localStorage.removeItem(Z(e))}catch(e){}}const te="\0CODEBLOCK_",re="\0INLINECODE_";function se(e){if(!e||"string"!=typeof e)return"";const n=[];let t=e;t=t.replace(/```(\w*)\n([\s\S]*?)```/g,(e,t,r)=>{const s=n.length,o=F(r.trimEnd()),a=t?` class="language-${F(t)}"`:"";return n.push(`
    ${o}
    `),`${te}${s}\0`});const r=[];t=t.replace(/`([^`\n]+)`/g,(e,n)=>{const t=r.length;return r.push(`${F(n)}`),`${re}${t}\0`}),t=F(t),t=oe(t,te,n),t=oe(t,re,r);const s=t.split("\n"),o=[];let a=!1,i="",c=!1,l=[];for(let e=0;e')){h(),b(),f(),o.push(n);continue}const t=n.match(/^ {0,3}(#{1,6})(?:\s+|(?=[\u3400-\u9FFF\d]))(.+)/);if(t){h(),b(),f();const e=t[1].length;o.push(`${t[2]}`);continue}if(e+10&&t.length===r.length){h(),b(),f();const n=r.map(k);for(o.push('
    '),o.push(""),t.forEach((e,t)=>{o.push(`${d(e)}`)}),o.push(""),e+=2;e"),t.forEach((e,t)=>{o.push(`${d(r[t]||"")}`)}),o.push(""),e++}o.push("
    "),e--;continue}}const r=n.match(/^ {0,3}>\s?(.*)/);if(r){h(),b(),c||(c=!0,o.push('
    ')),o.push(`

    ${d(r[1])}

    `);continue}c&&f();const y=n.match(/^ {0,3}[\-\*]\s+(.+)/);if(y){h(),f(),a&&"ul"===i||(b(),a=!0,i="ul",o.push('
      ')),o.push(`
    • ${d(y[1])}
    • `);continue}const v=n.match(/^ {0,3}\d+\.\s+(.+)/);v?(h(),f(),a&&"ol"===i||(b(),a=!0,i="ol",o.push('
        ')),o.push(`
      1. ${d(v[1])}
      2. `)):""!==n.trim()?/^(\*{3,}|-{3,}|_{3,})$/.test(n.trim())?(h(),b(),f(),o.push('
        ')):(b(),f(),l.push(d(n))):(h(),b())}return h(),b(),f(),o.join("\n");function d(e){return e=(e=(e=(e=(e=(e=e.replace(/\*\*(.+?)\*\*/g,"$1")).replace(/__(.+?)__/g,"$1")).replace(/\*(.+?)\*/g,"$1")).replace(/(?$1")).replace(/~~(.+?)~~/g,"$1")).replace(/\[([^\]]+)\]\(([^)]+)\)/g,(e,n,t)=>`${n}`)}function p(e){const n=m(e);return n.length>0&&n.every(e=>/^:?-{3,}:?$/.test(e.trim()))}function u(e){return""!==e.trim()&&e.includes("|")}function m(e){const n=e.trim().replace(/^\|/,"").replace(/\|$/,"");if(!n.includes("|"))return[];const t=[];let r="";for(let e=0;e0&&(o.push(`

        ${l.join("
        ")}

        `),l=[])}function b(){a&&(o.push("ul"===i?"
    ":""),a=!1,i="")}function f(){c&&(o.push("
    "),c=!1)}}function oe(e,n,t){return e.replace(new RegExp(n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(\\d+)\0","g"),(e,n)=>t[parseInt(n)]||"")}let ae,ie=null,ce=[],le=null,de=null,pe=null,ue=null,me=null,ke=null,ge=null,he=null,be=null,fe=null,ye=null,ve=null,xe=null,we=null,_e=!1,Ee=null,Ce=!0,Le=[],Ie="",Se=!1;function Ae(e,n){ie=e,le=n.messagesContainer,de=n.inputEl,pe=n.inputEl.parentElement,ue=n.sendBtn,me=n.clearBtn,ke=n.categorySelect,ge=n.roleSelect,he=n.historyPanel,be=n.welcomeEl,fe=n.newMsgBtn,ye=n.searchInput,ve=n.ariaLiveEl,xe=n.showLoading,we=n.hideLoading,ae=e.categoryId,Se=e.enableRag,function(){if(!de||!ue)return;ue.addEventListener("click",()=>{_e&&Ee?Ee.abort():He()}),de.addEventListener("keydown",e=>{"Enter"!==e.key||e.shiftKey||e.isComposing||(e.preventDefault(),He())}),de.addEventListener("input",()=>{qe(),function(){if(!de)return;de.style.height="auto",de.style.height=`${Math.min(de.scrollHeight,120)}px`}()}),de.addEventListener("focus",()=>{pe&&pe.classList.add("csk-input-wrap--focus")}),de.addEventListener("blur",()=>{pe&&pe.classList.remove("csk-input-wrap--focus")}),me&&me.addEventListener("click",()=>function(){if(!ie)return;if(ce=[],le){le.querySelectorAll(".csk-msg, .csk-loading").forEach(e=>e.remove())}me&&(me.style.display="none");Be(),ne(ie.integrateId);const e=function(){const e="undefined"!=typeof crypto&&crypto.randomUUID?crypto.randomUUID().substring(0,8):Math.random().toString(36).substring(2,10);return`sdk_${Date.now()}_${e}`}();d(e),I(ie.integrateId,ie.userId,e),t.lifecycleClear(ie.integrateId)}())}(),function(){if(!le)return;le.addEventListener("scroll",()=>{if(!le)return;const{scrollTop:e,scrollHeight:n,clientHeight:t}=le;Ce=n-e-t<80,Ce&&Me()}),fe&&fe.addEventListener("click",()=>{le&&G(le),Ce=!0,Me()})}(),function(){if(!ye)return;ye.addEventListener("input",()=>{Ie=ye.value.trim().toLowerCase(),De()})}(),e.showCategorySwitch&&ke&&async function(){if(!ke)return;try{const e=await async function(){const e=u("/category/tree");try{const n=await k(e);if(!n.ok)throw new g(h(n.status),`http_${n.status}`);const r=await n.json();return r.success&&Array.isArray(r.data)?(t.info(`加载分类树成功 count=${r.data.length}`),r.data):[]}catch(e){return e instanceof g?t.error(`加载分类树失败: ${e.message}`):t.error("加载分类树失败",e),[]}}();if(0===e.length)return;ke.innerHTML=``;const n=(e,t=0)=>{for(const r of e){const e=document.createElement("option");e.value=String(r.id),e.textContent=`${" ".repeat(t)}${r.name}`,void 0!==ae&&String(r.id)===String(ae)&&(e.selected=!0),ke.appendChild(e),r.children&&r.children.length>0&&n(r.children,t+1)}};n(e),t.info(`知识库分类加载成功 count=${e.length}`)}catch(e){t.error(o("category_load_error"),e)}}()}async function $e(){if(ie&&le&&(await C(),await Ne(),0===ce.length)){const e=function(e){try{const n=localStorage.getItem(Z(e));if(!n)return[];const r=JSON.parse(n);return r&&Array.isArray(r.messages)?(t.info(`加载历史消息 integrateId=${e} count=${r.messages.length}`),r.messages):[]}catch(e){return[]}}(ie.integrateId);e.length>0&&(ce=e,Pe(),t.info(`从本地缓存恢复 ${e.length} 条消息`))}}async function Ne(){if(!ie||!le)return;const e=p();if(e)try{const n=await w(e);n.messages.length>0&&(ce=n.messages.map((e,n)=>({id:M(),role:"USER"===e.messageType?"user":"ai",content:e.content,timestamp:new Date(e.createTime).getTime()})),Pe(),t.info(`从后端加载 ${ce.length} 条历史消息`),ee(l(),ce))}catch(e){}}function Me(){fe&&fe.classList.add("csk-newmsg--hidden")}function Fe(){le&&(Ce?G(le):fe&&fe.classList.remove("csk-newmsg--hidden"))}function ze(e){ue&&("stop"===e?(ue.classList.add("csk-send-btn--stop"),ue.removeAttribute("disabled"),ue.setAttribute("title",o("stop")),ue.setAttribute("aria-label",o("stop")),ue.innerHTML=''):(ue.classList.remove("csk-send-btn--stop"),ue.setAttribute("title",o("send")),ue.setAttribute("aria-label",o("send")),ue.innerHTML='',qe()))}function De(){const e=null==he?void 0:he.querySelector("#csk-history-list");if(!e||!ie)return;V(e,Ie?Le.filter(e=>(e.lastMessagePreview||e.chatId||e.id||"").toLowerCase().includes(Ie)):Le,e=>{Ue(e)},e=>{window.open(E(e),"_blank")},async e=>{if(!confirm(o("history_delete_confirm")))return;await _(e)&&(e===p()&&(ce=[],le&&le.querySelectorAll(".csk-msg, .csk-loading").forEach(e=>e.remove()),me&&(me.style.display="none"),Be()),Le=Le.filter(n=>(n.chatId||n.id)!==e),De())},p())}function Te(e,n){if(!le)return;const t=ce.find(n=>n.id===e&&"ai"===n.role);if(t){if("up"===n){const n="up"===t.feedback?void 0:"up";t.feedback=n,t.feedbackReason=void 0,t.feedbackComment=void 0;const r=le.querySelector(`[data-csk-msg-id="${e}"]`);return r&&Y(r,n),ie&&ee(ie.integrateId,ce),void(n&&v(String(e),"THUMBS_UP").then(e=>{}))}if("down"===n){const n=le.querySelector(`[data-csk-msg-id="${e}"]`);if(!n)return;if("down"===t.feedback)return t.feedback=void 0,t.feedbackReason=void 0,t.feedbackComment=void 0,n&&Y(n,void 0),void(ie&&ee(ie.integrateId,ce));!function(e,n,t){const r=e.querySelector(".csk-feedback-reason");r&&r.remove();const s=document.createElement("div");s.className="csk-feedback-reason";const a=document.createElement("div");a.className="csk-feedback-reason__title",a.textContent=o("feedback_reason_title"),s.appendChild(a);const i=document.createElement("div");i.className="csk-feedback-reason__options",Q.forEach(e=>{const n=document.createElement("button");n.type="button",n.className="csk-feedback-reason__btn",n.textContent=o(e.labelKey),n.addEventListener("click",n=>{n.stopPropagation(),t(e.key),s.remove()}),i.appendChild(n)}),s.appendChild(i);const c=document.createElement("div");c.className="csk-feedback-reason__comment-row";const l=document.createElement("input");l.type="text",l.className="csk-feedback-reason__comment",l.placeholder=o("feedback_reason_comment_placeholder"),l.maxLength=200,c.appendChild(l);const d=document.createElement("button");d.type="button",d.className="csk-feedback-reason__submit",d.textContent=o("feedback_reason_submit"),d.addEventListener("click",e=>{e.stopPropagation();const n=l.value.trim()||void 0;t("other",n),s.remove()}),c.appendChild(d),s.appendChild(c);const p=document.createElement("button");p.type="button",p.className="csk-feedback-reason__close",p.setAttribute("aria-label",o("close")),p.innerHTML='',p.addEventListener("click",e=>{e.stopPropagation(),s.remove()}),s.appendChild(p),e.appendChild(s)}(n,0,(r,s)=>{t.feedback="down",t.feedbackReason=r,t.feedbackComment=s,n&&Y(n,"down"),ie&&ee(ie.integrateId,ce),v(String(e),"THUMBS_DOWN",r,s).then(e=>{})})}}}function Be(){if(!be)return;const e=ce.length>0||le&&le.querySelector(".csk-msg");be.style.display=e?"none":""}function qe(){if(!ue||!de)return;de.value.trim().length>0&&!_e?ue.removeAttribute("disabled"):ue.setAttribute("disabled","true")}async function He(){if(!de||!ie||_e)return;const e=de.value.trim();if(""===e)return;de.value="",qe(),de.style.height="auto";const n=D(),t={id:M(),role:"user",content:e,timestamp:n};le&&j(le,e,n),ce.push(t),Be(),me&&ce.length>0&&(me.style.display="inline-flex"),le&&Fe(),await je(e)}async function je(e){if(!ie||!le)return;_e=!0,ze("stop"),ie.chatId||await C();const n=D(),r=Se;xe&&xe(),le&&Fe();const s=M();let i="";try{ie.streaming?i=await async function(e,n,t,r){Ee=new AbortController;const s=Ee.signal;return new Promise((a,i)=>{let c=null,l=null,d="",p=!1;f(e,e=>{if(d+=e,!p&&le){we&&we();const{wrapper:e,bubble:t}=function(e,n,t){const r=document.createElement("div");r.className="csk-msg csk-msg--ai csk-msg--streaming",t&&(r.dataset.cskMsgId=t);const s=document.createElement("div");s.className="csk-msg__avatar csk-msg__avatar--ai",s.innerHTML=T;const o=document.createElement("div");o.className="csk-msg__content";const a=document.createElement("div");a.className="csk-msg__bubble",a.innerHTML="";const i=document.createElement("div");i.className="csk-msg__time",i.textContent=z(n),o.appendChild(a);const c=document.createElement("div");return c.className="csk-msg__meta",c.appendChild(i),c.appendChild(W(a,r)),o.appendChild(c),r.appendChild(s),r.appendChild(o),e.appendChild(r),{wrapper:r,bubble:a}}(le,n,r);l=e,c=t,p=!0}c&&(c.innerHTML=se(d),function(e){if(e.querySelector(".csk-caret"))return;const n=document.createElement("span");n.className="csk-caret",n.setAttribute("aria-hidden","true"),e.appendChild(n)}(c)),le&&Fe()},()=>{if(l&&c){if(!p&&""===d)return void b(e).then(a).catch(i);d&&(c.innerHTML=se(d)),K(l,c)}a(d)},e=>{"network"!==e.type&&"cors"!==e.type||le&&function(e){if(document.getElementById("csk-offline-banner"))return;const n=document.createElement("div");n.id="csk-offline-banner",n.className="csk-offline-banner",n.textContent=o("error_offline"),e.insertBefore(n,e.firstChild)}(le),d.length>0?(c&&(c.innerHTML=se(d+"\n\n"+o("stream_interrupted")),l&&K(l,c)),a(d)):i(e)},ae,t,s)})}(e,n,r,s):(i=await b(e),we&&we(),le&&R(le,i,n,se,s));const t={id:s,role:"ai",content:i,timestamp:n};ce.push(t),ee(ie.integrateId,ce),le&&Fe(),function(e){if(!ve)return;const n=e.length>120?e.substring(0,120)+"...":e;ve.textContent=o("new_msg_announce")+":"+n}(i),le&&le.dispatchEvent(new CustomEvent("csk:newMessage",{bubbles:!0,detail:{msg:t}})),r&&async function(e,n){try{const t=await y(e,ae);if(t.length>0){const e=t.map(e=>{var n,t;return{documentId:e.documentId||"",title:e.title||"",sourceName:e.sourceName||"",chunkIndex:null!==(n=e.chunkIndex)&&void 0!==n?n:0,score:null!==(t=e.score)&&void 0!==t?t:0,snippet:e.snippet||""}});if(n.sources=e,le){const n=le.querySelector(".csk-msg--ai:last-of-type");n&&J(n,e)}ie&&ee(ie.integrateId,ce)}}catch(e){}}(e,t),ie.suggestions&&async function(e){const n=p();if(!n||!le)return;try{const t=await async function(e){if(!a||!e)return[];try{const n=new URLSearchParams;n.set("chatId",e);const t=u(`/ai/suggestions?${n.toString()}`),r=await k(t);if(!r.ok)return[];const s=await r.json();return s.success&&s.data&&Array.isArray(s.data.suggestions)?s.data.suggestions.filter(e=>"string"==typeof e&&e.trim().length>0):[]}catch(e){return[]}}(n);if(t.length>0&&le){const n=le.querySelector(`[data-csk-msg-id="${e}"]`);n&&function(e,n,t){if(function(e){const n=e.querySelector(".csk-suggestions");n&&n.remove()}(e),!n||0===n.length)return;const r=document.createElement("div");r.className="csk-suggestions";const s=document.createElement("div");s.className="csk-suggestions__list";for(const e of n){const n=document.createElement("button");n.type="button",n.className="csk-suggestion-item",n.textContent=e,n.addEventListener("click",n=>{n.stopPropagation(),t(e)}),s.appendChild(n)}r.appendChild(s);const o=e.querySelector(".csk-msg__content");o?o.appendChild(r):e.appendChild(r)}(n,t,Re)}}catch(e){}}(s),function(){const e=document.getElementById("csk-offline-banner");e&&e.remove()}()}catch(e){we&&we();const n=e instanceof g?e.message:o("error_send");if(le){const e=document.createElement("div");e.className="csk-msg csk-msg--ai";const t=document.createElement("div");t.className="csk-msg__bubble",t.style.color="#DC2626",t.textContent=`⚠ ${n}`,e.appendChild(t),le.appendChild(e)}t.error(`发送失败 integrateId=${ie.integrateId}`,e)}finally{_e=!1,Ee=null,ze("send"),qe()}}function Re(e){return!de||_e?Promise.resolve():(de.value=e,qe(),He())}function Pe(){if(!le)return;const e=le.querySelector(".csk-history-panel");le.querySelectorAll(".csk-msg, .csk-loading").forEach(e=>e.remove());for(const e of ce)if("user"===e.role)j(le,e.content,e.timestamp);else{const n=R(le,e.content,e.timestamp,se,e.id);e.sources&&e.sources.length>0&&J(n,e.sources),e.feedback&&Y(n,e.feedback)}Ce=!0,G(le),me&&ce.length>0&&(me.style.display="inline-flex"),Be(),e&&!le.contains(e)&&le.appendChild(e)}async function Ue(e){if(!ie||!le)return;const n=Le.find(n=>(n.chatId||n.id)===e);if(n&&void 0!==n.roleId){const e=String(n.roleId),t=l();e&&e!==t&&(c(e),ge&&(ge.value=e))}d(e),I(l(),ie.userId,e),he&&he.classList.add("csk-history-panel--hidden"),ce=[];le.querySelectorAll(".csk-msg, .csk-loading").forEach(e=>e.remove());try{const n=await w(e);n.messages.length>0&&(ce=n.messages.map(e=>({id:M(),role:"USER"===e.messageType?"user":"ai",content:e.content,timestamp:new Date(e.createTime).getTime()})),Pe(),t.info(`加载会话 ${e} 的 ${ce.length} 条消息`),ee(ie.integrateId,ce))}catch(e){}me&&ce.length>0&&(me.style.display="inline-flex"),Be()}let We=null,Ye=!1,Oe=null,Xe=null,Ke=null,Je=null,Ve=null,Ge=null,Qe=null,Ze=null,en=null,nn=null,tn=null,rn=null,sn=null,on=null,an=null,cn=null,ln=null,dn=null,pn=null;function un(){if(!an||!Oe)return;const e=Oe.getBoundingClientRect(),n=on?"left"===on.side:"left-bottom"===(null==We?void 0:We.position);n?(an.style.left=`${e.left}px`,an.style.right="auto"):(an.style.right=window.innerWidth-e.right+"px",an.style.left="auto"),an.style.bottom=window.innerHeight-e.top+8+"px",an.classList.toggle("csk-teaser--left",!!n),an.classList.toggle("csk-teaser--right",!n)}function mn(){if(!an)return;an.classList.add("csk-teaser--hidden"),an.removeEventListener("click",kn);const e=an.querySelector(".csk-teaser__close");e&&e.removeEventListener("click",gn)}function kn(e){e.target.closest(".csk-teaser__close")||(mn(),yn())}function gn(e){e.stopPropagation(),mn()}function hn(){an&&!an.classList.contains("csk-teaser--hidden")&&un()}const bn='button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';function fn(e){"Escape"===e.key&&Xe&&!Xe.classList.contains("csk-window--hidden")&&vn(),function(e){if(!Xe||Xe.classList.contains("csk-window--hidden"))return;if("Tab"!==e.key)return;const n=Array.from(Xe.querySelectorAll(bn));if(0===n.length)return;const t=n[0],r=n[n.length-1];e.shiftKey?document.activeElement===t&&(e.preventDefault(),r.focus()):document.activeElement===r&&(e.preventDefault(),t.focus())}(e)}function yn(){Xe&&(Xe.classList.remove("csk-window--hidden"),mn(),ln&&ln.classList.add("csk-launcher__badge--hidden"),setTimeout(()=>{Je&&Je.focus()},80))}function vn(){Xe&&Xe.classList.add("csk-window--hidden")}function xn(){Xe&&(Xe.classList.contains("csk-window--hidden")?yn():vn())}function wn(e){return`csk_size_${e}`}function _n(e){return`csk_position_${e}`}function En(e){return`csk_launcher_pos_${e}`}function Cn(e){Xe&&(Xe.classList.remove("csk-window--right","csk-window--left"),Xe.classList.add(`csk-window--${e}`),an&&(an.classList.remove("csk-teaser--right","csk-teaser--left"),an.classList.add(`csk-teaser--${e}`)))}const Ln={init:function(e){if(Ye)return;const i=function(e){var n,r,s,o,a,i,c,l,d,p,u,m,k;if(!e.integrateId||"string"!=typeof e.integrateId&&"number"!=typeof e.integrateId||"string"==typeof e.integrateId&&""===e.integrateId.trim())return t.error('integrateId 是必传参数(对应后端 roleId 客服角色 ID),请检查 init() 调用。示例:ChatbotSDK.init({ integrateId: 1, requestDomain: "https://api.example.com" })'),null;if(!e.requestDomain||"string"!=typeof e.requestDomain||""===e.requestDomain.trim())return t.error('requestDomain 是必传参数,请检查 init() 调用。示例:ChatbotSDK.init({ integrateId: 1, requestDomain: "https://api.example.com" })'),null;try{new URL(e.requestDomain)}catch(n){return t.error(`requestDomain 不是合法的 URL 格式:${e.requestDomain}。请提供完整的域名,如 https://api.example.com`),null}const g=String(e.integrateId).trim(),h=e.requestDomain.replace(/\/+$/,""),b=e.launcherIcon||``,f=e.primaryColor||"#4F46E5",y={integrateId:g,requestDomain:e.requestDomain.replace(/\/+$/,""),userId:e.userId,categoryId:e.categoryId,showCategorySwitch:null!==(n=e.showCategorySwitch)&&void 0!==n&&n,title:e.title||"AI 智能助手",width:null!==(r=e.width)&&void 0!==r?r:500,height:Math.max(400,null!==(s=e.height)&&void 0!==s?s:520),position:"left-bottom"===e.position?"left-bottom":"right-bottom",primaryColor:f,launcherIcon:b,showClear:null===(o=e.showClear)||void 0===o||o,showAdminPanel:null!==(a=e.showAdminPanel)&&void 0!==a&&a,quickReplies:Array.isArray(e.quickReplies)?e.quickReplies.map(e=>String(e).trim()).filter(Boolean):[],suggestions:null===(i=e.suggestions)||void 0===i||i,theme:"dark"===e.theme?"dark":"light",showTeaser:null===(c=e.showTeaser)||void 0===c||c,teaserText:"string"==typeof e.teaserText&&e.teaserText.trim()||"",resizable:null===(l=e.resizable)||void 0===l||l,watermark:"string"==typeof e.watermark&&e.watermark.trim()||void 0,streaming:null===(d=e.streaming)||void 0===d||d,enableRag:null===(p=e.enableRag)||void 0===p||p,rewriteStrategy:e.rewriteStrategy||"REWRITE",locale:e.locale||"zh-CN",debug:null===(u=e.debug)||void 0===u||u,sound:null!==(m=e.sound)&&void 0!==m&&m,notification:null!==(k=e.notification)&&void 0!==k&&k,onError:"function"==typeof e.onError?e.onError:void 0,onReady:"function"==typeof e.onReady?e.onReady:void 0,onMessage:"function"==typeof e.onMessage?e.onMessage:void 0,token:e.token,roles:e.roles,disclaimer:e.disclaimer,chatId:""};return t.info(`配置解析完成 integrateId(=roleId)=${y.integrateId} userId(=accountId)=${y.userId||"(未设置)"} requestDomain=${y.requestDomain}`),y}(e);if(!i)return;We=i,function(e){if(r[e])s=e;else{const n=e.split("-")[0],t=Object.keys(r).find(e=>e.startsWith(n));t&&(s=t)}}(We.locale),We.debug,n(We.onError),function(e){a=e}(We),N(We),Oe=B(We,xn),document.body.appendChild(Oe),ln=Oe.querySelector(".csk-launcher__badge"),function(e,n){try{const t=localStorage.getItem(En(e));if(!t)return;const r=JSON.parse(t);if("left"!==r.side&&"right"!==r.side||"number"!=typeof r.bottom)return;const s=window.innerHeight-76,o=Math.max(16,Math.min(r.bottom,s));n.classList.remove("csk-launcher--right","csk-launcher--left"),n.classList.add(`csk-launcher--${r.side}`),n.style.bottom=`${o}px`,on={side:r.side,bottom:o}}catch(e){}}(We.integrateId,Oe),rn=function(e,n,t){let r=!1,s=0,o=0,a=!1,i=0,c=0,l=0,d=!1;function p(){const n=parseFloat(getComputedStyle(e).bottom);return isNaN(n)?24:n}function u(n,t){if(d)return;r=!0,a=!1,s=n,o=t,i=p();const u=e.style,m=getComputedStyle(e);c=u.right?parseFloat(u.right):parseFloat(m.right),l=u.left?parseFloat(u.left):parseFloat(m.left)}function m(n,t,d){if(!r)return;const p=n-s,u=t-o;if(!a){if(Math.abs(p)<5&&Math.abs(u)<5)return;a=!0,e.classList.add("csk-launcher--dragging"),e.classList.remove("csk-launcher--right","csk-launcher--left"),e.style.transition="none",!isNaN(c)&&c>=0&&isNaN(l)?(e.style.right=`${c}px`,e.style.left="auto"):!isNaN(l)&&l>=0&&(e.style.left=`${l}px`,e.style.right="auto")}if(a){d.preventDefault();const n=Math.max(16,Math.min(i-u,H()));e.style.bottom=`${n}px`}}function k(){if(!r)return;if(r=!1,!a)return;const n=parseFloat(e.style.bottom)||p(),s=Math.max(16,Math.min(n,H())),o=e.getBoundingClientRect(),i=o.left+o.width/2<=window.innerWidth/2?"left":"right";e.classList.remove("csk-launcher--dragging"),e.classList.add(`csk-launcher--${i}`,"csk-launcher--snap"),e.style.left="",e.style.right="",e.style.bottom=`${s}px`,d=!0;const c=()=>{e.removeEventListener("transitionend",c),e.classList.remove("csk-launcher--snap"),e.style.transition="",d=!1};e.addEventListener("transitionend",c),setTimeout(()=>{d&&c()},350);const l=n=>{n.stopPropagation(),e.removeEventListener("click",l,!0)};e.addEventListener("click",l,!0);const u=e.querySelector(".csk-launcher__badge");u&&(u.style.display=""),t&&t({side:i,bottom:s})}function g(e){u(e.clientX,e.clientY)}function h(e){m(e.clientX,e.clientY,e)}function b(){k()}function f(e){1===e.touches.length&&u(e.touches[0].clientX,e.touches[0].clientY)}function y(e){1===e.touches.length&&m(e.touches[0].clientX,e.touches[0].clientY,e)}function v(){k()}return e.addEventListener("mousedown",g),document.addEventListener("mousemove",h),document.addEventListener("mouseup",b),e.addEventListener("touchstart",f,{passive:!0}),document.addEventListener("touchmove",y,{passive:!1}),document.addEventListener("touchend",v),()=>{e.removeEventListener("mousedown",g),document.removeEventListener("mousemove",h),document.removeEventListener("mouseup",b),e.removeEventListener("touchstart",f),document.removeEventListener("touchmove",y),document.removeEventListener("touchend",v)}}(Oe,0,e=>{const n=null==on?void 0:on.side;on=e,function(e,n){try{localStorage.setItem(En(e),JSON.stringify(n))}catch(e){}}(We.integrateId,e),n&&n!==e.side&&function(){if(!Xe)return;Xe.style.left="",Xe.style.top="",Xe.style.right="",Xe.style.bottom=""}(),Cn(e.side),an&&!an.classList.contains("csk-teaser--hidden")&&un()}),on&&Cn(on.side);const d=q(We);Xe=d.window,Ke=d.messagesContainer,Je=d.inputEl,Ve=d.sendBtn,Ge=d.clearBtn,Qe=d.categorySelect,Ze=d.historyPanel,en=d.showLoading,nn=d.hideLoading,pn=d.disclaimer,document.body.appendChild(Xe),an=d.teaserEl,document.body.appendChild(an),un();const m=Xe.querySelector(".csk-header");if(m&&(tn=function(e,n,t){let r=!1,s=0,o=0,a=0,i=0;const c=e=>{if(e.target.closest("button"))return;r=!0,s=e.clientX,o=e.clientY;const t=n.getBoundingClientRect();a=s-t.left,i=o-t.top,document.addEventListener("mousemove",l),document.addEventListener("mouseup",d)},l=e=>{if(!r)return;const t=e.clientX-a,s=e.clientY-i,o=window.innerWidth-n.offsetWidth,c=window.innerHeight-n.offsetHeight;n.style.right="auto",n.style.bottom="auto",n.style.left=`${Math.max(0,Math.min(t,o))}px`,n.style.top=`${Math.max(0,Math.min(s,c))}px`},d=()=>{if(r=!1,document.removeEventListener("mousemove",l),document.removeEventListener("mouseup",d),t){const e=n.getBoundingClientRect();t({x:e.left,y:e.top})}};return e.addEventListener("mousedown",c),()=>{e.removeEventListener("mousedown",c),document.removeEventListener("mousemove",l),document.removeEventListener("mouseup",d)}}(m,Xe,e=>{!function(e,n){try{localStorage.setItem(_n(e),JSON.stringify(n))}catch(e){}}(We.integrateId,e)})),function(e,n){try{const t=localStorage.getItem(_n(e));if(!t)return;const r=JSON.parse(t);if("number"!=typeof r.x||"number"!=typeof r.y)return;const s=window.innerWidth-n.offsetWidth,o=window.innerHeight-n.offsetHeight,a=Math.max(0,Math.min(r.x,s)),i=Math.max(0,Math.min(r.y,o));n.style.right="auto",n.style.bottom="auto",n.style.left=`${a}px`,n.style.top=`${i}px`}catch(e){}}(We.integrateId,Xe),We.resizable){const e=d.resizeHandle;sn=function(e,n,t){let r=!1,s=0,o=0,a=0,i=0;const c=(e,t)=>{r=!0,s=e,o=t;const c=n.getBoundingClientRect();a=c.width,i=c.height,n.classList.add("csk-window--resizing"),document.addEventListener("mousemove",p),document.addEventListener("mouseup",u),document.addEventListener("touchmove",g,{passive:!1}),document.addEventListener("touchend",h)},l=(e,t,c)=>{if(!r)return;c&&c.preventDefault();const l=e-s,d=t-o,p=Math.max(300,Math.min(a+l,window.innerWidth-24)),u=Math.max(300,Math.min(i+d,window.innerHeight-24));n.style.width=`${p}px`,n.style.height=`${u}px`,n.style.setProperty("--csk-window-width",`${p}px`),n.style.setProperty("--csk-window-height",`${u}px`),n.style.bottom="",n.style.right=""},d=()=>{if(!r)return;r=!1,n.classList.remove("csk-window--resizing"),document.removeEventListener("mousemove",p),document.removeEventListener("mouseup",u),document.removeEventListener("touchmove",g),document.removeEventListener("touchend",h);const e=n.getBoundingClientRect();t&&t({width:e.width,height:e.height})};function p(e){l(e.clientX,e.clientY,e)}function u(){d()}function m(e){e.preventDefault(),e.stopPropagation(),c(e.clientX,e.clientY)}function k(e){e.preventDefault(),e.stopPropagation(),1===e.touches.length&&c(e.touches[0].clientX,e.touches[0].clientY)}function g(e){1===e.touches.length&&l(e.touches[0].clientX,e.touches[0].clientY,e)}function h(){d()}return e.addEventListener("mousedown",m),e.addEventListener("touchstart",k,{passive:!1}),()=>{e.removeEventListener("mousedown",m),e.removeEventListener("touchstart",k),document.removeEventListener("mousemove",p),document.removeEventListener("mouseup",u),document.removeEventListener("touchmove",g),document.removeEventListener("touchend",h)}}(e,Xe,e=>{!function(e,n){try{localStorage.setItem(wn(e),JSON.stringify(n))}catch(e){}}(We.integrateId,e)}),function(e,n){try{const t=localStorage.getItem(wn(e));if(!t)return;const r=JSON.parse(t);if("number"!=typeof r.width||"number"!=typeof r.height)return;const s=Math.max(300,Math.min(r.width,window.innerWidth-24)),o=Math.max(300,Math.min(r.height,window.innerHeight-24));n.style.width=`${s}px`,n.style.height=`${o}px`,n.style.setProperty("--csk-window-width",`${s}px`),n.style.setProperty("--csk-window-height",`${o}px`)}catch(e){}}(We.integrateId,Xe)}if(Ae(We,{messagesContainer:Ke,inputEl:Je,sendBtn:Ve,clearBtn:Ge,categorySelect:Qe,historyPanel:Ze,welcomeEl:d.welcomeEl,newMsgBtn:d.newMsgBtn,searchInput:d.searchInput,ariaLiveEl:d.ariaLiveEl,roleSelect:d.roleSelect,showLoading:en,hideLoading:nn}),Xe.addEventListener("csk:categoryChange",e=>{var n,t;n=e.detail.categoryId,ae=n,Se=null===(t=null==ie?void 0:ie.enableRag)||void 0===t||t}),Xe.addEventListener("csk:loadHistory",()=>{!async function(){if(!he||!ie)return;const e=he.querySelector("#csk-history-list");if(e){e.innerHTML='
    加载中...
    ';try{const n=await x(1,50,ie.userId,l());Le=n.list.map(e=>({id:e.conversationId||"",chatId:e.conversationId||"",roleId:e.roleId,roleName:e.roleName,messageCount:e.messageCount,lastMessageTime:e.lastMessageTime,lastMessagePreview:e.lastMessagePreview,createdAt:e.firstMessageTime||e.createdAt})),Ie="",ye&&(ye.value=""),V(e,Le,e=>{Ue(e)},e=>{window.open(E(e),"_blank")},async e=>{confirm(o("history_delete_confirm"))&&await _(e)&&(e===p()&&(ce=[],le&&le.querySelectorAll(".csk-msg, .csk-loading").forEach(e=>e.remove()),me&&(me.style.display="none"),Be()),Le=Le.filter(n=>(n.chatId||n.id)!==e),De())},p())}catch(n){t.error(o("history_load_error"),n),e.innerHTML=`
    ${o("history_load_error")}
    `}}}()}),Xe.addEventListener("csk:quickReply",e=>{Re(e.detail.text)}),Xe.addEventListener("csk:retry",e=>{!async function(e){if(!ie||!le||_e)return;const n=ce.findIndex(n=>n.id===e&&"ai"===n.role);if(n<0)return;let t="";for(let e=n-1;e>=0;e--)if("user"===ce[e].role){t=ce[e].content;break}if(!t)return;const r=ce.splice(n),s=new Set(r.map(e=>e.id));le.querySelectorAll(".csk-msg").forEach(e=>{const n=e.dataset.cskMsgId;n&&s.has(n)&&e.remove()}),ee(ie.integrateId,ce),Be(),await je(t)}(e.detail.msgId)}),Xe.addEventListener("csk:feedback",e=>{Te(e.detail.msgId,e.detail.value)}),Xe.addEventListener("csk:newMessage",e=>{var n;if(Xe&&Xe.classList.contains("csk-window--hidden")&&(ln&&ln.classList.remove("csk-launcher__badge--hidden"),(null==We?void 0:We.sound)&&function(){try{dn||(dn=new(window.AudioContext||window.webkitAudioContext));const e=dn;if("suspended"===e.state)return void e.resume();const n=e.currentTime;[0,.15].forEach((t,r)=>{const s=e.createOscillator(),o=e.createGain();s.type="sine",s.frequency.value=0===r?880:1100,o.gain.setValueAtTime(.15,n+t),o.gain.exponentialRampToValueAtTime(.001,n+t+.12),s.connect(o),o.connect(e.destination),s.start(n+t),s.stop(n+t+.12)})}catch(e){}}(),(null==We?void 0:We.notification)&&function(){try{if(!("Notification"in window))return;"granted"===Notification.permission?new Notification((null==We?void 0:We.title)||"AI 智能助手",{body:"收到新消息",icon:void 0,silent:!0}):"denied"!==Notification.permission&&Notification.requestPermission()}catch(e){}}()),(null==We?void 0:We.onMessage)&&(null===(n=e.detail)||void 0===n?void 0:n.msg))try{We.onMessage(e.detail.msg)}catch(e){}}),Xe.addEventListener("csk:roleChange",e=>{e.detail.roleId&&async function(e){if(!ie||!le)return;const n=l();if(e===n)return;if(ce.length>0&&ee(n,ce),Ee&&(Ee.abort(),Ee=null),_e=!1,ze("send"),ce=[],le.querySelectorAll(".csk-msg, .csk-loading").forEach(e=>e.remove()),ne(n),I(n,ie.userId,void 0),ne(e),I(e,ie.userId,void 0),c(e),ge&&(ge.value=e),await C(!0))try{await Ne()}catch(e){}me&&0===ce.length&&(me.style.display="none"),Be(),Ce=!0,le&&G(le)}(String(e.detail.roleId))}),document.addEventListener("keydown",fn),window.addEventListener("resize",hn),Ye=!0,t.lifecycleInit(We.integrateId,We.requestDomain),We.onReady)try{We.onReady()}catch(e){}We.showTeaser&&(cn=setTimeout(()=>{Xe&&Xe.classList.contains("csk-window--hidden")&&function(){if(!an)return;un(),an.classList.remove("csk-teaser--hidden"),an.addEventListener("click",kn);const e=an.querySelector(".csk-teaser__close");e&&e.addEventListener("click",gn)}()},1500)),$e().catch(e=>{}),void 0===We.disclaimer&&async function(e){if(!a)return"";try{const n=u(`/ai/system-config/${encodeURIComponent(e)}`),t=await k(n);if(!t.ok)return"";const r=await t.json();return r.success&&r.data&&r.data.configValue||""}catch(e){return""}}("disclaimer").then(e=>{e&&pn&&(pn.innerHTML=e,pn.style.display="")}).catch(()=>{})},destroy:function(){Ye&&(cn&&(clearTimeout(cn),cn=null),mn(),an&&an.parentNode&&(an.parentNode.removeChild(an),an=null),Oe&&Oe.parentNode&&(Oe.parentNode.removeChild(Oe),Oe=null),Xe&&Xe.parentNode&&(Xe.parentNode.removeChild(Xe),Xe=null),tn&&(tn(),tn=null),rn&&(rn(),rn=null),sn&&(sn(),sn=null),on=null,S&&S.parentNode&&(S.parentNode.removeChild(S),S=null),document.querySelectorAll("style[data-csk-sdk]").forEach(e=>e.remove()),document.removeEventListener("keydown",fn),window.removeEventListener("resize",hn),n(void 0),null==We||We.integrateId,We=null,Ye=!1,Ke=null,Je=null,Ve=null,Ge=null,Qe=null,Ze=null,en=null,nn=null,ln=null,dn=null,pn=null,a=null,i=null)},open:yn,close:vn,toggle:xn,clearHistory:function(){We&&(Ge?Ge.click():ne(We.integrateId))}};return"undefined"!=typeof window&&(window.ChatbotSDK=Ln),Ln}(); diff --git a/client/rollup.config.js b/client/rollup.config.js index dcaac60..36ed811 100644 --- a/client/rollup.config.js +++ b/client/rollup.config.js @@ -9,6 +9,7 @@ const BACKEND_DIR = resolve(ROOT, '../src/main/resources/static/sdk'); const ASSETS = [ { src: resolve(ROOT, 'assets/launcher-logo.png'), name: 'launcher-logo.png' }, + { src: resolve(ROOT, 'assets/marked.min.js'), name: 'marked.min.js' }, ]; const BUNDLE_FILES = ['chatbot-sdk.js', 'chatbot-sdk.min.js']; diff --git a/client/src/index.ts b/client/src/index.ts index 9208001..3613154 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -12,6 +12,7 @@ import { parseConfig } from './config'; import { setDebug, logger, setErrorCallback } from './logger'; import { setApiConfig, clearApiConfig, fetchSystemConfig } from './api'; import { injectStyles, removeStyles } from './styles'; +import { preloadMarked, resetMarked } from './markdown'; import { createLauncher, createChatWindow, enableDrag, enableLauncherDrag, enableResize } from './dom'; import { initChat, initChatHistory, setCategory, loadHistoryConversations, sendQuickReply, retryFromMessage, handleFeedback, switchRole } from './chat'; import { clearMessages } from './storage'; @@ -93,6 +94,9 @@ function init(rawConfig: SDKConfig): void { // 5. 注入样式 injectStyles(config); + // 5.1 预加载 marked(外部 Markdown 渲染库,异步不阻塞 UI) + preloadMarked(); + // 6. 创建悬浮按钮 launcherEl = createLauncher(config, toggle); document.body.appendChild(launcherEl); @@ -417,6 +421,7 @@ function destroy(): void { audioCtx = null; disclaimerEl = null; clearApiConfig(); + resetMarked(); // 重置 marked 加载状态,允许下次 init 重试 logger.lifecycleDestroy(oldIntegrateId || ''); } diff --git a/client/src/markdown.ts b/client/src/markdown.ts index 6016973..6d6d065 100644 --- a/client/src/markdown.ts +++ b/client/src/markdown.ts @@ -1,290 +1,240 @@ /** - * 轻量级 Markdown 渲染器 - 无外部依赖,XSS 安全 + * Markdown 渲染模块 - 基于 marked 外部脚本 * - * 支持:代码块、行内代码、标题、粗体、斜体、列表、链接、引用、段落 - * 策略:先转义 HTML,再转 Markdown 为安全 HTML 标签 + * marked v18 以静态文件方式由 SDK 自动加载,宿主无需手动引入。 + * 加载失败或未就绪时降级为纯文本渲染,不丢消息。 + * + * XSS 安全策略: + * 1. 自定义 Renderer 全面接管输出,所有类名/标签由 SDK 生成 + * 2. 链接/图片 URL 白名单校验(仅 http/https) + * 3. 禁止 raw HTML 渲染 + * 4. 代码内容使用 escapeHtml 转义 */ import { escapeHtml } from './utils'; -/** 代码块占位符前缀 */ -const CODE_BLOCK_PREFIX = '\x00CODEBLOCK_'; +/** marked 是否已加载并完成配置 */ +let markedReady = false; -/** 行内代码占位符前缀 */ -const INLINE_CODE_PREFIX = '\x00INLINECODE_'; +/** 加载 Promise,用于等待就绪 */ +let markedLoadPromise: Promise | null = null; -/** - * 渲染 Markdown 文本为安全 HTML - * @param text Markdown 源文本 - * @returns 安全 HTML 字符串 - */ -export function renderMarkdown(text: string): string { - if (!text || typeof text !== 'string') return ''; +/** 已配置安全的 marked 实例闭包引用(不反复读取 window.marked,防止篡改) */ +let _markedInstance: any = null; - // 1. 提取代码块(防止内部 Markdown 被处理) - const codeBlocks: string[] = []; - let processed = text; - - // 提取围栏代码块 ``` - processed = processed.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => { - const idx = codeBlocks.length; - const escapedCode = escapeHtml(code.trimEnd()); - const langClass = lang ? ` class="language-${escapeHtml(lang)}"` : ''; - codeBlocks.push(`
    ${escapedCode}
    `); - return `${CODE_BLOCK_PREFIX}${idx}\x00`; - }); - - // 2. 提取行内代码 - const inlineCodes: string[] = []; - processed = processed.replace(/`([^`\n]+)`/g, (_match, code) => { - const idx = inlineCodes.length; - inlineCodes.push(`${escapeHtml(code)}`); - return `${INLINE_CODE_PREFIX}${idx}\x00`; - }); - - // 3. 转义剩余 HTML(代码块和行内代码已安全处理) - processed = escapeHtml(processed); - - // 4. 还原代码块和行内代码占位符(它们已经是安全 HTML) - processed = restorePlaceholders(processed, CODE_BLOCK_PREFIX, codeBlocks); - processed = restorePlaceholders(processed, INLINE_CODE_PREFIX, inlineCodes); - - // 5. 逐行处理 Markdown 语法 - const lines = processed.split('\n'); - const result: string[] = []; - let inList = false; - let listType = ''; // 'ul' 或 'ol' - let inBlockquote = false; - let paragraphBuffer: string[] = []; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - // 代码块已在占位符还原阶段处理,直接输出 - if (line.includes(CODE_BLOCK_PREFIX) || line.includes('
    ')) {
    -      flushParagraph();
    -      closeList();
    -      closeBlockquote();
    -      result.push(line);
    -      continue;
    -    }
    -
    -    // 标题
    -    // Accept standard headings and common no-space forms before CJK text or digits.
    -    const headingMatch = line.match(/^ {0,3}(#{1,6})(?:\s+|(?=[\u3400-\u9FFF\d]))(.+)/);
    -    if (headingMatch) {
    -      flushParagraph();
    -      closeList();
    -      closeBlockquote();
    -      const level = headingMatch[1].length;
    -      result.push(`${headingMatch[2]}`);
    -      continue;
    -    }
    -
    -    // 引用
    -    // GFM table: require a valid separator row to avoid matching ordinary pipe text.
    -    if (i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
    -      const headers = splitTableRow(line);
    -      const separators = splitTableRow(lines[i + 1]);
    -      if (headers.length > 0 && headers.length === separators.length) {
    -        flushParagraph();
    -        closeList();
    -        closeBlockquote();
    -
    -        const alignments = separators.map(getTableAlignment);
    -        result.push('
    '); - result.push(''); - headers.forEach((cell, index) => { - result.push(`${inlineFormat(cell)}`); - }); - result.push(''); - - i += 2; - while (i < lines.length && isTableDataRow(lines[i])) { - const cells = splitTableRow(lines[i]); - result.push(''); - headers.forEach((_header, index) => { - result.push(`${inlineFormat(cells[index] || '')}`); - }); - result.push(''); - i++; - } +// ==================== 工具函数 ==================== - result.push('
    '); - i--; - continue; - } +/** 从当前页面的 chatbot-sdk 脚本 src 推断 SDK 基础路径 */ +function getSdkBasePath(): string { + if (typeof document === 'undefined') return '/sdk/'; + const scripts = document.getElementsByTagName('script'); + for (let i = 0; i < scripts.length; i++) { + const src = scripts[i].src; + if (src && /\/chatbot-sdk(\.min)?\.js([?#].*)?$/.test(src)) { + return src.replace(/chatbot-sdk(\.min)?\.js.*$/, ''); } + } + return '/sdk/'; +} - const quoteMatch = line.match(/^ {0,3}>\s?(.*)/); - if (quoteMatch) { - flushParagraph(); - closeList(); - if (!inBlockquote) { - inBlockquote = true; - result.push('
    '); - } - result.push(`

    ${inlineFormat(quoteMatch[1])}

    `); - continue; - } else if (inBlockquote) { - closeBlockquote(); - } +/** 校验 URL 安全性:仅允许 http/https 且不含 HTML 属性破坏字符 */ +function isSafeUrl(url: string): boolean { + return /^https?:\/\/[^\s"'<>]*$/i.test(url.trim()); +} - // 无序列表 - const ulMatch = line.match(/^ {0,3}[\-\*]\s+(.+)/); - if (ulMatch) { - flushParagraph(); - closeBlockquote(); - if (!inList || listType !== 'ul') { - closeList(); - inList = true; - listType = 'ul'; - result.push('
      '); - } - result.push(`
    • ${inlineFormat(ulMatch[1])}
    • `); - continue; - } +// ==================== marked 配置 ==================== - // 有序列表 - const olMatch = line.match(/^ {0,3}\d+\.\s+(.+)/); - if (olMatch) { - flushParagraph(); - closeBlockquote(); - if (!inList || listType !== 'ol') { - closeList(); - inList = true; - listType = 'ol'; - result.push('
        '); - } - result.push(`
      1. ${inlineFormat(olMatch[1])}
      2. `); - continue; - } +/** + * 配置 marked 实例 + * - 输出统一使用 csk-md-* 类名前缀(兼容现有 styles.ts) + * - 链接/图片仅允许 http/https + * - 禁止 raw HTML + */ +function setupMarked(m: any): void { + // 保存到闭包变量,后续 renderMarkdown 只使用此引用,防止 window.marked 被篡改 + _markedInstance = m; + + // 全局选项:不自动转换换行为
        ,启用 GFM + m.setOptions({ breaks: false, gfm: true }); + + m.use({ + renderer: { + /** 标题 → */ + heading(this: any, token: any): string { + const text = this.parser.parseInline(token.tokens); + return `${text}\n`; + }, + + /** 段落 →

        */ + paragraph(this: any, token: any): string { + const text = this.parser.parseInline(token.tokens); + return `

        ${text}

        \n`; + }, + + /** 代码块 →
         */
        +      code(token: any): string {
        +        const langClass = token.lang ? ` class="language-${escapeHtml(token.lang)}"` : '';
        +        return `
        ${escapeHtml(token.text)}
        \n`; + }, + + /** 行内代码 → */ + codespan(token: any): string { + return `${escapeHtml(token.text)}`; + }, + + /** 斜体(补充 csk-md-* 类名体系) */ + em(this: any, token: any): string { + return `${this.parser.parseInline(token.tokens)}`; + }, + + /** 粗体(补充 csk-md-* 类名体系) */ + strong(this: any, token: any): string { + return `${this.parser.parseInline(token.tokens)}`; + }, + + /** 删除线(补充 csk-md-* 类名体系) */ + del(this: any, token: any): string { + return `${this.parser.parseInline(token.tokens)}`; + }, + + /** 引用块 →
        */ + blockquote(this: any, token: any): string { + const body = this.parser.parse(token.tokens); + return `
        \n${body}
        \n`; + }, + + /** 列表 →
          /
            */ + list(this: any, token: any): string { + const tag = token.ordered ? 'ol' : 'ul'; + const cls = token.ordered ? 'csk-md-ol' : 'csk-md-ul'; + const startAttr = token.ordered && token.start !== 1 ? ` start="${token.start}"` : ''; + const body = token.items.map((item: any) => this.parser.parse([item])).join(''); + return `<${tag} class="${cls}"${startAttr}>\n${body}\n`; + }, + + /** 列表项(支持任务列表 checkbox) */ + listitem(this: any, token: any): string { + let checkbox = ''; + if (token.task) { + checkbox = token.checked + ? ' ' + : ' '; + } + const body = this.parser.parse(token.tokens); + return `
          1. ${checkbox}${body}
          2. \n`; + }, + + /** 表格(包裹在可滚动容器中) */ + table(this: any, token: any): string { + const headerCells = token.header.map((cell: any) => { + const align = cell.align ? ` class="csk-md-align-${cell.align}"` : ''; + return `${this.parser.parseInline(cell.tokens)}`; + }).join(''); + const bodyRows = token.rows.map((row: any) => { + const cells = row.map((cell: any) => { + const align = cell.align ? ` class="csk-md-align-${cell.align}"` : ''; + return `${this.parser.parseInline(cell.tokens)}`; + }).join(''); + return `${cells}`; + }).join(''); + return `
            \n${headerCells}\n${bodyRows}\n
            \n`; + }, + + /** 链接 → (仅允许 http/https) */ + link(this: any, token: any): string { + const safeHref = isSafeUrl(token.href) ? token.href : '#'; + const titleAttr = token.title ? ` title="${escapeHtml(token.title)}"` : ''; + const text = this.parser.parseInline(token.tokens); + return `${text}`; + }, + + /** 图片 → (仅允许 http/https,非法降级为 alt 文本) */ + image(token: any): string { + if (!isSafeUrl(token.href)) return escapeHtml(token.text); + const titleAttr = token.title ? ` title="${escapeHtml(token.title)}"` : ''; + return `${escapeHtml(token.text)}`; + }, + + /** 水平线 →
            */ + hr(): string { + return '
            \n'; + }, + + /** 禁止 raw HTML(关键 XSS 防线) */ + html(): string { + return ''; + }, + }, + }); +} - // 空行 → 段落分隔 - if (line.trim() === '') { - flushParagraph(); - closeList(); - continue; - } +// ==================== 公开 API ==================== - // 水平线 - if (/^(\*{3,}|-{3,}|_{3,})$/.test(line.trim())) { - flushParagraph(); - closeList(); - closeBlockquote(); - result.push('
            '); - continue; +/** + * 预加载 marked 外部脚本 + * 在 SDK init 时调用,尽早开始加载,减少首次渲染时的等待 + */ +export function preloadMarked(): void { + if (markedLoadPromise) return; + + markedLoadPromise = new Promise((resolve) => { + // 1. 检查是否已存在(宿主手动引入或相同域名已加载) + const existing = (window as any).marked; + if (existing && typeof existing.parse === 'function') { + setupMarked(existing); + markedReady = true; + resolve(true); + return; } - // 普通文本 → 收集到段落缓冲 - closeList(); - closeBlockquote(); - paragraphBuffer.push(inlineFormat(line)); - } - - flushParagraph(); - closeList(); - closeBlockquote(); - - return result.join('\n'); - - // === 辅助函数 === - - /** 行内格式化:粗体、斜体、链接 */ - function inlineFormat(text: string): string { - // 粗体 **text** 或 __text__ - text = text.replace(/\*\*(.+?)\*\*/g, '$1'); - text = text.replace(/__(.+?)__/g, '$1'); - // 斜体 *text* 或 _text_ - text = text.replace(/\*(.+?)\*/g, '$1'); - text = text.replace(/(?$1'); - // 删除线 ~~text~~ - text = text.replace(/~~(.+?)~~/g, '$1'); - // 链接 [text](url) - text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, linkText, url) => { - // 只允许 http/https 链接,防止 javascript: 协议 - const safeUrl = /^https?:\/\//i.test(url) ? url : '#'; - return `${linkText}`; - }); - return text; - } - - /** 将段落缓冲输出为

            */ - function isTableSeparator(line: string): boolean { - const cells = splitTableRow(line); - return cells.length > 0 && cells.every(cell => /^:?-{3,}:?$/.test(cell.trim())); - } - - function isTableDataRow(line: string): boolean { - return line.trim() !== '' && line.includes('|'); - } - - function splitTableRow(line: string): string[] { - const trimmed = line.trim().replace(/^\|/, '').replace(/\|$/, ''); - if (!trimmed.includes('|')) return []; - - const cells: string[] = []; - let cell = ''; - for (let i = 0; i < trimmed.length; i++) { - const char = trimmed[i]; - if (char === '\\' && trimmed[i + 1] === '|') { - cell += '|'; - i++; - } else if (char === '|') { - cells.push(cell.trim()); - cell = ''; - } else { - cell += char; + // 2. 动态创建 '); - expect(result).not.toContain('\n```'); - expect(result).not.toContain('