ernoban's picture
Update index.html
523b298 verified
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenWebUI 模型头像URL修正工具 | 精致美化版</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* 基础变量(统一风格) */
:root {
--primary-color: #4A6CF7;
--primary-hover: #3a5af5;
--secondary-color: #6c757d;
--success-color: #10b981;
--warning-color: #f59e0b;
--error-color: #ef4444;
--background-color: #f8fafc;
--card-bg: #ffffff;
--text-color: #1e293b;
--light-text: #64748b;
--border-color: #e2e8f0;
--gap: 20px;
--card-padding: 25px;
--button-font-size: 14px;
--border-radius: 12px;
--box-shadow: 0 10px 25px -5px rgba(0,0,0,0.05), 0 10px 10px -5px rgba(0,0,0,0.02);
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
--switch-bg: #e2e8f0;
--switch-active: var(--primary-color);
}
/* 基础重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
line-height: 1.6;
padding: 20px;
max-width: 1200px;
margin: 20px auto;
background-color: var(--background-color);
background-image: linear-gradient(135deg, #f0f4ff 0%, #f8fafc 100%);
color: var(--text-color);
}
/* 顶部标题 */
.header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
}
.header h1 {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 8px;
background: linear-gradient(90deg, var(--primary-color), #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
position: relative;
display: inline-block;
}
.header h1::after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 4px;
background: linear-gradient(90deg, var(--primary-color), #8b5cf6);
border-radius: 2px;
}
.header p {
font-size: 1.1rem;
color: var(--light-text);
max-width: 700px;
margin: 0 auto;
line-height: 1.7;
}
/* 顶部标签栏(主功能切换) */
.main-tabs {
display: flex;
gap: 8px;
margin-bottom: var(--gap);
background: var(--card-bg);
border-radius: var(--border-radius);
padding: 6px;
box-shadow: var(--box-shadow);
}
.main-tab-button {
flex: 1;
border: none;
background: none;
padding: 14px 20px;
font-size: 16px;
font-weight: 600;
color: var(--light-text);
cursor: pointer;
border-radius: 8px;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.main-tab-button:hover {
background: #f1f5f9;
color: var(--primary-color);
}
.main-tab-button.active {
background: var(--primary-color);
color: white;
box-shadow: 0 4px 6px -1px rgba(74, 108, 247, 0.2), 0 2px 4px -1px rgba(74, 108, 247, 0.06);
}
/* 标签内容容器(统一卡片样式) */
.tab-content {
background-color: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
padding: var(--card-padding);
margin-bottom: var(--gap);
border: 1px solid var(--border-color);
transition: var(--transition);
}
.tab-content:hover {
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.08), 0 10px 10px -5px rgba(0,0,0,0.03);
}
.section-title {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
gap: 10px;
color: var(--primary-color);
}
.section-title i {
font-size: 1.2rem;
}
/* 上传与结果标签(主流程) */
.upload-section .input-group {
margin-bottom: 20px;
}
.upload-section label {
font-size: 15px;
font-weight: 500;
color: var(--text-color);
margin-bottom: 10px;
display: block;
}
/* 文件上传区域美化 */
.file-upload-container {
position: relative;
display: flex;
flex-direction: column;
gap: 15px;
}
.file-upload-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 16px 24px;
background: var(--primary-color);
color: white;
border-radius: var(--border-radius);
cursor: pointer;
transition: var(--transition);
border: none;
font-size: 16px;
font-weight: 600;
box-shadow: 0 4px 6px -1px rgba(74, 108, 247, 0.2), 0 2px 4px -1px rgba(74, 108, 247, 0.06);
}
.file-upload-btn:hover {
background: var(--primary-hover);
transform: translateY(-2px);
box-shadow: 0 6px 10px -1px rgba(74, 108, 247, 0.3), 0 4px 6px -1px rgba(74, 108, 247, 0.1);
}
.file-upload-btn i {
font-size: 18px;
}
.file-name {
font-size: 14px;
padding: 12px 15px;
background: #f8fafc;
border-radius: var(--border-radius);
border: 1px dashed var(--border-color);
display: flex;
align-items: center;
gap: 10px;
}
.file-name i {
color: var(--success-color);
}
.file-name.empty {
color: var(--light-text);
}
.file-name.empty i {
color: var(--light-text);
}
/* 开关控件美化 */
.options-container {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 20px;
}
.option-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
background: #f8fafc;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
transition: var(--transition);
}
.option-item:hover {
border-color: var(--primary-color);
background: #eef2ff;
}
.option-label {
font-size: 15px;
font-weight: 500;
color: var(--text-color);
display: flex;
align-items: center;
gap: 12px;
}
.option-label i {
color: var(--primary-color);
font-size: 18px;
}
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 26px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--switch-bg);
transition: var(--transition);
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 3px;
bottom: 3px;
background-color: white;
transition: var(--transition);
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--switch-active);
}
input:checked + .slider:before {
transform: translateX(24px);
}
/* 按钮样式 */
.action-buttons {
display: flex;
gap: var(--gap);
margin-bottom: 20px;
flex-wrap: wrap;
}
.button {
border: none;
border-radius: var(--border-radius);
padding: 14px 24px;
font-size: 15px;
cursor: pointer;
transition: var(--transition);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font-weight: 600;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.05);
}
.button-primary {
background-color: var(--primary-color);
color: white;
}
.button-primary:hover {
background-color: var(--primary-hover);
}
.button-success {
background-color: var(--success-color);
color: white;
}
.button-success:hover {
background-color: #059669;
}
.button-secondary {
background-color: #e2e8f0;
color: var(--text-color);
}
.button-secondary:hover {
background-color: #cbd5e1;
}
.button-danger {
background-color: var(--error-color);
color: white;
}
.button-danger:hover {
background-color: #dc2626;
}
/* 结果区域(日志+输出) */
.result-subtabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
background: #f8fafc;
border-radius: var(--border-radius);
padding: 6px;
}
.result-subtab-button {
flex: 1;
border: none;
background: none;
padding: 12px 20px;
font-size: 14px;
font-weight: 500;
color: var(--light-text);
cursor: pointer;
border-radius: 8px;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.result-subtab-button:hover {
background: #eef2ff;
color: var(--primary-color);
}
.result-subtab-button.active {
background: var(--primary-color);
color: white;
box-shadow: 0 4px 6px -1px rgba(74, 108, 247, 0.2), 0 2px 4px -1px rgba(74, 108, 247, 0.06);
}
.log-container {
max-height: 300px;
overflow-y: auto;
padding: 15px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
margin-bottom: 15px;
background: #f8fafc;
}
.log-summary {
font-size: 15px;
font-weight: 600;
margin-bottom: 15px;
padding: 12px 15px;
border-radius: var(--border-radius);
background: #e0e7ff;
color: var(--primary-color);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
.log-summary span {
background: white;
padding: 5px 12px;
border-radius: 20px;
font-weight: 600;
}
.log-item {
font-size: 14px;
padding: 12px 15px;
margin-bottom: 10px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 10px;
transition: var(--transition);
}
.log-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05), 0 2px 4px -1px rgba(0,0,0,0.02);
}
.log-item i {
font-size: 16px;
}
.log-update {
background-color: #d1fae5;
color: var(--success-color);
border-left: 4px solid var(--success-color);
}
.log-skip {
background-color: #fffbeb;
color: var(--warning-color);
border-left: 4px solid var(--warning-color);
}
.log-notfound {
background-color: #fee2e2;
color: var(--error-color);
border-left: 4px solid var(--error-color);
}
.output-container pre {
font-size: 14px;
padding: 15px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background-color: #f8fafc;
max-height: 300px;
overflow-y: auto;
margin-top: 10px;
line-height: 1.5;
font-family: 'Fira Code', 'Consolas', monospace;
}
/* 规则管理标签(优化后) */
.rules-section .rules-subtabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
background: #f8fafc;
border-radius: var(--border-radius);
padding: 6px;
}
.rules-subtab-button {
flex: 1;
border: none;
background: none;
padding: 12px 20px;
font-size: 14px;
font-weight: 500;
color: var(--light-text);
cursor: pointer;
border-radius: 8px;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.rules-subtab-button:hover {
background: #eef2ff;
color: var(--primary-color);
}
.rules-subtab-button.active {
background: var(--primary-color);
color: white;
box-shadow: 0 4px 6px -1px rgba(74, 108, 247, 0.2), 0 2px 4px -1px rgba(74, 108, 247, 0.06);
}
/* 现有规则列表(合并添加规则) */
.rules-list {
max-height: 300px;
overflow-y: auto;
padding: 15px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
margin-bottom: 20px;
background: #f8fafc;
}
.rules-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color);
}
.rules-list-header h4 {
font-size: 16px;
font-weight: 600;
color: var(--text-color);
}
.rules-count {
background: var(--primary-color);
color: white;
padding: 3px 10px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
}
.rules-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
margin-bottom: 12px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background-color: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.03);
transition: var(--transition);
}
.rules-list-item:hover {
transform: translateX(3px);
border-left: 3px solid var(--primary-color);
}
.rules-list-item .keyword {
font-weight: 600;
font-size: 15px;
color: var(--text-color);
background: #e0e7ff;
padding: 3px 10px;
border-radius: 20px;
display: inline-block;
}
.rules-list-item .url {
font-size: 14px;
color: var(--secondary-color);
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
margin-top: 10px;
padding-left: 5px;
}
.rules-list-item .delete-btn {
padding: 6px 12px;
font-size: 13px;
background-color: var(--error-color);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 5px;
}
.rules-list-item .delete-btn:hover {
background-color: #dc2626;
transform: translateY(-2px);
}
/* 添加规则表单(合并至现有规则下方) */
.add-rule-form {
padding: 20px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
margin-bottom: 20px;
background: white;
box-shadow: var(--box-shadow);
}
.add-rule-form h4 {
font-size: 16px;
margin-bottom: 15px;
color: var(--text-color);
display: flex;
align-items: center;
gap: 10px;
}
.add-rule-form label {
font-size: 14px;
font-weight: 500;
color: var(--text-color);
margin-bottom: 8px;
display: block;
}
.add-rule-form input[type="text"] {
width: 100%;
padding: 12px 15px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
font-size: 14px;
margin-bottom: 15px;
transition: var(--transition);
}
.add-rule-form input[type="text"]:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.2);
}
/* 原始JSON区域(集中规则操作按钮) */
.raw-editor textarea {
width: 100%;
height: 200px;
padding: 15px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
font-size: 14px;
resize: vertical;
margin-bottom: 20px;
font-family: 'Fira Code', 'Consolas', monospace;
line-height: 1.5;
transition: var(--transition);
}
.raw-editor textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.2);
}
.rules-action-buttons {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
/* 状态提示 */
.status {
padding: 15px 20px;
border-radius: var(--border-radius);
font-size: 15px;
margin-bottom: var(--gap);
display: flex;
align-items: center;
gap: 12px;
box-shadow: var(--box-shadow);
position: relative;
overflow: hidden;
}
.status::before {
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 4px;
}
.status.success {
background-color: #d1fae5;
color: var(--success-color);
}
.status.success::before {
background-color: var(--success-color);
}
.status.error {
background-color: #fee2e2;
color: var(--error-color);
}
.status.error::before {
background-color: var(--error-color);
}
.status i {
font-size: 18px;
}
/* 页脚 */
.footer {
text-align: center;
margin-top: 40px;
padding: 20px;
color: var(--light-text);
font-size: 14px;
}
.footer a {
color: var(--primary-color);
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
/* 响应式调整 */
@media (max-width: 768px) {
.main-tabs {
flex-direction: column;
gap: 5px;
}
.result-subtabs {
flex-direction: column;
gap: 5px;
}
.rules-subtabs {
flex-direction: column;
gap: 5px;
}
.header h1 {
font-size: 1.8rem;
}
.header p {
font-size: 1rem;
}
.action-buttons, .rules-action-buttons {
flex-direction: column;
}
.button {
width: 100%;
}
}
</style>
</head>
<body>
<div id="app">
<!-- 顶部标题 -->
<div class="header">
<h1><i class="fas fa-cogs"></i> OpenWebUI 模型头像URL修正工具</h1>
<p>自动检测并修正模型配置文件中的头像URL,支持自定义规则管理,提升OpenWebUI界面美观度</p>
</div>
<!-- 全局状态提示 -->
<div v-if="statusMessage.text" :class="['status', statusMessage.type]">
<i :class="statusMessage.icon"></i>
<span>{{ statusMessage.text }}</span>
</div>
<!-- 顶部主标签栏(切换主功能) -->
<div class="main-tabs">
<button @click="activeMainTab='upload'" :class="['main-tab-button', { active: activeMainTab==='upload' }]">
<i class="fas fa-upload"></i> 上传与结果
</button>
<button @click="activeMainTab='rules'" :class="['main-tab-button', { active: activeMainTab==='rules' }]">
<i class="fas fa-list"></i> 规则管理
</button>
</div>
<!-- 主标签内容(动态切换) -->
<div class="tab-content">
<!-- 1. 上传与结果标签(主流程) -->
<div v-if="activeMainTab==='upload'">
<div class="upload-section">
<h3 class="section-title"><i class="fas fa-file-import"></i> 模型文件处理</h3>
<!-- 文件上传 - 美化版 -->
<div class="input-group">
<label>选择模型JSON文件:</label>
<div class="file-upload-container">
<input type="file" id="jsonFile" @change="handleFileChange" accept=".json" ref="fileInput" style="display: none;">
<button class="file-upload-btn" @click="triggerFileInput">
<i class="fas fa-folder-open"></i> 选择JSON文件
</button>
<div :class="['file-name', fileContent ? '' : 'empty']">
<i :class="fileContent ? 'fas fa-check-circle' : 'fas fa-info-circle'"></i>
{{ fileContent ? originalFileName : '未选择文件' }}
</div>
</div>
</div>
<!-- 功能选项 - 美化版 -->
<div class="options-container">
<div class="option-item">
<div class="option-label">
<i class="fas fa-binoculars"></i>
<span>仅预览模式(不修改文件)</span>
</div>
<label class="switch">
<input type="checkbox" v-model="isDryRun">
<span class="slider"></span>
</label>
</div>
<div class="option-item">
<div class="option-label">
<i class="fas fa-sync-alt"></i>
<span>覆盖已有非默认URL</span>
</div>
<label class="switch">
<input type="checkbox" v-model="overwriteExisting">
<span class="slider"></span>
</label>
</div>
</div>
<!-- 核心操作按钮 -->
<div class="action-buttons">
<button @click="processFile" class="button button-primary" :disabled="!fileContent">
<i class="fas fa-bolt"></i> {{ isDryRun ? '开始预览' : '开始处理' }}
</button>
<button @click="downloadJson" class="button button-success" :disabled="!outputJson">
<i class="fas fa-download"></i> 下载修正文件
</button>
<button @click="resetFile" class="button button-secondary">
<i class="fas fa-redo"></i> 重置
</button>
</div>
</div>
<!-- 结果区域(日志+输出,子标签切换) -->
<div v-if="logs.length > 0 || outputJson">
<h3 class="section-title"><i class="fas fa-chart-bar"></i> 处理结果</h3>
<div class="result-subtabs">
<button @click="activeResultSubtab='logs'" :class="['result-subtab-button', { active: activeResultSubtab==='logs' }]">
<i class="fas fa-list"></i> 处理日志
</button>
<button @click="activeResultSubtab='output'" :class="['result-subtab-button', { active: activeResultSubtab==='output' }]">
<i class="fas fa-code"></i> 输出JSON
</button>
</div>
<!-- 处理日志 -->
<div v-if="activeResultSubtab==='logs'" class="log-container">
<div class="log-summary">
处理结果:共{{ totalModels }}条模型
<span>更新: {{ updatedCount }}</span>
<span>跳过: {{ skippedCount }}</span>
<span>未匹配: {{ notFoundCount }}</span>
</div>
<div v-for="(log, index) in logs" :key="index" :class="['log-item', `log-${log.type}`]">
<i :class="log.icon"></i>
<span>{{ log.message }}</span>
</div>
</div>
<!-- 输出JSON -->
<div v-if="activeResultSubtab==='output' && !isDryRun" class="output-container">
<pre>{{ outputJson }}</pre>
</div>
</div>
</div>
<!-- 2. 规则管理标签(优化后) -->
<div v-if="activeMainTab==='rules'">
<div class="rules-section">
<h3 class="section-title"><i class="fas fa-sliders-h"></i> 规则管理</h3>
<!-- 规则管理子标签 -->
<div class="rules-subtabs">
<button @click="activeRulesSubtab='list'" :class="['rules-subtab-button', { active: activeRulesSubtab==='list' }]">
<i class="fas fa-th-list"></i> 现有规则
</button>
<button @click="activeRulesSubtab='raw'" :class="['rules-subtab-button', { active: activeRulesSubtab==='raw' }]">
<i class="fas fa-code"></i> 原始JSON
</button>
</div>
<!-- 现有规则列表(合并添加规则) -->
<div v-if="activeRulesSubtab==='list'">
<div class="rules-list">
<div class="rules-list-header">
<h4>当前规则列表</h4>
<div class="rules-count">{{ providerMap.length }} 条规则</div>
</div>
<div v-if="providerMap.length === 0" class="empty-rules">
<p style="font-size:14px; color:var(--light-text); text-align:center; padding:20px;">
<i class="fas fa-inbox" style="font-size:24px; margin-bottom:10px;"></i><br>
暂无规则,请添加新规则
</p>
</div>
<div v-for="(rule, index) in providerMap" :key="index" class="rules-list-item">
<div>
<span class="keyword">{{ rule[0] }}</span>
<a :href="rule[1]" target="_blank" class="url">{{ rule[1] }}</a>
</div>
<button @click="deleteRule(index)" class="delete-btn">
<i class="fas fa-trash-alt"></i> 删除
</button>
</div>
</div>
<!-- 添加规则表单 -->
<div class="add-rule-form">
<h4><i class="fas fa-plus-circle"></i> 添加新规则</h4>
<label>
匹配关键词(如"gpt"):
<input type="text" v-model="newKeyword" placeholder="请输入关键词" required>
</label>
<label>
头像URL(完整链接):
<input type="text" v-model="newUrl" placeholder="请输入图片URL" required>
</label>
<button @click="addRule" class="button button-primary" :disabled="!newKeyword || !newUrl">
<i class="fas fa-save"></i> 添加规则
</button>
</div>
</div>
<!-- 原始JSON区域(集中规则操作) -->
<div v-if="activeRulesSubtab==='raw'">
<div class="raw-editor">
<textarea v-model="rulesText" placeholder="规则格式:[['关键词1','URL1'],['关键词2','URL2']]"></textarea>
</div>
<div class="rules-action-buttons">
<button @click="saveRules" class="button button-primary">
<i class="fas fa-save"></i> 保存规则
</button>
<button @click="importRules" class="button button-secondary">
<i class="fas fa-file-import"></i> 导入规则
</button>
<button @click="exportRules" class="button button-secondary">
<i class="fas fa-file-export"></i> 导出规则
</button>
<button @click="resetRules" class="button button-danger">
<i class="fas fa-undo"></i> 重置为默认
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 页脚 -->
<div class="footer">
<p>图像 CDN 服务由 lobechat 提供 再次感谢 lobechat 提供的图像 CDN 服务</p>
</div>
</div>
<script>
const { createApp, ref, reactive, watch, onMounted } = Vue;
// 默认规则(可自定义)
const DEFAULT_RULES = [
["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.svg"],
["gemini_pipe_new.", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini.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.svg"],
["tts-", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dalle.svg"],
["dall-e", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dalle.svg"],
["claude", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/claude.svg"],
["gemini", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini.svg"],
["ernie", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/wenxin.svg"],
["baidu", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/wenxin.svg"],
["command", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/cohere.svg"],
["cohere", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/cohere.svg"],
["deepseek", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek.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.svg"],
["meta", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/meta.svg"],
["groq", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/groq.svg"],
["qwq", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen.svg"],
["qvq", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen.svg"],
["qwen", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen.svg"],
["abab", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax.svg"],
["minimax", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax.svg"],
["mistral", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral.svg"],
["kimi", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi.svg"],
["moonshot", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi.svg"],
["ollama", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama.svg"],
["hunyuan", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/hunyuan.svg"],
["tencent", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/hunyuan.svg"],
["yi", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/yi.svg"],
["glm", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qingyan.svg"],
["zhipu", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qingyan.svg"],
["open-mixtral","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral.svg"],
["ministral","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral.svg"],
["codestral","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral.svg"],
["pixtral","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral.svg"],
["doubao","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/doubao.svg"]
];
// 本地存储键
const STORAGE_KEY = "openwebui_avatar_rules";
createApp({
setup() {
// 1. 标签切换状态
const activeMainTab = ref("upload"); // 默认显示"上传与结果"
const activeResultSubtab = ref("logs"); // 结果子标签(默认日志)
const activeRulesSubtab = ref("list"); // 规则子标签(默认现有规则)
// 2. 核心功能状态
const fileContent = ref(null);
const originalFileName = ref("");
const isDryRun = ref(false);
const overwriteExisting = ref(false);
const providerMap = ref([]);
const rulesText = ref("");
const newKeyword = ref("");
const newUrl = ref("");
const logs = ref([]);
const outputJson = ref("");
const error = ref("");
const statusMessage = reactive({ text: "", type: "success", icon: "" });
const fileInput = ref(null);
// 3. 统计数据
const totalModels = ref(0);
const updatedCount = ref(0);
const skippedCount = ref(0);
const notFoundCount = ref(0);
// 4. 初始化加载规则
onMounted(() => {
loadRules();
});
// 5. 监听规则变化,同步原始JSON
watch(providerMap, (newRules) => {
rulesText.value = JSON.stringify(newRules, null, 2);
}, { deep: true });
// 6. 规则管理函数
const loadRules = () => {
try {
const stored = localStorage.getItem(STORAGE_KEY);
providerMap.value = stored ? JSON.parse(stored) : [...DEFAULT_RULES];
showStatus("规则加载成功", "success", "fas fa-check-circle");
} catch (e) {
console.error("加载规则失败:", e);
providerMap.value = [...DEFAULT_RULES];
showStatus("加载规则失败,使用默认规则", "error", "fas fa-exclamation-circle");
}
};
const saveRules = () => {
try {
if (activeRulesSubtab.value === "raw") {
const parsed = JSON.parse(rulesText.value);
if (!Array.isArray(parsed) || !parsed.every(item => Array.isArray(item) && item.length === 2)) {
throw new Error("规则格式错误,请检查JSON结构");
}
providerMap.value = parsed;
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(providerMap.value));
showStatus("规则保存成功", "success", "fas fa-check-circle");
} catch (e) {
console.error("保存规则失败:", e);
showStatus(`保存失败:${e.message}`, "error", "fas fa-exclamation-circle");
}
};
const importRules = () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const rules = JSON.parse(event.target.result);
if (Array.isArray(rules) && rules.every(item => Array.isArray(item) && item.length === 2)) {
providerMap.value = rules;
saveRules();
showStatus("规则导入成功", "success", "fas fa-check-circle");
} else {
throw new Error("导入文件格式错误,需为规则JSON数组");
}
} catch (e) {
console.error("导入失败:", e);
showStatus(`导入失败:${e.message}`, "error", "fas fa-exclamation-circle");
}
};
reader.readAsText(file);
};
input.click();
};
const exportRules = () => {
const blob = new Blob([JSON.stringify(providerMap.value, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "openwebui_avatar_rules.json";
a.click();
URL.revokeObjectURL(url);
showStatus("规则导出成功", "success", "fas fa-check-circle");
};
const resetRules = () => {
if (confirm("确定要重置为默认规则吗?当前规则将丢失")) {
providerMap.value = [...DEFAULT_RULES];
localStorage.removeItem(STORAGE_KEY);
showStatus("规则已重置为默认", "success", "fas fa-check-circle");
}
};
const addRule = () => {
if (!newKeyword.value || !newUrl.value) {
showStatus("关键词和URL不能为空", "error", "fas fa-exclamation-circle");
return;
}
if (!newUrl.value.startsWith('http')) {
showStatus("URL格式无效,必须以http/https开头", "error", "fas fa-exclamation-circle");
return;
}
const exists = providerMap.value.some(rule => rule[0].toLowerCase() === newKeyword.value.toLowerCase());
if (exists) {
showStatus("该关键词已存在,请修改后重试", "error", "fas fa-exclamation-circle");
return;
}
providerMap.value.push([newKeyword.value, newUrl.value]);
newKeyword.value = "";
newUrl.value = "";
saveRules();
showStatus("规则添加成功", "success", "fas fa-check-circle");
};
const deleteRule = (index) => {
if (confirm("确定要删除该规则吗?")) {
providerMap.value.splice(index, 1);
saveRules();
showStatus("规则删除成功", "success", "fas fa-check-circle");
}
};
// 7. 文件处理函数
const triggerFileInput = () => {
fileInput.value.click();
};
const handleFileChange = (e) => {
const file = e.target.files[0];
if (!file) return;
originalFileName.value = file.name;
const reader = new FileReader();
reader.onload = (event) => {
fileContent.value = event.target.result;
error.value = "";
showStatus(`文件 "${file.name}" 加载成功`, "success", "fas fa-check-circle");
};
reader.onerror = () => {
showStatus("文件读取失败,请检查文件完整性", "error", "fas fa-exclamation-circle");
};
reader.readAsText(file);
};
const resetFile = () => {
fileContent.value = null;
originalFileName.value = "";
logs.value = [];
outputJson.value = "";
fileInput.value.value = "";
showStatus("文件已重置", "success", "fas fa-redo");
};
const processFile = () => {
if (!fileContent.value) {
showStatus("请先选择模型JSON文件", "error", "fas fa-exclamation-circle");
return;
}
if (providerMap.value.length === 0) {
showStatus("请先添加或导入规则", "error", "fas fa-exclamation-circle");
return;
}
try {
const data = JSON.parse(fileContent.value);
if (!Array.isArray(data)) {
throw new Error("JSON文件格式错误,顶层应为模型数组");
}
// 初始化统计
totalModels.value = data.length;
updatedCount.value = 0;
skippedCount.value = 0;
notFoundCount.value = 0;
logs.value = [];
// 处理每个模型
const processed = data.map(model => {
const clone = { ...model };
const modelId = clone.id || "";
const currentUrl = clone.meta?.profile_image_url || "";
// 添加base_model_id字段(与meta同级)
if (clone.base_model_id === undefined) {
clone.base_model_id = null;
}
// 查找匹配的URL(按规则顺序匹配)
let newUrl = null;
for (const [keyword, url] of providerMap.value) {
if (modelId.toLowerCase().includes(keyword.toLowerCase())) {
newUrl = url;
break;
}
}
// 处理URL逻辑
if (newUrl) {
if (currentUrl === newUrl) {
skippedCount.value++;
logs.value.push({
type: "skip",
message: `[跳过] ${modelId}:已使用正确URL`,
icon: "fas fa-redo"
});
} else if (currentUrl === "" || currentUrl === "/static/favicon.png" || overwriteExisting.value) {
if (!isDryRun.value) {
clone.meta = clone.meta || {};
clone.meta.profile_image_url = newUrl;
}
updatedCount.value++;
logs.value.push({
type: "update",
message: `[更新] ${modelId}:旧URL→${currentUrl || "空"},新URL→${newUrl}`,
icon: "fas fa-sync-alt"
});
} else {
skippedCount.value++;
logs.value.push({
type: "skip",
message: `[跳过] ${modelId}:已有非默认URL(未勾选覆盖)`,
icon: "fas fa-ban"
});
}
} else {
notFoundCount.value++;
logs.value.push({
type: "notfound",
message: `[未匹配] ${modelId}:无对应规则`,
icon: "fas fa-search"
});
}
return clone;
});
// 生成输出JSON(非预览模式)
if (!isDryRun.value) {
outputJson.value = JSON.stringify(processed, null, 2);
} else {
outputJson.value = "";
}
showStatus(`处理完成:共${totalModels.value}条模型`, "success", "fas fa-check-circle");
} catch (e) {
console.error("处理失败:", e);
showStatus(`处理失败:${e.message}`, "error", "fas fa-exclamation-circle");
}
};
const downloadJson = () => {
if (!outputJson.value) return;
const blob = new Blob([outputJson.value], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${originalFileName.value.replace(".json", "")}_modified.json`;
a.click();
URL.revokeObjectURL(url);
showStatus("文件下载成功", "success", "fas fa-download");
};
// 8. 状态提示函数
const showStatus = (message, type = "success", icon = "fas fa-info-circle") => {
statusMessage.text = message;
statusMessage.type = type;
statusMessage.icon = icon;
setTimeout(() => {
statusMessage.text = "";
}, 4000);
};
// 9. 返回响应式状态与函数
return {
activeMainTab,
activeResultSubtab,
activeRulesSubtab,
fileContent,
originalFileName,
isDryRun,
overwriteExisting,
providerMap,
rulesText,
newKeyword,
newUrl,
logs,
outputJson,
error,
statusMessage,
fileInput,
totalModels,
updatedCount,
skippedCount,
notFoundCount,
loadRules,
saveRules,
importRules,
exportRules,
resetRules,
addRule,
deleteRule,
triggerFileInput,
handleFileChange,
resetFile,
processFile,
downloadJson
};
}
}).mount("#app");
</script>
</body>
</html>