docs: API Keys 数据库存储设计文档
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
168
docs/plans/2026-01-23-api-keys-db-design.md
Normal file
168
docs/plans/2026-01-23-api-keys-db-design.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# 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表 → 有值返回 / 无值读环境变量
|
||||
```
|
||||
|
||||
## 向后兼容
|
||||
|
||||
- 现有环境变量配置继续有效
|
||||
- 数据库未配置时自动回退到环境变量
|
||||
- 无需数据迁移,新功能即插即用
|
||||
Reference in New Issue
Block a user