ouyang2008 发表于 2025-6-13 15:29:17

修改版 OpenWebUI 模型 Profile Image URL 修正工具

<p>OpenWebUI 在模型管理上有一个非常不合理的设计:所有模型的 logo 图标都以 base64 格式直接嵌入在配置文件中,导致</p>
<p><code>/v1/models</code></p>
<p>或</p>
<p><code>/api/models</code></p>
<p>请求体积巨大。反正很麻烦就对了,我们可以将其改成使用CDN的图像链接来改善这一问题。</p>
<blockquote>
<p>https://www.bigseek.com/ai-264-1-1.html</p>
</blockquote>
<p>都存在一个很扯淡的问题,opwenwebui之前如果不在模型设置界面手动对模型进行一次修改,你添加的自定义设置项是无法生效的。这导致批量化又退化回了手动一个个操作。增加 <code>&quot;base_model_id&quot;: null</code> 字段即可生效。<br />
<img src="data/attachment/forum/202506/13/153030f5ctv33v3mq66584.webp" alt="28df86ac2c4eccac0fdb34300b6f409e59dc07ea.webp" title="28df86ac2c4eccac0fdb34300b6f409e59dc07ea.webp" /></p>
<p>于是就在原网页版的基础上进行了微小的修改(感谢 @xiniah),即实现了真正的批量替换。同时其他设置项也是可以生效了。</p>
<p>以下为网页代码,复制到html文件即可运行。</p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;zh-CN&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;OpenWebUI 模型 Profile Image URL 修正工具&lt;/title&gt;
    &lt;script src=&quot;https://unpkg.com/vue@3/dist/vue.global.prod.js&quot;&gt;&lt;/script&gt;
    &lt;style&gt;
      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, 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 {
            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 { display: inline-block; width: auto;}

    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;div id=&quot;app&quot; class=&quot;container&quot;&gt;
    &lt;h1&gt;OpenWebUI 模型 Profile Image URL 批量修正工具&lt;/h1&gt;

    &lt;p style=&quot;text-align: center; margin-bottom: 20px;&quot;&gt;
      此工具可以修正模型的 Profile Image URL
    &lt;/p&gt;
    &lt;p style=&quot;text-align: center; margin-bottom: 20px;&quot;&gt;
      并通过每个模型添加 &lt;code&gt;&quot;base_model_id&quot;: null&lt;/code&gt; 字段。实现了不再需要修改一次来触发的问题。
    &lt;/p&gt;

   &lt;div v-if=&quot;statusMessage.text&quot; :class=&quot;['status', statusMessage.type]&quot;&gt;
      {{ statusMessage.text }}
    &lt;/div&gt;

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

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

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

    &lt;button @click=&quot;processJson&quot; :disabled=&quot;!fileContent&quot;&gt;开始处理模型文件&lt;/button&gt;

    &lt;div v-if=&quot;error&quot; class=&quot;error&quot;&gt;
      处理错误: {{ error }}
    &lt;/div&gt;

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

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

&lt;script&gt;
    const { createApp, ref, reactive, onMounted, watch } = Vue;

    // --- 默认配置数据 ---
    const DEFAULT_PROVIDER_MAP = [
      // (关键词,image_url),顺序即优先级
      [&quot;openrouter/&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openrouter.svg&quot;],
      [&quot;aliyun/&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/aliyun-color.svg&quot;],
      [&quot;gemini_pipe_new.&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg&quot;],
      [&quot;gpt&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg&quot;],
      [&quot;openai&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg&quot;],
      [&quot;o1-&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg&quot;],
      [&quot;o3-&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg&quot;],
      [&quot;whisper&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg&quot;],
      [&quot;text-embedding-&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dalle-color.svg&quot;],
      [&quot;tts-&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dalle-color.svg&quot;],
      [&quot;dall-e&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dalle-color.svg&quot;],
      [&quot;claude&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/claude-color.svg&quot;],
      [&quot;gemini&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg&quot;],
      [&quot;ernie&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/wenxin-color.svg&quot;],
      [&quot;baidu&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/wenxin-color.svg&quot;],
      [&quot;command&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/cohere-color.svg&quot;],
      [&quot;cohere&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/cohere-color.svg&quot;],
      [&quot;deepseek&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek-color.svg&quot;],
      [&quot;grok&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/grok.svg&quot;],
      [&quot;llama&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/meta-color.svg&quot;],
      [&quot;meta&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/meta-color.svg&quot;],
      [&quot;groq&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/groq-color.svg&quot;],
      [&quot;qwq&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen-color.svg&quot;],
      [&quot;qvq&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen-color.svg&quot;],
      [&quot;qwen&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen-color.svg&quot;],
      [&quot;abab&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax-color.svg&quot;],
      [&quot;minimax&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax-color.svg&quot;],
      [&quot;mistral&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral-color.svg&quot;],
      [&quot;kimi&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi-color.svg&quot;],
      [&quot;moonshot&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi-color.svg&quot;],
      [&quot;ollama&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama-color.svg&quot;],
      [&quot;hunyuan&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/hunyuan-color.svg&quot;],
      [&quot;tencent&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/hunyuan-color.svg&quot;],
      [&quot;yi&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/yi-color.svg&quot;],
      [&quot;glm&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qingyan-color.svg&quot;],
      [&quot;zhipu&quot;, &quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qingyan-color.svg&quot;],
      [&quot;open-mixtral&quot;,&quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral-color.svg&quot;],
      [&quot;ministral&quot;,&quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral-color.svg&quot;],
      [&quot;codestral&quot;,&quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral-color.svg&quot;],
      [&quot;pixtral&quot;,&quot;https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral-color.svg&quot;],
    ];
    const DEFAULT_URLS = new Set([&quot;&quot;, &quot;/static/favicon.png&quot;]); // 使用 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) =&gt; {
                statusMessage.text = message;
                statusMessage.type = type === 'error' ? 'error' : 'success';
                setTimeout(() =&gt; {
                  statusMessage.text = '';
                }, duration);
            };

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

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

            const resetRules = () =&gt; {
                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 = () =&gt; {
                try {
                  const blob = new Blob(, { 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(&quot;导出规则失败:&quot;, e);
                  showStatus('导出规则失败。', 'error');
                }
            };

            const handleRuleImport = (event) =&gt; {
                const file = event.target.files;
                if (!file) return;

                const reader = new FileReader();
                reader.onload = (e) =&gt; {
                  try {
                        const importedRules = JSON.parse(e.target.result);
                        // Validation
                         if (Array.isArray(importedRules) &amp;&amp; importedRules.every(item =&gt; Array.isArray(item) &amp;&amp; item.length === 2 &amp;&amp; typeof item === 'string' &amp;&amp; typeof item === 'string')) {
                            providerMap.value = importedRules;
                            rulesText.value = JSON.stringify(providerMap.value, null, 2);
                            saveRules(); // Optionally save imported rules immediately
                            showStatus('规则已成功导入并保存!');
                        } else {
                           showStatus('导入的文件格式无效,请确保其为 [[&quot;关键字&quot;,&quot;URL&quot;],...] 的 JSON 数组。', 'error');
                        }
                  } catch (err) {
                        console.error(&quot;导入规则失败:&quot;, 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) =&gt; {
                  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) =&gt; { /* Can add logic here if needed */ });

            // --- Model File Handling ---
            const handleFileChange = (event) =&gt; {
                const file = event.target.files;
               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) =&gt; {
                  fileContent.value = e.target.result;
                };
                reader.onerror = (e) =&gt; {
                  error.value = '读取模型文件时出错: ' + e.target.error;
                  clearResults();
                }
                reader.readAsText(file, 'UTF-8');
            };

            const clearResults = () =&gt; {
               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) =&gt; {
                const idLower = modelId.toLowerCase();
                // Use the reactive providerMap.value here
                for (const of providerMap.value) {
                  if (idLower.includes(key)) {
                        return url;
                  }
                }
                return null;
            };

            const processJson = () =&gt; {
                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(&quot;JSON 文件顶层结构必须是一个数组。&quot;);
                  }

                  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: ` 记录缺少 'id' 或 'meta' 字段,已跳过。`});
                           skipped++;
                           continue;
                        }

                        const correctUrl = findCorrectUrl(modelId);

                        if (correctUrl) {
                            if (currentUrl === correctUrl) {
                              skipped++;
                              tempLogs.push({ type: 'skip', message: ` ${modelId} 已是正确 URL: &lt;code&gt;${currentUrl}&lt;/code&gt;` });
                            } 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: ` ${modelId}&lt;br&gt;  原 URL: &lt;code&gt;${currentUrl || '空'}&lt;/code&gt;&lt;br&gt;  新 URL: &lt;code&gt;${correctUrl}&lt;/code&gt;` });
                            } 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: ` ${modelId} (覆盖已有)&lt;br&gt;  原 URL: &lt;code&gt;${currentUrl}&lt;/code&gt;&lt;br&gt;  新 URL: &lt;code&gt;${correctUrl}&lt;/code&gt;` });
                              } else { // Skip if overwrite is not checked
                                    skipped++;
                                    tempLogs.push({ type: 'skip', message: ` ${modelId} URL 已存在且与映射不符 (未勾选覆盖): &lt;code&gt;${currentUrl}&lt;/code&gt;` });
                              }
                            }
                        } else {
                            notFound++;
                            tempLogs.push({ type: 'notfound', message: ` ${modelId} 未匹配到规则, 当前 URL: &lt;code&gt;${currentUrl || '空'}&lt;/code&gt;` });
                        }
                  }

                  // 更新摘要信息,加入添加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 = () =&gt; {
                if (!outputJson.value) return;
                const blob = new Blob(, { 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) =&gt; {
                return `log-${log.type}`;
            };

            // --- Lifecycle Hook ---
             onMounted(() =&gt; {
                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');
&lt;/script&gt;

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

attribt 发表于 2025-6-18 11:00:05

感谢分享实用工具

冷白 发表于 2025-7-6 15:00:05

感谢分享实用工具

halczy 发表于 2025-7-8 17:00:04

这工具挺实用的
页: [1]
查看完整版本: 修改版 OpenWebUI 模型 Profile Image URL 修正工具