diff --git a/docs/VISION.md b/docs/VISION.md index ed6b7f2d..e7107b40 100644 --- a/docs/VISION.md +++ b/docs/VISION.md @@ -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 使用 diff --git a/web/static/css/style.css b/web/static/css/style.css index 37e31a54..e5195903 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -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); diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 5d46973f..1ab0e671 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -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", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 39a8adb9..0af6f76b 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -1963,8 +1963,6 @@ "visionSkipPreprocessHint": "0 表示始终 JPEG 压缩;需同时满足长边与 payload 限制。", "visionDetail": "Image detail", "visionTimeout": "超时(秒)", - "visionAllowedRoots": "额外允许路径根目录", - "visionAllowedRootsPlaceholder": "每行一个绝对路径,可选", "visionTestFillRequired": "请填写视觉模型,并确保 API Key 可用(可复用 OpenAI)", "testConnection": "测试连接", "testFillRequired": "请先填写 API Key 和模型", diff --git a/web/static/js/auth.js b/web/static/js/auth.js index 0b39f90a..5f3721e4 100644 --- a/web/static/js/auth.js +++ b/web/static/js/auth.js @@ -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, '
'); - } - } else { - return escapeHtml(src).replace(/\n/g, '
'); +/** @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, '
'); } function setupLoginUI() { diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 5b25d0e6..f54956c2 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -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, '
'); - } + if (typeof window.csMarkdownSanitize !== 'undefined') { + formattedContent = window.csMarkdownSanitize.formatMarkdownToHtml(text, { profile: 'chat' }); } else { formattedContent = escapeHtmlLocal(text).replace(/\n/g, '
'); } @@ -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解析后,代码块会被包裹在
标签中
-    // 2. 浏览器不会执行
标签内的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, '
'); - } else if (typeof DOMPurify !== 'undefined') { - // 直接解析Markdown(代码块会被包裹在/
中,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, '
'); - } + } 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, '
'); @@ -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); diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index 571261c1..a52ea6cf 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -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, '
'); - } + if (typeof window.csMarkdownSanitize !== 'undefined') { + return window.csMarkdownSanitize.formatMarkdownToHtml(text, { profile: 'chat' }); } + const raw = text == null ? '' : String(text); return escapeHtmlLocal(raw).replace(/\n/g, '
'); } @@ -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 += `
${formatMarkdown(streamBody)}
`; + content += `
${formatMarkdown(streamBody, timelineMarkdownOpts)}
`; } else if (type === 'tool_call' && options.data) { const data = options.data; const args = parseToolCallArgsFromData(data); @@ -3155,7 +3145,7 @@ function addTimelineItem(timeline, type, options) { `; } else if (type === 'eino_agent_reply' && options.message) { - content += `
${formatMarkdown(options.message)}
`; + content += `
${formatMarkdown(options.message, timelineMarkdownOpts)}
`; } 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 += `
${formatMarkdown(streamBody)}
`; + content += `
${formatMarkdown(streamBody, timelineMarkdownOpts)}
`; } else if (type === 'progress' && options.message) { content += `
${escapeHtml(options.message)}
`; } else if (type === 'user_interrupt_continue' && options.message) { const streamBody = typeof formatTimelineStreamBody === 'function' ? formatTimelineStreamBody(options.message, options.data) : options.message; - content += `
${formatMarkdown(streamBody)}
`; + content += `
${formatMarkdown(streamBody, timelineMarkdownOpts)}
`; } item.innerHTML = content; diff --git a/web/static/js/sanitize-markdown.js b/web/static/js/sanitize-markdown.js new file mode 100644 index 00000000..b3f21174 --- /dev/null +++ b/web/static/js/sanitize-markdown.js @@ -0,0 +1,181 @@ +/** + * 统一的 Markdown → 安全 HTML 渲染(DOMPurify + marked)。 + * 时间线/过程详情使用 stricter profile,整页 HTML 回退为转义
。
+ */
+(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 (/]*>/gi);
+        return tags != null && tags.length >= 8;
+    }
+
+    function formatHtmlAsEscapedPre(text) {
+        return '
' + escapeHtmlLocal(text) + '
'; + } + + 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, '
'); + } + + 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, '
'); + } else if (hasHtmlTags) { + html = src; + } else { + html = escapeHtmlLocal(src).replace(/\n/g, '
'); + } + + 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); diff --git a/web/static/js/settings.js b/web/static/js/settings.js index a973b5e8..69d341ae 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -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' }; } diff --git a/web/templates/index.html b/web/templates/index.html index d038a876..4e5337b4 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -2544,10 +2544,6 @@ -
- - -
@@ -3512,6 +3508,7 @@ +