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:
@@ -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('确定要退出登录吗?')) {
|
||||
|
||||
Reference in New Issue
Block a user