mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-05 13:58:15 +02:00
Add files via upload
This commit is contained in:
+2
-8
@@ -22,7 +22,6 @@ vision:
|
||||
skip_preprocess_below_bytes: 2097152 # 低于 2MB 且长边<=max_dimension 时原图直传;0=始终 JPEG 压缩
|
||||
detail: low # low | high | auto
|
||||
timeout_seconds: 60
|
||||
# allowed_roots: [] # 额外绝对路径根
|
||||
```
|
||||
|
||||
`enabled: false` 时不注册工具。
|
||||
@@ -31,14 +30,9 @@ vision:
|
||||
|
||||
**系统设置 → 基本设置 → 视觉分析(analyze_image)** 可配置启用开关、视觉模型、API Key/Base URL(留空复用 OpenAI)、预处理参数;**保存并应用** 后写入 `config.yaml` 并重新注册 MCP 工具。
|
||||
|
||||
## 路径白名单
|
||||
## 路径
|
||||
|
||||
默认可读:
|
||||
|
||||
- 进程工作目录(`cwd`)及其子路径
|
||||
- `chat_uploads/`
|
||||
- `agent.result_storage_dir`(默认 `tmp/`)
|
||||
- `vision.allowed_roots` 中配置的绝对路径
|
||||
`analyze_image` 可读取服务器上任意可读的图片文件路径(绝对路径或相对于进程工作目录的相对路径)。仍校验图片扩展名与常规文件类型。
|
||||
|
||||
## Agent 使用
|
||||
|
||||
|
||||
@@ -4069,6 +4069,11 @@ header {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 整页 HTML(如 HTTP 探测响应)在时间线中仅以转义文本展示 */
|
||||
.timeline-item-content .sanitized-raw-html-fallback {
|
||||
max-height: 320px;
|
||||
}
|
||||
|
||||
.tool-result-section.error .tool-result {
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
border-color: var(--error-color);
|
||||
|
||||
@@ -1974,8 +1974,6 @@
|
||||
"visionSkipPreprocessHint": "0 = always JPEG compress; must also fit long-edge and payload limits.",
|
||||
"visionDetail": "Image detail",
|
||||
"visionTimeout": "Timeout (seconds)",
|
||||
"visionAllowedRoots": "Extra allowed path roots",
|
||||
"visionAllowedRootsPlaceholder": "One absolute path per line, optional",
|
||||
"visionTestFillRequired": "Enter vision model and ensure API Key is available (or reuse OpenAI)",
|
||||
"testConnection": "Test Connection",
|
||||
"testFillRequired": "Please fill in API Key and Model first",
|
||||
|
||||
@@ -1963,8 +1963,6 @@
|
||||
"visionSkipPreprocessHint": "0 表示始终 JPEG 压缩;需同时满足长边与 payload 限制。",
|
||||
"visionDetail": "Image detail",
|
||||
"visionTimeout": "超时(秒)",
|
||||
"visionAllowedRoots": "额外允许路径根目录",
|
||||
"visionAllowedRootsPlaceholder": "每行一个绝对路径,可选",
|
||||
"visionTestFillRequired": "请填写视觉模型,并确保 API Key 可用(可复用 OpenAI)",
|
||||
"testConnection": "测试连接",
|
||||
"testFillRequired": "请先填写 API Key 和模型",
|
||||
|
||||
+6
-41
@@ -343,48 +343,13 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatMarkdown(text) {
|
||||
const sanitizeConfig = {
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'],
|
||||
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
};
|
||||
|
||||
const raw = text == null ? '' : String(text);
|
||||
const src = typeof window.normalizeAssistantMarkdownSource === 'function'
|
||||
? window.normalizeAssistantMarkdownSource(raw)
|
||||
: raw;
|
||||
|
||||
if (typeof DOMPurify !== 'undefined') {
|
||||
if (typeof marked !== 'undefined' && !/<[a-z][\s\S]*>/i.test(src)) {
|
||||
try {
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
});
|
||||
const parsedContent = marked.parse(src, { async: false });
|
||||
return DOMPurify.sanitize(parsedContent, sanitizeConfig);
|
||||
} catch (e) {
|
||||
console.error('Markdown 解析失败:', e);
|
||||
return DOMPurify.sanitize(src, sanitizeConfig);
|
||||
}
|
||||
} else {
|
||||
return DOMPurify.sanitize(src, sanitizeConfig);
|
||||
}
|
||||
} else if (typeof marked !== 'undefined') {
|
||||
try {
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
});
|
||||
return marked.parse(src, { async: false });
|
||||
} catch (e) {
|
||||
console.error('Markdown 解析失败:', e);
|
||||
return escapeHtml(src).replace(/\n/g, '<br>');
|
||||
}
|
||||
} else {
|
||||
return escapeHtml(src).replace(/\n/g, '<br>');
|
||||
/** @param {string} text @param {{ profile?: 'chat'|'timeline' }} [options] */
|
||||
function formatMarkdown(text, options) {
|
||||
if (typeof window.csMarkdownSanitize !== 'undefined') {
|
||||
return window.csMarkdownSanitize.formatMarkdownToHtml(text, options);
|
||||
}
|
||||
const raw = text == null ? '' : String(text);
|
||||
return escapeHtml(raw).replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
function setupLoginUI() {
|
||||
|
||||
+10
-116
@@ -1862,25 +1862,9 @@ function refreshSystemReadyMessageBubbles() {
|
||||
div.textContent = s;
|
||||
return div.innerHTML;
|
||||
};
|
||||
const defaultSanitizeConfig = {
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'],
|
||||
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
};
|
||||
let formattedContent;
|
||||
if (typeof marked !== 'undefined') {
|
||||
try {
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
const src = typeof window.normalizeAssistantMarkdownSource === 'function'
|
||||
? window.normalizeAssistantMarkdownSource(text)
|
||||
: text;
|
||||
const parsed = marked.parse(src, { async: false });
|
||||
formattedContent = typeof DOMPurify !== 'undefined'
|
||||
? DOMPurify.sanitize(parsed, defaultSanitizeConfig)
|
||||
: parsed;
|
||||
} catch (e) {
|
||||
formattedContent = escapeHtmlLocal(text).replace(/\n/g, '<br>');
|
||||
}
|
||||
if (typeof window.csMarkdownSanitize !== 'undefined') {
|
||||
formattedContent = window.csMarkdownSanitize.formatMarkdownToHtml(text, { profile: 'chat' });
|
||||
} else {
|
||||
formattedContent = escapeHtmlLocal(text).replace(/\n/g, '<br>');
|
||||
}
|
||||
@@ -1936,13 +1920,6 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
|
||||
// 解析 Markdown 或 HTML 格式
|
||||
let formattedContent;
|
||||
const defaultSanitizeConfig = {
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'],
|
||||
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
};
|
||||
|
||||
// HTML实体编码函数
|
||||
const escapeHtml = (text) => {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
@@ -1950,31 +1927,6 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
return div.innerHTML;
|
||||
};
|
||||
|
||||
// 注意:代码块内容不需要转义,因为:
|
||||
// 1. Markdown解析后,代码块会被包裹在<code>或<pre>标签中
|
||||
// 2. 浏览器不会执行<code>和<pre>标签内的HTML(它们是文本节点)
|
||||
// 3. DOMPurify会保留这些标签内的文本内容
|
||||
// 这样既能防止XSS,又能正常显示代码
|
||||
|
||||
const parseMarkdown = (raw) => {
|
||||
if (typeof marked === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
});
|
||||
const src = typeof window.normalizeAssistantMarkdownSource === 'function'
|
||||
? window.normalizeAssistantMarkdownSource(raw)
|
||||
: raw;
|
||||
return marked.parse(src, { async: false });
|
||||
} catch (e) {
|
||||
console.error('Markdown 解析失败:', e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 助手消息中的已知中文错误前缀做国际化替换(后端固定返回中文)
|
||||
let displayContent = content;
|
||||
if (role === 'assistant' && typeof displayContent === 'string' && typeof window.t === 'function') {
|
||||
@@ -1989,57 +1941,11 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
// 对于用户消息,直接转义HTML,不进行Markdown解析,以保留所有特殊字符
|
||||
if (role === 'user') {
|
||||
formattedContent = escapeHtml(content).replace(/\n/g, '<br>');
|
||||
} else if (typeof DOMPurify !== 'undefined') {
|
||||
// 直接解析Markdown(代码块会被包裹在<code>/<pre>中,DOMPurify会保留其文本内容)
|
||||
let parsedContent = parseMarkdown(role === 'assistant' ? displayContent : content);
|
||||
if (!parsedContent) {
|
||||
parsedContent = content;
|
||||
}
|
||||
|
||||
// 使用DOMPurify清理,只添加必要的URL验证钩子(DOMPurify默认会处理事件处理器等)
|
||||
if (DOMPurify.addHook) {
|
||||
// 移除之前可能存在的钩子
|
||||
try {
|
||||
DOMPurify.removeHook('uponSanitizeAttribute');
|
||||
} catch (e) {
|
||||
// 钩子不存在,忽略
|
||||
}
|
||||
|
||||
// 只验证URL属性,防止危险协议(DOMPurify默认会处理事件处理器、style等)
|
||||
DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
|
||||
const attrName = data.attrName.toLowerCase();
|
||||
|
||||
// 只验证URL属性(src, href)
|
||||
if ((attrName === 'src' || attrName === 'href') && data.attrValue) {
|
||||
const value = data.attrValue.trim().toLowerCase();
|
||||
// 禁止危险协议
|
||||
if (value.startsWith('javascript:') ||
|
||||
value.startsWith('vbscript:') ||
|
||||
value.startsWith('data:text/html') ||
|
||||
value.startsWith('data:text/javascript')) {
|
||||
data.keepAttr = false;
|
||||
return;
|
||||
}
|
||||
// 对于img的src,禁止可疑的短URL(防止404和XSS)
|
||||
if (attrName === 'src' && node.tagName && node.tagName.toLowerCase() === 'img') {
|
||||
if (value.length <= 2 || /^[a-z]$/i.test(value)) {
|
||||
data.keepAttr = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
formattedContent = DOMPurify.sanitize(parsedContent, defaultSanitizeConfig);
|
||||
} else if (typeof marked !== 'undefined') {
|
||||
const rawForParse = role === 'assistant' ? displayContent : content;
|
||||
const parsedContent = parseMarkdown(rawForParse);
|
||||
if (parsedContent) {
|
||||
formattedContent = parsedContent;
|
||||
} else {
|
||||
formattedContent = escapeHtml(rawForParse).replace(/\n/g, '<br>');
|
||||
}
|
||||
} else if (typeof window.csMarkdownSanitize !== 'undefined') {
|
||||
formattedContent = window.csMarkdownSanitize.formatMarkdownToHtml(
|
||||
role === 'assistant' ? displayContent : content,
|
||||
{ profile: 'chat' }
|
||||
);
|
||||
} else {
|
||||
const rawForEscape = role === 'assistant' ? displayContent : content;
|
||||
formattedContent = escapeHtml(rawForEscape).replace(/\n/g, '<br>');
|
||||
@@ -2047,21 +1953,9 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
|
||||
bubble.innerHTML = formattedContent;
|
||||
|
||||
// 最后的安全检查:只处理明显的可疑图片(防止404和XSS)
|
||||
// DOMPurify已经处理了大部分XSS向量,这里只做必要的补充
|
||||
const images = bubble.querySelectorAll('img');
|
||||
images.forEach(img => {
|
||||
const src = img.getAttribute('src');
|
||||
if (src) {
|
||||
const trimmedSrc = src.trim();
|
||||
// 只检查明显的可疑URL(短字符串、单个字符)
|
||||
if (trimmedSrc.length <= 2 || /^[a-z]$/i.test(trimmedSrc)) {
|
||||
img.remove();
|
||||
}
|
||||
} else {
|
||||
img.remove();
|
||||
}
|
||||
});
|
||||
if (typeof window.csMarkdownSanitize !== 'undefined') {
|
||||
window.csMarkdownSanitize.stripSuspiciousImages(bubble);
|
||||
}
|
||||
|
||||
// 为每个表格添加独立的滚动容器
|
||||
wrapTablesInBubble(bubble);
|
||||
|
||||
+15
-25
@@ -320,12 +320,8 @@ function applyEinoTimelineRole(item, data) {
|
||||
}
|
||||
}
|
||||
|
||||
// markdown 渲染(用于最终合并渲染;流式增量阶段用纯转义避免部分语法不稳定)
|
||||
const assistantMarkdownSanitizeConfig = {
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'],
|
||||
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
};
|
||||
/** 过程详情时间线:更严消毒(无 img;整页 HTML 见 sanitize-markdown.js) */
|
||||
const timelineMarkdownOpts = { profile: 'timeline' };
|
||||
|
||||
function escapeHtmlLocal(text) {
|
||||
if (!text) return '';
|
||||
@@ -627,20 +623,10 @@ function setTimelineItemContentStreamRich(contentEl, html) {
|
||||
}
|
||||
|
||||
function formatAssistantMarkdownContent(text) {
|
||||
const raw = text == null ? '' : String(text);
|
||||
const src = normalizeAssistantMarkdownSource(raw);
|
||||
if (typeof marked !== 'undefined') {
|
||||
try {
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
const parsed = marked.parse(src, { async: false });
|
||||
if (typeof DOMPurify !== 'undefined') {
|
||||
return DOMPurify.sanitize(parsed, assistantMarkdownSanitizeConfig);
|
||||
}
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
return escapeHtmlLocal(raw).replace(/\n/g, '<br>');
|
||||
}
|
||||
if (typeof window.csMarkdownSanitize !== 'undefined') {
|
||||
return window.csMarkdownSanitize.formatMarkdownToHtml(text, { profile: 'chat' });
|
||||
}
|
||||
const raw = text == null ? '' : String(text);
|
||||
return escapeHtmlLocal(raw).replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
@@ -668,6 +654,10 @@ function updateAssistantBubbleContent(assistantMessageId, content, renderMarkdow
|
||||
wrapTablesInBubble(bubble);
|
||||
}
|
||||
if (copyBtn) bubble.appendChild(copyBtn);
|
||||
|
||||
if (typeof window.csMarkdownSanitize !== 'undefined') {
|
||||
window.csMarkdownSanitize.stripSuspiciousImages(bubble);
|
||||
}
|
||||
}
|
||||
|
||||
const conversationExecutionTracker = {
|
||||
@@ -1661,7 +1651,7 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
const contentEl = item.querySelector('.timeline-item-content');
|
||||
if (contentEl) {
|
||||
if (typeof formatMarkdown === 'function') {
|
||||
setTimelineItemContentStreamRich(contentEl, formatMarkdown(s.buffer));
|
||||
setTimelineItemContentStreamRich(contentEl, formatMarkdown(s.buffer, timelineMarkdownOpts));
|
||||
} else {
|
||||
setTimelineItemContentStreamPlain(contentEl, s.buffer);
|
||||
}
|
||||
@@ -2004,7 +1994,7 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
item.appendChild(contentEl);
|
||||
}
|
||||
if (typeof formatMarkdown === 'function') {
|
||||
setTimelineItemContentStreamRich(contentEl, formatMarkdown(full));
|
||||
setTimelineItemContentStreamRich(contentEl, formatMarkdown(full, timelineMarkdownOpts));
|
||||
} else {
|
||||
setTimelineItemContentStreamPlain(contentEl, full);
|
||||
}
|
||||
@@ -3126,7 +3116,7 @@ function addTimelineItem(timeline, type, options) {
|
||||
const streamBody = typeof formatTimelineStreamBody === 'function'
|
||||
? formatTimelineStreamBody(options.message, options.data)
|
||||
: options.message;
|
||||
content += `<div class="timeline-item-content">${formatMarkdown(streamBody)}</div>`;
|
||||
content += `<div class="timeline-item-content">${formatMarkdown(streamBody, timelineMarkdownOpts)}</div>`;
|
||||
} else if (type === 'tool_call' && options.data) {
|
||||
const data = options.data;
|
||||
const args = parseToolCallArgsFromData(data);
|
||||
@@ -3155,7 +3145,7 @@ function addTimelineItem(timeline, type, options) {
|
||||
</div>
|
||||
`;
|
||||
} else if (type === 'eino_agent_reply' && options.message) {
|
||||
content += `<div class="timeline-item-content">${formatMarkdown(options.message)}</div>`;
|
||||
content += `<div class="timeline-item-content">${formatMarkdown(options.message, timelineMarkdownOpts)}</div>`;
|
||||
} else if (type === 'tool_result' && options.data) {
|
||||
const data = options.data;
|
||||
const isError = data.isError || !data.success;
|
||||
@@ -3184,14 +3174,14 @@ function addTimelineItem(timeline, type, options) {
|
||||
const streamBody = typeof formatTimelineStreamBody === 'function'
|
||||
? formatTimelineStreamBody(options.message, options.data)
|
||||
: options.message;
|
||||
content += `<div class="timeline-item-content">${formatMarkdown(streamBody)}</div>`;
|
||||
content += `<div class="timeline-item-content">${formatMarkdown(streamBody, timelineMarkdownOpts)}</div>`;
|
||||
} else if (type === 'progress' && options.message) {
|
||||
content += `<div class="timeline-item-content timeline-eino-trace"><pre class="tool-result">${escapeHtml(options.message)}</pre></div>`;
|
||||
} else if (type === 'user_interrupt_continue' && options.message) {
|
||||
const streamBody = typeof formatTimelineStreamBody === 'function'
|
||||
? formatTimelineStreamBody(options.message, options.data)
|
||||
: options.message;
|
||||
content += `<div class="timeline-item-content">${formatMarkdown(streamBody)}</div>`;
|
||||
content += `<div class="timeline-item-content">${formatMarkdown(streamBody, timelineMarkdownOpts)}</div>`;
|
||||
}
|
||||
|
||||
item.innerHTML = content;
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* 统一的 Markdown → 安全 HTML 渲染(DOMPurify + marked)。
|
||||
* 时间线/过程详情使用 stricter profile,整页 HTML 回退为转义 <pre>。
|
||||
*/
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
const CHAT_SANITIZE_CONFIG = {
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote',
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img',
|
||||
'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'],
|
||||
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
};
|
||||
|
||||
/** 过程详情时间线:禁止 img,减少外连与恶意资源 */
|
||||
const TIMELINE_SANITIZE_CONFIG = {
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote',
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a',
|
||||
'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'],
|
||||
ALLOWED_ATTR: ['href', 'title', 'alt', 'class'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
};
|
||||
|
||||
const DANGEROUS_URL_PREFIXES = [
|
||||
'javascript:',
|
||||
'vbscript:',
|
||||
'data:text/html',
|
||||
'data:text/javascript',
|
||||
'data:application/javascript',
|
||||
];
|
||||
|
||||
let domPurifyHooksInstalled = false;
|
||||
|
||||
function escapeHtmlLocal(text) {
|
||||
if (text == null || text === '') return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = String(text);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function installDomPurifyHooks() {
|
||||
if (domPurifyHooksInstalled || typeof DOMPurify === 'undefined' || !DOMPurify.addHook) {
|
||||
return;
|
||||
}
|
||||
DOMPurify.addHook('uponSanitizeAttribute', function (node, data) {
|
||||
const attrName = (data.attrName || '').toLowerCase();
|
||||
if ((attrName !== 'src' && attrName !== 'href') || !data.attrValue) {
|
||||
return;
|
||||
}
|
||||
const value = String(data.attrValue).trim().toLowerCase();
|
||||
for (let i = 0; i < DANGEROUS_URL_PREFIXES.length; i++) {
|
||||
if (value.indexOf(DANGEROUS_URL_PREFIXES[i]) === 0) {
|
||||
data.keepAttr = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (value.indexOf('blob:') === 0) {
|
||||
data.keepAttr = false;
|
||||
return;
|
||||
}
|
||||
if (attrName === 'src' && node.tagName && node.tagName.toLowerCase() === 'img') {
|
||||
if (value.length <= 2 || /^[a-z]$/i.test(value)) {
|
||||
data.keepAttr = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
domPurifyHooksInstalled = true;
|
||||
}
|
||||
|
||||
/** 探测工具返回的整页 HTML,不宜当作富文本渲染 */
|
||||
function isHeavyRawHtml(src) {
|
||||
const s = String(src);
|
||||
if (/<!DOCTYPE\s+html/i.test(s) || /<\s*html\b/i.test(s)) {
|
||||
return true;
|
||||
}
|
||||
if (/<\s*(head|body|iframe|object|embed|form|script|style|meta|link|base)\b/i.test(s)) {
|
||||
return true;
|
||||
}
|
||||
const tags = s.match(/<[a-z][^>]*>/gi);
|
||||
return tags != null && tags.length >= 8;
|
||||
}
|
||||
|
||||
function formatHtmlAsEscapedPre(text) {
|
||||
return '<pre class="tool-result sanitized-raw-html-fallback">' + escapeHtmlLocal(text) + '</pre>';
|
||||
}
|
||||
|
||||
function normalizeSource(text) {
|
||||
const raw = text == null ? '' : String(text);
|
||||
if (typeof global.normalizeAssistantMarkdownSource === 'function') {
|
||||
return global.normalizeAssistantMarkdownSource(raw);
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
function parseMarkdownSrc(src) {
|
||||
if (typeof marked === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
return marked.parse(src, { async: false });
|
||||
} catch (e) {
|
||||
console.error('Markdown 解析失败:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeConfigForProfile(profile) {
|
||||
return profile === 'timeline' ? TIMELINE_SANITIZE_CONFIG : CHAT_SANITIZE_CONFIG;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string|null|undefined} text
|
||||
* @param {{ profile?: 'chat'|'timeline' }} [options]
|
||||
* @returns {string} 安全 HTML
|
||||
*/
|
||||
function formatMarkdownToHtml(text, options) {
|
||||
const profile = (options && options.profile === 'timeline') ? 'timeline' : 'chat';
|
||||
const src = normalizeSource(text);
|
||||
|
||||
if (isHeavyRawHtml(src)) {
|
||||
return formatHtmlAsEscapedPre(src);
|
||||
}
|
||||
|
||||
if (typeof DOMPurify === 'undefined') {
|
||||
return escapeHtmlLocal(src).replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
installDomPurifyHooks();
|
||||
const config = sanitizeConfigForProfile(profile);
|
||||
|
||||
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) {
|
||||
if (typeof DOMPurify === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
installDomPurifyHooks();
|
||||
return DOMPurify.sanitize(html, sanitizeConfigForProfile(profile || 'chat'));
|
||||
}
|
||||
|
||||
function stripSuspiciousImages(root) {
|
||||
if (!root || !root.querySelectorAll) {
|
||||
return;
|
||||
}
|
||||
root.querySelectorAll('img').forEach(function (img) {
|
||||
const src = (img.getAttribute('src') || '').trim();
|
||||
if (!src || src.length <= 2 || /^[a-z]$/i.test(src)) {
|
||||
img.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
global.csMarkdownSanitize = {
|
||||
CHAT_SANITIZE_CONFIG: CHAT_SANITIZE_CONFIG,
|
||||
TIMELINE_SANITIZE_CONFIG: TIMELINE_SANITIZE_CONFIG,
|
||||
installDomPurifyHooks: installDomPurifyHooks,
|
||||
formatMarkdownToHtml: formatMarkdownToHtml,
|
||||
sanitizeRichHtml: sanitizeRichHtml,
|
||||
isHeavyRawHtml: isHeavyRawHtml,
|
||||
escapeHtmlLocal: escapeHtmlLocal,
|
||||
stripSuspiciousImages: stripSuspiciousImages,
|
||||
};
|
||||
|
||||
global.formatMarkdown = function formatMarkdown(text, options) {
|
||||
return formatMarkdownToHtml(text, options);
|
||||
};
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
@@ -1375,11 +1375,6 @@ function fillVisionConfigFromCurrent(v) {
|
||||
const d = (v.detail || 'low').toString().toLowerCase();
|
||||
det.value = ['low', 'auto', 'high'].includes(d) ? d : 'low';
|
||||
}
|
||||
const rootsEl = document.getElementById('vision-allowed-roots');
|
||||
if (rootsEl) {
|
||||
const roots = Array.isArray(v.allowed_roots) ? v.allowed_roots : [];
|
||||
rootsEl.value = roots.join('\n');
|
||||
}
|
||||
syncVisionFormEnabled();
|
||||
}
|
||||
|
||||
@@ -1388,8 +1383,6 @@ function collectVisionConfigFromForm() {
|
||||
const n = parseInt(document.getElementById(id)?.value, 10);
|
||||
return Number.isNaN(n) ? fallback : n;
|
||||
};
|
||||
const rootsRaw = document.getElementById('vision-allowed-roots')?.value || '';
|
||||
const allowed_roots = rootsRaw.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
||||
const provider = document.getElementById('vision-provider')?.value.trim() || '';
|
||||
return {
|
||||
enabled: document.getElementById('vision-enabled')?.checked === true,
|
||||
@@ -1403,8 +1396,7 @@ function collectVisionConfigFromForm() {
|
||||
jpeg_quality: parseIntOr('vision-jpeg-quality', 82),
|
||||
max_payload_bytes: parseIntOr('vision-max-payload-bytes', 524288),
|
||||
skip_preprocess_below_bytes: parseIntOr('vision-skip-preprocess-bytes', 2097152),
|
||||
detail: document.getElementById('vision-detail')?.value || 'low',
|
||||
allowed_roots: allowed_roots
|
||||
detail: document.getElementById('vision-detail')?.value || 'low'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2544,10 +2544,6 @@
|
||||
<label for="vision-timeout-seconds" data-i18n="settingsBasic.visionTimeout">超时(秒)</label>
|
||||
<input type="number" id="vision-timeout-seconds" min="5" step="1" placeholder="60" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vision-allowed-roots" data-i18n="settingsBasic.visionAllowedRoots">额外允许路径根目录</label>
|
||||
<textarea id="vision-allowed-roots" rows="2" data-i18n="settingsBasic.visionAllowedRootsPlaceholder" data-i18n-attr="placeholder" placeholder="每行一个绝对路径,可选"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
|
||||
@@ -3512,6 +3508,7 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
|
||||
<!-- DOMPurify for HTML sanitization to prevent XSS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/purify.min.js"></script>
|
||||
<script src="/static/js/sanitize-markdown.js"></script>
|
||||
<!-- Cytoscape.js for attack chain visualization -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.27.0/dist/cytoscape.min.js"></script>
|
||||
<!-- ELK.js for high-quality DAG layout (reduces edge crossings) -->
|
||||
|
||||
Reference in New Issue
Block a user