feat: API Keys 支持数据库存储和 Web 界面管理

- database.py: 新增 get_api_key/set_api_key/delete_api_key/get_all_api_keys 函数
- qwen.py/deepseek.py/analyzer.py: 改用 db.get_api_key() 读取配置
- app.py: 新增管理员 API 接口 (/api/admin/api-keys)
- settings 页面: 管理员可见的 API 密钥配置区域

API Key 优先级: 数据库 > 环境变量

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 22:32:17 +08:00
parent dba052bb5e
commit eb88d2638f
5 changed files with 304 additions and 6 deletions

View File

@@ -726,6 +726,50 @@ async def admin_delete_invite(invite_id: int, admin: User = Depends(require_admi
return {"message": "邀请码已删除"}
# ===== API 密钥管理(管理员) =====
@app.get("/api/admin/api-keys")
async def admin_get_api_keys(admin: User = Depends(require_admin)):
"""获取所有 API Keys 状态(掩码显示)"""
return db.get_all_api_keys(masked=True)
@app.get("/api/admin/api-keys/{provider}")
async def admin_get_api_key(provider: str, admin: User = Depends(require_admin)):
"""获取指定 API Key 完整值"""
if provider not in db.API_KEY_ENV_MAP:
raise HTTPException(status_code=400, detail=f"未知的 provider: {provider}")
value = db.get_api_key(provider)
return {
"provider": provider,
"name": db.API_KEY_NAMES.get(provider, provider),
"value": value,
}
class ApiKeyInput(BaseModel):
value: str
@app.put("/api/admin/api-keys/{provider}")
async def admin_set_api_key(provider: str, data: ApiKeyInput, admin: User = Depends(require_admin)):
"""设置/更新 API Key"""
if provider not in db.API_KEY_ENV_MAP:
raise HTTPException(status_code=400, detail=f"未知的 provider: {provider}")
db.set_api_key(provider, data.value)
return {"message": "API Key 已保存", "provider": provider}
@app.delete("/api/admin/api-keys/{provider}")
async def admin_delete_api_key(provider: str, admin: User = Depends(require_admin)):
"""删除 API Key回退到环境变量"""
if provider not in db.API_KEY_ENV_MAP:
raise HTTPException(status_code=400, detail=f"未知的 provider: {provider}")
db.delete_api_key(provider)
return {"message": "API Key 已删除,将使用环境变量配置", "provider": provider}
# ===== 页面路由 =====
@@ -8030,6 +8074,18 @@ def get_settings_page_html() -> str:
</form>
</div>
<!-- API 密钥配置(仅管理员可见) -->
<div class="section" id="api-keys-section" style="display: none;">
<h2>API 密钥配置</h2>
<p style="color: #64748B; margin-bottom: 20px;">配置 AI 服务的 API 密钥,用于食物图片识别功能</p>
<div id="api-keys-list">
<div style="text-align: center; padding: 40px; color: #94A3B8;">
加载中...
</div>
</div>
</div>
<!-- 账户管理 -->
<div class="section">
<h2>账户</h2>
@@ -8509,6 +8565,9 @@ def get_settings_page_html() -> str:
document.getElementById('account-badge').style.display = 'block';
// 显示管理后台入口
document.getElementById('admin-entry').style.display = 'block';
// 显示 API 密钥配置区域
document.getElementById('api-keys-section').style.display = 'block';
loadApiKeys();
}
// 保存到 localStorage
localStorage.setItem('user', JSON.stringify(user));
@@ -8522,6 +8581,8 @@ def get_settings_page_html() -> str:
if (user.is_admin) {
document.getElementById('account-badge').style.display = 'block';
document.getElementById('admin-entry').style.display = 'block';
document.getElementById('api-keys-section').style.display = 'block';
loadApiKeys();
}
}
}
@@ -8536,11 +8597,144 @@ def get_settings_page_html() -> str:
if (user.is_admin) {
document.getElementById('account-badge').style.display = 'block';
document.getElementById('admin-entry').style.display = 'block';
document.getElementById('api-keys-section').style.display = 'block';
loadApiKeys();
}
}
}
}
// API 密钥管理
async function loadApiKeys() {
try {
const response = await fetch('/api/admin/api-keys', { credentials: 'same-origin' });
if (response.ok) {
const keys = await response.json();
renderApiKeys(keys);
} else {
document.getElementById('api-keys-list').innerHTML = '<div style="color: #EF4444; padding: 20px;">加载失败,请刷新重试</div>';
}
} catch (error) {
console.error('加载 API 密钥失败:', error);
document.getElementById('api-keys-list').innerHTML = '<div style="color: #EF4444; padding: 20px;">加载失败,请刷新重试</div>';
}
}
function renderApiKeys(keys) {
const container = document.getElementById('api-keys-list');
const providers = Object.keys(keys);
container.innerHTML = providers.map(provider => {
const key = keys[provider];
const hasValue = key.value !== null;
const sourceLabel = key.source === 'database' ? '数据库' : (key.source === 'env' ? '环境变量' : '未配置');
const sourceColor = key.source === 'database' ? '#10B981' : (key.source === 'env' ? '#F59E0B' : '#94A3B8');
return `
<div class="api-key-item" style="padding: 20px; border: 2px solid #E2E8F0; border-radius: 8px; margin-bottom: 15px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<div>
<div style="font-weight: 600; color: #1E293B;">${key.name}</div>
<div style="font-size: 0.85rem; color: ${sourceColor}; margin-top: 4px;">
来源: ${sourceLabel}
</div>
</div>
<div style="display: flex; gap: 8px;">
${hasValue ? `<button class="btn btn-secondary btn-small" onclick="toggleApiKeyVisibility('${provider}')">显示</button>` : ''}
${key.source === 'database' ? `<button class="btn btn-danger btn-small" onclick="deleteApiKey('${provider}')">删除</button>` : ''}
</div>
</div>
<div style="display: flex; gap: 10px;">
<input type="password" id="api-key-${provider}" class="api-key-input"
value="${hasValue ? key.value : ''}"
placeholder="${hasValue ? '' : '未配置,请输入 API Key'}"
style="flex: 1; padding: 10px 14px; border: 2px solid #E2E8F0; border-radius: 6px; font-family: monospace;">
<button class="btn btn-primary btn-small" onclick="saveApiKey('${provider}')">保存</button>
</div>
</div>
`;
}).join('');
}
async function toggleApiKeyVisibility(provider) {
const input = document.getElementById(`api-key-${provider}`);
if (input.type === 'password') {
// 获取完整值
try {
const response = await fetch(`/api/admin/api-keys/${provider}`, { credentials: 'same-origin' });
if (response.ok) {
const data = await response.json();
input.value = data.value || '';
input.type = 'text';
// 更新按钮文字
event.target.textContent = '隐藏';
}
} catch (error) {
console.error('获取 API 密钥失败:', error);
showAlert('获取失败', 'error');
}
} else {
input.type = 'password';
event.target.textContent = '显示';
// 重新加载掩码值
loadApiKeys();
}
}
async function saveApiKey(provider) {
const input = document.getElementById(`api-key-${provider}`);
const value = input.value.trim();
if (!value) {
showAlert('请输入 API Key', 'error');
return;
}
try {
const response = await fetch(`/api/admin/api-keys/${provider}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ value })
});
if (response.ok) {
showAlert('API Key 已保存', 'success');
loadApiKeys();
} else {
const error = await response.json();
showAlert(error.detail || '保存失败', 'error');
}
} catch (error) {
console.error('保存 API 密钥失败:', error);
showAlert('保存失败', 'error');
}
}
async function deleteApiKey(provider) {
if (!confirm('确定要删除此 API Key 吗?删除后将回退到使用环境变量配置。')) {
return;
}
try {
const response = await fetch(`/api/admin/api-keys/${provider}`, {
method: 'DELETE',
credentials: 'same-origin'
});
if (response.ok) {
showAlert('API Key 已删除', 'success');
loadApiKeys();
} else {
const error = await response.json();
showAlert(error.detail || '删除失败', 'error');
}
} catch (error) {
console.error('删除 API 密钥失败:', error);
showAlert('删除失败', 'error');
}
}
// 退出登录
async function logoutAccount() {
if (!confirm('确定要退出登录吗?')) {