subaccount

This commit is contained in:
朱潮 2026-02-26 23:56:45 +08:00
parent 912d5ebbed
commit c9c9a71452
4 changed files with 478 additions and 46 deletions

View File

@ -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 (

View File

@ -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"}

View File

@ -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

View File

@ -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", "智能助手")