feat: 核心模块增强 — 加密 + 运行时配置 + 日志缓冲

- crypto.py: API Key 加密/解密工具
- runtime_config.py: 运行时动态配置管理
- log_buffer.py: 内存日志缓冲区
- config.py: 新增加密配置项
- http_client.py: 增强重试和错误处理
This commit is contained in:
shangfangjian
2026-09-19 11:58:03 +08:00
parent b3e2c52b49
commit 786f10aa11
57 changed files with 3178 additions and 488 deletions
+201
View File
@@ -0,0 +1,201 @@
/**
* Admin 后台 - 专家与终裁独立 LLM 配置卡片
*
* 每个角色(5 专家 + 终裁)可独立覆盖 模型 / 接口地址 / API Key;
* 留空字段不改动,「恢复继承」删除该角色全部覆盖。
*/
import { useCallback, useEffect, useState } from 'react'
import { fetchLLMAgents, updateSetting, clearSetting } from './dal'
import type { LLMAgentConfig } from './types'
import { Card, CardBody, CardHeader, Badge, Alert, Spinner, SkeletonBlock } from './components'
type FieldKey = 'model' | 'base_url' | 'api_key'
const FIELD_META: { key: FieldKey; label: string; sensitive: boolean; hint: string }[] = [
{ key: 'model', label: '模型', sensitive: false, hint: '留空保持现状;未覆盖时继承默认' },
{ key: 'base_url', label: '接口地址', sensitive: false, hint: '留空保持现状;未覆盖时继承全局' },
{ key: 'api_key', label: 'API Key', sensitive: true, hint: '留空保持现状;未覆盖时继承全局' },
]
const KEY_BY_FIELD: Record<FieldKey, (id: string) => string> = {
model: id => `AGENT_${id.toUpperCase()}_MODEL`,
base_url: id => `AGENT_${id.toUpperCase()}_BASE_URL`,
api_key: id => `AGENT_${id.toUpperCase()}_API_KEY`,
}
export default function AgentLLMCard() {
const [agents, setAgents] = useState<LLMAgentConfig[]>([])
const [loading, setLoading] = useState(true)
const [expandedId, setExpandedId] = useState<string | null>(null)
const [form, setForm] = useState<Record<FieldKey, string>>({ model: '', base_url: '', api_key: '' })
const [busy, setBusy] = useState(false)
const [notice, setNotice] = useState<{ ok: boolean; text: string } | null>(null)
const load = useCallback(async () => {
setLoading(true)
try {
setAgents(await fetchLLMAgents())
} catch {
setAgents([])
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
load()
}, [load])
function toggleExpand(agent: LLMAgentConfig) {
if (expandedId === agent.id) {
setExpandedId(null)
return
}
setExpandedId(agent.id)
setNotice(null)
// 预填非敏感覆盖值;API Key 不回填
setForm({
model: agent.fields.model.origin === 'db' ? agent.fields.model.masked : '',
base_url: agent.fields.base_url.origin === 'db' ? agent.fields.base_url.masked : '',
api_key: '',
})
}
async function handleSave(agent: LLMAgentConfig) {
setBusy(true)
setNotice(null)
try {
const nonEmpty = (FIELD_META.filter(f => form[f.key].trim())).map(f => f)
if (nonEmpty.length === 0) {
setNotice({ ok: false, text: '没有需要保存的修改(全部为空)' })
return
}
for (const f of nonEmpty) {
await updateSetting(KEY_BY_FIELD[f.key](agent.id), form[f.key].trim())
}
setNotice({ ok: true, text: `${agent.label} 配置已保存,立即生效` })
setExpandedId(null)
await load()
} catch (err) {
setNotice({ ok: false, text: err instanceof Error ? err.message.split('\n')[0] : '保存失败' })
} finally {
setBusy(false)
}
}
async function handleReset(agent: LLMAgentConfig) {
setBusy(true)
setNotice(null)
try {
for (const f of FIELD_META) {
await clearSetting(KEY_BY_FIELD[f.key](agent.id))
}
setNotice({ ok: true, text: `${agent.label} 已恢复继承默认` })
setExpandedId(null)
await load()
} catch (err) {
setNotice({ ok: false, text: err instanceof Error ? err.message.split('\n')[0] : '恢复失败' })
} finally {
setBusy(false)
}
}
const hasOverride = (agent: LLMAgentConfig) =>
Object.values(agent.fields).some(f => f.origin === 'db')
return (
<Card>
<CardHeader
title="专家与终裁 LLM 配置"
description="可为每个角色单独指定模型、接口地址或 API Key;未覆盖的角色按「专家层/终裁层默认 → 全局」继承"
action={
<button onClick={load} disabled={loading} className="btn btn-sm">
{loading ? (<><Spinner /> </>) : '刷新'}
</button>
}
/>
<CardBody className="px-0 sm:px-0">
{loading ? (
<div className="space-y-2 px-4 sm:px-5">
{[1, 2, 3, 4, 5, 6].map(i => <SkeletonBlock key={i} className="h-9 w-full" />)}
</div>
) : agents.length === 0 ? (
<p className="py-6 text-center text-xs text-ink-400"></p>
) : (
<>
{notice && (
<div className="px-4 pb-2 sm:px-5">
<Alert kind={notice.ok ? 'ok' : 'error'} title={notice.text} />
</div>
)}
{agents.map(agent => {
const expanded = expandedId === agent.id
return (
<div key={agent.id} className="border-b border-ink-200 last:border-b-0">
{/* 行:角色名 + 生效模型 + 配置按钮 */}
<div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-1 px-4 py-2.5 sm:px-5">
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
<span className="font-serif text-sm font-bold text-ink-900">{agent.label}</span>
{hasOverride(agent) && <Badge status="success"></Badge>}
</div>
<div className="flex min-w-0 items-center gap-2">
<span className="truncate font-mono text-2xs text-ink-500">{agent.effective_model}</span>
<button onClick={() => toggleExpand(agent)} disabled={busy} className="btn btn-sm flex-shrink-0">
{expanded ? '收起' : '配置'}
</button>
</div>
</div>
{/* 展开的编辑表单 */}
{expanded && (
<div className="space-y-3 border-t border-ink-200 bg-paper-100/40 px-4 py-3 sm:px-5">
{FIELD_META.map(f => {
const state = agent.fields[f.key]
return (
<div key={f.key} className="grid gap-1 sm:grid-cols-[96px_minmax(0,1fr)] sm:items-center sm:gap-3">
<label className="text-xs text-ink-500">{f.label}</label>
<div>
<input
type={f.sensitive ? 'password' : 'text'}
value={form[f.key]}
onChange={e => setForm(prev => ({ ...prev, [f.key]: e.target.value }))}
placeholder={
f.key === 'api_key' && state.origin === 'db'
? `已覆盖(${state.masked}),留空保持不变`
: f.hint
}
autoComplete="off"
className="field w-full"
/>
</div>
</div>
)
})}
<div className="flex items-center justify-between gap-2 pt-1">
<p className="text-2xs text-ink-400">
:{agent.effective_model}
</p>
<div className="flex gap-2">
{hasOverride(agent) && (
<button onClick={() => handleReset(agent)} disabled={busy} className="btn btn-sm">
</button>
)}
<button onClick={() => handleSave(agent)} disabled={busy} className="btn btn-solid btn-sm">
{busy ? (<><Spinner /> </>) : '保存'}
</button>
</div>
</div>
</div>
)}
</div>
)
})}
</>
)}
</CardBody>
</Card>
)
}