subaccount
This commit is contained in:
parent
912d5ebbed
commit
c9c9a71452
@ -8,13 +8,18 @@ CREATE TABLE IF NOT EXISTS agent_user (
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
last_login TIMESTAMP WITH TIME ZONE,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
is_admin BOOLEAN DEFAULT FALSE
|
||||
is_admin BOOLEAN DEFAULT FALSE,
|
||||
is_subaccount BOOLEAN DEFAULT FALSE, -- 是否是子账号
|
||||
parent_id UUID REFERENCES agent_user(id) ON DELETE CASCADE, -- 主账号ID
|
||||
CONSTRAINT check_not_self_parent CHECK (parent_id IS NULL OR parent_id != id)
|
||||
);
|
||||
|
||||
-- agent_user 索引
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_user_username ON agent_user(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_user_email ON agent_user(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_user_is_active ON agent_user(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_user_parent_id ON agent_user(parent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_user_is_subaccount ON agent_user(is_subaccount);
|
||||
|
||||
-- 2. 创建 agent_bots 表
|
||||
CREATE TABLE IF NOT EXISTS agent_bots (
|
||||
|
||||
@ -61,6 +61,8 @@ async def get_or_create_single_agent_bot(user_id: str, pool):
|
||||
"""
|
||||
获取或创建用户的单智能体 bot
|
||||
|
||||
子账号直接使用主账号的智能体,不进行复制
|
||||
|
||||
Args:
|
||||
user_id: 用户 ID
|
||||
pool: 数据库连接池
|
||||
@ -73,14 +75,39 @@ async def get_or_create_single_agent_bot(user_id: str, pool):
|
||||
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
# 检查用户是否已有 single_agent_bot_id
|
||||
# 检查用户信息(包括子账号状态和主账号ID)
|
||||
await cursor.execute("""
|
||||
SELECT single_agent_bot_id
|
||||
SELECT single_agent_bot_id, is_subaccount, parent_id
|
||||
FROM agent_user
|
||||
WHERE id = %s
|
||||
""", (user_id,))
|
||||
row = await cursor.fetchone()
|
||||
|
||||
if not row:
|
||||
return {"enabled": False}
|
||||
|
||||
user_bot_id, is_subaccount, parent_id = row
|
||||
|
||||
# 子账号直接使用主账号的智能体
|
||||
if is_subaccount and parent_id:
|
||||
await cursor.execute("""
|
||||
SELECT single_agent_bot_id
|
||||
FROM agent_user
|
||||
WHERE id = %s
|
||||
""", (parent_id,))
|
||||
parent_row = await cursor.fetchone()
|
||||
|
||||
if parent_row and parent_row[0]:
|
||||
return {
|
||||
"enabled": True,
|
||||
"bot_name": TEMPLATE_BOT_NAME,
|
||||
"bot_id": str(parent_row[0])
|
||||
}
|
||||
else:
|
||||
# 主账号也没有智能体,返回空
|
||||
return {"enabled": False}
|
||||
|
||||
# 主账号逻辑:检查是否已有 bot_id
|
||||
user_bot_id = row[0] if row and row[0] else None
|
||||
|
||||
# 如果用户没有 bot_id,自动复制模板
|
||||
@ -271,10 +298,58 @@ async def is_admin_user(authorization: Optional[str]) -> bool:
|
||||
return row and row[0]
|
||||
|
||||
|
||||
async def is_subaccount_user(authorization: Optional[str]) -> bool:
|
||||
"""
|
||||
检查当前请求是否来自子账号
|
||||
|
||||
Args:
|
||||
authorization: Authorization header 值
|
||||
|
||||
Returns:
|
||||
bool: 是否是子账号
|
||||
"""
|
||||
user_valid, user_id, _ = await verify_user_auth(authorization)
|
||||
if not user_valid or not user_id:
|
||||
return False
|
||||
|
||||
pool = get_db_pool_manager().pool
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("""
|
||||
SELECT is_subaccount FROM agent_user WHERE id = %s
|
||||
""", (user_id,))
|
||||
row = await cursor.fetchone()
|
||||
return row and row[0]
|
||||
|
||||
|
||||
async def get_parent_user_id(user_id: str) -> Optional[str]:
|
||||
"""
|
||||
获取用户的主账号ID(如果是子账号)
|
||||
|
||||
Args:
|
||||
user_id: 用户 UUID
|
||||
|
||||
Returns:
|
||||
Optional[str]: 主账号ID,如果不是子账号则返回自身ID
|
||||
"""
|
||||
pool = get_db_pool_manager().pool
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("""
|
||||
SELECT parent_id FROM agent_user WHERE id = %s
|
||||
""", (user_id,))
|
||||
row = await cursor.fetchone()
|
||||
if row and row[0]:
|
||||
return str(row[0])
|
||||
return user_id
|
||||
|
||||
|
||||
async def check_bot_access(bot_id: str, user_id: str, required_permission: str) -> bool:
|
||||
"""
|
||||
检查用户对 Bot 的访问权限
|
||||
|
||||
子账号可以访问主账号的所有 Bot
|
||||
|
||||
Args:
|
||||
bot_id: Bot UUID
|
||||
user_id: 用户 UUID
|
||||
@ -287,11 +362,16 @@ async def check_bot_access(bot_id: str, user_id: str, required_permission: str)
|
||||
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
# 检查是否是所有者
|
||||
# 获取用户的主账号ID(如果是子账号)
|
||||
effective_user_id = await get_parent_user_id(user_id)
|
||||
# 如果返回的是自身ID,说明不是子账号,保持原样
|
||||
# 如果返回的是其他ID,说明是子账号,使用主账号ID检查权限
|
||||
|
||||
# 检查是否是所有者(包括主账号的所有权)
|
||||
await cursor.execute("""
|
||||
SELECT id FROM agent_bots
|
||||
WHERE id = %s AND owner_id = %s
|
||||
""", (bot_id, user_id))
|
||||
""", (bot_id, effective_user_id))
|
||||
if await cursor.fetchone():
|
||||
return True
|
||||
|
||||
@ -339,6 +419,8 @@ async def is_bot_owner(bot_id: str, user_id: str) -> bool:
|
||||
"""
|
||||
检查用户是否是 Bot 的所有者
|
||||
|
||||
子账号可以"拥有"主账号的 Bot(用于编辑权限)
|
||||
|
||||
Args:
|
||||
bot_id: Bot UUID (可能是 bot_id 字段)
|
||||
user_id: 用户 UUID
|
||||
@ -350,10 +432,13 @@ async def is_bot_owner(bot_id: str, user_id: str) -> bool:
|
||||
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
# 获取用户的主账号ID(如果是子账号)
|
||||
effective_user_id = await get_parent_user_id(user_id)
|
||||
|
||||
await cursor.execute("""
|
||||
SELECT id FROM agent_bots
|
||||
WHERE id = %s AND owner_id = %s
|
||||
""", (bot_id, user_id))
|
||||
""", (bot_id, effective_user_id))
|
||||
return await cursor.fetchone() is not None
|
||||
|
||||
|
||||
@ -361,6 +446,8 @@ async def get_user_bot_role(bot_id: str, user_id: str) -> Optional[str]:
|
||||
"""
|
||||
获取用户在 Bot 中的角色
|
||||
|
||||
子账号被视为 Bot 的所有者(如果主账号是所有者)
|
||||
|
||||
Args:
|
||||
bot_id: Bot UUID
|
||||
user_id: 用户 UUID
|
||||
@ -372,11 +459,14 @@ async def get_user_bot_role(bot_id: str, user_id: str) -> Optional[str]:
|
||||
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
# 检查是否是所有者
|
||||
# 获取用户的主账号ID(如果是子账号)
|
||||
effective_user_id = await get_parent_user_id(user_id)
|
||||
|
||||
# 检查是否是所有者(包括主账号的所有权)
|
||||
await cursor.execute("""
|
||||
SELECT id FROM agent_bots
|
||||
WHERE id = %s AND owner_id = %s
|
||||
""", (bot_id, user_id))
|
||||
""", (bot_id, effective_user_id))
|
||||
if await cursor.fetchone():
|
||||
return 'owner'
|
||||
|
||||
@ -434,6 +524,8 @@ class UserLoginResponse(BaseModel):
|
||||
username: str
|
||||
email: Optional[str] = None
|
||||
is_admin: bool = False
|
||||
is_subaccount: bool = False # 是否是子账号
|
||||
parent_id: Optional[str] = None # 主账号ID
|
||||
expires_at: str
|
||||
single_agent: Optional[dict] = None # 单智能体模式配置
|
||||
|
||||
@ -444,6 +536,7 @@ class UserVerifyResponse(BaseModel):
|
||||
user_id: Optional[str] = None
|
||||
username: Optional[str] = None
|
||||
is_admin: bool = False
|
||||
is_subaccount: bool = False # 是否是子账号
|
||||
|
||||
|
||||
class UserInfoResponse(BaseModel):
|
||||
@ -452,6 +545,8 @@ class UserInfoResponse(BaseModel):
|
||||
username: str
|
||||
email: Optional[str] = None
|
||||
is_admin: bool = False
|
||||
is_subaccount: bool = False # 是否是子账号
|
||||
parent_id: Optional[str] = None # 主账号ID
|
||||
created_at: str
|
||||
last_login: Optional[str] = None
|
||||
|
||||
@ -463,6 +558,24 @@ class UserSearchResponse(BaseModel):
|
||||
email: Optional[str] = None
|
||||
|
||||
|
||||
# --- 子账号相关 ---
|
||||
class SubAccountCreateRequest(BaseModel):
|
||||
"""创建子账号请求"""
|
||||
username: str
|
||||
password: str
|
||||
email: Optional[str] = None
|
||||
|
||||
|
||||
class SubAccountListItem(BaseModel):
|
||||
"""子账号列表项"""
|
||||
id: str
|
||||
username: str
|
||||
email: Optional[str] = None
|
||||
is_active: bool
|
||||
created_at: str
|
||||
last_login: Optional[str] = None
|
||||
|
||||
|
||||
class UserProfileUpdateRequest(BaseModel):
|
||||
"""用户更新个人信息请求"""
|
||||
username: Optional[str] = None
|
||||
@ -1086,6 +1199,65 @@ async def migrate_single_agent_mode():
|
||||
await conn.commit()
|
||||
|
||||
|
||||
async def migrate_subaccount_support():
|
||||
"""
|
||||
添加子账号支持相关字段到 agent_user 表
|
||||
"""
|
||||
pool = get_db_pool_manager().pool
|
||||
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
# 检查 is_subaccount 字段是否存在
|
||||
await cursor.execute("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'agent_user' AND column_name = 'is_subaccount'
|
||||
""")
|
||||
has_is_subaccount = await cursor.fetchone()
|
||||
|
||||
if not has_is_subaccount:
|
||||
logger.info("Adding subaccount support columns to agent_user table")
|
||||
# 添加 is_subaccount 字段
|
||||
await cursor.execute("""
|
||||
ALTER TABLE agent_user
|
||||
ADD COLUMN is_subaccount BOOLEAN DEFAULT FALSE
|
||||
""")
|
||||
|
||||
# 添加 parent_id 字段
|
||||
await cursor.execute("""
|
||||
ALTER TABLE agent_user
|
||||
ADD COLUMN parent_id UUID REFERENCES agent_user(id) ON DELETE CASCADE
|
||||
""")
|
||||
|
||||
# 添加索引
|
||||
await cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_user_parent_id ON agent_user(parent_id)
|
||||
""")
|
||||
await cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_user_is_subaccount ON agent_user(is_subaccount)
|
||||
""")
|
||||
|
||||
# 添加约束防止自引用(使用 DO 块处理约束可能已存在的情况)
|
||||
await cursor.execute("""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'check_not_self_parent'
|
||||
) THEN
|
||||
ALTER TABLE agent_user
|
||||
ADD CONSTRAINT check_not_self_parent
|
||||
CHECK (parent_id IS NULL OR parent_id != id);
|
||||
END IF;
|
||||
END $$;
|
||||
""")
|
||||
|
||||
logger.info("Subaccount support migration completed")
|
||||
else:
|
||||
logger.info("Subaccount support columns already exist")
|
||||
|
||||
await conn.commit()
|
||||
|
||||
|
||||
async def init_bot_manager_tables():
|
||||
"""
|
||||
初始化 Bot Manager 相关的所有数据库表
|
||||
@ -1101,6 +1273,8 @@ async def init_bot_manager_tables():
|
||||
await migrate_add_marketplace_fields()
|
||||
# 4. Single Agent Mode 字段迁移
|
||||
await migrate_single_agent_mode()
|
||||
# 5. Subaccount Support 字段迁移
|
||||
await migrate_subaccount_support()
|
||||
|
||||
# SQL 表创建语句
|
||||
tables_sql = [
|
||||
@ -1212,6 +1386,8 @@ async def get_bots(authorization: Optional[str] = Header(None)):
|
||||
"""
|
||||
获取所有 Bot(拥有的和分享给我的)
|
||||
|
||||
子账号可以看到主账号的所有 Bot
|
||||
|
||||
Args:
|
||||
authorization: Bearer token
|
||||
|
||||
@ -1230,7 +1406,19 @@ async def get_bots(authorization: Optional[str] = Header(None)):
|
||||
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
# 所有用户(包括 admin)只能看到拥有的 Bot 和分享给自己的 Bot(且未过期)
|
||||
# 获取用户的主账号ID(如果是子账号)
|
||||
await cursor.execute("""
|
||||
SELECT COALESCE(parent_id, id) as effective_owner_id
|
||||
FROM agent_user
|
||||
WHERE id = %s
|
||||
""", (user_id,))
|
||||
row = await cursor.fetchone()
|
||||
effective_owner_id = row[0] if row else user_id
|
||||
|
||||
# 用户可以看到:
|
||||
# 1. 自己拥有的 Bot
|
||||
# 2. 主账号拥有的 Bot(如果是子账号)
|
||||
# 3. 分享给自己的 Bot(未过期)
|
||||
await cursor.execute("""
|
||||
SELECT DISTINCT b.id, b.name, b.bot_id, b.created_at, b.updated_at, b.settings,
|
||||
u.id as owner_id, u.username as owner_username,
|
||||
@ -1240,11 +1428,12 @@ async def get_bots(authorization: Optional[str] = Header(None)):
|
||||
LEFT JOIN agent_user u ON b.owner_id = u.id
|
||||
LEFT JOIN bot_shares s ON b.id = s.bot_id AND s.user_id = %s
|
||||
WHERE b.owner_id = %s
|
||||
OR b.owner_id = %s
|
||||
OR (s.user_id IS NOT NULL
|
||||
AND s.user_id = %s
|
||||
AND (s.expires_at IS NULL OR s.expires_at > NOW()))
|
||||
ORDER BY b.created_at DESC
|
||||
""", (user_id, user_id, user_id))
|
||||
""", (user_id, user_id, effective_owner_id, user_id))
|
||||
rows = await cursor.fetchall()
|
||||
|
||||
return [
|
||||
@ -1252,8 +1441,8 @@ async def get_bots(authorization: Optional[str] = Header(None)):
|
||||
id=str(row[0]),
|
||||
name=row[1],
|
||||
bot_id=str(row[0]),
|
||||
is_owner=(row[6] is not None and str(row[6]) == user_id),
|
||||
is_shared=(row[6] is not None and str(row[6]) != user_id and row[8] is not None),
|
||||
is_owner=(row[6] is not None and str(row[6]) in (user_id, effective_owner_id)),
|
||||
is_shared=(row[6] is not None and str(row[6]) != user_id and str(row[6]) != effective_owner_id and row[8] is not None),
|
||||
is_published=row[11] if row[11] else False,
|
||||
copied_from=str(row[12]) if row[12] else None,
|
||||
owner={"id": str(row[6]), "username": row[7]} if row[6] is not None else None,
|
||||
@ -1294,8 +1483,8 @@ async def create_bot(request: BotCreate, authorization: Optional[str] = Header(N
|
||||
# 自动生成 bot_id
|
||||
bot_id = str(uuid.uuid4())
|
||||
|
||||
# 使用用户 ID
|
||||
owner_id = user_id
|
||||
# 获取用户的主账号ID(如果是子账号,新 bot 归属主账号)
|
||||
owner_id = await get_parent_user_id(user_id)
|
||||
|
||||
try:
|
||||
async with pool.connection() as conn:
|
||||
@ -1501,8 +1690,9 @@ async def get_bot_settings(bot_uuid: str, authorization: Optional[str] = Header(
|
||||
bot_id, bot_name, settings_json, updated_at, is_published, copied_from, owner_id = row
|
||||
settings = settings_json if settings_json else {}
|
||||
|
||||
# 判断当前用户是否是所有者(确保类型一致)
|
||||
is_owner = (str(owner_id) == str(user_id))
|
||||
# 判断当前用户是否是所有者(子账号使用主账号ID判断)
|
||||
effective_user_id = await get_parent_user_id(user_id)
|
||||
is_owner = (str(owner_id) == str(effective_user_id))
|
||||
|
||||
# 获取关联的模型信息
|
||||
# 注意:model_id 现在来自 New API,格式为 "Provider/ModelName"
|
||||
@ -1521,7 +1711,7 @@ async def get_bot_settings(bot_uuid: str, authorization: Optional[str] = Header(
|
||||
)
|
||||
|
||||
# 处理 dataset_ids
|
||||
# 单智能体模式:加载用户的所有知识库
|
||||
# 单智能体模式:加载用户的所有知识库(子账号使用主账号的知识库)
|
||||
# 普通模式:从 settings 读取
|
||||
if SINGLE_AGENT_MODE:
|
||||
await cursor.execute("""
|
||||
@ -1529,7 +1719,7 @@ async def get_bot_settings(bot_uuid: str, authorization: Optional[str] = Header(
|
||||
FROM user_datasets
|
||||
WHERE user_id = %s
|
||||
ORDER BY created_at DESC
|
||||
""", (user_id,))
|
||||
""", (effective_user_id,))
|
||||
user_datasets = await cursor.fetchall()
|
||||
dataset_ids = [row[0] for row in user_datasets] if user_datasets else []
|
||||
else:
|
||||
@ -2248,7 +2438,7 @@ async def user_login(request: UserLoginRequest):
|
||||
# 1. 验证用户名和密码
|
||||
password_hash = hash_password(request.password)
|
||||
await cursor.execute("""
|
||||
SELECT id, username, email, is_active, is_admin
|
||||
SELECT id, username, email, is_active, is_admin, is_subaccount, parent_id
|
||||
FROM agent_user
|
||||
WHERE username = %s AND password_hash = %s
|
||||
""", (request.username, password_hash))
|
||||
@ -2260,7 +2450,7 @@ async def user_login(request: UserLoginRequest):
|
||||
detail="用户名或密码错误"
|
||||
)
|
||||
|
||||
user_id, username, email, is_active, is_admin = row
|
||||
user_id, username, email, is_active, is_admin, is_subaccount, parent_id = row
|
||||
|
||||
if not is_active:
|
||||
raise HTTPException(
|
||||
@ -2364,6 +2554,8 @@ async def user_login(request: UserLoginRequest):
|
||||
username=username,
|
||||
email=email,
|
||||
is_admin=is_admin or False,
|
||||
is_subaccount=is_subaccount or False,
|
||||
parent_id=str(parent_id) if parent_id else None,
|
||||
expires_at=expires_at.isoformat(),
|
||||
single_agent=single_agent_config
|
||||
)
|
||||
@ -2383,22 +2575,25 @@ async def user_verify(authorization: Optional[str] = Header(None)):
|
||||
valid, user_id, username = await verify_user_auth(authorization)
|
||||
|
||||
is_admin_flag = False
|
||||
is_subaccount_flag = False
|
||||
if valid and user_id:
|
||||
pool = get_db_pool_manager().pool
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("""
|
||||
SELECT is_admin FROM agent_user WHERE id = %s
|
||||
SELECT is_admin, is_subaccount FROM agent_user WHERE id = %s
|
||||
""", (user_id,))
|
||||
row = await cursor.fetchone()
|
||||
if row:
|
||||
is_admin_flag = row[0] or False
|
||||
is_subaccount_flag = row[1] or False
|
||||
|
||||
return UserVerifyResponse(
|
||||
valid=valid,
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
is_admin=is_admin_flag
|
||||
is_admin=is_admin_flag,
|
||||
is_subaccount=is_subaccount_flag
|
||||
)
|
||||
|
||||
|
||||
@ -2426,7 +2621,7 @@ async def get_current_user(authorization: Optional[str] = Header(None)):
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("""
|
||||
SELECT id, username, email, is_admin, created_at, last_login
|
||||
SELECT id, username, email, is_admin, is_subaccount, parent_id, created_at, last_login
|
||||
FROM agent_user
|
||||
WHERE id = %s
|
||||
""", (user_id,))
|
||||
@ -2438,13 +2633,15 @@ async def get_current_user(authorization: Optional[str] = Header(None)):
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
user_id, username, email, is_admin, created_at, last_login = row
|
||||
user_id, username, email, is_admin, is_subaccount, parent_id, created_at, last_login = row
|
||||
|
||||
return UserInfoResponse(
|
||||
id=str(user_id),
|
||||
username=username,
|
||||
email=email,
|
||||
is_admin=is_admin or False,
|
||||
is_subaccount=is_subaccount or False,
|
||||
parent_id=str(parent_id) if parent_id else None,
|
||||
created_at=created_at.isoformat() if created_at else "",
|
||||
last_login=last_login.isoformat() if last_login else None
|
||||
)
|
||||
@ -2676,6 +2873,9 @@ class UserListResponse(BaseModel):
|
||||
is_active: bool = True
|
||||
created_at: str
|
||||
last_login: Optional[str] = None
|
||||
parent_id: Optional[str] = None
|
||||
is_subaccount: bool = False
|
||||
subaccount_count: int = 0
|
||||
|
||||
|
||||
class UserCreateRequest(BaseModel):
|
||||
@ -2719,9 +2919,13 @@ async def get_all_users(authorization: Optional[str] = Header(None)):
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("""
|
||||
SELECT id, username, email, is_admin, is_active, created_at, last_login
|
||||
FROM agent_user
|
||||
ORDER BY created_at DESC
|
||||
SELECT
|
||||
u.id, u.username, u.email, u.is_admin, u.is_active,
|
||||
u.created_at, u.last_login, u.parent_id,
|
||||
COALESCE(u.is_subaccount, FALSE),
|
||||
(SELECT COUNT(*) FROM agent_user sub WHERE sub.parent_id = u.id) as subaccount_count
|
||||
FROM agent_user u
|
||||
ORDER BY u.created_at DESC
|
||||
""")
|
||||
rows = await cursor.fetchall()
|
||||
|
||||
@ -2733,7 +2937,10 @@ async def get_all_users(authorization: Optional[str] = Header(None)):
|
||||
is_admin=row[3] or False,
|
||||
is_active=row[4],
|
||||
created_at=row[5].isoformat() if row[5] else "",
|
||||
last_login=row[6].isoformat() if row[6] else None
|
||||
last_login=row[6].isoformat() if row[6] else None,
|
||||
parent_id=str(row[7]) if row[7] else None,
|
||||
is_subaccount=row[8] or False,
|
||||
subaccount_count=row[9] or 0
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
@ -2762,9 +2969,13 @@ async def get_user(user_id: str, authorization: Optional[str] = Header(None)):
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("""
|
||||
SELECT id, username, email, is_admin, is_active, created_at, last_login
|
||||
FROM agent_user
|
||||
WHERE id = %s
|
||||
SELECT
|
||||
u.id, u.username, u.email, u.is_admin, u.is_active,
|
||||
u.created_at, u.last_login, u.parent_id,
|
||||
COALESCE(u.is_subaccount, FALSE),
|
||||
(SELECT COUNT(*) FROM agent_user sub WHERE sub.parent_id = u.id) as subaccount_count
|
||||
FROM agent_user u
|
||||
WHERE u.id = %s
|
||||
""", (user_id,))
|
||||
row = await cursor.fetchone()
|
||||
|
||||
@ -2781,7 +2992,10 @@ async def get_user(user_id: str, authorization: Optional[str] = Header(None)):
|
||||
is_admin=row[3] or False,
|
||||
is_active=row[4],
|
||||
created_at=row[5].isoformat() if row[5] else "",
|
||||
last_login=row[6].isoformat() if row[6] else None
|
||||
last_login=row[6].isoformat() if row[6] else None,
|
||||
parent_id=str(row[7]) if row[7] else None,
|
||||
is_subaccount=row[8] or False,
|
||||
subaccount_count=row[9] or 0
|
||||
)
|
||||
|
||||
|
||||
@ -2851,7 +3065,10 @@ async def create_user(
|
||||
is_admin=request.is_admin,
|
||||
is_active=True,
|
||||
created_at=created_at.isoformat() if created_at else "",
|
||||
last_login=None
|
||||
last_login=None,
|
||||
parent_id=None,
|
||||
is_subaccount=False,
|
||||
subaccount_count=0
|
||||
)
|
||||
|
||||
|
||||
@ -2925,7 +3142,7 @@ async def update_user(
|
||||
UPDATE agent_user
|
||||
SET {', '.join(update_fields)}
|
||||
WHERE id = %s
|
||||
RETURNING id, username, email, is_admin, is_active, created_at, last_login
|
||||
RETURNING id, username, email, is_admin, is_active, created_at, last_login, parent_id, is_subaccount
|
||||
""", values)
|
||||
row = await cursor.fetchone()
|
||||
|
||||
@ -2935,6 +3152,12 @@ async def update_user(
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
# 查询子账号数量
|
||||
await cursor.execute("""
|
||||
SELECT COUNT(*) FROM agent_user WHERE parent_id = %s
|
||||
""", (row[0],))
|
||||
subaccount_count = (await cursor.fetchone())[0]
|
||||
|
||||
await conn.commit()
|
||||
|
||||
return UserListResponse(
|
||||
@ -2944,7 +3167,10 @@ async def update_user(
|
||||
is_admin=row[3] or False,
|
||||
is_active=row[4],
|
||||
created_at=row[5].isoformat() if row[5] else "",
|
||||
last_login=row[6].isoformat() if row[6] else None
|
||||
last_login=row[6].isoformat() if row[6] else None,
|
||||
parent_id=str(row[7]) if row[7] else None,
|
||||
is_subaccount=row[8] or False,
|
||||
subaccount_count=subaccount_count or 0
|
||||
)
|
||||
|
||||
|
||||
@ -3490,6 +3716,9 @@ async def copy_marketplace_bot(
|
||||
|
||||
pool = get_db_pool_manager().pool
|
||||
|
||||
# 获取用户的主账号ID(如果是子账号,新 bot 归属主账号)
|
||||
effective_user_id = await get_parent_user_id(user_id)
|
||||
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
# 获取原始 Bot 信息
|
||||
@ -3526,12 +3755,12 @@ async def copy_marketplace_bot(
|
||||
'skills': settings.get('skills'),
|
||||
}
|
||||
|
||||
# 插入新 Bot
|
||||
# 插入新 Bot(使用 effective_user_id 作为 owner_id)
|
||||
await cursor.execute("""
|
||||
INSERT INTO agent_bots (name, bot_id, owner_id, settings, copied_from)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
RETURNING id, created_at, updated_at
|
||||
""", (new_name, new_bot_id, user_id, json.dumps(new_settings), original_id))
|
||||
""", (new_name, new_bot_id, effective_user_id, json.dumps(new_settings), original_id))
|
||||
new_row = await cursor.fetchone()
|
||||
new_id, created_at, updated_at = new_row
|
||||
|
||||
@ -3548,7 +3777,7 @@ async def copy_marketplace_bot(
|
||||
is_shared=False,
|
||||
is_published=False,
|
||||
copied_from=str(original_id),
|
||||
owner={"id": str(user_id), "username": user_username},
|
||||
owner={"id": str(effective_user_id), "username": user_username},
|
||||
role=None,
|
||||
description=new_settings.get('description'),
|
||||
avatar_url=new_settings.get('avatar_url'),
|
||||
@ -3582,12 +3811,15 @@ async def toggle_bot_publication(
|
||||
|
||||
pool = get_db_pool_manager().pool
|
||||
|
||||
# 获取用户的主账号ID(如果是子账号)
|
||||
effective_user_id = await get_parent_user_id(user_id)
|
||||
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
# 检查是否是所有者
|
||||
# 检查是否是所有者(包括子账号的主账号)
|
||||
await cursor.execute("""
|
||||
SELECT id, is_published FROM agent_bots WHERE id = %s AND owner_id = %s
|
||||
""", (bot_uuid, user_id))
|
||||
""", (bot_uuid, effective_user_id))
|
||||
row = await cursor.fetchone()
|
||||
|
||||
if not row:
|
||||
@ -3668,8 +3900,9 @@ async def sync_bot_from_source(
|
||||
detail="This bot is not copied from marketplace"
|
||||
)
|
||||
|
||||
# 检查是否是所有者
|
||||
if str(owner_id) != str(user_id):
|
||||
# 检查是否是所有者(子账号使用主账号ID判断)
|
||||
effective_user_id = await get_parent_user_id(user_id)
|
||||
if str(owner_id) != str(effective_user_id):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Only bot owner can sync from source"
|
||||
@ -3809,3 +4042,188 @@ async def get_newapi_models(authorization: Optional[str] = Header(None)):
|
||||
|
||||
return models
|
||||
|
||||
|
||||
# ============== 子账号管理 API ==============
|
||||
|
||||
@router.post("/api/v1/subaccounts", response_model=UserLoginResponse)
|
||||
async def create_subaccount(
|
||||
request: SubAccountCreateRequest,
|
||||
authorization: Optional[str] = Header(None)
|
||||
):
|
||||
"""
|
||||
创建子账号
|
||||
|
||||
要求:
|
||||
- 当前用户必须是主账号(is_subaccount = false)
|
||||
- 子账号用户名不能与已有用户重复
|
||||
- 子账号的 parent_id 指向当前用户
|
||||
|
||||
Args:
|
||||
request: 子账号创建请求
|
||||
authorization: Bearer token
|
||||
|
||||
Returns:
|
||||
UserLoginResponse: 新创建的子账号信息(不含 token)
|
||||
"""
|
||||
# 验证当前用户
|
||||
user_valid, user_id, username = await verify_user_auth(authorization)
|
||||
if not user_valid:
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
pool = get_db_pool_manager().pool
|
||||
|
||||
# 检查是否为子账号(子账号不能创建子账号)
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("""
|
||||
SELECT is_subaccount FROM agent_user WHERE id = %s
|
||||
""", (user_id,))
|
||||
row = await cursor.fetchone()
|
||||
if row and row[0]:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Subaccounts cannot create subaccounts"
|
||||
)
|
||||
|
||||
# 检查用户名是否已存在
|
||||
await cursor.execute("""
|
||||
SELECT id FROM agent_user WHERE username = %s
|
||||
""", (request.username,))
|
||||
if await cursor.fetchone():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Username already exists"
|
||||
)
|
||||
|
||||
# 检查邮箱是否已存在
|
||||
if request.email:
|
||||
await cursor.execute("""
|
||||
SELECT id FROM agent_user WHERE email = %s
|
||||
""", (request.email,))
|
||||
if await cursor.fetchone():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Email already exists"
|
||||
)
|
||||
|
||||
# 创建子账号
|
||||
password_hash = hash_password(request.password)
|
||||
await cursor.execute("""
|
||||
INSERT INTO agent_user (username, email, password_hash, is_subaccount, parent_id)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
RETURNING id, username, email, is_admin, is_subaccount, parent_id, created_at
|
||||
""", (request.username, request.email, password_hash, True, user_id))
|
||||
row = await cursor.fetchone()
|
||||
await conn.commit()
|
||||
|
||||
new_user_id, new_username, new_email, is_admin, is_subaccount, parent_id, created_at = row
|
||||
|
||||
return UserLoginResponse(
|
||||
token="", # 不返回 token,需要子账号自己登录
|
||||
user_id=str(new_user_id),
|
||||
username=new_username,
|
||||
email=new_email,
|
||||
is_admin=is_admin or False,
|
||||
is_subaccount=is_subaccount or False,
|
||||
parent_id=str(parent_id) if parent_id else None,
|
||||
expires_at=""
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/v1/subaccounts", response_model=List[SubAccountListItem])
|
||||
async def list_subaccounts(authorization: Optional[str] = Header(None)):
|
||||
"""
|
||||
获取当前主账号的所有子账号列表
|
||||
|
||||
Args:
|
||||
authorization: Bearer token
|
||||
|
||||
Returns:
|
||||
List[SubAccountListItem]: 子账号列表
|
||||
"""
|
||||
# 验证当前用户
|
||||
user_valid, user_id, _ = await verify_user_auth(authorization)
|
||||
if not user_valid:
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
pool = get_db_pool_manager().pool
|
||||
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("""
|
||||
SELECT id, username, email, is_active, created_at, last_login
|
||||
FROM agent_user
|
||||
WHERE parent_id = %s
|
||||
ORDER BY created_at DESC
|
||||
""", (user_id,))
|
||||
rows = await cursor.fetchall()
|
||||
|
||||
result = []
|
||||
for row in rows:
|
||||
result.append(SubAccountListItem(
|
||||
id=str(row[0]),
|
||||
username=row[1],
|
||||
email=row[2],
|
||||
is_active=row[3],
|
||||
created_at=row[4].isoformat() if row[4] else "",
|
||||
last_login=row[5].isoformat() if row[5] else None
|
||||
))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.delete("/api/v1/subaccounts/{subaccount_id}")
|
||||
async def delete_subaccount(
|
||||
subaccount_id: str,
|
||||
authorization: Optional[str] = Header(None)
|
||||
):
|
||||
"""
|
||||
删除子账号
|
||||
|
||||
要求:
|
||||
- 只能删除属于自己的子账号
|
||||
- 主账号不能被此接口删除
|
||||
|
||||
Args:
|
||||
subaccount_id: 子账号 UUID
|
||||
authorization: Bearer token
|
||||
|
||||
Returns:
|
||||
dict: 删除结果
|
||||
"""
|
||||
# 验证当前用户
|
||||
user_valid, user_id, _ = await verify_user_auth(authorization)
|
||||
if not user_valid:
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
pool = get_db_pool_manager().pool
|
||||
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
# 验证子账号属于当前用户
|
||||
await cursor.execute("""
|
||||
SELECT parent_id FROM agent_user WHERE id = %s
|
||||
""", (subaccount_id,))
|
||||
row = await cursor.fetchone()
|
||||
|
||||
if not row:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Subaccount not found"
|
||||
)
|
||||
|
||||
parent_id = row[0]
|
||||
if str(parent_id) != user_id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Subaccount does not belong to you"
|
||||
)
|
||||
|
||||
# 删除子账号(级联删除相关的 tokens 等)
|
||||
await cursor.execute("""
|
||||
DELETE FROM agent_user WHERE id = %s
|
||||
""", (subaccount_id,))
|
||||
await conn.commit()
|
||||
|
||||
return {"message": "Subaccount deleted successfully"}
|
||||
|
||||
|
||||
@ -60,6 +60,8 @@ async def verify_user_and_get_session(authorization: Optional[str]) -> tuple[str
|
||||
"""
|
||||
验证用户并获取 New API session cookies 和 user_id
|
||||
|
||||
子账号无法访问支付功能
|
||||
|
||||
Args:
|
||||
authorization: Authorization header
|
||||
|
||||
@ -67,7 +69,7 @@ async def verify_user_and_get_session(authorization: Optional[str]) -> tuple[str
|
||||
tuple[str, Optional[dict], Optional[int]]: (user_id, cookies_dict, new_api_user_id)
|
||||
|
||||
Raises:
|
||||
HTTPException: 认证失败
|
||||
HTTPException: 认证失败或子账号访问
|
||||
"""
|
||||
token = extract_api_key_from_auth(authorization)
|
||||
if not token:
|
||||
@ -79,7 +81,7 @@ async def verify_user_and_get_session(authorization: Optional[str]) -> tuple[str
|
||||
async with conn.cursor() as cursor:
|
||||
# 验证 token 并获取用户信息
|
||||
await cursor.execute("""
|
||||
SELECT u.id, u.new_api_session, u.new_api_user_id
|
||||
SELECT u.id, u.new_api_session, u.new_api_user_id, u.is_subaccount
|
||||
FROM agent_user_tokens t
|
||||
JOIN agent_user u ON t.user_id = u.id
|
||||
WHERE t.token = %s
|
||||
@ -91,7 +93,14 @@ async def verify_user_and_get_session(authorization: Optional[str]) -> tuple[str
|
||||
if not row:
|
||||
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
||||
|
||||
user_id, new_api_session, new_api_user_id = row
|
||||
user_id, new_api_session, new_api_user_id, is_subaccount = row
|
||||
|
||||
# 子账号不能访问支付功能
|
||||
if is_subaccount:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Subaccounts cannot access payment features"
|
||||
)
|
||||
|
||||
# 如果有 session,构建 cookies
|
||||
cookies = None
|
||||
|
||||
@ -117,6 +117,6 @@ NEW_API_ADMIN_KEY = os.getenv("NEW_API_ADMIN_KEY", "")
|
||||
# ============================================================
|
||||
# Single Agent Mode Configuration
|
||||
# ============================================================
|
||||
SINGLE_AGENT_MODE = os.getenv("SINGLE_AGENT_MODE", "false") == "true"
|
||||
SINGLE_AGENT_MODE = os.getenv("SINGLE_AGENT_MODE", "true") == "true"
|
||||
TEMPLATE_BOT_ID = os.getenv("TEMPLATE_BOT_ID", "403a2b63-88e4-4db1-b712-8dcf31fc98ea")
|
||||
TEMPLATE_BOT_NAME = os.getenv("TEMPLATE_BOT_NAME", "智能助手")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user