diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/.DS_Store differ diff --git a/sundynix-admin/src/api.ts b/sundynix-admin/src/api.ts index 83b2fbc..c5cffba 100644 --- a/sundynix-admin/src/api.ts +++ b/sundynix-admin/src/api.ts @@ -71,7 +71,7 @@ export async function me(): Promise { } // ---- 模型配置(id 为雪花字符串)---- -export type Kind = "chat" | "embedding"; +export type Kind = "chat" | "voice" | "embedding"; export interface Model { id: string; @@ -232,6 +232,7 @@ export interface WechatMPConfig { appid: string; token: string; // 消息推送签名校验用(与公众平台服务器配置一致) app_secret: string; // 单管理员后台:明文回显 + welcome: string; // 关注后自动回复的欢迎语(空=用默认) enabled: boolean; } @@ -255,17 +256,48 @@ export async function getWechatMP(): Promise { const res = guard(await fetch(`${ADMIN}/wechat-mp`, { headers: authHeaders() })); const d = (await res.json().catch(() => ({}))) as Partial & { error?: string }; if (!res.ok) throw new Error(d.error ?? `wechat-mp failed: ${res.status}`); - return { appid: d.appid ?? "", token: d.token ?? "", app_secret: d.app_secret ?? "", enabled: !!d.enabled }; + return { appid: d.appid ?? "", token: d.token ?? "", app_secret: d.app_secret ?? "", welcome: d.welcome ?? "", enabled: !!d.enabled }; } // app_secret 传空串 = 沿用已保存的。 -export async function saveWechatMP(body: { appid: string; app_secret: string; token: string }): Promise<{ enabled: boolean }> { +export async function saveWechatMP(body: { appid: string; app_secret: string; token: string; welcome: string }): Promise<{ enabled: boolean }> { const res = guard(await fetch(`${ADMIN}/wechat-mp`, { method: "PUT", headers: authHeaders(true), body: JSON.stringify(body) })); const d = (await res.json().catch(() => ({}))) as { enabled?: boolean; error?: string }; if (!res.ok) throw new Error(d.error ?? `save failed: ${res.status}`); return { enabled: !!d.enabled }; } +// —— 语音(火山豆包)配置 · 新版 API Key 鉴权 —— +export interface VoiceConfig { + api_key: string; // 明文回显(单管理员后台) + asr_resource_id: string; + tts_resource_id: string; + tts_voice_type: string; + asr_enabled: boolean; + tts_enabled: boolean; +} + +export async function getVoiceConfig(): Promise { + const res = guard(await fetch(`${ADMIN}/voice`, { headers: authHeaders() })); + const d = (await res.json().catch(() => ({}))) as Partial & { error?: string }; + if (!res.ok) throw new Error(d.error ?? `voice config failed: ${res.status}`); + return { + api_key: d.api_key ?? "", asr_resource_id: d.asr_resource_id ?? "", + tts_resource_id: d.tts_resource_id ?? "", tts_voice_type: d.tts_voice_type ?? "", + asr_enabled: !!d.asr_enabled, tts_enabled: !!d.tts_enabled, + }; +} + +// api_key 传空串 = 沿用已保存的。 +export async function saveVoiceConfig(body: { + api_key: string; asr_resource_id: string; tts_resource_id: string; tts_voice_type: string; +}): Promise<{ asr_enabled: boolean; tts_enabled: boolean }> { + const res = guard(await fetch(`${ADMIN}/voice`, { method: "PUT", headers: authHeaders(true), body: JSON.stringify(body) })); + const d = (await res.json().catch(() => ({}))) as { asr_enabled?: boolean; tts_enabled?: boolean; error?: string }; + if (!res.ok) throw new Error(d.error ?? `save failed: ${res.status}`); + return { asr_enabled: !!d.asr_enabled, tts_enabled: !!d.tts_enabled }; +} + export interface WechatPayConfig { mchid: string; cert_serial: string; @@ -669,11 +701,27 @@ export interface ToolGroup { up: boolean; tools: ToolInfo[] | null; } +export interface NatsStreamHealth { + name: string; + leader: string; + replicas_healthy: number; + replicas_total: number; + messages: number; +} +export interface NatsClusterStatus { + connected: boolean; + connected_to: string; + known_servers: number; + rtt_ms: number; + streams: NatsStreamHealth[] | null; + degraded: number; +} export interface SystemStatus { checked_at: string; infra: StatusItem[]; services: StatusItem[]; tools: ToolGroup[]; + nats?: NatsClusterStatus; } export async function getStatus(): Promise { diff --git a/sundynix-admin/src/components/ModelManager.tsx b/sundynix-admin/src/components/ModelManager.tsx index a0401fe..536461c 100644 --- a/sundynix-admin/src/components/ModelManager.tsx +++ b/sundynix-admin/src/components/ModelManager.tsx @@ -37,10 +37,18 @@ export function ModelManager({ const set = (k: keyof ModelInput, v: string) => setForm((f) => ({ ...f, [k]: v })); + const editing = !!form.id; + + // onEdit 把某行载入表单就地编辑(api_key 留空=沿用现有密钥,后端按 id 更新而非新增)。 + const onEdit = (m: Model) => { + setForm({ id: m.id, kind, provider: m.provider, base_url: m.base_url, api_key: "", model: m.model }); + setMsg(""); + }; + const onSave = async () => { try { await saveModel({ ...form, kind }); - setMsg("✓ 已保存"); + setMsg(editing ? "✓ 已更新并热更新" : "✓ 已保存"); setForm(empty); refresh(); } catch (e) { @@ -107,6 +115,12 @@ export function ModelManager({ 激活 )} + + + {editing && ( + + )} {msg && {msg}} diff --git a/sundynix-admin/src/pages/LoginConfigPage.tsx b/sundynix-admin/src/pages/LoginConfigPage.tsx index 077042c..ffce5f7 100644 --- a/sundynix-admin/src/pages/LoginConfigPage.tsx +++ b/sundynix-admin/src/pages/LoginConfigPage.tsx @@ -8,6 +8,7 @@ export function LoginConfigPage() { const [appid, setAppid] = useState(""); const [token, setToken] = useState(""); const [secret, setSecret] = useState(""); // 留空=沿用已存 + const [welcome, setWelcome] = useState(""); const [busy, setBusy] = useState(false); const [err, setErr] = useState(""); const [ok, setOk] = useState(false); @@ -19,6 +20,7 @@ export function LoginConfigPage() { setAppid(c.appid); setToken(c.token); setSecret(c.app_secret); + setWelcome(c.welcome); }) .catch((e) => setErr((e as Error).message)); }, []); @@ -29,8 +31,8 @@ export function LoginConfigPage() { setErr(""); setOk(false); try { - const r = await saveWechatMP({ appid: appid.trim(), app_secret: secret, token: token.trim() }); - setCfg((c) => (c ? { ...c, appid: appid.trim(), token: token.trim(), app_secret: secret, enabled: r.enabled } : c)); + const r = await saveWechatMP({ appid: appid.trim(), app_secret: secret, token: token.trim(), welcome: welcome.trim() }); + setCfg((c) => (c ? { ...c, appid: appid.trim(), token: token.trim(), app_secret: secret, welcome: welcome.trim(), enabled: r.enabled } : c)); setOk(true); } catch (e) { setErr((e as Error).message); @@ -77,6 +79,12 @@ export function LoginConfigPage() { setToken(e.target.value)} placeholder="自定义一串字母数字" className="mt-1 block w-full rounded-lg border border-gray-200 px-2.5 py-1.5 font-mono text-sm text-gray-800 focus:border-violet-400 focus:outline-none" /> +