peterll 发表于 2025-6-13 15:26:27

OpenWebUI优化: models 图像批量替换成 CDN 地址脚本分享

<h2>前言</h2>
<p>OpenWebUI 在模型管理上有一个非常不合理的设计:所有模型的 logo 图标都以 base64 格式直接嵌入在配置文件中,导致 <code>/v1/models</code> 或 <code>/api/models</code> 请求体积巨大。base64 编码不仅让单个 logo 体积增大约 30%,而且所有模型的 logo 都会被一次性拉取,前端首次加载极其卡慢,尤其是在带宽有限的 VPS 或小鸡上,体验极差。 😅</p>
<p>更离谱的是,这些 base64 图标因为被放在 API 路径下,无法享受 CDN 缓存优化,导致每次刷新都要重新拉取全部模型信息和 logo,进一步加剧了带宽和加载压力。</p>
<h2>解决思路</h2>
<ul>
<li>将所有模型的 profile_image_url 字段批量替换为外链 SVG 图标(如 lobehub 提供的 CDN 地址),极大减小配置文件体积。</li>
<li>通过 CDN 缓存这些静态资源,前端只需加载一次 logo,后续访问均可走缓存,极大提升加载速度和带宽利用率。</li>
<li>这样不仅优化了 OpenWebUI 的加载体验,也为后续自定义和维护模型图标提供了极大便利。</li>
</ul>
<p>让 Cline 帮我写了个 python 脚本用于批量替换</p>
<p><strong>1. 在 OpenWebUI 中导出模型预设</strong></p>
<ul>
<li>打开 OpenWebUI,进入 “模型” 管理页面。</li>
<li>需要首先对模型有所修改,比如增加描述,导出的 metadata 才会有 <code>profile_image_url</code> 参数</li>
<li>在页面右下角,点击 “导出预设” 按钮(如下图红色箭头所示),将当前所有模型配置导出为本地 JSON 文件。</li>
</ul>
<p><img src="data/attachment/forum/202506/13/152711eqn03zem3jp9ekqe.jpeg" alt="563d2ce0e9368fa4b083bb1f239ce56bfa2aadd7.jpeg" title="563d2ce0e9368fa4b083bb1f239ce56bfa2aadd7.jpeg" /></p>
<p><strong>2. 使用脚本批量修正 profile_image_url</strong></p>
<ul>
<li>自动查找最新导出的 JSON 文件并覆盖原文件:</li>
</ul>
<pre><code class="language-undefined">python update_profile_image_url.py
</code></pre>
<ul>
<li>指定输入文件并输出到新文件:</li>
</ul>
<pre><code class="language-cpp">python update_profile_image_url.py models-export-xxxxxx.json -o output.json
</code></pre>
<ul>
<li>仅预览详细日志不写入文件(推荐先 dry-run 检查):</li>
</ul>
<pre><code class="language-css">python update_profile_image_url.py --dry-run
</code></pre>
<p><strong>3. 在 OpenWebUI 中导入修正后的模型预设</strong></p>
<ul>
<li>回到 “模型” 管理页面,点击 “导入预设” 按钮,选择刚刚修正过的 JSON 文件导入即可。</li>
</ul>
<h1>脚本如下:</h1>
<pre><code>&quot;&quot;&quot;
name: 批量修正 OpenWebUI 导出的模型 JSON 文件中 profile_image_url 字段的脚本
author: Hardship2495 &amp; Cline
version: 1.0

【功能说明】
- 根据模型 id 中的关键词,自动匹配并修正每个模型的 profile_image_url 字段。
- 只在 profile_image_url 为空、为默认值或错误时才进行替换,已是正确 URL 则跳过。
- 匹配优先级:提供商前缀 &gt; 关键词,顺序可在 PROVIDER_MAP 中维护。
- 支持详细日志输出,显示每条记录的处理情况。

【使用方法】
1. 自动查找当前目录下最新的 models-export-*.json 文件并覆盖原文件:
   python update_profile_image_url.py

2. 指定输入文件并输出到新文件:
   python update_profile_image_url.py models-export-xxxxxxx.json -o output.json

3. 仅预览详细日志不写入文件(推荐先 dry-run 检查):
   python update_profile_image_url.py --dry-run

【参数说明】
- json_file      输入的模型配置 JSON 文件路径(可选,默认自动查找最新)
- -o, --output   输出文件名(可选,默认覆盖原文件)
- --dry-run      只预览详细日志,不写入文件

【映射表维护】
- PROVIDER_MAP 为 (关键词,image_url) 的有序列表,支持随时增删和调整优先级。
- 关键词区分优先级,前缀(如 openrouter/)优先于普通关键词(如 gpt)。
- 若有新模型或新提供商,只需在 PROVIDER_MAP 中添加对应项即可。

【日志说明】
-    表示已替换的模型,显示 id、原始 URL、新 URL
-    表示无需替换的模型,显示 id 及原因
- 未匹配到任何关键词的模型,显示 id 及当前 URL

【适用场景】
- OpenWebUI 导出的模型配置批量修正
- 需要统一或纠正 profile_image_url 字段的场景

&quot;&quot;&quot;

import os
import sys
import json
import glob
import argparse

# 1. 维护模型提供商关键词与 image_url 的映射(可随时增删)
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;),
]

DEFAULT_URLS = {&quot;&quot;,&quot;/static/favicon.png&quot;}

def find_correct_url (model_id: str) -&gt; str:
    &quot;&quot;&quot;根据 id 匹配对应的 image_url,优先顺序为 PROVIDER_MAP 顺序&quot;&quot;&quot;
    id_lower = model_id.lower ()
    for key, url in PROVIDER_MAP:
      if key in id_lower:
            return url
    return None

def get_latest_json_file ():
    files = glob.glob (&quot;models-export-*.json&quot;)
    if not files:
      print (&quot;未找到 models-export-*.json 文件。&quot;)
      sys.exit (1)
    files.sort (key=os.path.getmtime, reverse=True)
    return files

def main ():
    parser = argparse.ArgumentParser (description=&quot;批量修正 OpenWebUI 模型 profile_image_url&quot;)
    parser.add_argument (&quot;json_file&quot;, nargs=&quot;?&quot;, help=&quot;模型配置 JSON 文件路径(可选,默认自动查找最新)&quot;)
    parser.add_argument (&quot;-o&quot;, &quot;--output&quot;, help=&quot;输出文件名(可选,默认覆盖原文件)&quot;)
    parser.add_argument (&quot;--dry-run&quot;, action=&quot;store_true&quot;, help=&quot;只预览不写入文件&quot;)
    args = parser.parse_args ()

    json_file = args.json_file or get_latest_json_file ()
    with open (json_file, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
      data = json.load (f)

    updated, skipped, not_found = 0, 0, 0
    update_logs = []
    skip_logs = []
    notfound_logs = []

    for model in data:
      model_id = model.get (&quot;id&quot;, &quot;&quot;)
      meta = model.get (&quot;meta&quot;, {})
      if not meta:
            continue
      current_url = meta.get (&quot;profile_image_url&quot;, &quot;&quot;)
      correct_url = find_correct_url (model_id)
      if correct_url:
            if current_url == correct_url:
                skipped += 1
                skip_logs.append (f&quot; {model_id} 已是正确 URL: {current_url}&quot;)
            elif current_url in DEFAULT_URLS or not current_url:
                update_logs.append (f&quot; {model_id}\n原 URL: {current_url}\n新 URL: {correct_url}&quot;)
                meta [&quot;profile_image_url&quot;] = correct_url
                updated += 1
            else:
                skipped += 1
                skip_logs.append (f&quot; {model_id} URL 已存在且与映射不符: {current_url}&quot;)
      else:
            not_found += 1
            notfound_logs.append (f&quot; {model_id} 未匹配到任何提供商关键词,当前 URL: {current_url}&quot;)

    print (f&quot;处理完成:共 {len (data)} 条,更新 {updated} 条,跳过 {skipped} 条,未匹配到提供商 {not_found} 条。&quot;)
    print (&quot;=&quot;*40)
    if update_logs:
      print (&quot;更新记录:&quot;)
      for log in update_logs:
            print (log)
    if skip_logs:
      print (&quot;\n 跳过记录:&quot;)
      for log in skip_logs:
            print (log)
    if notfound_logs:
      print (&quot;\n 未匹配到提供商的模型:&quot;)
      for log in notfound_logs:
            print (log)
    print (&quot;=&quot;*40)

    if not args.dry_run:
      output_file = args.output or json_file
      with open (output_file, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:
            json.dump (data, f, ensure_ascii=False, indent=2)
      print (f&quot;已写入:{output_file}&quot;)
    else:
      print (&quot;dry-run 预览模式,未写入文件。&quot;)

if __name__ == &quot;__main__&quot;:
    main ()
</code></pre>
<p><strong>脚本运行结果</strong>:<br />
<img src="data/attachment/forum/202506/13/152749zhbb3hbobzq3pphh.jpeg" alt="4480d8b5e1cbbbeafa6c0a52a5eb0acb97ef8ae2.jpeg" title="4480d8b5e1cbbbeafa6c0a52a5eb0acb97ef8ae2.jpeg" /></p>

halczy 发表于 2025-6-23 12:00:03

优化思路挺不错的

作别西天 发表于 2025-7-11 11:00:04

这优化挺有一手~

艾哥 发表于 2025-7-14 15:00:03

这优化挺有想法

fjord 发表于 2025-8-18 16:00:03

这优化挺有一手

cirock 发表于 2025-8-20 09:30:03

这优化挺有一套~

侧面 发表于 2025-8-23 21:00:03

优化思路挺不错的
页: [1]
查看完整版本: OpenWebUI优化: models 图像批量替换成 CDN 地址脚本分享