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(),
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
last_login TIMESTAMP WITH TIME ZONE,
|
last_login TIMESTAMP WITH TIME ZONE,
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
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 索引
|
-- agent_user 索引
|
||||||
CREATE INDEX IF NOT EXISTS idx_agent_user_username ON agent_user(username);
|
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_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_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 表
|
-- 2. 创建 agent_bots 表
|
||||||
CREATE TABLE IF NOT EXISTS 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
|
获取或创建用户的单智能体 bot
|
||||||
|
|
||||||
|
子账号直接使用主账号的智能体,不进行复制
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id: 用户 ID
|
user_id: 用户 ID
|
||||||
pool: 数据库连接池
|
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 pool.connection() as conn:
|
||||||
async with conn.cursor() as cursor:
|
async with conn.cursor() as cursor:
|
||||||
# 检查用户是否已有 single_agent_bot_id
|
# 检查用户信息(包括子账号状态和主账号ID)
|
||||||
await cursor.execute("""
|
await cursor.execute("""
|
||||||
SELECT single_agent_bot_id
|
SELECT single_agent_bot_id, is_subaccount, parent_id
|
||||||
FROM agent_user
|
FROM agent_user
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
""", (user_id,))
|
""", (user_id,))
|
||||||
row = await cursor.fetchone()
|
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
|
user_bot_id = row[0] if row and row[0] else None
|
||||||
|
|
||||||
# 如果用户没有 bot_id,自动复制模板
|
# 如果用户没有 bot_id,自动复制模板
|
||||||
@ -271,10 +298,58 @@ async def is_admin_user(authorization: Optional[str]) -> bool:
|
|||||||
return row and row[0]
|
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:
|
async def check_bot_access(bot_id: str, user_id: str, required_permission: str) -> bool:
|
||||||
"""
|
"""
|
||||||
检查用户对 Bot 的访问权限
|
检查用户对 Bot 的访问权限
|
||||||
|
|
||||||
|
子账号可以访问主账号的所有 Bot
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
bot_id: Bot UUID
|
bot_id: Bot UUID
|
||||||
user_id: 用户 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 pool.connection() as conn:
|
||||||
async with conn.cursor() as cursor:
|
async with conn.cursor() as cursor:
|
||||||
# 检查是否是所有者
|
# 获取用户的主账号ID(如果是子账号)
|
||||||
|
effective_user_id = await get_parent_user_id(user_id)
|
||||||
|
# 如果返回的是自身ID,说明不是子账号,保持原样
|
||||||
|
# 如果返回的是其他ID,说明是子账号,使用主账号ID检查权限
|
||||||
|
|
||||||
|
# 检查是否是所有者(包括主账号的所有权)
|
||||||
await cursor.execute("""
|
await cursor.execute("""
|
||||||
SELECT id FROM agent_bots
|
SELECT id FROM agent_bots
|
||||||
WHERE id = %s AND owner_id = %s
|
WHERE id = %s AND owner_id = %s
|
||||||
""", (bot_id, user_id))
|
""", (bot_id, effective_user_id))
|
||||||
if await cursor.fetchone():
|
if await cursor.fetchone():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -339,6 +419,8 @@ async def is_bot_owner(bot_id: str, user_id: str) -> bool:
|
|||||||
"""
|
"""
|
||||||
检查用户是否是 Bot 的所有者
|
检查用户是否是 Bot 的所有者
|
||||||
|
|
||||||
|
子账号可以"拥有"主账号的 Bot(用于编辑权限)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
bot_id: Bot UUID (可能是 bot_id 字段)
|
bot_id: Bot UUID (可能是 bot_id 字段)
|
||||||
user_id: 用户 UUID
|
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 pool.connection() as conn:
|
||||||
async with conn.cursor() as cursor:
|
async with conn.cursor() as cursor:
|
||||||
|
# 获取用户的主账号ID(如果是子账号)
|
||||||
|
effective_user_id = await get_parent_user_id(user_id)
|
||||||
|
|
||||||
await cursor.execute("""
|
await cursor.execute("""
|
||||||
SELECT id FROM agent_bots
|
SELECT id FROM agent_bots
|
||||||
WHERE id = %s AND owner_id = %s
|
WHERE id = %s AND owner_id = %s
|
||||||
""", (bot_id, user_id))
|
""", (bot_id, effective_user_id))
|
||||||
return await cursor.fetchone() is not None
|
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 中的角色
|
||||||
|
|
||||||
|
子账号被视为 Bot 的所有者(如果主账号是所有者)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
bot_id: Bot UUID
|
bot_id: Bot UUID
|
||||||
user_id: 用户 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 pool.connection() as conn:
|
||||||
async with conn.cursor() as cursor:
|
async with conn.cursor() as cursor:
|
||||||
# 检查是否是所有者
|
# 获取用户的主账号ID(如果是子账号)
|
||||||
|
effective_user_id = await get_parent_user_id(user_id)
|
||||||
|
|
||||||
|
# 检查是否是所有者(包括主账号的所有权)
|
||||||
await cursor.execute("""
|
await cursor.execute("""
|
||||||
SELECT id FROM agent_bots
|
SELECT id FROM agent_bots
|
||||||
WHERE id = %s AND owner_id = %s
|
WHERE id = %s AND owner_id = %s
|
||||||
""", (bot_id, user_id))
|
""", (bot_id, effective_user_id))
|
||||||
if await cursor.fetchone():
|
if await cursor.fetchone():
|
||||||
return 'owner'
|
return 'owner'
|
||||||
|
|
||||||
@ -434,6 +524,8 @@ class UserLoginResponse(BaseModel):
|
|||||||
username: str
|
username: str
|
||||||
email: Optional[str] = None
|
email: Optional[str] = None
|
||||||
is_admin: bool = False
|
is_admin: bool = False
|
||||||
|
is_subaccount: bool = False # 是否是子账号
|
||||||
|
parent_id: Optional[str] = None # 主账号ID
|
||||||
expires_at: str
|
expires_at: str
|
||||||
single_agent: Optional[dict] = None # 单智能体模式配置
|
single_agent: Optional[dict] = None # 单智能体模式配置
|
||||||
|
|
||||||
@ -444,6 +536,7 @@ class UserVerifyResponse(BaseModel):
|
|||||||
user_id: Optional[str] = None
|
user_id: Optional[str] = None
|
||||||
username: Optional[str] = None
|
username: Optional[str] = None
|
||||||
is_admin: bool = False
|
is_admin: bool = False
|
||||||
|
is_subaccount: bool = False # 是否是子账号
|
||||||
|
|
||||||
|
|
||||||
class UserInfoResponse(BaseModel):
|
class UserInfoResponse(BaseModel):
|
||||||
@ -452,6 +545,8 @@ class UserInfoResponse(BaseModel):
|
|||||||
username: str
|
username: str
|
||||||
email: Optional[str] = None
|
email: Optional[str] = None
|
||||||
is_admin: bool = False
|
is_admin: bool = False
|
||||||
|
is_subaccount: bool = False # 是否是子账号
|
||||||
|
parent_id: Optional[str] = None # 主账号ID
|
||||||
created_at: str
|
created_at: str
|
||||||
last_login: Optional[str] = None
|
last_login: Optional[str] = None
|
||||||
|
|
||||||
@ -463,6 +558,24 @@ class UserSearchResponse(BaseModel):
|
|||||||
email: Optional[str] = None
|
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):
|
class UserProfileUpdateRequest(BaseModel):
|
||||||
"""用户更新个人信息请求"""
|
"""用户更新个人信息请求"""
|
||||||
username: Optional[str] = None
|
username: Optional[str] = None
|
||||||
@ -1086,6 +1199,65 @@ async def migrate_single_agent_mode():
|
|||||||
await conn.commit()
|
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():
|
async def init_bot_manager_tables():
|
||||||
"""
|
"""
|
||||||
初始化 Bot Manager 相关的所有数据库表
|
初始化 Bot Manager 相关的所有数据库表
|
||||||
@ -1101,6 +1273,8 @@ async def init_bot_manager_tables():
|
|||||||
await migrate_add_marketplace_fields()
|
await migrate_add_marketplace_fields()
|
||||||
# 4. Single Agent Mode 字段迁移
|
# 4. Single Agent Mode 字段迁移
|
||||||
await migrate_single_agent_mode()
|
await migrate_single_agent_mode()
|
||||||
|
# 5. Subaccount Support 字段迁移
|
||||||
|
await migrate_subaccount_support()
|
||||||
|
|
||||||
# SQL 表创建语句
|
# SQL 表创建语句
|
||||||
tables_sql = [
|
tables_sql = [
|
||||||
@ -1212,6 +1386,8 @@ async def get_bots(authorization: Optional[str] = Header(None)):
|
|||||||
"""
|
"""
|
||||||
获取所有 Bot(拥有的和分享给我的)
|
获取所有 Bot(拥有的和分享给我的)
|
||||||
|
|
||||||
|
子账号可以看到主账号的所有 Bot
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
authorization: Bearer token
|
authorization: Bearer token
|
||||||
|
|
||||||
@ -1230,7 +1406,19 @@ async def get_bots(authorization: Optional[str] = Header(None)):
|
|||||||
|
|
||||||
async with pool.connection() as conn:
|
async with pool.connection() as conn:
|
||||||
async with conn.cursor() as cursor:
|
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("""
|
await cursor.execute("""
|
||||||
SELECT DISTINCT b.id, b.name, b.bot_id, b.created_at, b.updated_at, b.settings,
|
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,
|
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 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
|
LEFT JOIN bot_shares s ON b.id = s.bot_id AND s.user_id = %s
|
||||||
WHERE b.owner_id = %s
|
WHERE b.owner_id = %s
|
||||||
|
OR b.owner_id = %s
|
||||||
OR (s.user_id IS NOT NULL
|
OR (s.user_id IS NOT NULL
|
||||||
AND s.user_id = %s
|
AND s.user_id = %s
|
||||||
AND (s.expires_at IS NULL OR s.expires_at > NOW()))
|
AND (s.expires_at IS NULL OR s.expires_at > NOW()))
|
||||||
ORDER BY b.created_at DESC
|
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()
|
rows = await cursor.fetchall()
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -1252,8 +1441,8 @@ async def get_bots(authorization: Optional[str] = Header(None)):
|
|||||||
id=str(row[0]),
|
id=str(row[0]),
|
||||||
name=row[1],
|
name=row[1],
|
||||||
bot_id=str(row[0]),
|
bot_id=str(row[0]),
|
||||||
is_owner=(row[6] is not None and str(row[6]) == user_id),
|
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 row[8] is not None),
|
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,
|
is_published=row[11] if row[11] else False,
|
||||||
copied_from=str(row[12]) if row[12] else None,
|
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,
|
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
|
||||||
bot_id = str(uuid.uuid4())
|
bot_id = str(uuid.uuid4())
|
||||||
|
|
||||||
# 使用用户 ID
|
# 获取用户的主账号ID(如果是子账号,新 bot 归属主账号)
|
||||||
owner_id = user_id
|
owner_id = await get_parent_user_id(user_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with pool.connection() as conn:
|
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
|
bot_id, bot_name, settings_json, updated_at, is_published, copied_from, owner_id = row
|
||||||
settings = settings_json if settings_json else {}
|
settings = settings_json if settings_json else {}
|
||||||
|
|
||||||
# 判断当前用户是否是所有者(确保类型一致)
|
# 判断当前用户是否是所有者(子账号使用主账号ID判断)
|
||||||
is_owner = (str(owner_id) == str(user_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"
|
# 注意:model_id 现在来自 New API,格式为 "Provider/ModelName"
|
||||||
@ -1521,7 +1711,7 @@ async def get_bot_settings(bot_uuid: str, authorization: Optional[str] = Header(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 处理 dataset_ids
|
# 处理 dataset_ids
|
||||||
# 单智能体模式:加载用户的所有知识库
|
# 单智能体模式:加载用户的所有知识库(子账号使用主账号的知识库)
|
||||||
# 普通模式:从 settings 读取
|
# 普通模式:从 settings 读取
|
||||||
if SINGLE_AGENT_MODE:
|
if SINGLE_AGENT_MODE:
|
||||||
await cursor.execute("""
|
await cursor.execute("""
|
||||||
@ -1529,7 +1719,7 @@ async def get_bot_settings(bot_uuid: str, authorization: Optional[str] = Header(
|
|||||||
FROM user_datasets
|
FROM user_datasets
|
||||||
WHERE user_id = %s
|
WHERE user_id = %s
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
""", (user_id,))
|
""", (effective_user_id,))
|
||||||
user_datasets = await cursor.fetchall()
|
user_datasets = await cursor.fetchall()
|
||||||
dataset_ids = [row[0] for row in user_datasets] if user_datasets else []
|
dataset_ids = [row[0] for row in user_datasets] if user_datasets else []
|
||||||
else:
|
else:
|
||||||
@ -2248,7 +2438,7 @@ async def user_login(request: UserLoginRequest):
|
|||||||
# 1. 验证用户名和密码
|
# 1. 验证用户名和密码
|
||||||
password_hash = hash_password(request.password)
|
password_hash = hash_password(request.password)
|
||||||
await cursor.execute("""
|
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
|
FROM agent_user
|
||||||
WHERE username = %s AND password_hash = %s
|
WHERE username = %s AND password_hash = %s
|
||||||
""", (request.username, password_hash))
|
""", (request.username, password_hash))
|
||||||
@ -2260,7 +2450,7 @@ async def user_login(request: UserLoginRequest):
|
|||||||
detail="用户名或密码错误"
|
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:
|
if not is_active:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -2364,6 +2554,8 @@ async def user_login(request: UserLoginRequest):
|
|||||||
username=username,
|
username=username,
|
||||||
email=email,
|
email=email,
|
||||||
is_admin=is_admin or False,
|
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(),
|
expires_at=expires_at.isoformat(),
|
||||||
single_agent=single_agent_config
|
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)
|
valid, user_id, username = await verify_user_auth(authorization)
|
||||||
|
|
||||||
is_admin_flag = False
|
is_admin_flag = False
|
||||||
|
is_subaccount_flag = False
|
||||||
if valid and user_id:
|
if valid and user_id:
|
||||||
pool = get_db_pool_manager().pool
|
pool = get_db_pool_manager().pool
|
||||||
async with pool.connection() as conn:
|
async with pool.connection() as conn:
|
||||||
async with conn.cursor() as cursor:
|
async with conn.cursor() as cursor:
|
||||||
await cursor.execute("""
|
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,))
|
""", (user_id,))
|
||||||
row = await cursor.fetchone()
|
row = await cursor.fetchone()
|
||||||
if row:
|
if row:
|
||||||
is_admin_flag = row[0] or False
|
is_admin_flag = row[0] or False
|
||||||
|
is_subaccount_flag = row[1] or False
|
||||||
|
|
||||||
return UserVerifyResponse(
|
return UserVerifyResponse(
|
||||||
valid=valid,
|
valid=valid,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
username=username,
|
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 pool.connection() as conn:
|
||||||
async with conn.cursor() as cursor:
|
async with conn.cursor() as cursor:
|
||||||
await cursor.execute("""
|
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
|
FROM agent_user
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
""", (user_id,))
|
""", (user_id,))
|
||||||
@ -2438,13 +2633,15 @@ async def get_current_user(authorization: Optional[str] = Header(None)):
|
|||||||
detail="User not found"
|
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(
|
return UserInfoResponse(
|
||||||
id=str(user_id),
|
id=str(user_id),
|
||||||
username=username,
|
username=username,
|
||||||
email=email,
|
email=email,
|
||||||
is_admin=is_admin or False,
|
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 "",
|
created_at=created_at.isoformat() if created_at else "",
|
||||||
last_login=last_login.isoformat() if last_login else None
|
last_login=last_login.isoformat() if last_login else None
|
||||||
)
|
)
|
||||||
@ -2676,6 +2873,9 @@ class UserListResponse(BaseModel):
|
|||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
created_at: str
|
created_at: str
|
||||||
last_login: Optional[str] = None
|
last_login: Optional[str] = None
|
||||||
|
parent_id: Optional[str] = None
|
||||||
|
is_subaccount: bool = False
|
||||||
|
subaccount_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
class UserCreateRequest(BaseModel):
|
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 pool.connection() as conn:
|
||||||
async with conn.cursor() as cursor:
|
async with conn.cursor() as cursor:
|
||||||
await cursor.execute("""
|
await cursor.execute("""
|
||||||
SELECT id, username, email, is_admin, is_active, created_at, last_login
|
SELECT
|
||||||
FROM agent_user
|
u.id, u.username, u.email, u.is_admin, u.is_active,
|
||||||
ORDER BY created_at DESC
|
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()
|
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_admin=row[3] or False,
|
||||||
is_active=row[4],
|
is_active=row[4],
|
||||||
created_at=row[5].isoformat() if row[5] else "",
|
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
|
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 pool.connection() as conn:
|
||||||
async with conn.cursor() as cursor:
|
async with conn.cursor() as cursor:
|
||||||
await cursor.execute("""
|
await cursor.execute("""
|
||||||
SELECT id, username, email, is_admin, is_active, created_at, last_login
|
SELECT
|
||||||
FROM agent_user
|
u.id, u.username, u.email, u.is_admin, u.is_active,
|
||||||
WHERE id = %s
|
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,))
|
""", (user_id,))
|
||||||
row = await cursor.fetchone()
|
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_admin=row[3] or False,
|
||||||
is_active=row[4],
|
is_active=row[4],
|
||||||
created_at=row[5].isoformat() if row[5] else "",
|
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_admin=request.is_admin,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
created_at=created_at.isoformat() if created_at else "",
|
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
|
UPDATE agent_user
|
||||||
SET {', '.join(update_fields)}
|
SET {', '.join(update_fields)}
|
||||||
WHERE id = %s
|
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)
|
""", values)
|
||||||
row = await cursor.fetchone()
|
row = await cursor.fetchone()
|
||||||
|
|
||||||
@ -2935,6 +3152,12 @@ async def update_user(
|
|||||||
detail="User not found"
|
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()
|
await conn.commit()
|
||||||
|
|
||||||
return UserListResponse(
|
return UserListResponse(
|
||||||
@ -2944,7 +3167,10 @@ async def update_user(
|
|||||||
is_admin=row[3] or False,
|
is_admin=row[3] or False,
|
||||||
is_active=row[4],
|
is_active=row[4],
|
||||||
created_at=row[5].isoformat() if row[5] else "",
|
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
|
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 pool.connection() as conn:
|
||||||
async with conn.cursor() as cursor:
|
async with conn.cursor() as cursor:
|
||||||
# 获取原始 Bot 信息
|
# 获取原始 Bot 信息
|
||||||
@ -3526,12 +3755,12 @@ async def copy_marketplace_bot(
|
|||||||
'skills': settings.get('skills'),
|
'skills': settings.get('skills'),
|
||||||
}
|
}
|
||||||
|
|
||||||
# 插入新 Bot
|
# 插入新 Bot(使用 effective_user_id 作为 owner_id)
|
||||||
await cursor.execute("""
|
await cursor.execute("""
|
||||||
INSERT INTO agent_bots (name, bot_id, owner_id, settings, copied_from)
|
INSERT INTO agent_bots (name, bot_id, owner_id, settings, copied_from)
|
||||||
VALUES (%s, %s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s)
|
||||||
RETURNING id, created_at, updated_at
|
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_row = await cursor.fetchone()
|
||||||
new_id, created_at, updated_at = new_row
|
new_id, created_at, updated_at = new_row
|
||||||
|
|
||||||
@ -3548,7 +3777,7 @@ async def copy_marketplace_bot(
|
|||||||
is_shared=False,
|
is_shared=False,
|
||||||
is_published=False,
|
is_published=False,
|
||||||
copied_from=str(original_id),
|
copied_from=str(original_id),
|
||||||
owner={"id": str(user_id), "username": user_username},
|
owner={"id": str(effective_user_id), "username": user_username},
|
||||||
role=None,
|
role=None,
|
||||||
description=new_settings.get('description'),
|
description=new_settings.get('description'),
|
||||||
avatar_url=new_settings.get('avatar_url'),
|
avatar_url=new_settings.get('avatar_url'),
|
||||||
@ -3582,12 +3811,15 @@ async def toggle_bot_publication(
|
|||||||
|
|
||||||
pool = get_db_pool_manager().pool
|
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 pool.connection() as conn:
|
||||||
async with conn.cursor() as cursor:
|
async with conn.cursor() as cursor:
|
||||||
# 检查是否是所有者
|
# 检查是否是所有者(包括子账号的主账号)
|
||||||
await cursor.execute("""
|
await cursor.execute("""
|
||||||
SELECT id, is_published FROM agent_bots WHERE id = %s AND owner_id = %s
|
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()
|
row = await cursor.fetchone()
|
||||||
|
|
||||||
if not row:
|
if not row:
|
||||||
@ -3668,8 +3900,9 @@ async def sync_bot_from_source(
|
|||||||
detail="This bot is not copied from marketplace"
|
detail="This bot is not copied from marketplace"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 检查是否是所有者
|
# 检查是否是所有者(子账号使用主账号ID判断)
|
||||||
if str(owner_id) != str(user_id):
|
effective_user_id = await get_parent_user_id(user_id)
|
||||||
|
if str(owner_id) != str(effective_user_id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=403,
|
status_code=403,
|
||||||
detail="Only bot owner can sync from source"
|
detail="Only bot owner can sync from source"
|
||||||
@ -3809,3 +4042,188 @@ async def get_newapi_models(authorization: Optional[str] = Header(None)):
|
|||||||
|
|
||||||
return models
|
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
|
验证用户并获取 New API session cookies 和 user_id
|
||||||
|
|
||||||
|
子账号无法访问支付功能
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
authorization: Authorization header
|
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)
|
tuple[str, Optional[dict], Optional[int]]: (user_id, cookies_dict, new_api_user_id)
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: 认证失败
|
HTTPException: 认证失败或子账号访问
|
||||||
"""
|
"""
|
||||||
token = extract_api_key_from_auth(authorization)
|
token = extract_api_key_from_auth(authorization)
|
||||||
if not token:
|
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:
|
async with conn.cursor() as cursor:
|
||||||
# 验证 token 并获取用户信息
|
# 验证 token 并获取用户信息
|
||||||
await cursor.execute("""
|
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
|
FROM agent_user_tokens t
|
||||||
JOIN agent_user u ON t.user_id = u.id
|
JOIN agent_user u ON t.user_id = u.id
|
||||||
WHERE t.token = %s
|
WHERE t.token = %s
|
||||||
@ -91,7 +93,14 @@ async def verify_user_and_get_session(authorization: Optional[str]) -> tuple[str
|
|||||||
if not row:
|
if not row:
|
||||||
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
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
|
# 如果有 session,构建 cookies
|
||||||
cookies = None
|
cookies = None
|
||||||
|
|||||||
@ -117,6 +117,6 @@ NEW_API_ADMIN_KEY = os.getenv("NEW_API_ADMIN_KEY", "")
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# Single Agent Mode Configuration
|
# 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_ID = os.getenv("TEMPLATE_BOT_ID", "403a2b63-88e4-4db1-b712-8dcf31fc98ea")
|
||||||
TEMPLATE_BOT_NAME = os.getenv("TEMPLATE_BOT_NAME", "智能助手")
|
TEMPLATE_BOT_NAME = os.getenv("TEMPLATE_BOT_NAME", "智能助手")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user