Files
DDUp/docs/plans/2026-01-23-api-keys-db-design.md
2026-01-23 22:33:50 +08:00

169 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# API Keys 数据库存储设计
## 背景
将 AI 服务的 API Keys 从环境变量迁移到数据库存储,实现:
- 运维便利:无需重启服务即可更新配置
- 安全集中管理:敏感信息统一管理
- Web 界面配置:管理员可通过设置页面直接管理
## 设计决策
| 决策点 | 选择 | 原因 |
|--------|------|------|
| 管理范围 | 仅 AI API Keys | JWT_SECRET 启动时需要,保持环境变量 |
| 存储方式 | 明文存储 | 简单直接,依赖数据库访问控制 |
| 优先级 | 数据库优先 | 方便运行时覆盖环境变量 |
| 访问权限 | 仅管理员 | Web 界面配置入口仅 admin 可见 |
## 数据模型
复用现有 `config` 表,使用 `api_key.` 前缀:
```
key | value
---------------------------|------------------
api_key.dashscope | sk-xxx...
api_key.deepseek | sk-xxx...
api_key.anthropic | sk-xxx...
```
## API 接口
| 方法 | 路径 | 功能 |
|------|------|------|
| GET | `/api/admin/api-keys` | 获取所有 API Keys掩码显示 |
| GET | `/api/admin/api-keys/{provider}` | 获取指定 API Key 完整值 |
| PUT | `/api/admin/api-keys/{provider}` | 设置/更新 API Key |
| DELETE | `/api/admin/api-keys/{provider}` | 删除 API Key回退到环境变量 |
## 需修改的文件
### 1. src/vitals/core/database.py
新增函数:
- `get_api_key(provider: str) -> str | None` - 读取 API Key
- `set_api_key(provider: str, value: str)` - 保存 API Key
- `delete_api_key(provider: str)` - 删除 API Key
- `get_all_api_keys(masked: bool = True) -> dict` - 获取所有 API Keys
```python
API_KEY_ENV_MAP = {
"dashscope": "DASHSCOPE_API_KEY",
"deepseek": "DEEPSEEK_API_KEY",
"anthropic": "ANTHROPIC_API_KEY",
}
def get_api_key(provider: str) -> str | None:
"""获取 API Key数据库优先环境变量备用"""
with get_connection() as (conn, cursor):
cursor.execute(
"SELECT value FROM config WHERE `key` = %s",
(f"api_key.{provider}",)
)
row = cursor.fetchone()
if row and row["value"]:
return row["value"]
# 回退到环境变量
env_var = API_KEY_ENV_MAP.get(provider)
return os.environ.get(env_var) if env_var else None
```
### 2. src/vitals/vision/providers/qwen.py
```python
# 修改前
api_key = os.environ.get("DASHSCOPE_API_KEY")
# 修改后
from vitals.core import database as db
api_key = db.get_api_key("dashscope")
```
### 3. src/vitals/vision/providers/deepseek.py
```python
# 修改前
api_key = os.environ.get("DEEPSEEK_API_KEY")
# 修改后
from vitals.core import database as db
api_key = db.get_api_key("deepseek")
```
### 4. src/vitals/vision/analyzer.py
```python
# 修改前
api_key = os.environ.get("ANTHROPIC_API_KEY")
# 修改后
from vitals.core import database as db
api_key = db.get_api_key("anthropic")
```
### 5. src/vitals/web/app.py
新增管理员 API 接口:
```python
@app.get("/api/admin/api-keys")
async def get_api_keys(request: Request):
"""获取所有 API Keys掩码显示"""
user = await get_current_user(request)
if not user or not user.get("is_admin"):
raise HTTPException(status_code=403)
return db.get_all_api_keys(masked=True)
@app.get("/api/admin/api-keys/{provider}")
async def get_api_key(provider: str, request: Request):
"""获取指定 API Key 完整值"""
# 权限检查...
return {"provider": provider, "value": db.get_api_key(provider)}
@app.put("/api/admin/api-keys/{provider}")
async def set_api_key(provider: str, request: Request):
"""设置 API Key"""
# 权限检查...
body = await request.json()
db.set_api_key(provider, body["value"])
return {"success": True}
@app.delete("/api/admin/api-keys/{provider}")
async def delete_api_key(provider: str, request: Request):
"""删除 API Key"""
# 权限检查...
db.delete_api_key(provider)
return {"success": True}
```
### 6. src/vitals/web/templates/settings.html
在设置页面增加 API 密钥管理卡片(仅管理员可见):
```html
{% if user.is_admin %}
<div class="card">
<h3>API 密钥配置</h3>
<!-- 三个 API Key 输入框 -->
<!-- 掩码显示,点击显示完整值 -->
<!-- 保存/删除按钮 -->
</div>
{% endif %}
```
## 数据流
```
管理员 → Web界面 → /api/admin/api-keys → database.set_api_key() → config表
食物识别 → qwen.py → database.get_api_key() → 查config表 → 有值返回 / 无值读环境变量
```
## 向后兼容
- 现有环境变量配置继续有效
- 数据库未配置时自动回退到环境变量
- 无需数据迁移,新功能即插即用