diff --git a/.gitignore b/.gitignore index d9390ee..d1172cb 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,9 @@ tts.db tts.db-* tts.db.* installed.lock + +# UI handoff bundle (for external AI redesign work; not deployed) +ui-handoff/ + +# Docs (本地草稿,不入库) +docs/ diff --git a/router/admin.html b/router/admin.html index 21d73ca..f7f19c3 100644 --- a/router/admin.html +++ b/router/admin.html @@ -8,360 +8,806 @@
+ + + +
+ {{ t.type === 'ok' ? '✓' : t.type === 'err' ? '!' : '⚠' }} + {{ t.msg }} + +
+
+ -
-

火山 TTS · 管理后台

-
输入 OPENAI_TTS_API_KEY 登录
- - -
{{ loginErr }}
+ - -
-
-
- + +
+
- -
-
仪表盘
-
音色
-
设置
-
- - -
-
-
-
模式
-
{{ overview.mode }}
-
-
-
音色数
-
{{ overview.voice_count }}
-
{{ overview.voice_enabled_count }} 启用
-
-
-
运行时间
-
{{ formatUptime(overview.uptime_seconds) }}
-
-
-
数据库
-
{{ shortPath(overview.db_path) || '—' }}
-
-
-
-
Commit
-
{{ overview.commit || 'dev' }}
-
-
- - -
-
-
共 {{ voices.length }} 条
- -
-
- - - - - - - - - - - - - - - - - - - - - -
名称Speaker资源 IDModel启用操作
{{ v.name }}{{ v.speaker }}{{ v.resource_id }}{{ v.model || '—' }} - - - -
-
还没有音色。点击右上角"新增音色"开始添加。
-
-
- - -
-
-
鉴权 Key (登录 + 客户端调用)
-
客户端用这个 Key 调 /v1/audio/speech,也是登录 /admin 的密码。改完下一次请求立即生效(改完会被当前会话自踢,需重新登录)。
-
- -
- - +
- -
-
火山 TTS 上游凭证
-
调火山 v3 上游用的凭证。改完点保存,立即生效,无需重启服务。
- -
- -
- - + + + + -
- -
路由与格式
-
- - -
-
- - -
未传 voice 时使用。必须是"音色" tab 里存在的 name
-
-
- - -
-
- - -
-
- - -
-
{{ settingsErr }}
-
✓ 已保存,运行时已 reload
-
- - +
+
+ ⚠ +
+
跨域 CORS 未配置
+
同源可用,跨域请求会被 403。在「设置」中配置允许的来源。
+
-
-
跨域 CORS
-
- 控制哪些前端域名能跨域调 /v1/audio/speech。同源始终放行。 +
+
+

{{ pageTitle }}

+

{{ pageSub }}

-
- -
勾上后下面白名单失效。仅测试用
+
+
-
- - -
当前: {{ corsCurrentLabel }}
-
-
{{ corsErr }}
-
✓ 已保存,跨域请求从下个请求开始按新配置
-
- -
-
-
+ - -
-
- ⚠ -
-
跨域 CORS 未配置
-
同源可用,跨域会 403。设置 → 跨域 CORS 配。
+ +
+
+
+
+ 运行模式 + + + {{ overview.installed ? '已就绪' : '待配置' }} + +
+
{{ overview.mode || '—' }}
+
+
+
音色
+
{{ overview.voice_count ?? 0 }}条
+
+ {{ overview.voice_enabled_count ?? 0 }} 启用 · + {{ (overview.voice_count ?? 0) - (overview.voice_enabled_count ?? 0) }} 停用 +
+
+
+
运行时间
+
{{ formatUptime(overview.uptime_seconds) }}
+
{{ formatStart(overview.start_time) }}
+
+
+
数据库
+
{{ shortPath(overview.db_path) || '—' }}
+
{{ overview.db_path || '' }}
+
- -
-
-
⚠ {{ actionErr }}
+
+
+
+
系统信息
+
运行版本与资源
+
+
+
+
Commit
{{ overview.commit || 'dev' }}
+
版本
{{ overview.version || '—' }}
+
运行模式
{{ overview.mode || '—' }}
+
数据库路径
{{ overview.db_path || '—' }}
+
锁文件
{{ overview.lock_path || '—' }}
+
+
内存
+
+ goroutines {{ overview.memory?.goroutines ?? '—' }} · heap {{ formatBytes(overview.memory?.heap_alloc) }} +
+
+
+
+ + + +
+
+
+ +
+ + + +
+
+
+ {{ filteredVoices.length }} / {{ voices.length }} + 共 {{ voices.length }} 条 +
+
+ +
+ ⚠ +
+
操作失败
+
{{ actionErr }}
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
名称Speaker资源 IDModel状态操作
+
{{ v.name }}
+
{{ v.description }}
+
{{ v.speaker }}{{ v.resource_id }}{{ v.model || '—' }} + + + {{ v.enabled ? '启用' : '停用' }} + + + + +
+ +
+
♪
+
还没有音色
+
点击右上角「+ 新增音色」开始添加第一个火山 TTS 音色
+ +
+ +
+
⌕
+
没有匹配的音色
+
尝试调整搜索词或筛选条件
+ +
+
+
+ + +
+
+
+
+
+
鉴权 Key
+
客户端调用 /v1/audio/speech 与登录 /admin 共用
+
+ 敏感 +
+
+ +
{{ settings.auth_key || '(未设置)' }}
+
+
+ +
+ + +
+
改了之后,你的浏览器 401 自动跳回登录页,用新 Key 重登。
+
+
+ +
+
+
+
火山 TTS 上游凭证
+
调火山 v3 上游用的凭证与默认路由
+
+
+
+ +
{{ settings.api_key || '(未设置)' }}
+
+
+ +
+ + +
+
独立端点 PUT /api/settings/api-key,与其他字段分开传输
+
+
+
+
+ + +
+
+ + +
必须是「音色」tab 里存在的 name
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+
+
+
跨域 CORS
+
控制哪些前端域名能跨域调 /v1/audio/speech
+
+ + + {{ settings.cors_configured ? '已配置' : '未配置' }} + +
+
+ +
勾上后下面白名单失效。仅测试用
+
+
+ + +
一行一个。当前: {{ corsCurrentLabel }}
+
+
+ +
+
+
+
+
- - diff --git a/router/setup.html b/router/setup.html index 957873b..b5354e2 100644 --- a/router/setup.html +++ b/router/setup.html @@ -8,96 +8,197 @@
+
-
+

火山 TTS 聚合平台 · 首次安装

填写以下信息完成初始化。token 来自启动日志或环境变量 TTS_ADMIN_KEY。
- + +
+
+
1
+
上游凭证
+
+
+
+
2
+
默认路由
+
+
+
+
3
+
音色列表
+
+
+
+
4
+
完成安装
+
+
-
-
OpenAI 鉴权 + 火山引擎 TTS 凭证
-
从控制台/面板获取。两个 Key 仅在此页面输入,不会回显,装完不用再碰 env。
+
+ ⚠ +
{{ errorMsg }}
+
+ + +
+
+
+
OpenAI 鉴权 + 火山引擎凭证
+
两个 Key 仅在此页面输入,不会回显,装完不用再碰 env。
+
+ 第 1 步 +
@@ -106,7 +207,18 @@
-
调火山 v3 上游用的凭证,从 火山控制台 获取。
+
调火山 v3 上游用的凭证,从 火山控制台 获取。
+
+
+ + +
+
+
+
默认路由与格式
+
未传 voice/format 时使用。装完可在 /admin → 设置 改。
+
+ 第 2 步
@@ -144,11 +256,24 @@
-
-
音色列表 (至少 1 条)
-
填入火山 TTS 音色 ID(S_ 开头的为复刻音色)。可后续在 /admin 中增删。
+ +
+
+
+
音色列表 (至少 1 条)
+
填入火山 TTS 音色 ID(S_ 开头的为复刻音色)。可后续在 /admin 中增删。
+
+ 第 3 步 +
+
+
对外名 (voice)
+
Speaker
+
资源 ID
+
Model
+
+
- + @@ -157,13 +282,53 @@
-
-
- 提交后立即进入正常模式;后续管理请访问 /admin。 + +
+
+
+
确认安装
+
检查以下配置,点击「完成安装」提交。提交后立即进入正常模式。
+
+ 第 4 步 +
+
+
+
OpenAI Key
+
{{ form.auth_key ? mask(form.auth_key) : '(未填)' }}
+
+
+
火山 API Key
+
{{ form.api_key ? mask(form.api_key) : '(未填)' }}
+
+
+
默认资源 ID
+
{{ form.default_resource_id || '—' }}
+
+
+
默认音色
+
{{ form.default_speaker || '—' }}
+
+
+
输出格式 / 采样率
+
{{ form.default_format }} · {{ form.sample_rate }} Hz
+
+
+
音色数
+
{{ form.voices.length }} 条 ({{ form.voices.filter(v => v.name).length }} 已填名)
+
+
+
+ + +
+ +
提交后立即进入正常模式;后续管理请访问 /admin。
+
+ +
-
@@ -171,6 +336,7 @@ const { createApp, ref, onMounted } = Vue; createApp({ setup() { + const step = ref(1); const form = ref({ auth_key: '', api_key: '', @@ -190,9 +356,34 @@ const removeVoice = (i) => { if (form.value.voices.length > 1) form.value.voices.splice(i, 1); }; + const next = () => { + errorMsg.value = ''; + if (step.value === 1) { + if (!form.value.auth_key || !form.value.api_key) { + errorMsg.value = '请填写 OpenAI 鉴权 Key 和火山 API Key'; + return; + } + } else if (step.value === 2) { + if (!form.value.default_resource_id || !form.value.default_speaker || !form.value.token) { + errorMsg.value = '请填写默认资源 ID、默认音色和初始化 Token'; + return; + } + } else if (step.value === 3) { + if (!form.value.voices.length || form.value.voices.some(v => !v.name || !v.speaker || !v.resource_id)) { + errorMsg.value = '音色列表不完整(每条都需要 name + speaker + resource_id)'; + return; + } + } + step.value = Math.min(4, step.value + 1); + }; + const prev = () => { errorMsg.value = ''; step.value = Math.max(1, step.value - 1); }; + const mask = (s) => { + if (!s) return ''; + if (s.length <= 8) return '****'; + return s.slice(0, 4) + '****' + s.slice(-4); + }; onMounted(async () => { - // 预填旧 env 变量(只填非空字段;api_key 永不预填,需手动输入) try { const r = await axios.get('/api/setup/prefill'); const s = r.data.settings || {}; @@ -207,14 +398,6 @@ const submit = async () => { errorMsg.value = ''; - if (!form.value.auth_key || !form.value.api_key || !form.value.default_resource_id || !form.value.default_speaker || !form.value.token) { - errorMsg.value = '请填写所有必填项(OpenAI Key / 火山 API Key / 资源 ID / 默认音色 / Token)'; - return; - } - if (!form.value.voices.length || form.value.voices.some(v => !v.name || !v.speaker || !v.resource_id)) { - errorMsg.value = '音色列表不完整(每条都需要 name + speaker + resource_id)'; - return; - } submitting.value = true; try { const r = await axios.post('/api/setup', { @@ -229,16 +412,15 @@ }, voices: form.value.voices, }); - // 成功:跳 /admin(M2 才会有,先跳 /dashboard) - window.location.href = r.data.redirect || '/dashboard'; + window.location.href = r.data.redirect || '/admin'; } catch (e) { const msg = e.response?.data?.error?.message || e.message; - errorMsg.value = `安装失败: ${msg}`; + errorMsg.value = '安装失败: ' + msg; submitting.value = false; } }; - return { form, errorMsg, submitting, addVoice, removeVoice, submit }; + return { step, form, errorMsg, submitting, addVoice, removeVoice, next, prev, mask, submit }; }, }).mount('#app'); diff --git a/store/voices.go b/store/voices.go index 586851e..6a25d02 100644 --- a/store/voices.go +++ b/store/voices.go @@ -10,17 +10,21 @@ import ( ) // Voice 是一行音色记录;时间字段保持 ISO8601 字符串(SQLite TEXT 默认)。 +// +// JSON tag 是为了前端(admin.html)能直接读取 — 之前没加 tag 时 Go 的 +// "Name" / "Speaker" 等大写字段会原样输出,前端用 v.name 拿到 undefined, +// 整张表看起来"空"但其实有数据。补 tag 后前端能正常显示。 type Voice struct { - ID int64 - Name string - Speaker string - ResourceID string - Model string - Language string - Description string - Enabled bool - CreatedAt string - UpdatedAt string + ID int64 `json:"id"` + Name string `json:"name"` + Speaker string `json:"speaker"` + ResourceID string `json:"resource_id"` + Model string `json:"model"` + Language string `json:"language"` + Description string `json:"description"` + Enabled bool `json:"enabled"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } // ErrDuplicate 表示 name 唯一冲突;controller 翻译为 409。