Add files via upload

This commit is contained in:
公明 authored and GitHub committed 2026-06-09 11:03:51 +08:00
1 parent c2892ccd33
commit cb17d3a5c1
4 files changed
+52 -18

No files matched your search

+41 -14
View File
@@ -68,9 +68,25 @@
domPurifyHooksInstalled = true; domPurifyHooksInstalled = true;
} }
/** 明显 Markdown 结构时,不应因零散 HTML 标签误判为整页 HTML */
function looksLikeMarkdown(src) {
const s = String(src);
return /^#{1,6}\s/m.test(s)
|| /^\s*[-*+]\s/m.test(s)
|| /^\s*\d+\.\s/m.test(s)
|| /\*\*[^*\n]+\*\*/.test(s)
|| /`[^`\n]+`/.test(s)
|| /^```/m.test(s)
|| /^\|.+\|/m.test(s)
|| /^\s*>\s/m.test(s);
}
/** 探测工具返回的整页 HTML,不宜当作富文本渲染 */ /** 探测工具返回的整页 HTML,不宜当作富文本渲染 */
function isHeavyRawHtml(src) { function isHeavyRawHtml(src) {
const s = String(src); const s = String(src);
if (looksLikeMarkdown(s)) {
return false;
}
if (/<!DOCTYPE\s+html/i.test(s) || /<\s*html\b/i.test(s)) { if (/<!DOCTYPE\s+html/i.test(s) || /<\s*html\b/i.test(s)) {
return true; return true;
} }
@@ -81,6 +97,10 @@
return tags != null && tags.length >= 8; return tags != null && tags.length >= 8;
} }
function escapePlainTextAsHtml(text) {
return escapeHtmlLocal(text).replace(/\n/g, '<br>');
}
function formatHtmlAsEscapedPre(text) { function formatHtmlAsEscapedPre(text) {
return '<pre class="tool-result sanitized-raw-html-fallback">' + escapeHtmlLocal(text) + '</pre>'; return '<pre class="tool-result sanitized-raw-html-fallback">' + escapeHtmlLocal(text) + '</pre>';
} }
@@ -115,6 +135,23 @@
* @param {{ profile?: 'chat'|'timeline' }} [options] * @param {{ profile?: 'chat'|'timeline' }} [options]
* @returns {string} 安全 HTML * @returns {string} 安全 HTML
*/ */
function buildRichHtmlFromSource(src) {
const hasHtmlTags = /<[a-z][\s\S]*>/i.test(src);
const preferMarkdown = typeof marked !== 'undefined'
&& (looksLikeMarkdown(src) || !hasHtmlTags);
if (preferMarkdown) {
const parsed = parseMarkdownSrc(src);
if (parsed != null) {
return parsed;
}
}
if (hasHtmlTags) {
return src;
}
return escapePlainTextAsHtml(src);
}
function formatMarkdownToHtml(text, options) { function formatMarkdownToHtml(text, options) {
const profile = (options && options.profile === 'timeline') ? 'timeline' : 'chat'; const profile = (options && options.profile === 'timeline') ? 'timeline' : 'chat';
const src = normalizeSource(text); const src = normalizeSource(text);
@@ -124,24 +161,13 @@
} }
if (typeof DOMPurify === 'undefined') { if (typeof DOMPurify === 'undefined') {
return escapeHtmlLocal(src).replace(/\n/g, '<br>'); console.warn('DOMPurify 未加载,Markdown 已降级为纯文本渲染(已转义,防 XSS)');
return escapePlainTextAsHtml(src);
} }
installDomPurifyHooks(); installDomPurifyHooks();
const config = sanitizeConfigForProfile(profile); const config = sanitizeConfigForProfile(profile);
return DOMPurify.sanitize(buildRichHtmlFromSource(src), config);
let html;
const hasHtmlTags = /<[a-z][\s\S]*>/i.test(src);
if (typeof marked !== 'undefined' && !hasHtmlTags) {
const parsed = parseMarkdownSrc(src);
html = parsed != null ? parsed : escapeHtmlLocal(src).replace(/\n/g, '<br>');
} else if (hasHtmlTags) {
html = src;
} else {
html = escapeHtmlLocal(src).replace(/\n/g, '<br>');
}
return DOMPurify.sanitize(html, config);
} }
function sanitizeRichHtml(html, profile) { function sanitizeRichHtml(html, profile) {
@@ -171,6 +197,7 @@
formatMarkdownToHtml: formatMarkdownToHtml, formatMarkdownToHtml: formatMarkdownToHtml,
sanitizeRichHtml: sanitizeRichHtml, sanitizeRichHtml: sanitizeRichHtml,
isHeavyRawHtml: isHeavyRawHtml, isHeavyRawHtml: isHeavyRawHtml,
looksLikeMarkdown: looksLikeMarkdown,
escapeHtmlLocal: escapeHtmlLocal, escapeHtmlLocal: escapeHtmlLocal,
stripSuspiciousImages: stripSuspiciousImages, stripSuspiciousImages: stripSuspiciousImages,
}; };
+6
View File
@@ -0,0 +1,6 @@
/**
* marked v11.1.1 - a markdown parser
* Copyright (c) 2011-2023, Christopher Jeffrey. (MIT Licensed)
* https://github.com/markedjs/marked
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function p(e){return e.replace(h,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const u=/(^|[^\[])\^/g;function k(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(u,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function g(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return null}return e}const f={exec:()=>null};function d(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length<t;)n.push("");for(;s<n.length;s++)n[s]=n[s].trim().replace(/\\\|/g,"|");return n}function x(e,t,n){const s=e.length;if(0===s)return"";let r=0;for(;r<s;){const i=e.charAt(s-r-1);if(i!==t||n){if(i===t||!n)break;r++}else r++}return e.slice(0,s-r)}function b(e,t,n,s){const r=t.href,i=t.title?c(t.title):null,l=e[1].replace(/\\([\[\]])/g,"$1");if("!"!==e[0].charAt(0)){s.state.inLink=!0;const e={type:"link",raw:n,href:r,title:i,text:l,tokens:s.inlineTokens(l)};return s.state.inLink=!1,e}return{type:"image",raw:n,href:r,title:i,text:c(l)}}class w{options;rules;lexer;constructor(t){this.options=t||e.defaults}space(e){const t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:x(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=x(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){const e=x(t[0].replace(/^ *>[ \t]?/gm,""),"\n"),n=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=n,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={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:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l="",o="",a=!1;for(;e;){let n=!1;if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;l=t[0],e=e.substring(l.length);let s=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=0;this.options.pedantic?(h=2,o=s.trimStart()):(h=t[2].search(/[^ ]/),h=h>4?1:h,o=s.slice(h),h+=t[1].length);let p=!1;if(!s&&/^ *$/.test(c)&&(l+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,h-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,h-1)}}#`);for(;e;){const a=e.split("\n",1)[0];if(c=a,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),r.test(c))break;if(i.test(c))break;if(t.test(c))break;if(n.test(e))break;if(c.search(/[^ ]/)>=h||!c.trim())o+="\n"+c.slice(h);else{if(p)break;if(s.search(/[^ ]/)>=4)break;if(r.test(s))break;if(i.test(s))break;if(n.test(s))break;o+="\n"+c}p||c.trim()||(p=!0),l+=a+"\n",e=e.substring(aLine truncated
+2
View File
@@ -0,0 +1,2 @@
/*! @license DOMPurify 3.0.8 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.0.8/LICENSE */
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=b(Array.prototype.forEach),m=b(Array.prototype.pop),f=b(Array.prototype.push),p=b(String.prototype.toLowerCase),d=b(String.prototype.toString),h=b(String.prototype.match),g=b(String.prototype.replace),T=b(String.prototype.indexOf),y=b(String.prototype.trim),E=b(RegExp.prototype.test),A=(_=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return s(_,t)});var _;function b(e){return function(t){for(var n=arguments.length,o=new Array(n>1?n-1:0),r=1;r<n;r++)o[r-1]=arguments[r];return c(e,t,o)}}function N(e,o){let r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:p;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function S(e){for(let t=0;t<e.length;t++)void 0===r(e,t)&&(e[t]=null);return e}function R(t){const n=l(null);for(const[o,i]of e(t))void 0!==r(t,o)&&(Array.isArray(i)?n[o]=S(i):i&&"object"==typeof i&&i.constructor===Object?n[o]=R(i):n[o]=i);return n}function w(e,t){for(;null!==e;){const n=r(e,t);if(n){if(n.get)return b(n.get);if("function"==typeof n.value)return b(n.value)}e=o(e)}return function(e){return console.warn("fallback value for",e),null}}const D=i(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","section","select","shadow","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),L=i(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","filter","font","g","glyph","glyphref","hkern","image","line","lineargradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),v=i(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),x=i(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),k=i(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),C=i(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),O=i(["#text"]),I=i(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","face","for","headers","height","hidden","high","href","hreflang","id","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","pattern","placeholder","playsinline","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","rLine truncated
+3 -4
View File
@@ -3517,10 +3517,9 @@
</div> </div>
</div> </div>
<!-- Marked.js for Markdown parsing --> <!-- Marked.js + DOMPurify:本地 vendor,避免 CDN 不可用导致 Markdown 降级为纯文本 -->
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script> <script src="/static/vendor/marked.min.js"></script>
<!-- DOMPurify for HTML sanitization to prevent XSS --> <script src="/static/vendor/purify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/purify.min.js"></script>
<script src="/static/js/sanitize-markdown.js"></script> <script src="/static/js/sanitize-markdown.js"></script>
<!-- Cytoscape.js for attack chain visualization --> <!-- Cytoscape.js for attack chain visualization -->
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.27.0/dist/cytoscape.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/cytoscape@3.27.0/dist/cytoscape.min.js"></script>