mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-13 04:45:19 +00:00
1. 后端建表: 每个用户保存自己的一组私有API 2. 后端提供该表的增删改查的接口
This commit is contained in:
parent
54b7621467
commit
e066353306
2 changed files with 518 additions and 0 deletions
331
backend/open_webui/models/user_model_credentials.py
Normal file
331
backend/open_webui/models/user_model_credentials.py
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
"""
|
||||
用户私有模型凭据管理 - 数据模型层
|
||||
|
||||
功能:
|
||||
1. 允许用户保存自己的 LLM API 凭据(OpenAI/Claude/自建等)
|
||||
2. 提供凭据的增删改查操作
|
||||
3. 自动掩码 API Key 防止泄露
|
||||
4. 支持多 Provider(OpenAI/Ollama/兼容接口等)
|
||||
|
||||
核心设计:
|
||||
- 每个用户可以添加多个私有模型配置
|
||||
- 存储 base_url、api_key 等信息
|
||||
- 返回给前端时自动掩码 API Key(仅显示前 4 位和后 4 位)
|
||||
- 所有操作都强制用户隔离(通过 user_id 校验)
|
||||
|
||||
建表参考(SQLite 默认路径 backend/data/webui.db):
|
||||
cd /home/gaofeng/open-webui-next/backend
|
||||
source .venv/bin/activate
|
||||
sqlite3 data/webui.db "
|
||||
CREATE TABLE IF NOT EXISTS user_model_credential (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT,
|
||||
name TEXT,
|
||||
model_id TEXT NOT NULL,
|
||||
base_url TEXT,
|
||||
api_key TEXT NOT NULL,
|
||||
config TEXT,
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS ix_user_model_credential_user_id ON user_model_credential (user_id);
|
||||
"
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.internal.db import Base, JSONField, get_db
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import BigInteger, Column, String, Text
|
||||
|
||||
####################
|
||||
# DB Schema - 数据库表定义
|
||||
####################
|
||||
|
||||
|
||||
class UserModelCredential(Base):
|
||||
"""
|
||||
用户私有模型凭据表 - 存储每个用户自己配置的 LLM API 凭据
|
||||
|
||||
字段说明:
|
||||
- id: 凭据唯一标识(纳秒时间戳)
|
||||
- user_id: 所属用户 ID(外键,索引)
|
||||
- name: 用户可读名称(如 "我的 GPT-4")
|
||||
- model_id: 上游模型名称/ID(如 "gpt-4", "claude-3-opus")
|
||||
- base_url: API 基础地址(如 "https://api.openai.com/v1")
|
||||
- api_key: API 密钥(明文存储,返回时掩码)
|
||||
- config: 扩展配置(JSON,可存 organization、headers 等)
|
||||
- created_at/updated_at: 创建/更新时间戳
|
||||
"""
|
||||
__tablename__ = "user_model_credential"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
user_id = Column(String, index=True)
|
||||
|
||||
name = Column(String, nullable=True) # 用户可读名称
|
||||
model_id = Column(String, nullable=False) # 上游模型名/ID
|
||||
base_url = Column(Text, nullable=True)
|
||||
api_key = Column(Text, nullable=False)
|
||||
|
||||
config = Column(JSONField, nullable=True) # 预留自定义字段,如org、headers等
|
||||
|
||||
created_at = Column(BigInteger)
|
||||
updated_at = Column(BigInteger)
|
||||
|
||||
|
||||
class UserModelCredentialModel(BaseModel):
|
||||
"""
|
||||
用户私有模型凭据数据模型 - 内部使用的完整数据模型
|
||||
|
||||
用途:数据库操作和内部逻辑使用的完整模型(包含明文 api_key)
|
||||
注意:不应直接返回给前端,应使用 UserModelCredentialResponse
|
||||
"""
|
||||
id: str
|
||||
user_id: str
|
||||
name: Optional[str] = None
|
||||
model_id: str
|
||||
base_url: Optional[str] = None
|
||||
api_key: str # 明文 API Key,仅内部使用
|
||||
config: Optional[dict] = None
|
||||
created_at: int
|
||||
updated_at: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class UserModelCredentialResponse(BaseModel):
|
||||
"""
|
||||
用户私有模型凭据响应模型 - 返回给前端的安全模型
|
||||
|
||||
用途:API 响应时使用,将 api_key 替换为掩码后的 api_key_masked
|
||||
安全设计:前端永远看不到完整的 API Key,只能看到前 4 位和后 4 位
|
||||
"""
|
||||
id: str
|
||||
user_id: str
|
||||
name: Optional[str] = None
|
||||
model_id: str
|
||||
base_url: Optional[str] = None
|
||||
api_key_masked: str # 掩码后的 API Key(如 "sk-1***5678")
|
||||
config: Optional[dict] = None
|
||||
created_at: int
|
||||
updated_at: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class UserModelCredentialForm(BaseModel):
|
||||
"""
|
||||
用户私有模型凭据表单模型 - 前端提交的数据格式
|
||||
|
||||
用途:创建/更新凭据时前端提交的数据
|
||||
字段:不包含 id、user_id、时间戳(由后端自动生成)
|
||||
"""
|
||||
name: Optional[str] = None
|
||||
model_id: str
|
||||
base_url: Optional[str] = None
|
||||
api_key: str # 前端提交时是明文,后端直接存储
|
||||
config: Optional[dict] = None
|
||||
|
||||
|
||||
def mask_api_key(key: str) -> str:
|
||||
"""
|
||||
掩码 API Key - 保护敏感信息
|
||||
|
||||
规则:
|
||||
- 空字符串:返回空
|
||||
- 长度 ≤ 8:全部替换为 *
|
||||
- 长度 > 8:保留前 4 位和后 4 位,中间用 * 填充
|
||||
|
||||
示例:
|
||||
- "sk-1234567890abcdef" → "sk-1**********cdef"
|
||||
- "short" → "*****"
|
||||
- "" → ""
|
||||
"""
|
||||
if not key:
|
||||
return ""
|
||||
if len(key) <= 8:
|
||||
return "*" * len(key)
|
||||
return f"{key[:4]}{'*' * (len(key) - 8)}{key[-4:]}"
|
||||
|
||||
|
||||
class UserModelCredentialsTable:
|
||||
"""
|
||||
用户私有模型凭据数据访问层 - CRUD 操作
|
||||
|
||||
功能:
|
||||
1. 增:insert_new_credential - 创建新凭据
|
||||
2. 删:delete_credential_by_id_and_user_id - 删除凭据(用户隔离)
|
||||
3. 改:update_credential_by_id_and_user_id - 更新凭据(用户隔离)
|
||||
4. 查:get_credentials_by_user_id - 获取用户的所有凭据
|
||||
get_credential_by_id_and_user_id - 获取单个凭据(用户隔离)
|
||||
|
||||
安全设计:
|
||||
- 所有操作都强制校验 user_id,防止跨用户访问
|
||||
- 更新/删除/查询单条时,必须同时匹配 id 和 user_id
|
||||
"""
|
||||
def insert_new_credential(
|
||||
self, user_id: str, form: UserModelCredentialForm
|
||||
) -> Optional[UserModelCredentialModel]:
|
||||
"""
|
||||
创建新的用户私有模型凭据
|
||||
|
||||
流程:
|
||||
1. 生成唯一 ID(纳秒时间戳,确保唯一性)
|
||||
2. 关联当前用户 ID
|
||||
3. 保存表单数据(model_id、base_url、api_key 等)
|
||||
4. 设置创建时间和更新时间
|
||||
5. 持久化到数据库
|
||||
|
||||
Args:
|
||||
user_id: 当前用户 ID
|
||||
form: 前端提交的凭据表单数据
|
||||
|
||||
Returns:
|
||||
UserModelCredentialModel: 创建成功的凭据对象
|
||||
"""
|
||||
with get_db() as db:
|
||||
now = int(time.time())
|
||||
# 使用纳秒时间戳生成唯一 ID
|
||||
cred = UserModelCredential(
|
||||
**{
|
||||
"id": str(time.time_ns()),
|
||||
"user_id": user_id,
|
||||
"name": form.name,
|
||||
"model_id": form.model_id,
|
||||
"base_url": form.base_url,
|
||||
"api_key": form.api_key, # 明文存储
|
||||
"config": form.config,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
)
|
||||
db.add(cred)
|
||||
db.commit()
|
||||
db.refresh(cred)
|
||||
return UserModelCredentialModel.model_validate(cred)
|
||||
|
||||
def update_credential_by_id_and_user_id(
|
||||
self, cred_id: str, user_id: str, form: UserModelCredentialForm
|
||||
) -> Optional[UserModelCredentialModel]:
|
||||
"""
|
||||
更新用户私有模型凭据
|
||||
|
||||
安全校验:
|
||||
- 凭据必须存在
|
||||
- 凭据必须属于当前用户(user_id 匹配)
|
||||
- 不满足条件返回 None
|
||||
|
||||
更新策略:
|
||||
- 仅更新前端提交的字段(exclude_none=True)
|
||||
- 自动更新 updated_at 时间戳
|
||||
- created_at 不会被修改
|
||||
|
||||
Args:
|
||||
cred_id: 凭据 ID
|
||||
user_id: 当前用户 ID
|
||||
form: 更新的数据
|
||||
|
||||
Returns:
|
||||
UserModelCredentialModel: 更新后的凭据对象,权限不足返回 None
|
||||
"""
|
||||
with get_db() as db:
|
||||
# === 1. 权限校验:凭据必须存在且属于当前用户 ===
|
||||
cred = db.get(UserModelCredential, cred_id)
|
||||
if not cred or cred.user_id != user_id:
|
||||
return None
|
||||
|
||||
# === 2. 更新字段:仅更新提交的非空字段 ===
|
||||
for field, value in form.model_dump(exclude_none=True).items():
|
||||
setattr(cred, field, value)
|
||||
cred.updated_at = int(time.time())
|
||||
|
||||
# === 3. 持久化 ===
|
||||
db.commit()
|
||||
db.refresh(cred)
|
||||
return UserModelCredentialModel.model_validate(cred)
|
||||
|
||||
def delete_credential_by_id_and_user_id(
|
||||
self, cred_id: str, user_id: str
|
||||
) -> bool:
|
||||
"""
|
||||
删除用户私有模型凭据
|
||||
|
||||
安全校验:
|
||||
- 凭据必须存在
|
||||
- 凭据必须属于当前用户(user_id 匹配)
|
||||
- 不满足条件返回 False
|
||||
|
||||
Args:
|
||||
cred_id: 凭据 ID
|
||||
user_id: 当前用户 ID
|
||||
|
||||
Returns:
|
||||
bool: 删除成功返回 True,权限不足或不存在返回 False
|
||||
"""
|
||||
with get_db() as db:
|
||||
# === 1. 权限校验:凭据必须存在且属于当前用户 ===
|
||||
cred = db.get(UserModelCredential, cred_id)
|
||||
if not cred or cred.user_id != user_id:
|
||||
return False
|
||||
|
||||
# === 2. 删除凭据 ===
|
||||
db.delete(cred)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
def get_credentials_by_user_id(
|
||||
self, user_id: str
|
||||
) -> list[UserModelCredentialModel]:
|
||||
"""
|
||||
获取用户的所有私有模型凭据
|
||||
|
||||
用途:前端模型选择器展示用户的所有私有模型
|
||||
返回:用户创建的所有凭据列表(包含明文 api_key)
|
||||
|
||||
Args:
|
||||
user_id: 用户 ID
|
||||
|
||||
Returns:
|
||||
list[UserModelCredentialModel]: 用户的所有凭据列表,无数据返回空列表
|
||||
"""
|
||||
with get_db() as db:
|
||||
# 查询该用户的所有凭据
|
||||
creds = (
|
||||
db.query(UserModelCredential).filter_by(user_id=user_id).all()
|
||||
)
|
||||
return (
|
||||
[UserModelCredentialModel.model_validate(c) for c in creds]
|
||||
if creds
|
||||
else []
|
||||
)
|
||||
|
||||
def get_credential_by_id_and_user_id(
|
||||
self, cred_id: str, user_id: str
|
||||
) -> Optional[UserModelCredentialModel]:
|
||||
"""
|
||||
获取单个用户私有模型凭据
|
||||
|
||||
安全校验:
|
||||
- 凭据必须存在
|
||||
- 凭据必须属于当前用户(user_id 匹配)
|
||||
- 不满足条件返回 None
|
||||
|
||||
用途:查看/编辑凭据详情时使用
|
||||
|
||||
Args:
|
||||
cred_id: 凭据 ID
|
||||
user_id: 当前用户 ID
|
||||
|
||||
Returns:
|
||||
UserModelCredentialModel: 凭据对象,权限不足返回 None
|
||||
"""
|
||||
with get_db() as db:
|
||||
# === 1. 权限校验:凭据必须存在且属于当前用户 ===
|
||||
cred = db.get(UserModelCredential, cred_id)
|
||||
if not cred or cred.user_id != user_id:
|
||||
return None
|
||||
return UserModelCredentialModel.model_validate(cred)
|
||||
|
||||
|
||||
UserModelCredentials = UserModelCredentialsTable()
|
||||
187
backend/open_webui/routers/user_models.py
Normal file
187
backend/open_webui/routers/user_models.py
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
"""
|
||||
用户私有模型凭据管理 - API 路由层
|
||||
|
||||
提供的接口:
|
||||
- GET /api/user/models/credentials - 获取当前用户的所有私有模型凭据
|
||||
- POST /api/user/models/credentials - 创建新的私有模型凭据
|
||||
- PUT /api/user/models/credentials/{id} - 更新指定的私有模型凭据
|
||||
- DELETE /api/user/models/credentials/{id} - 删除指定的私有模型凭据
|
||||
|
||||
安全设计:
|
||||
- 所有接口都需要用户认证(get_verified_user)
|
||||
- 返回数据时自动掩码 API Key(仅显示前 4 位和后 4 位)
|
||||
- 更新/删除操作强制用户隔离(仅能操作自己的凭据)
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.utils.auth import get_verified_user
|
||||
from open_webui.models.user_model_credentials import (
|
||||
UserModelCredentialForm,
|
||||
UserModelCredentialResponse,
|
||||
UserModelCredentials,
|
||||
mask_api_key,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _mask_response(model):
|
||||
"""
|
||||
响应数据掩码处理 - 保护 API Key 不被前端完整获取
|
||||
|
||||
转换流程:
|
||||
1. 将内部模型(包含明文 api_key)转换为响应模型
|
||||
2. 不回传明文 api_key,只返回掩码字段 api_key_masked
|
||||
|
||||
Args:
|
||||
model: UserModelCredentialModel(包含明文 api_key)
|
||||
|
||||
Returns:
|
||||
UserModelCredentialResponse: 掩码后的响应对象
|
||||
"""
|
||||
return UserModelCredentialResponse(
|
||||
**{
|
||||
**{
|
||||
k: v
|
||||
for k, v in model.model_dump().items()
|
||||
if k not in ["api_key"]
|
||||
},
|
||||
"api_key_masked": mask_api_key(model.api_key), # 掩码 API Key
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", response_model=list[UserModelCredentialResponse])
|
||||
async def list_user_credentials(user=Depends(get_verified_user)):
|
||||
"""
|
||||
获取当前用户的所有私有模型凭据
|
||||
|
||||
用途:前端模型选择器初始化时调用,获取用户保存的所有私有 API 配置
|
||||
返回:掩码后的凭据列表(api_key_masked 字段,不暴露明文)
|
||||
|
||||
权限:仅返回当前用户的凭据
|
||||
|
||||
Returns:
|
||||
list[UserModelCredentialResponse]: 用户的所有私有模型凭据列表
|
||||
"""
|
||||
# 查询当前用户的所有凭据
|
||||
creds = UserModelCredentials.get_credentials_by_user_id(user.id)
|
||||
# 掩码 API Key 后返回
|
||||
return [_mask_response(c) for c in creds]
|
||||
|
||||
|
||||
@router.post("/", response_model=UserModelCredentialResponse)
|
||||
async def create_user_credential(
|
||||
form_data: UserModelCredentialForm, user=Depends(get_verified_user)
|
||||
):
|
||||
"""
|
||||
创建新的私有模型凭据
|
||||
|
||||
用途:用户在前端添加自己的 LLM API(如自己的 OpenAI Key、Claude Key 等)
|
||||
流程:
|
||||
1. 接收前端提交的表单数据(model_id、base_url、api_key 等)
|
||||
2. 自动关联当前用户 ID
|
||||
3. 生成唯一凭据 ID
|
||||
4. 保存到数据库
|
||||
5. 返回掩码后的凭据对象
|
||||
|
||||
Args:
|
||||
form_data: 前端提交的凭据表单(不包含 id 和 user_id)
|
||||
user: 当前认证用户(由依赖注入自动获取)
|
||||
|
||||
Returns:
|
||||
UserModelCredentialResponse: 创建成功的凭据对象(API Key 已掩码)
|
||||
|
||||
Raises:
|
||||
HTTPException(400): 创建失败时抛出
|
||||
"""
|
||||
# 创建新凭据,自动关联当前用户 ID
|
||||
cred = UserModelCredentials.insert_new_credential(user.id, form_data)
|
||||
if not cred:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
)
|
||||
# 掩码 API Key 后返回
|
||||
return _mask_response(cred)
|
||||
|
||||
|
||||
@router.put("/{cred_id}", response_model=UserModelCredentialResponse)
|
||||
async def update_user_credential(
|
||||
cred_id: str, form_data: UserModelCredentialForm, user=Depends(get_verified_user)
|
||||
):
|
||||
"""
|
||||
更新指定的私有模型凭据
|
||||
|
||||
用途:用户编辑自己保存的 API 凭据(修改 base_url、api_key、model_id 等)
|
||||
安全校验:
|
||||
- 凭据必须存在
|
||||
- 凭据必须属于当前用户(user_id 匹配)
|
||||
- 不满足条件返回 404
|
||||
|
||||
更新策略:仅更新前端提交的字段(部分更新)
|
||||
|
||||
Args:
|
||||
cred_id: 凭据 ID(路径参数)
|
||||
form_data: 更新的数据
|
||||
user: 当前认证用户(由依赖注入自动获取)
|
||||
|
||||
Returns:
|
||||
UserModelCredentialResponse: 更新后的凭据对象(API Key 已掩码)
|
||||
|
||||
Raises:
|
||||
HTTPException(404): 凭据不存在或不属于当前用户
|
||||
"""
|
||||
# 更新凭据,自动校验用户权限
|
||||
cred = UserModelCredentials.update_credential_by_id_and_user_id(
|
||||
cred_id, user.id, form_data
|
||||
)
|
||||
if not cred:
|
||||
# 凭据不存在或权限不足
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
# 掩码 API Key 后返回
|
||||
return _mask_response(cred)
|
||||
|
||||
|
||||
@router.delete("/{cred_id}", response_model=bool)
|
||||
async def delete_user_credential(cred_id: str, user=Depends(get_verified_user)):
|
||||
"""
|
||||
删除指定的私有模型凭据
|
||||
|
||||
用途:用户删除自己保存的 API 凭据
|
||||
安全校验:
|
||||
- 凭据必须存在
|
||||
- 凭据必须属于当前用户(user_id 匹配)
|
||||
- 不满足条件返回 404
|
||||
|
||||
Args:
|
||||
cred_id: 凭据 ID(路径参数)
|
||||
user: 当前认证用户(由依赖注入自动获取)
|
||||
|
||||
Returns:
|
||||
bool: 删除成功返回 True
|
||||
|
||||
Raises:
|
||||
HTTPException(404): 凭据不存在或不属于当前用户
|
||||
"""
|
||||
# 删除凭据,自动校验用户权限
|
||||
result = UserModelCredentials.delete_credential_by_id_and_user_id(
|
||||
cred_id, user.id
|
||||
)
|
||||
if not result:
|
||||
# 凭据不存在或权限不足
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
return True
|
||||
Loading…
Reference in a new issue