Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
27 KiB
JSON
1 line
27 KiB
JSON
{"repo": "b31o8321/dzmm", "files": {"frontend/src/views/ModelsView.vue": {"df38037b6e3510d2e035e20600f49bd1f48ff077": {"t": 1786068462.6600518, "n": 24217, "h": "b9e429cfc3ee2521", "c": "<script setup lang=\"ts\">\nimport { computed, onMounted, reactive, ref } from 'vue'\nimport { ElMessage, ElMessageBox } from 'element-plus'\nimport { useModelConfigsStore } from '@/stores/modelConfigs'\nimport { backendOrigin } from '@/api/client'\nimport type { ModelConfig, ModelConfigIn } from '@/api/types'\n\nconst store = useModelConfigsStore()\nconst recsOpen = ref(['recs'])\nconst dialogOpen = ref(false)\nconst editingId = ref<number | null>(null)\nconst submitting = ref(false)\nconst testing = ref<number | null>(null)\nconst removing = ref<number | null>(null)\nconst settingDefault = ref<number | null>(null)\n\nconst form = reactive<ModelConfigIn>({\n name: '',\n type: 'ollama',\n base_url: 'http://localhost:11434',\n model_name: '',\n api_key: '',\n timeout: 60,\n max_concurrent: 0,\n})\n\nfunction resetForm() {\n Object.assign(form, {\n name: '',\n type: 'ollama',\n base_url: 'http://localhost:11434',\n model_name: '',\n api_key: '',\n timeout: 60,\n max_concurrent: 0,\n })\n}\n\nconst TYPE_DEFAULTS: Record<string, { base_url: string; model_placeholder: string }> = {\n ollama: { base_url: 'http://localhost:11434', model_placeholder: '例如:qwen2.5:7b' },\n lm_studio: { base_url: 'http://localhost:1234/v1', model_placeholder: '例如:Qwen2.5-7B-Instruct(LM Studio 中加载的模型 id)' },\n openai_compat: { base_url: 'https://api.openai.com/v1', model_placeholder: '例如:gpt-4o-mini' },\n}\n\nconst modelPlaceholder = computed(\n () => TYPE_DEFAULTS[form.type]?.model_placeholder ?? '',\n)\n\nfunction onTypeChange(t: string) {\n // Auto-fill base_url with the type's default — but only if user hasn't\n // typed a custom URL or is leaving an empty form.\n const defaults = TYPE_DEFAULTS[t]\n if (!defaults) return\n const isDefaultUrl = Object.values(TYPE_DEFAULTS).some((d) => d.base_url === form.base_url)\n if (!form.base_url || isDefaultUrl) {\n form.base_url = defaults.base_url\n }\n // Clear api_key when switching to a local type (not used).\n if (t !== 'openai_compat') {\n form.api_key = ''\n }\n}\n\nfunction openCreate() {\n editingId.value = null\n resetForm()\n dialogOpen.value = true\n}\n\nfunction openEdit(row: ModelConfig) {\n editingId.value = row.id\n Object.assign(form, {\n name: row.name,\n type: row.type,\n base_url: row.base_url,\n model_name: row.model_name,\n api_key: '',\n timeout: row.timeout,\n max_concurrent: row.max_concurrent ?? 0,\n })\n dialogOpen.value = true\n}\n\nasync function onSubmit() {\n submitting.value = true\n try {\n const payload: ModelConfigIn = { ...form }\n if (!payload.api_key) delete (payload as any).api_key\n if (editingId.value === null) {\n await store.create(payload)\n ElMessage.success('已添加')\n } else {\n await store.update(editingId.value, payload)\n ElMessage.success('已更新')\n }\n dialogOpen.value = false\n resetForm()\n editingId.value = null\n } catch (e: any) {\n ElMessage.error(e.message)\n } finally {\n submitting.value = false\n }\n}\n\nasync function onDelete(row: ModelConfig) {\n try {\n await ElMessageBox.confirm(\n `确认删除模型配置 \"${row.name}\"?`,\n '确认',\n { type: 'warning' },\n )\n } catch {\n return\n }\n removing.value = row.id\n try {\n await store.remove(row.id)\n ElMessage.success('已删除')\n } catch (e: any) {\n ElMessage.error(e.message)\n } finally {\n removing.value = null\n }\n}\n\nasync function onTest(id: number) {\n testing.value = id\n try {\n const r = await store.test(id)\n if (r.ok) {\n ElMessageBox.alert(r.info, '连接成功', { type: 'success' })\n } else {\n ElMessageBox.alert(r.info, '连接失败', { type: 'error' })\n }\n } catch (e: any) {\n ElMessage.error(e.message)\n } finally {\n testing.value = null\n }\n}\n\nasync function onSetDefault(row: ModelConfig) {\n if (row.is_default) return\n settingDefault.value = row.id\n try {\n await store.setDefault(row.id)\n ElMessage.success(`已把 \"${row.name}\" 设为默认模型`)\n } catch (e: any) {\n ElMessage.error(e.message)\n } finally {\n settingDefault.value = null\n }\n}\n\nfunction copyText(text: string) {\n navigator.clipboard.writeText(text).then(() => ElMessage.success('已复制'))\n}\n\nconst gmLocalModels = [\n { id: 'qwen2.5:14b', vram: '~10 GB', note: '中文叙事明显优于 7b,格式遵循稳定,推荐首选' },\n { id: 'qwen2.5:32b', vram: '~20 GB', note: '本地中文 RP 最佳,指令遵循和叙事质量均衡' },\n { id: 'mistral-nemo:12b', vram: '~8 GB', note: '创意写作强,多语言,指令遵循好' },\n { id: 'gemma3:12b', vram: '~8 GB', note: 'Google 最新,格式遵循稳定,创意性好' },\n { id: 'llama3.1:8b', vram: '~6 GB', note: '效果中等,VRAM 有限时的最低可用选项' },\n]\n\nconst gmCloudModels = [\n { id: 'deepseek-chat', baseUrl: 'https://api.deepseek.com/v1', note: '即 DeepSeek-V3,中文 RP 性价比最高,注意勿用 deepseek-reasoner(r1)' },\n { id: 'gpt-4o-mini', baseUrl: 'https://api.openai.com/v1', note: '稳定可靠,格式遵循极好,综合性价比高' },\n { id: 'gpt-4o', baseUrl: 'https://api.openai.com/v1', note: '叙事质量最强,成本较高' },\n { id: 'gemini-2.0-flash', baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai', note: '速度快,中文可用,需 Google API Key' },\n { id: 'claude-haiku-4-5-20251001', baseUrl: '(需 OpenAI 兼容转发层)', note: 'Roleplay 细腻,直接调用建议用官方 SDK 而非此处' },\n]\n\n// TTS external services with install/uninstall scripts (uv-based, no Docker required)\ninterface TtsExternalService {\n id: string\n name: string\n tier: number\n platform: string\n quality: string\n baseUrl: string\n defaultVoice: string\n note: string\n installScript: string\n startScript: string\n uninstallScript: string\n healthPath: string\n}\n\nconst ttsExternalServices: TtsExternalService[] = [\n {\n id: 'fish_speech',\n name: 'Fish-Speech',\n tier: 3,\n platform: '跨平台(macOS / Linux / Windows)',\n quality: '★★★★★',\n baseUrl: 'http://localhost:8080',\n defaultVoice: 'default',\n note: '最佳中文音质,支持零样本声音克隆。CPU 可运行(慢),有 GPU 速度快很多。',\n installScript: `#!/bin/bash\n# Fish-Speech 安装脚本(需要 uv:curl -LsSf https://astral.sh/uv/install.sh | sh)\nset -e\nVENV=\"$HOME/.dzmm/fish_speech_env\"\necho \"[1/2] 创建 Python 3.10 环境...\"\nuv venv \"$VENV\" --python 3.10\necho \"[2/2] 安装 Fish-Speech...\"\nuv pip install --python \"$VENV/bin/python\" fish-speech\necho \"\"\necho \"✅ 安装完成!运行以下命令启动服务:\"\necho \" bash ~/.dzmm/start_fish_speech.sh\"`,\n startScript: `#!/bin/bash\n# Fish-Speech 启动脚本\nVENV=\"$HOME/.dzmm/fish_speech_env\"\necho \"正在启动 Fish-Speech(端口 8080)...\"\n\"$VENV/bin/python\" -m fish_speech.api --listen 0.0.0.0:8080`,\n uninstallScript: `#!/bin/bash\n# Fish-Speech 卸载脚本\nrm -rf \"$HOME/.dzmm/fish_speech_env\"\nrm -f \"$HOME/.dzmm/start_fish_speech.sh\"\necho \"✅ Fish-Speech 已卸载\"`,\n healthPath: '/health',\n },\n {\n id: 'mlx_qwen3_tts',\n name: 'MLX-Qwen3-TTS',\n tier: 3,\n platform: 'Apple Silicon(M1/M2/M3/M4)',\n quality: '★★★★★',\n baseUrl: 'http://localhost:8000',\n defaultVoice: 'default',\n note: '阿里 Qwen3-TTS,M 系芯片原生 MLX 加速,0.2–0.3s/句,中文自然度极高。仅限 Apple Silicon Mac。',\n installScript: `#!/bin/bash\n# MLX-Qwen3-TTS 安装脚本(需要 uv + Apple Silicon Mac)\nset -e\nVENV=\"$HOME/.dzmm/mlx_tts_env\"\necho \"[1/2] 创建 Python 3.11 环境...\"\nuv venv \"$VENV\" --python 3.11\necho \"[2/2] 安装 MLX-Qwen3-TTS...\"\nuv pip install --python \"$VENV/bin/python\" mlx-lm mlx-audio\nuv pip install --python \"$VENV/bin/python\" fastapi uvicorn huggingface_hub\n# 下载启动脚本\ncat > \"$HOME/.dzmm/mlx_tts_server.py\" << 'EOF'\nimport asyncio, io, os\nfrom fastapi import FastAPI\nfrom fastapi.responses import Response\nfrom pydantic import BaseModel\nimport mlx_audio, soundfile as sf, numpy as np\n\napp = FastAPI()\n_model = None\n\ndef get_model():\n global _model\n if _model is None:\n from mlx_audio.tts.models.qwen3 import Qwen3TTS\n _model = Qwen3TTS.from_pretrained(\"Qwen/Qwen3-TTS-0.6B\")\n return _model\n\nclass Req(BaseModel):\n input: str\n voice: str = \"default\"\n\n@app.post(\"/v1/audio/speech\")\nasync def synth(req: Req):\n model = get_model()\n audio = await asyncio.get_event_loop().run_in_executor(\n None, lambda: model.generate(req.input))\n buf = io.BytesIO()\n sf.write(buf, np.array(audio), 22050, format=\"WAV\")\n buf.seek(0)\n return Response(content=buf.read(), media_type=\"audio/wav\")\n\n@app.get(\"/health\")\ndef health(): return {\"ok\": True}\nEOF\necho \"\"\necho \"✅ 安装完成!运行以下命令启动服务:\"\necho \" $VENV/bin/python $HOME/.dzmm/mlx_tts_server.py\"`,\n startScript: `#!/bin/bash\nVENV=\"$HOME/.dzmm/mlx_tts_env\"\necho \"正在启动 MLX-Qwen3-TTS(端口 8000)...\"\n\"$VENV/bin/python\" \"$HOME/.dzmm/mlx_tts_server.py\"`,\n uninstallScript: `#!/bin/bash\nrm -rf \"$HOME/.dzmm/mlx_tts_env\"\nrm -f \"$HOME/.dzmm/mlx_tts_server.py\"\necho \"✅ MLX-Qwen3-TTS 已卸载\"`,\n healthPath: '/health',\n },\n]\n\nconst ttsServiceStatus = ref<Record<string, 'idle' | 'checking' | 'ok' | 'fail'>>({})\nconst ttsExpandedScript = ref<Record<string, 'install' | 'start' | 'uninstall' | null>>({})\n\nasync function checkTtsService(svc: TtsExternalService) {\n ttsServiceStatus.value[svc.id] = 'checking'\n try {\n const r = await fetch(`${backendOrigin}/tts/probe?url=${encodeURIComponent(svc.baseUrl)}`)\n const data = await r.json()\n ttsServiceStatus.value[svc.id] = data.ok ? 'ok' : 'fail'\n } catch {\n ttsServiceStatus.value[svc.id] = 'fail'\n }\n}\n\nfunction downloadScript(filename: string, content: string) {\n const blob = new Blob([content], { type: 'text/plain' })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = filename\n a.click()\n URL.revokeObjectURL(url)\n}\n\nfunction toggleScript(id: string, type: 'install' | 'start' | 'uninstall') {\n ttsExpandedScript.value[id] = ttsExpandedScript.value[id] === type ? null : type\n}\n\nonMounted(() => store.refresh())\n</script>\n\n<template>\n <div class=\"p-6\">\n <div class=\"flex items-center justify-between mb-4\">\n <h2 class=\"text-2xl font-bold\">模型配置</h2>\n <el-button type=\"primary\" @click=\"openCreate\">+ 新增</el-button>\n </div>\n\n <el-collapse v-model=\"recsOpen\" class=\"mb-4 border border-slate-200 rounded-lg overflow-hidden bg-white\">\n <el-collapse-item name=\"recs\">\n <template #title>\n <span class=\"font-semibold text-slate-700 pl-1\">按用途推荐模型</span>\n </template>\n <div class=\"px-4 pb-4\">\n <el-tabs>\n\n <!-- GM 引擎 -->\n <el-tab-pane label=\"GM 叙事引擎\" name=\"gm\">\n <div class=\"flex items-center gap-2 mb-3\">\n <el-tag type=\"danger\" size=\"small\">避免推理模型</el-tag>\n <el-tooltip content=\"deepseek-r1、QwQ、qwen3 思考模式等推理模型会用思考过程占满输出,导致 XML 状态标签全部丢失,游戏无法正常运行。\" placement=\"right\" :width=\"300\">\n <span class=\"text-xs text-slate-500 cursor-help\">deepseek-r1 / QwQ / qwen3 思考模式 — 会破坏输出格式 ?</span>\n </el-tooltip>\n </div>\n\n <p class=\"text-xs font-semibold text-slate-600 mb-2\">本地模型(Ollama)</p>\n <table class=\"w-full text-sm mb-4\">\n <thead>\n <tr class=\"border-b border-slate-200 text-xs text-slate-500\">\n <th class=\"text-left py-1 pr-4 font-medium\">模型</th>\n <th class=\"text-left py-1 pr-4 font-medium\">显存需求</th>\n <th class=\"text-left py-1 font-medium\">适用场景 / 说明</th>\n </tr>\n </thead>\n <tbody class=\"text-slate-700\">\n <tr v-for=\"m in gmLocalModels\" :key=\"m.id\" class=\"border-b border-slate-100 last:border-0\">\n <td class=\"py-1.5 pr-4\">\n <code class=\"bg-slate-100 text-slate-800 px-1.5 py-0.5 rounded text-xs\">{{ m.id }}</code>\n <el-button\n link size=\"small\"\n class=\"ml-1 text-xs text-slate-400\"\n @click=\"copyText(`ollama pull ${m.id}`)\"\n >复制拉取命令</el-button>\n </td>\n <td class=\"py-1.5 pr-4 text-xs text-slate-500 whitespace-nowrap\">{{ m.vram }}</td>\n <td class=\"py-1.5 text-xs text-slate-600\">{{ m.note }}</td>\n </tr>\n </tbody>\n </table>\n\n <p class=\"text-xs font-semibold text-slate-600 mb-2\">云端模型(OpenAI 兼容接口)</p>\n <table class=\"w-full text-sm\">\n <thead>\n <tr class=\"border-b border-slate-200 text-xs text-slate-500\">\n <th class=\"text-left py-1 pr-4 font-medium\">模型 ID</th>\n <th class=\"text-left py-1 pr-4 font-medium\">Base URL</th>\n <th class=\"text-left py-1 font-medium\">说明</th>\n </tr>\n </thead>\n <tbody class=\"text-slate-700\">\n <tr v-for=\"m in gmCloudModels\" :key=\"m.id\" class=\"border-b border-slate-100 last:border-0\">\n <td class=\"py-1.5 pr-4\">\n <code class=\"bg-slate-100 text-slate-800 px-1.5 py-0.5 rounded text-xs\">{{ m.id }}</code>\n <el-button link size=\"small\" class=\"ml-1 text-xs text-slate-400\" @click=\"copyText(m.id)\">复制</el-button>\n </td>\n <td class=\"py-1.5 pr-4 text-xs text-slate-500\">\n <code class=\"text-xs\">{{ m.baseUrl }}</code>\n </td>\n <td class=\"py-1.5 text-xs text-slate-600\">{{ m.note }}</td>\n </tr>\n </tbody>\n </table>\n </el-tab-pane>\n\n <!-- TTS -->\n <el-tab-pane label=\"TTS 语音合成\" name=\"tts\">\n <p class=\"text-xs text-slate-500 mb-4\">在<strong>「设置」→「语音朗读」</strong>切换模式。按性能需求选择合适方案。</p>\n\n <!-- Tier 1: 开箱即用 -->\n <div class=\"mb-3\">\n <div class=\"flex items-center gap-2 mb-2\">\n <el-tag type=\"success\" size=\"small\">开箱即用</el-tag>\n <span class=\"text-xs text-slate-500\">无需额外安装</span>\n </div>\n <div class=\"border border-slate-200 rounded-lg p-3 bg-slate-50 flex items-center justify-between\">\n <div>\n <span class=\"text-sm font-medium\">edge-tts</span>\n <span class=\"text-xs text-slate-500 ml-2\">微软在线语音 · 中文音色丰富 · 需联网</span>\n </div>\n <el-tag type=\"info\" size=\"small\">内置默认</el-tag>\n </div>\n </div>\n\n <!-- Tier 2: 需要一定性能 -->\n <div class=\"mb-3\">\n <div class=\"flex items-center gap-2 mb-2\">\n <el-tag type=\"warning\" size=\"small\">需要一定性能</el-tag>\n <span class=\"text-xs text-slate-500\">本地运行,离线可用,效果更好</span>\n </div>\n <div class=\"border border-slate-200 rounded-lg p-3 bg-slate-50 flex items-center justify-between\">\n <div>\n <span class=\"text-sm font-medium\">CosyVoice</span>\n <span class=\"text-xs text-slate-500 ml-2\">阿里 300M 模型 · 7 种中文音色 · CPU 可运行</span>\n </div>\n <el-button size=\"small\" @click=\"$router.push('/settings')\">前往设置安装</el-button>\n </div>\n </div>\n\n <!-- Tier 3: 需要很好的性能 -->\n <div>\n <div class=\"flex items-center gap-2 mb-2\">\n <el-tag type=\"danger\" size=\"small\">需要较好性能</el-tag>\n <span class=\"text-xs text-slate-500\">音质最佳,需要独立启动外部服务</span>\n </div>\n <div class=\"space-y-3\">\n <div\n v-for=\"svc in ttsExternalServices\"\n :key=\"svc.id\"\n class=\"border border-slate-200 rounded-lg p-3 bg-white\"\n >\n <!-- Header row -->\n <div class=\"flex items-start justify-between gap-2 mb-2\">\n <div class=\"flex-1\">\n <div class=\"flex items-center gap-2 flex-wrap\">\n <span class=\"text-sm font-medium\">{{ svc.name }}</span>\n <span class=\"text-xs text-slate-400\">{{ svc.quality }}</span>\n <span class=\"text-xs text-slate-500\">{{ svc.platform }}</span>\n </div>\n <p class=\"text-xs text-slate-500 mt-0.5\">{{ svc.note }}</p>\n </div>\n <!-- Status + check -->\n <div class=\"flex items-center gap-2 shrink-0\">\n <el-tag\n v-if=\"ttsServiceStatus[svc.id] === 'ok'\" type=\"success\" size=\"small\"\n >已连接</el-tag>\n <el-tag\n v-else-if=\"ttsServiceStatus[svc.id] === 'fail'\" type=\"danger\" size=\"small\"\n >未运行</el-tag>\n <el-button\n size=\"small\"\n :loading=\"ttsServiceStatus[svc.id] === 'checking'\"\n @click=\"checkTtsService(svc)\"\n >检查连接</el-button>\n </div>\n </div>\n\n <!-- Base URL row -->\n <div class=\"flex items-center gap-2 mb-2\">\n <span class=\"text-xs text-slate-500 shrink-0\">Base URL:</span>\n <code class=\"bg-slate-100 text-slate-800 px-1.5 py-0.5 rounded text-xs flex-1\">{{ svc.baseUrl }}</code>\n <el-button type=\"button\" link size=\"small\" class=\"text-xs text-slate-400 shrink-0\" @click=\"copyText(svc.baseUrl)\">复制</el-button>\n </div>\n\n <!-- Script buttons -->\n <div class=\"flex items-center gap-2 flex-wrap\">\n <el-button\n size=\"small\"\n :type=\"ttsExpandedScript[svc.id] === 'install' ? 'primary' : 'default'\"\n plain\n @click=\"toggleScript(svc.id, 'install')\"\n >安装脚本</el-button>\n <el-button\n size=\"small\"\n :type=\"ttsExpandedScript[svc.id] === 'start' ? 'primary' : 'default'\"\n plain\n @click=\"toggleScript(svc.id, 'start')\"\n >启动脚本</el-button>\n <el-button\n size=\"small\"\n :type=\"ttsExpandedScript[svc.id] === 'uninstall' ? 'danger' : 'default'\"\n plain\n @click=\"toggleScript(svc.id, 'uninstall')\"\n >卸载脚本</el-button>\n </div>\n\n <!-- Expanded script panel -->\n <div\n v-if=\"ttsExpandedScript[svc.id]\"\n class=\"mt-2 bg-slate-900 rounded p-3 relative\"\n >\n <div class=\"flex items-center justify-between mb-1\">\n <span class=\"text-xs text-slate-400\">\n {{ ttsExpandedScript[svc.id] === 'install' ? '安装脚本' : ttsExpandedScript[svc.id] === 'start' ? '启动脚本' : '卸载脚本' }}\n </span>\n <div class=\"flex gap-2\">\n <el-button\n type=\"button\"\n link\n size=\"small\"\n class=\"!text-slate-300 text-xs\"\n @click=\"copyText(ttsExpandedScript[svc.id] === 'install' ? svc.installScript : ttsExpandedScript[svc.id] === 'start' ? svc.startScript : svc.uninstallScript)\"\n >复制</el-button>\n <el-button\n type=\"button\"\n link\n size=\"small\"\n class=\"!text-slate-300 text-xs\"\n @click=\"downloadScript(\n `${svc.id}_${ttsExpandedScript[svc.id]}.sh`,\n ttsExpandedScript[svc.id] === 'install' ? svc.installScript : ttsExpandedScript[svc.id] === 'start' ? svc.startScript : svc.uninstallScript\n )\"\n >下载</el-button>\n </div>\n </div>\n <pre class=\"text-xs text-green-300 whitespace-pre-wrap font-mono leading-5 max-h-48 overflow-y-auto\">{{\n ttsExpandedScript[svc.id] === 'install' ? svc.installScript\n : ttsExpandedScript[svc.id] === 'start' ? svc.startScript\n : svc.uninstallScript\n }}</pre>\n </div>\n </div>\n </div>\n </div>\n </el-tab-pane>\n\n </el-tabs>\n </div>\n </el-collapse-item>\n </el-collapse>\n\n <p class=\"text-xs text-slate-500 mb-2\">\n ⭐ <strong>默认模型</strong>:用于\"创建存档向导\"等没有 session 上下文的 LLM 调用。同一时刻只有一条配置能被标为默认。\n </p>\n\n <el-table :data=\"store.items\" v-loading=\"store.loading\" border>\n <el-table-column label=\"默认\" width=\"64\" align=\"center\">\n <template #default=\"{ row }\">\n <span v-if=\"row.is_default\" class=\"text-yellow-500\" title=\"当前默认模型\">★</span>\n <span v-else class=\"text-slate-300\">—</span>\n </template>\n </el-table-column>\n <el-table-column prop=\"name\" label=\"名称\" width=\"160\" />\n <el-table-column prop=\"type\" label=\"类型\" width=\"140\" />\n <el-table-column prop=\"base_url\" label=\"Base URL\" />\n <el-table-column prop=\"model_name\" label=\"模型\" width=\"200\" />\n <el-table-column label=\"API Key\" width=\"100\">\n <template #default=\"{ row }\">\n {{ row.api_key_ref ? '已设置' : '—' }}\n </template>\n </el-table-column>\n <el-table-column label=\"操作\" width=\"320\">\n <template #default=\"{ row }\">\n <el-button\n v-if=\"!row.is_default\"\n size=\"small\"\n :loading=\"settingDefault === row.id\"\n @click=\"onSetDefault(row)\"\n >设为默认</el-button>\n <el-button\n size=\"small\"\n :loading=\"testing === row.id\"\n @click=\"onTest(row.id)\"\n >测试</el-button>\n <el-button size=\"small\" @click=\"openEdit(row)\">编辑</el-button>\n <el-button\n size=\"small\"\n type=\"danger\"\n :loading=\"removing === row.id\"\n @click=\"onDelete(row)\"\n >删除</el-button>\n </template>\n </el-table-column>\n </el-table>\n\n <el-dialog\n v-model=\"dialogOpen\"\n :title=\"editingId === null ? '新增模型配置' : '编辑模型配置'\"\n width=\"520px\"\n >\n <el-form :model=\"form\" label-width=\"100px\">\n <el-form-item label=\"名称\" required>\n <el-input v-model=\"form.name\" placeholder=\"例如:本地 qwen\" />\n </el-form-item>\n <el-form-item label=\"类型\" required>\n <el-select v-model=\"form.type\" @change=\"onTypeChange\">\n <el-option label=\"Ollama 本地\" value=\"ollama\" />\n <el-option label=\"LM Studio 本地\" value=\"lm_studio\" />\n <el-option label=\"OpenAI 兼容(云端)\" value=\"openai_compat\" />\n </el-select>\n </el-form-item>\n <el-form-item label=\"Base URL\" required>\n <el-input v-model=\"form.base_url\" />\n </el-form-item>\n <el-form-item label=\"模型\" required>\n <el-input\n v-model=\"form.model_name\"\n :placeholder=\"modelPlaceholder\"\n />\n </el-form-item>\n <el-form-item label=\"API Key\" v-if=\"form.type === 'openai_compat'\">\n <el-input\n v-model=\"form.api_key\"\n type=\"password\"\n show-password\n :placeholder=\"editingId !== null ? '留空则保留原密钥' : ''\"\n />\n </el-form-item>\n <el-form-item label=\"超时(秒)\">\n <el-input-number v-model=\"form.timeout\" :min=\"5\" :max=\"300\" />\n </el-form-item>\n <el-form-item label=\"并发上限\">\n <el-input-number v-model=\"form.max_concurrent\" :min=\"0\" :max=\"20\" />\n <span class=\"text-xs text-slate-500 ml-2\">\n 0 = 不限。云端免费层(智谱 glm-4-flash 等)必须设 1,否则并发请求会被全部 429。\n </span>\n </el-form-item>\n </el-form>\n <template #footer>\n <el-button @click=\"dialogOpen = false\">取消</el-button>\n <el-button type=\"primary\" :loading=\"submitting\" @click=\"onSubmit\">保存</el-button>\n </template>\n </el-dialog>\n </div>\n</template>\n"}}}} |