diff --git a/create_tables.sql b/create_tables.sql index 7bd06d4..5a24fd3 100644 --- a/create_tables.sql +++ b/create_tables.sql @@ -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 ( diff --git a/routes/bot_manager.py b/routes/bot_manager.py index d0dcecc..dc88e96 100644 --- a/routes/bot_manager.py +++ b/routes/bot_manager.py @@ -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"} + diff --git a/routes/payment.py b/routes/payment.py index 8bf788a..5bbb61e 100644 --- a/routes/payment.py +++ b/routes/payment.py @@ -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 diff --git a/utils/settings.py b/utils/settings.py index f8406ef..033e5a8 100644 --- a/utils/settings.py +++ b/utils/settings.py @@ -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", "智能助手")