发帖
 找回密码
 立即注册
搜索
0 0 0
技术交流 27 0 前天 15:29

OpenWebUI 在模型管理上有一个非常不合理的设计:所有模型的 logo 图标都以 base64 格式直接嵌入在配置文件中,导致

/v1/models

/api/models

请求体积巨大。反正很麻烦就对了,我们可以将其改成使用CDN的图像链接来改善这一问题。

https://www.bigseek.com/ai-264-1-1.html

都存在一个很扯淡的问题,opwenwebui之前如果不在模型设置界面手动对模型进行一次修改,你添加的自定义设置项是无法生效的。这导致批量化又退化回了手动一个个操作。增加 "base_model_id": null 字段即可生效。
28df86ac2c4eccac0fdb34300b6f409e59dc07ea.webp

于是就在原网页版的基础上进行了微小的修改(感谢 @xiniah),即实现了真正的批量替换。同时其他设置项也是可以生效了。

以下为网页代码,复制到html文件即可运行。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>OpenWebUI 模型 Profile Image URL 修正工具</title>
    <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
    <style>
        body {
            font-family: sans-serif;
            line-height: 1.6;
            padding: 20px;
            max-width: 900px;
            margin: 0 auto;
            background-color: #f8f9fa;
            color: #333;
        }
        .container {
            background-color: #fff;
            padding: 30px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        }
        h1 {
            text-align: center;
            color: #007bff;
            margin-bottom: 20px;
        }
        .input-group, .options, .rules-section {
            margin-bottom: 20px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        input[type="file"], textarea {
            display: block;
            width: 100%;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
            box-sizing: border-box;
        }
        textarea {
            min-height: 150px;
            font-family: monospace;
            resize: vertical;
        }
        .options label {
            display: inline-flex; /* Align checkbox and text */
            align-items: center;
            margin-right: 15px; /* Space between options */
            font-weight: normal; /* Regular weight for options */
        }
        .options input[type="checkbox"] {
            margin-right: 8px;
        }
        button {
            background-color: #007bff;
            color: white;
            border: none;
            padding: 10px 15px; /* Slightly smaller padding */
            border-radius: 4px;
            cursor: pointer;
            font-size: 0.95em; /* Slightly smaller font */
            transition: background-color 0.3s ease;
            margin-right: 10px;
            margin-top: 5px; /* Add top margin for buttons */
            margin-bottom: 5px;
        }
        button:hover:not(:disabled) {
            background-color: #0056b3;
        }
        button:disabled {
            background-color: #cccccc;
            cursor: not-allowed;
        }
        .button-secondary {
             background-color: #6c757d;
        }
         .button-secondary:hover:not(:disabled) {
             background-color: #5a6268;
         }
         .button-danger {
             background-color: #dc3545;
         }
         .button-danger:hover:not(:disabled) {
             background-color: #c82333;
         }
        .logs, .output {
            margin-top: 25px;
            border: 1px solid #eee;
            padding: 15px;
            border-radius: 4px;
            background-color: #f8f9fa;
        }
        h2 {
            margin-top: 0;
            font-size: 1.2em;
            color: #555;
            border-bottom: 1px solid #ddd;
            padding-bottom: 5px;
            margin-bottom: 10px;
        }
        pre {
            white-space: pre-wrap;
            word-wrap: break-word;
            font-family: monospace;
            background-color: #e9ecef;
            padding: 10px;
            border-radius: 4px;
            max-height: 300px;
            overflow-y: auto;
            color: #495057;
        }
        .log-item {
            padding: 5px 0;
            border-bottom: 1px dashed #ddd;
            font-size: 0.9em;
        }
        .log-item:last-child {
            border-bottom: none;
        }
        .log-item code {
             font-family: monospace;
             background-color: #e0e0e0;
             padding: 2px 4px;
             border-radius: 3px;
        }
        .log-update { color: #28a745; } /* Green */
        .log-skip { color: #ffc107; }   /* Yellow */
        .log-notfound { color: #dc3545; } /* Red */
        .log-summary { font-weight: bold; margin-bottom: 15px; }
        .error, .success {
            font-weight: bold;
            margin-top: 15px;
            padding: 10px;
            border-radius: 4px;
        }
         .error { color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; }
         .success { color: #155724; background-color: #d4edda; border: 1px solid #c3e6cb;}
        .download-section {
             margin-top: 20px;
        }
        details {
            border: 1px solid #ccc;
            border-radius: 4px;
            padding: 10px;
            background-color: #fdfdfd;
        }
        summary {
            font-weight: bold;
            cursor: pointer;
            color: #0056b3;
        }
        .rules-editor {
            margin-top: 15px;
        }
        .rules-actions button {
            margin-right: 5px;
        }
        .import-rules {
            margin-top: 10px;
        }
        .import-rules input[type="file"] { display: inline-block; width: auto;}

    </style>
</head>
<body>

<div id="app" class="container">
    <h1>OpenWebUI 模型 Profile Image URL 批量修正工具</h1>
  
    <p style="text-align: center; margin-bottom: 20px;">
        此工具可以修正模型的 Profile Image URL
    </p>
    <p style="text-align: center; margin-bottom: 20px;">
        并通过每个模型添加 <code>"base_model_id": null</code> 字段。实现了不再需要修改一次来触发的问题。
    </p>

     <div v-if="statusMessage.text" :class="['status', statusMessage.type]">
        {{ statusMessage.text }}
    </div>

    <div class="input-group">
        <label for="jsonFile">选择 OpenWebUI 导出的 JSON 文件 (models-export-*.json):</label>
        <input type="file" id="jsonFile" @change="handleFileChange" accept=".json" ref="fileInputRef">
    </div>

    <div class="options">
        <label for="dryRun">
            <input type="checkbox" id="dryRun" v-model="isDryRun">
            仅预览修正日志 (Dry Run)
        </label>
         <label for="overwriteExisting">
            <input type="checkbox" id="overwriteExisting" v-model="overwriteExisting">
            允许覆盖已存在的非默认 URL
        </label>
    </div>

    <!-- Rules Editor Section -->
    <div class="rules-section">
        <details>
            <summary>编辑/导入/导出匹配规则 (PROVIDER_MAP)</summary>
            <div class="rules-editor">
                <label for="rulesText">规则 (JSON 格式: <code>[["keyword1", "url1"], ["keyword2", "url2"], ...]</code>)</label>
                <textarea id="rulesText" v-model="rulesText" rows="10"></textarea>
                <div class="rules-actions">
                    <button @click="saveRules" class="button-secondary">保存规则到浏览器</button>
                    <button @click="resetRules" class="button-danger">重置为默认规则</button>
                    <button @click="exportRules" class="button-secondary">导出当前规则</button>
                </div>
                 <div class="import-rules">
                     <label for="importFile">导入规则:</label>
                    <input type="file" id="importFile" @change="handleRuleImport" accept=".json" ref="importFileRef">
                 </div>
            </div>
        </details>
    </div>
    <!-- End Rules Editor Section -->

    <button @click="processJson" :disabled="!fileContent">开始处理模型文件</button>

    <div v-if="error" class="error">
        处理错误: {{ error }}
    </div>

    <div v-if="logs.length > 0" class="logs">
        <h2>处理日志</h2>
        <p class="log-summary">{{ summary }}</p>
        <div v-for="(log, index) in logs" :key="index" :class="['log-item', logClass(log)]">
             <span v-html="log.message.replace(/\[(UPDATE|SKIP|NOT FOUND)\]/g, '<strong>[$1]</strong>')"></span>
        </div>
    </div>

    <div v-if="outputJson && !isDryRun" class="output download-section">
        <h2>修正后的 JSON</h2>
        <button @click="downloadJson" :disabled="!outputJson">下载修正后的模型文件</button>
        <pre>{{ outputJson }}</pre>
    </div>
</div>

<script>
    const { createApp, ref, reactive, onMounted, watch } = Vue;

    // --- 默认配置数据 ---
    const DEFAULT_PROVIDER_MAP = [
        // (关键词,image_url),顺序即优先级
        ["openrouter/", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openrouter.svg"],
        ["aliyun/", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/aliyun-color.svg"],
        ["gemini_pipe_new.", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg"],
        ["gpt", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"],
        ["openai", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"],
        ["o1-", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"],
        ["o3-", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"],
        ["whisper", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"],
        ["text-embedding-", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dalle-color.svg"],
        ["tts-", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dalle-color.svg"],
        ["dall-e", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dalle-color.svg"],
        ["claude", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/claude-color.svg"],
        ["gemini", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg"],
        ["ernie", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/wenxin-color.svg"],
        ["baidu", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/wenxin-color.svg"],
        ["command", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/cohere-color.svg"],
        ["cohere", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/cohere-color.svg"],
        ["deepseek", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek-color.svg"],
        ["grok", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/grok.svg"],
        ["llama", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/meta-color.svg"],
        ["meta", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/meta-color.svg"],
        ["groq", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/groq-color.svg"],
        ["qwq", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen-color.svg"],
        ["qvq", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen-color.svg"],
        ["qwen", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen-color.svg"],
        ["abab", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax-color.svg"],
        ["minimax", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax-color.svg"],
        ["mistral", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral-color.svg"],
        ["kimi", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi-color.svg"],
        ["moonshot", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi-color.svg"],
        ["ollama", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama-color.svg"],
        ["hunyuan", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/hunyuan-color.svg"],
        ["tencent", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/hunyuan-color.svg"],
        ["yi", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/yi-color.svg"],
        ["glm", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qingyan-color.svg"],
        ["zhipu", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qingyan-color.svg"],
        ["open-mixtral","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral-color.svg"],
        ["ministral","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral-color.svg"],
        ["codestral","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral-color.svg"],
        ["pixtral","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral-color.svg"],
    ];
    const DEFAULT_URLS = new Set(["", "/static/favicon.png"]); // 使用 Set 以提高查找效率
    const LOCAL_STORAGE_KEY = 'openwebui_profile_rules';

    createApp({
        setup() {
            // --- Vue 响应式状态 ---
            const fileContent = ref(null); // 存储文件原始内容 (string)
            const jsonData = ref([]);    // 存储解析后的 JSON 对象 (array)
            const isDryRun = ref(false); // 是否为 Dry Run 模式
            const overwriteExisting = ref(false); // 是否覆盖已有 URL
            const logs = reactive([]);   // 存储日志消息对象 { type: 'update'|'skip'|'notfound', message: string }
            const summary = ref('');     // 存储处理结果摘要
            const outputJson = ref('');  // 存储处理后的 JSON 字符串
            const error = ref('');       // 存储错误信息
            const originalFileName = ref(''); // 存储原始文件名
            const providerMap = ref([]); // 当前使用的规则
            const rulesText = ref(''); // TextArea绑定的规则文本
            const statusMessage = reactive({ text: '', type: 'success' }); // 提示信息
            const fileInputRef = ref(null); // Ref for model file input
            const importFileRef = ref(null); // Ref for rules import file input

            // --- 方法 ---

            const showStatus = (message, type = 'success', duration = 4000) => {
                statusMessage.text = message;
                statusMessage.type = type === 'error' ? 'error' : 'success';
                setTimeout(() => {
                    statusMessage.text = '';
                }, duration);
            };

             // --- Rules Management ---
            const loadRules = () => {
                try {
                    const storedRules = localStorage.getItem(LOCAL_STORAGE_KEY);
                    if (storedRules) {
                        const parsedRules = JSON.parse(storedRules);
                        // Basic validation
                        if (Array.isArray(parsedRules) && parsedRules.every(item => Array.isArray(item) && item.length === 2 && typeof item[0] === 'string' && typeof item[1] === 'string')) {
                             providerMap.value = parsedRules;
                             showStatus('已从浏览器加载保存的规则。');
                        } else {
                             console.warn("LocalStorage 中的规则格式无效,将使用默认规则。");
                             providerMap.value = JSON.parse(JSON.stringify(DEFAULT_PROVIDER_MAP)); // Deep copy
                             localStorage.removeItem(LOCAL_STORAGE_KEY); // Remove invalid data
                        }
                    } else {
                        providerMap.value = JSON.parse(JSON.stringify(DEFAULT_PROVIDER_MAP)); // Deep copy
                    }
                } catch (e) {
                    console.error("加载或解析 LocalStorage 规则时出错:", e);
                    showStatus('加载本地规则失败,使用默认规则。', 'error');
                    providerMap.value = JSON.parse(JSON.stringify(DEFAULT_PROVIDER_MAP)); // Deep copy
                }
                 rulesText.value = JSON.stringify(providerMap.value, null, 2);
            };

            const saveRules = () => {
                try {
                    const newRules = JSON.parse(rulesText.value);
                    // Basic validation before saving
                     if (Array.isArray(newRules) && newRules.every(item => Array.isArray(item) && item.length === 2 && typeof item[0] === 'string' && typeof item[1] === 'string')) {
                        providerMap.value = newRules;
                        localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newRules));
                        showStatus('规则已成功保存到浏览器!');
                    } else {
                         showStatus('规则格式无效,请确保其为[["关键字","URL"],...] 的格式。未保存。', 'error');
                    }
                } catch (e) {
                    console.error("保存规则到 LocalStorage 时出错:", e);
                    showStatus(`保存规则失败: ${e.message}`, 'error');
                }
            };

            const resetRules = () => {
                if (confirm('确定要重置为默认规则吗?当前编辑和已保存的规则将丢失。')) {
                    providerMap.value = JSON.parse(JSON.stringify(DEFAULT_PROVIDER_MAP)); // Deep copy
                    rulesText.value = JSON.stringify(providerMap.value, null, 2);
                    localStorage.removeItem(LOCAL_STORAGE_KEY); // Remove custom rules from storage
                    showStatus('已重置为默认规则。');
                }
            };

            const exportRules = () => {
                try {
                    const blob = new Blob([JSON.stringify(providerMap.value, null, 2)], { type: 'application/json;charset=utf-8' });
                    const url = URL.createObjectURL(blob);
                    const link = document.createElement('a');
                    link.href = url;
                    link.download = 'openwebui_profile_rules.json';
                    document.body.appendChild(link);
                    link.click();
                    document.body.removeChild(link);
                    URL.revokeObjectURL(url);
                    showStatus('规则已导出。');
                } catch (e) {
                    console.error("导出规则失败:", e);
                    showStatus('导出规则失败。', 'error');
                }
            };

            const handleRuleImport = (event) => {
                const file = event.target.files[0];
                if (!file) return;

                const reader = new FileReader();
                reader.onload = (e) => {
                    try {
                        const importedRules = JSON.parse(e.target.result);
                        // Validation
                         if (Array.isArray(importedRules) && importedRules.every(item => Array.isArray(item) && item.length === 2 && typeof item[0] === 'string' && typeof item[1] === 'string')) {
                            providerMap.value = importedRules;
                            rulesText.value = JSON.stringify(providerMap.value, null, 2);
                            saveRules(); // Optionally save imported rules immediately
                            showStatus('规则已成功导入并保存!');
                        } else {
                             showStatus('导入的文件格式无效,请确保其为 [["关键字","URL"],...] 的 JSON 数组。', 'error');
                        }
                    } catch (err) {
                        console.error("导入规则失败:", err);
                        showStatus(`导入规则失败: ${err.message}`, 'error');
                    } finally {
                         // Reset file input to allow importing the same file again
                        if (importFileRef.value) {
                           importFileRef.value.value = '';
                        }
                    }
                };
                 reader.onerror = (e) => {
                    showStatus('读取导入文件时出错。', 'error');
                     if (importFileRef.value) {
                        importFileRef.value.value = '';
                     }
                 }
                reader.readAsText(file, 'UTF-8');
            };

            // Watch rulesText for external changes (like import) to potentially update storage implicitly or provide feedback
            // watch(rulesText, (newValue) => { /* Can add logic here if needed */ });

            // --- Model File Handling ---
            const handleFileChange = (event) => {
                const file = event.target.files[0];
                 clearResults(); // Clear previous results when a new file is selected
                if (!file) {
                    fileContent.value = null;
                    originalFileName.value = '';
                    return;
                }
                originalFileName.value = file.name;
                const reader = new FileReader();
                reader.onload = (e) => {
                    fileContent.value = e.target.result;
                };
                reader.onerror = (e) => {
                    error.value = '读取模型文件时出错: ' + e.target.error;
                    clearResults();
                }
                reader.readAsText(file, 'UTF-8');
            };

            const clearResults = () => {
                 jsonData.value = [];
                 logs.splice(0, logs.length); // 清空数组
                 summary.value = '';
                 outputJson.value = '';
                 error.value = '';
                  // Reset file input if needed, though usually not necessary on selection
                // if(fileInputRef.value) fileInputRef.value.value = '';
            }

            const findCorrectUrl = (modelId) => {
                const idLower = modelId.toLowerCase();
                // Use the reactive providerMap.value here
                for (const [key, url] of providerMap.value) {
                    if (idLower.includes(key)) {
                        return url;
                    }
                }
                return null;
            };

            const processJson = () => {
                clearResults(); // Start fresh

                if (!fileContent.value) {
                    error.value = '请先选择一个模型 JSON 文件。';
                    return;
                }
                if (!providerMap.value || providerMap.value.length === 0) {
                    error.value = '匹配规则为空,请先定义或导入规则。';
                    return;
                }

                try {
                    let data = JSON.parse(fileContent.value);
                    if (!Array.isArray(data)) {
                        throw new Error("JSON 文件顶层结构必须是一个数组。");
                    }

                    jsonData.value = data; // Store parsed data

                    let updated = 0, skipped = 0, notFound = 0;
                    let addedBaseModelId = 0; // 计数添加了 base_model_id 的记录
                    const tempLogs = []; // Temporary log storage

                    for (const model of jsonData.value) {
                        const modelId = model?.id ?? '';
                        const meta = model?.meta ?? {};
                        const currentUrl = meta.profile_image_url ?? '';

                        // 添加 base_model_id 字段(与 meta 同级)
                        if (!isDryRun.value) {
                            if (model.base_model_id === undefined) {
                                model.base_model_id = null;
                                addedBaseModelId++;
                            }
                        }

                        if (!modelId || typeof meta !== 'object') {
                           tempLogs.push({ type: 'skip', message: `[SKIP] 记录缺少 'id' 或 'meta' 字段,已跳过。`});
                           skipped++;
                           continue;
                        }

                        const correctUrl = findCorrectUrl(modelId);

                        if (correctUrl) {
                            if (currentUrl === correctUrl) {
                                skipped++;
                                tempLogs.push({ type: 'skip', message: `[SKIP] ${modelId} 已是正确 URL: <code>${currentUrl}</code>` });
                            } else if (DEFAULT_URLS.has(currentUrl) || !currentUrl) {
                                // Update if empty or default
                                if (!isDryRun.value) {
                                    if (!model.meta) model.meta = {};
                                     model.meta.profile_image_url = correctUrl;
                                }
                                updated++;
                                tempLogs.push({ type: 'update', message: `[UPDATE] ${modelId}<br>  原 URL: <code>${currentUrl || '空'}</code><br>  新 URL: <code>${correctUrl}</code>` });
                            } else {
                                // Existing, non-default URL found
                                if (overwriteExisting.value) { // Check the overwrite option
                                    if (!isDryRun.value) {
                                        if (!model.meta) model.meta = {};
                                        model.meta.profile_image_url = correctUrl;
                                    }
                                    updated++; // Count as updated
                                    tempLogs.push({ type: 'update', message: `[UPDATE] ${modelId} (覆盖已有)<br>  原 URL: <code>${currentUrl}</code><br>  新 URL: <code>${correctUrl}</code>` });
                                } else { // Skip if overwrite is not checked
                                    skipped++;
                                    tempLogs.push({ type: 'skip', message: `[SKIP] ${modelId} URL 已存在且与映射不符 (未勾选覆盖): <code>${currentUrl}</code>` });
                                }
                            }
                        } else {
                            notFound++;
                            tempLogs.push({ type: 'notfound', message: `[NOT FOUND] ${modelId} 未匹配到规则, 当前 URL: <code>${currentUrl || '空'}</code>` });
                        }
                    }

                    // 更新摘要信息,加入添加base_model_id的统计
                    summary.value = `处理完成: 共 ${jsonData.value.length} 条, 更新URL ${updated} 条, 添加base_model_id ${addedBaseModelId} 条, 跳过 ${skipped} 条, 未匹配到规则 ${notFound} 条。`;
                    logs.push(...tempLogs); // Push logs reactively

                    if (!isDryRun.value) {
                        outputJson.value = JSON.stringify(jsonData.value, null, 2);
                    } else {
                        outputJson.value = '';
                        summary.value += ' (Dry Run 预览模式)';
                    }

                } catch (e) {
                    error.value = `JSON 解析或处理失败: ${e.message}`;
                    console.error(e);
                }
            };

            const downloadJson = () => {
                if (!outputJson.value) return;
                const blob = new Blob([outputJson.value], { type: 'application/json;charset=utf-8' });
                const url = URL.createObjectURL(blob);
                const link = document.createElement('a');
                link.href = url;
                const baseName = originalFileName.value.replace(/\.json$/i, '');
                link.download = `${baseName}-modified.json`;
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
                URL.revokeObjectURL(url);
            };

            const logClass = (log) => {
                return `log-${log.type}`;
            };

            // --- Lifecycle Hook ---
             onMounted(() => {
                loadRules(); // Load rules when component is mounted
             });

            return {
                fileContent,
                jsonData,
                isDryRun,
                overwriteExisting,
                logs,
                summary,
                outputJson,
                error,
                originalFileName,
                providerMap,
                rulesText,
                statusMessage,
                fileInputRef,
                importFileRef,
                handleFileChange,
                processJson,
                downloadJson,
                logClass,
                // Rules methods
                loadRules,
                saveRules,
                resetRules,
                exportRules,
                handleRuleImport,
            };
        }
    }).mount('#app');
</script>

<footer style="text-align: center; margin-top: 30px; font-size: 0.9em; color: #6c757d;">
       <p>
          原始 Python 脚本及核心逻辑思路参考自:
          <a href="https://linux.do/t/topic/554075" target="_blank" rel="noopener noreferrer">LINUX DO</a>.
         <br>初版网页出自 <a href="https://linux.do/t/topic/554075/26" target="_blank" rel="noopener noreferrer">xiniah</a>
         <br>修改意见来自 <a href="https://github.com/U8F69/open-webui/issues/143" target="_blank" rel="noopener noreferrer">方块佬</a>,同时也感谢大佬的二开版OpenWebui。
         <br>此 HTML/Vue 版本由 AI 根据脚本逻辑创建并添加额外功能。
      </p>
    </footer>
</html>
──── 0人觉得很赞 ────

使用道具 举报

您需要登录后才可以回帖 立即登录
高级模式