fix(auth): OpenAI 端 key 也走 DB,setup 是单一配置入口
修 M2/M3 遗漏:OPENAI_TTS_API_KEY (admin + 合成鉴权) 此前只能从 env 读,setup 前端没收集。装完用户还要回去设 env,体验断,违反 'setup 是单一配置源' 承诺。 改动: - setting/config.go: LoadRuntimeConfig 顺便把 auth_key 灌到 setting.Auth.APIKeys。优先级 DB > env(env 仅作 fallback,首次启 动无 DB 时仍可用,向后兼容)。 - controller/setup.go: validateSetupSettings 加 auth_key 为必填, 写 DB 后立即 setting.Auth.APIKeys = [auth_key](本进程内立刻生效)。 - controller/settings.go: GET /api/settings 返 auth_key (打码) + auth_key_set;新增 SettingsAuthKeyHandler (PUT /api/settings/auth-key), 改完单独刷新 setting.Auth.APIKeys(避免被自踢,只下次请求生效)。 - router/router.go: 挂 /api/settings/auth-key 路由。 - router/setup.html: 加 OpenAI 鉴权 Key 字段(密码框,带提示); 表单 default + submit 都加 auth_key。 - router/admin.html: 设置 tab 拆两张卡 — 鉴权 Key(独立保存按钮, 改完会自踢当前会话)+ 火山 TTS 凭证;vue setup 加 authKeyInput + saveAuthKey 函数。 - main.go: 把 LoadRuntimeConfig(st) 提到 LogStartupSummary 之前, 让启动日志反映真实运行时状态(DB 已加载的 key 数量)而不是 过时的 env 状态。 未 push(等用户当前 M3 测完一起或单独)
This commit is contained in:
@@ -17,6 +17,8 @@ import (
|
|||||||
type SettingsResponse struct {
|
type SettingsResponse struct {
|
||||||
APIKey string `json:"api_key"` // 打码形式,例如 S_G8****naJ1
|
APIKey string `json:"api_key"` // 打码形式,例如 S_G8****naJ1
|
||||||
APIKeySet bool `json:"api_key_set"` // 是否已设置(用于前端判断要不要提示必填)
|
APIKeySet bool `json:"api_key_set"` // 是否已设置(用于前端判断要不要提示必填)
|
||||||
|
AuthKey string `json:"auth_key"` // 鉴权 key 打码(客户端访问 + admin 登录用)
|
||||||
|
AuthKeySet bool `json:"auth_key_set"`
|
||||||
DefaultResourceID string `json:"default_resource_id"`
|
DefaultResourceID string `json:"default_resource_id"`
|
||||||
DefaultSpeaker string `json:"default_speaker"`
|
DefaultSpeaker string `json:"default_speaker"`
|
||||||
DefaultFormat string `json:"default_format"`
|
DefaultFormat string `json:"default_format"`
|
||||||
@@ -50,6 +52,8 @@ func SettingsGetHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
resp := SettingsResponse{
|
resp := SettingsResponse{
|
||||||
APIKey: maskAPIKeyField(all["api_key"]),
|
APIKey: maskAPIKeyField(all["api_key"]),
|
||||||
APIKeySet: all["api_key"] != "",
|
APIKeySet: all["api_key"] != "",
|
||||||
|
AuthKey: maskAPIKeyField(all["auth_key"]),
|
||||||
|
AuthKeySet: all["auth_key"] != "",
|
||||||
DefaultResourceID: all["default_resource_id"],
|
DefaultResourceID: all["default_resource_id"],
|
||||||
DefaultSpeaker: all["default_speaker"],
|
DefaultSpeaker: all["default_speaker"],
|
||||||
DefaultFormat: all["default_format"],
|
DefaultFormat: all["default_format"],
|
||||||
@@ -243,6 +247,50 @@ func SettingsAPIKeyHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
|
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SettingsAuthKeyRequest 是 PUT /api/settings/auth-key 的 body。
|
||||||
|
// auth_key 是 admin 鉴权和 /v1/audio/speech 鉴权用的 key(火山上游 key 是 api_key,这是两套)。
|
||||||
|
type SettingsAuthKeyRequest struct {
|
||||||
|
AuthKey string `json:"auth_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SettingsAuthKeyHandler PUT /api/settings/auth-key
|
||||||
|
// 鉴权: RequireAdmin。改完立即更新 setting.Auth.APIKeys(进程内生效),
|
||||||
|
// 下一个请求就用新 key — admin 自己改完要等下一次请求才能验证(避免改完立刻自踢)。
|
||||||
|
func SettingsAuthKeyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPut {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s := GetAdminStore()
|
||||||
|
if s == nil {
|
||||||
|
middleware.SendJSONError(w, http.StatusServiceUnavailable, "database not ready", "configuration_error", "db_not_ready")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, 1<<10)
|
||||||
|
var body SettingsAuthKeyRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
middleware.SendJSONError(w, http.StatusBadRequest, "invalid JSON body", "invalid_request_error", "bad_request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key := trimAll(body.AuthKey)
|
||||||
|
if key == "" {
|
||||||
|
middleware.SendJSONError(w, http.StatusBadRequest, "auth_key cannot be empty", "invalid_request_error", "missing_field")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.SettingsSet("auth_key", key); err != nil {
|
||||||
|
log.Printf("[settings] auth-key set: %v", err)
|
||||||
|
middleware.SendJSONError(w, http.StatusInternalServerError, "write auth_key failed", "server_error", "db_write_failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 立即生效:不重新 LoadRuntimeConfig(那会覆盖其它字段),
|
||||||
|
// 只单独刷新 Auth.APIKeys
|
||||||
|
setting.Auth.APIKeys = []string{key}
|
||||||
|
log.Printf("[settings] auth_key updated, runtime active (next request uses new key)")
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
// maskAPIKeyField 复用 setting 包的打码风格(前 4 后 4 中间 ****)。
|
// maskAPIKeyField 复用 setting 包的打码风格(前 4 后 4 中间 ****)。
|
||||||
// 单独导出版本避免从 setting 包拉整个 APIKeyMask 之类的工具(那个是 unexported)。
|
// 单独导出版本避免从 setting 包拉整个 APIKeyMask 之类的工具(那个是 unexported)。
|
||||||
func maskAPIKeyField(s string) string {
|
func maskAPIKeyField(s string) string {
|
||||||
|
|||||||
+10
-1
@@ -169,6 +169,13 @@ func SetupSubmitHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 立即把 auth_key 灌到 setting.Auth.APIKeys,这样后续 /v1/audio/speech 和 /admin
|
||||||
|
// 在本进程内能立刻用新 key(无需等 LoadRuntimeConfig)。
|
||||||
|
authKey := strings.TrimSpace(body.Settings["auth_key"])
|
||||||
|
if authKey != "" {
|
||||||
|
setting.Auth.APIKeys = []string{authKey}
|
||||||
|
}
|
||||||
|
|
||||||
// 清空旧 voices 再插入(假设是首次安装;若不是,name 冲突会变成 409)
|
// 清空旧 voices 再插入(假设是首次安装;若不是,name 冲突会变成 409)
|
||||||
// 这里选择 "清空+插入" 语义,符合"setup 是首次安装"的产品定位
|
// 这里选择 "清空+插入" 语义,符合"setup 是首次安装"的产品定位
|
||||||
// 如果想保留旧 voices,可以改成 UPSERT,但 M1 不做
|
// 如果想保留旧 voices,可以改成 UPSERT,但 M1 不做
|
||||||
@@ -224,8 +231,10 @@ func SetupSubmitHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validateSetupSettings 校验必填项。
|
// validateSetupSettings 校验必填项。
|
||||||
|
// auth_key (鉴权) 也是必填 — 让 setup 成为"单一配置入口",
|
||||||
|
// 用户装完不用再回去设 OPENAI_TTS_API_KEY env。
|
||||||
func validateSetupSettings(m map[string]string) error {
|
func validateSetupSettings(m map[string]string) error {
|
||||||
required := []string{"api_key", "default_resource_id", "default_speaker"}
|
required := []string{"api_key", "auth_key", "default_resource_id", "default_speaker"}
|
||||||
var missing []string
|
var missing []string
|
||||||
for _, k := range required {
|
for _, k := range required {
|
||||||
if strings.TrimSpace(m[k]) == "" {
|
if strings.TrimSpace(m[k]) == "" {
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ func main() {
|
|||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
log.SetPrefix("[TTS-Server] ")
|
log.SetPrefix("[TTS-Server] ")
|
||||||
|
|
||||||
// 1) 加载引导环境变量(PORT / OPENAI_TTS_API_KEY / TTS_ADMIN_KEY 等)
|
// 1) 加载引导环境变量(PORT / TTS_ADMIN_KEY / OPENAI_TTS_API_KEY 兜底)
|
||||||
|
// 注: Auth.APIKeys 在 InitAllConfigs 里先读 env,后被 LoadRuntimeConfig 覆盖为 DB 值。
|
||||||
setting.InitAllConfigs()
|
setting.InitAllConfigs()
|
||||||
metrics.Init()
|
metrics.Init()
|
||||||
middleware.InitRateLimiter()
|
middleware.InitRateLimiter()
|
||||||
setting.LogStartupSummary()
|
|
||||||
|
|
||||||
// 2) 启动期关键步骤:打开/建库 → 检测 lock → 判定模式
|
// 2) 启动期关键步骤:打开/建库 → 检测 lock → 判定模式
|
||||||
dbPath := ttsDBPath()
|
dbPath := ttsDBPath()
|
||||||
@@ -47,23 +47,28 @@ func main() {
|
|||||||
if res.Corrupted {
|
if res.Corrupted {
|
||||||
log.Printf("[main] 注意: 启动时检测到 db 损坏并已自愈回退(备份=%s)", res.BackupTo)
|
log.Printf("[main] 注意: 启动时检测到 db 损坏并已自愈回退(备份=%s)", res.BackupTo)
|
||||||
}
|
}
|
||||||
// 注入 setup 控制器需要的 store + dbPath(无论哪种模式都注入,正常模式下备用)
|
|
||||||
|
// 3) 注入 setup/admin 控制器需要的句柄(M1+M2)
|
||||||
controller.SetSetupState(st, dbPath)
|
controller.SetSetupState(st, dbPath)
|
||||||
// 注入 admin 控制器需要的 store + 指标文本写入器(M2)
|
|
||||||
controller.SetAdminStore(st)
|
controller.SetAdminStore(st)
|
||||||
controller.SetMetricsTextWriter(func(w http.ResponseWriter) error {
|
controller.SetMetricsTextWriter(func(w http.ResponseWriter) error {
|
||||||
metrics.Meter.Handler().ServeHTTP(w, &http.Request{})
|
metrics.Meter.Handler().ServeHTTP(w, &http.Request{})
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
// M3: 从 store 加载运行时 TTS 配置(替代原来的 env-based InitTTSConfig)
|
|
||||||
|
// 4) M3: 从 store 加载运行时 TTS 配置(替代原来的 env-based InitTTSConfig)
|
||||||
|
// 必须在 LogStartupSummary 之前,这样日志显示的是真实状态(API key 已从 DB 加载,不再读 env)
|
||||||
if st != nil {
|
if st != nil {
|
||||||
if err := setting.LoadRuntimeConfig(st); err != nil {
|
if err := setting.LoadRuntimeConfig(st); err != nil {
|
||||||
log.Printf("[main] TTS 运行时配置加载失败:%v(将保持 install mode 或返 503)", err)
|
log.Printf("[main] TTS 运行时配置加载失败:%v", err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[main] TTS 运行时配置已加载(api_key=***, speaker=%s, resource=%s, format=%s)",
|
log.Printf("[main] TTS 运行时配置已加载(api_key=***, speaker=%s, resource=%s, format=%s)",
|
||||||
setting.TTSOptions.Speaker, setting.TTSOptions.ResourceID, setting.TTSOptions.Format)
|
setting.TTSOptions.Speaker, setting.TTSOptions.ResourceID, setting.TTSOptions.Format)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5) 启动摘要日志(此时 Auth.APIKeys 已是 DB 值,日志反映真实状态)
|
||||||
|
setting.LogStartupSummary()
|
||||||
log.Printf("[main] 当前模式: %s (db=%s lock=%s)", res.Mode, dbPath, res.LockPath)
|
log.Printf("[main] 当前模式: %s (db=%s lock=%s)", res.Mode, dbPath, res.LockPath)
|
||||||
|
|
||||||
controller.InitController()
|
controller.InitController()
|
||||||
|
|||||||
+35
-4
@@ -228,8 +228,21 @@
|
|||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<div v-if="tab==='settings'">
|
<div v-if="tab==='settings'">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">火山 TTS 凭证</div>
|
<div class="card-title">鉴权 Key (登录 + 客户端调用)</div>
|
||||||
<div class="card-desc" style="color: var(--text-dim); font-size: 12px; margin-bottom: 16px;">改完点保存,立即生效,无需重启服务。</div>
|
<div class="card-desc" style="color: var(--text-dim); font-size: 12px; margin-bottom: 16px;">客户端用这个 Key 调 <code>/v1/audio/speech</code>,也是登录 <code>/admin</code> 的密码。改完下一次请求立即生效(改完会被当前会话自踢,需重新登录)。</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>当前 Key (打码: <span style="color: var(--accent); font-family: monospace;">{{ settings.auth_key || '(未设置)' }}</span>)</label>
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<input type="password" v-model="authKeyInput" :placeholder="settings.auth_key_set ? '不改留空' : '输入新 Key'" style="flex: 1;">
|
||||||
|
<button class="btn btn-primary" @click="saveAuthKey" :disabled="!authKeyInput">更新 Key</button>
|
||||||
|
</div>
|
||||||
|
<div class="hint">独立端点 /api/settings/auth-key。改了之后,你的浏览器 401 自动跳回登录页,用新 Key 重登。</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">火山 TTS 上游凭证</div>
|
||||||
|
<div class="card-desc" style="color: var(--text-dim); font-size: 12px; margin-bottom: 16px;">调火山 v3 上游用的凭证。改完点保存,立即生效,无需重启服务。</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>API Key (打码显示: <span style="color: var(--accent); font-family: monospace;">{{ settings.api_key || '(未设置)' }}</span>)</label>
|
<label>API Key (打码显示: <span style="color: var(--accent); font-family: monospace;">{{ settings.api_key || '(未设置)' }}</span>)</label>
|
||||||
@@ -331,6 +344,7 @@
|
|||||||
const settings = ref({});
|
const settings = ref({});
|
||||||
const settingsForm = ref({ default_resource_id: '', default_speaker: '', default_format: 'mp3', sample_rate: 24000, model: '' });
|
const settingsForm = ref({ default_resource_id: '', default_speaker: '', default_format: 'mp3', sample_rate: 24000, model: '' });
|
||||||
const apiKeyInput = ref('');
|
const apiKeyInput = ref('');
|
||||||
|
const authKeyInput = ref('');
|
||||||
const savingSettings = ref(false);
|
const savingSettings = ref(false);
|
||||||
const settingsErr = ref('');
|
const settingsErr = ref('');
|
||||||
const settingsOk = ref(false);
|
const settingsOk = ref(false);
|
||||||
@@ -422,6 +436,23 @@
|
|||||||
settingsErr.value = e.response?.data?.error?.message || e.message;
|
settingsErr.value = e.response?.data?.error?.message || e.message;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const saveAuthKey = async () => {
|
||||||
|
settingsErr.value = ''; settingsOk.value = false;
|
||||||
|
if (!authKeyInput.value) { settingsErr.value = 'Auth Key 不能为空'; return; }
|
||||||
|
try {
|
||||||
|
await http.put('/settings/auth-key', { auth_key: authKeyInput.value });
|
||||||
|
authKeyInput.value = '';
|
||||||
|
// 改完自己会 401,清掉 session key 让用户重新登录
|
||||||
|
setTimeout(() => {
|
||||||
|
sessionStorage.removeItem('ttsAdminKey');
|
||||||
|
apiKey.value = '';
|
||||||
|
}, 200);
|
||||||
|
settingsOk.value = true;
|
||||||
|
setTimeout(() => settingsOk.value = false, 3000);
|
||||||
|
} catch (e) {
|
||||||
|
settingsErr.value = e.response?.data?.error?.message || e.message;
|
||||||
|
}
|
||||||
|
};
|
||||||
const resetSettingsForm = () => { loadSettings(); };
|
const resetSettingsForm = () => { loadSettings(); };
|
||||||
|
|
||||||
const openAdd = () => {
|
const openAdd = () => {
|
||||||
@@ -466,8 +497,8 @@
|
|||||||
|
|
||||||
return { apiKey, keyInput, loginErr, login, logout, tab, overview, voices, actionErr,
|
return { apiKey, keyInput, loginErr, login, logout, tab, overview, voices, actionErr,
|
||||||
showAdd, form, addErr, adding, openAdd, submitAdd, toggle, remove,
|
showAdd, form, addErr, adding, openAdd, submitAdd, toggle, remove,
|
||||||
settings, settingsForm, apiKeyInput, savingSettings, settingsErr, settingsOk,
|
settings, settingsForm, apiKeyInput, authKeyInput, savingSettings, settingsErr, settingsOk,
|
||||||
loadSettings, saveSettings, saveApiKey, resetSettingsForm,
|
loadSettings, saveSettings, saveApiKey, saveAuthKey, resetSettingsForm,
|
||||||
formatUptime, shortPath, reloadAll };
|
formatUptime, shortPath, reloadAll };
|
||||||
},
|
},
|
||||||
}).mount('#app');
|
}).mount('#app');
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ func Setup() *mux.Router {
|
|||||||
r.Handle("/api/settings", middleware.RequireAdmin(http.HandlerFunc(controller.SettingsGetHandler))).Methods("GET")
|
r.Handle("/api/settings", middleware.RequireAdmin(http.HandlerFunc(controller.SettingsGetHandler))).Methods("GET")
|
||||||
r.Handle("/api/settings", middleware.RequireAdmin(http.HandlerFunc(controller.SettingsUpdateHandler))).Methods("PUT")
|
r.Handle("/api/settings", middleware.RequireAdmin(http.HandlerFunc(controller.SettingsUpdateHandler))).Methods("PUT")
|
||||||
r.Handle("/api/settings/api-key", middleware.RequireAdmin(http.HandlerFunc(controller.SettingsAPIKeyHandler))).Methods("PUT")
|
r.Handle("/api/settings/api-key", middleware.RequireAdmin(http.HandlerFunc(controller.SettingsAPIKeyHandler))).Methods("PUT")
|
||||||
|
r.Handle("/api/settings/auth-key", middleware.RequireAdmin(http.HandlerFunc(controller.SettingsAuthKeyHandler))).Methods("PUT")
|
||||||
|
|
||||||
// 业务路由
|
// 业务路由
|
||||||
r.HandleFunc("/v1/audio/speech", controller.OpenaiTTSHandler).Methods("POST", "OPTIONS")
|
r.HandleFunc("/v1/audio/speech", controller.OpenaiTTSHandler).Methods("POST", "OPTIONS")
|
||||||
|
|||||||
+13
-5
@@ -96,11 +96,17 @@
|
|||||||
<div v-if="errorMsg" class="banner">⚠ {{ errorMsg }}</div>
|
<div v-if="errorMsg" class="banner">⚠ {{ errorMsg }}</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">火山引擎 TTS 凭证</div>
|
<div class="card-title">OpenAI 鉴权 + 火山引擎 TTS 凭证</div>
|
||||||
<div class="card-desc">从火山引擎控制台获取,API Key 仅在此页面输入,不会回显。</div>
|
<div class="card-desc">从控制台/面板获取。两个 Key 仅在此页面输入,不会回显,装完不用再碰 env。</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>API Key <span class="req">*</span></label>
|
<label>OpenAI 鉴权 Key <span class="req">*</span></label>
|
||||||
|
<input type="password" v-model="form.auth_key" placeholder="自定,例如 my-secret-key-2024" autocomplete="off">
|
||||||
|
<div class="hint">客户端用这个 Key 调 <code>/v1/audio/speech</code>,也是登录 <code>/admin</code> 的密码。装完可在 <code>/admin → 设置</code> 改。</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>火山 TTS API Key <span class="req">*</span></label>
|
||||||
<input type="password" v-model="form.api_key" placeholder="例如: 5b4d7c2a-..." autocomplete="off">
|
<input type="password" v-model="form.api_key" placeholder="例如: 5b4d7c2a-..." autocomplete="off">
|
||||||
|
<div class="hint">调火山 v3 上游用的凭证,从 <a href="https://console.volcengine.com/" target="_blank">火山控制台</a> 获取。</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field-row">
|
<div class="field-row">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -166,6 +172,7 @@
|
|||||||
createApp({
|
createApp({
|
||||||
setup() {
|
setup() {
|
||||||
const form = ref({
|
const form = ref({
|
||||||
|
auth_key: '',
|
||||||
api_key: '',
|
api_key: '',
|
||||||
default_resource_id: 'volc.megatts.default',
|
default_resource_id: 'volc.megatts.default',
|
||||||
default_speaker: '',
|
default_speaker: '',
|
||||||
@@ -200,8 +207,8 @@
|
|||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
errorMsg.value = '';
|
errorMsg.value = '';
|
||||||
if (!form.value.api_key || !form.value.default_resource_id || !form.value.default_speaker || !form.value.token) {
|
if (!form.value.auth_key || !form.value.api_key || !form.value.default_resource_id || !form.value.default_speaker || !form.value.token) {
|
||||||
errorMsg.value = '请填写所有必填项(API Key / 资源 ID / 默认音色 / Token)';
|
errorMsg.value = '请填写所有必填项(OpenAI Key / 火山 API Key / 资源 ID / 默认音色 / Token)';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!form.value.voices.length || form.value.voices.some(v => !v.name || !v.speaker || !v.resource_id)) {
|
if (!form.value.voices.length || form.value.voices.some(v => !v.name || !v.speaker || !v.resource_id)) {
|
||||||
@@ -213,6 +220,7 @@
|
|||||||
const r = await axios.post('/api/setup', {
|
const r = await axios.post('/api/setup', {
|
||||||
token: form.value.token,
|
token: form.value.token,
|
||||||
settings: {
|
settings: {
|
||||||
|
auth_key: form.value.auth_key,
|
||||||
api_key: form.value.api_key,
|
api_key: form.value.api_key,
|
||||||
default_resource_id: form.value.default_resource_id,
|
default_resource_id: form.value.default_resource_id,
|
||||||
default_speaker: form.value.default_speaker,
|
default_speaker: form.value.default_speaker,
|
||||||
|
|||||||
+19
-1
@@ -127,7 +127,7 @@ func normalizeOrigin(origin string) string {
|
|||||||
return strings.ToLower(origin)
|
return strings.ToLower(origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadRuntimeConfig 从 store 加载 TTS 全局配置到 TTSOptions / TTSTimeout 内存。
|
// LoadRuntimeConfig 从 store 加载 TTS 全局配置到 TTSOptions / TTSTimeout / Auth.APIKeys 内存。
|
||||||
// 启动期(master 模式)调一次,或 PUT /api/settings 后调一次(改完立即生效)。
|
// 启动期(master 模式)调一次,或 PUT /api/settings 后调一次(改完立即生效)。
|
||||||
//
|
//
|
||||||
// 与原 InitTTSConfig 的区别:
|
// 与原 InitTTSConfig 的区别:
|
||||||
@@ -148,6 +148,11 @@ func normalizeOrigin(origin string) string {
|
|||||||
// BYTEDANCE_TTS_EXPLICIT_LANGUAGE → explicit_language
|
// BYTEDANCE_TTS_EXPLICIT_LANGUAGE → explicit_language
|
||||||
// BYTEDANCE_TTS_ENABLE_SUBTITLE → enable_subtitle (默认 false)
|
// BYTEDANCE_TTS_ENABLE_SUBTITLE → enable_subtitle (默认 false)
|
||||||
// BYTEDANCE_TTS_TIMEOUT → TTSTimeout (默认 30s)
|
// BYTEDANCE_TTS_TIMEOUT → TTSTimeout (默认 30s)
|
||||||
|
//
|
||||||
|
// 鉴权 key(auth_key)优先级:DB > env OPENAI_TTS_API_KEY
|
||||||
|
// - 首次启动(无 DB 数据):用 env,保证向后兼容
|
||||||
|
// - 已 install:用 DB,env 不再读
|
||||||
|
// - DB 没 auth_key 但 env 有(env fallback):仍用 env
|
||||||
func LoadRuntimeConfig(s Store) error {
|
func LoadRuntimeConfig(s Store) error {
|
||||||
all, err := s.SettingsGetAll()
|
all, err := s.SettingsGetAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -217,6 +222,19 @@ func LoadRuntimeConfig(s Store) error {
|
|||||||
EnableSubtitle: enableSubtitle,
|
EnableSubtitle: enableSubtitle,
|
||||||
Additions: adds,
|
Additions: adds,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 鉴权 key:DB > env(向后兼容)
|
||||||
|
authKey := all["auth_key"]
|
||||||
|
if authKey == "" {
|
||||||
|
authKey = os.Getenv("OPENAI_TTS_API_KEY")
|
||||||
|
}
|
||||||
|
// 用临时 slice 避免和 InitAuthConfig 抢同一个 Auth.APIKeys 底层
|
||||||
|
if authKey != "" {
|
||||||
|
Auth.APIKeys = []string{authKey}
|
||||||
|
} else {
|
||||||
|
Auth.APIKeys = nil
|
||||||
|
}
|
||||||
|
|
||||||
TTSConfigErr = nil
|
TTSConfigErr = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user