Download index.html from ernoban/OpenWebUI-Model-Avatar-URL-Correction-Tool: direct link, hf CLI and curl.
- Browser
- Download file 53.8 kB
-
https://huggingface.co/spaces/ernoban/OpenWebUI-Model-Avatar-URL-Correction-Tool/resolve/main/index.html
- Command line
-
hf download hf://spaces/ernoban/OpenWebUI-Model-Avatar-URL-Correction-Tool/index.html
-
curl -L -o index.html https://huggingface.co/spaces/ernoban/OpenWebUI-Model-Avatar-URL-Correction-Tool/resolve/main/index.html
53.8 kB
| <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> |