Add files via upload

This commit is contained in:
公明
2026-06-04 13:34:26 +08:00
committed by GitHub
parent 9a3f5e54b0
commit 679a8192ae
10 changed files with 221 additions and 207 deletions
+2 -8
View File
@@ -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 使用
+5
View File
@@ -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);
-2
View File
@@ -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",
-2
View File
@@ -1963,8 +1963,6 @@
"visionSkipPreprocessHint": "0 表示始终 JPEG 压缩;需同时满足长边与 payload 限制。",
"visionDetail": "Image detail",
"visionTimeout": "超时(秒)",
"visionAllowedRoots": "额外允许路径根目录",
"visionAllowedRootsPlaceholder": "每行一个绝对路径,可选",
"visionTestFillRequired": "请填写视觉模型,并确保 API Key 可用(可复用 OpenAI",
"testConnection": "测试连接",
"testFillRequired": "请先填写 API Key 和模型",
+6 -41
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+181
View File
@@ -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);
+1 -9
View File
@@ -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'
};
}
+1 -4
View File
@@ -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) -->