增加智能体广场
This commit is contained in:
parent
144739fb4a
commit
aa654fe024
2
.playwright-mcp/console-2026-02-21T06-43-52-154Z.log
Normal file
2
.playwright-mcp/console-2026-02-21T06-43-52-154Z.log
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[ 200ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:8001/manage/chat/:0
|
||||||
|
[ 250ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:8001/favicon.ico:0
|
||||||
@ -22,12 +22,16 @@ CREATE TABLE IF NOT EXISTS agent_bots (
|
|||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
settings JSONB DEFAULT '{"language": "zh", "enable_memori": false, "enable_thinking": false, "tool_response": false}'::jsonb,
|
settings JSONB DEFAULT '{"language": "zh", "enable_memori": false, "enable_thinking": false, "tool_response": false}'::jsonb,
|
||||||
owner_id UUID NOT NULL REFERENCES agent_user(id) ON DELETE RESTRICT,
|
owner_id UUID NOT NULL REFERENCES agent_user(id) ON DELETE RESTRICT,
|
||||||
|
is_published BOOLEAN DEFAULT FALSE, -- 是否发布到智能体广场
|
||||||
|
copied_from UUID REFERENCES agent_bots(id) ON DELETE SET NULL, -- 复制来源的bot id
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|
||||||
-- agent_bots 索引
|
-- agent_bots 索引
|
||||||
CREATE INDEX IF NOT EXISTS idx_agent_bots_owner_id ON agent_bots(owner_id);
|
CREATE INDEX IF NOT EXISTS idx_agent_bots_owner_id ON agent_bots(owner_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_agent_bots_is_published ON agent_bots(is_published) WHERE is_published = TRUE;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_agent_bots_copied_from ON agent_bots(copied_from);
|
||||||
|
|
||||||
-- 3. 创建 agent_user_tokens 表
|
-- 3. 创建 agent_user_tokens 表
|
||||||
CREATE TABLE IF NOT EXISTS agent_user_tokens (
|
CREATE TABLE IF NOT EXISTS agent_user_tokens (
|
||||||
|
|||||||
@ -651,6 +651,7 @@
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
color: var(--text);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
}
|
}
|
||||||
@ -660,6 +661,15 @@
|
|||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark .suggestion-chip {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .suggestion-chip:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Messages ===== */
|
/* ===== Messages ===== */
|
||||||
.message {
|
.message {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import logging
|
|||||||
import uuid
|
import uuid
|
||||||
import hashlib
|
import hashlib
|
||||||
import secrets
|
import secrets
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from fastapi import APIRouter, HTTPException, Header
|
from fastapi import APIRouter, HTTPException, Header
|
||||||
@ -19,6 +21,39 @@ logger = logging.getLogger('app')
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
# ============== 辅助函数 ==============
|
||||||
|
|
||||||
|
def copy_skills_folder(source_bot_id: str, target_bot_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
复制智能体的 skills 文件夹
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_bot_id: 源智能体 ID
|
||||||
|
target_bot_id: 目标智能体 ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功复制
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
source_skills_path = os.path.join('projects', 'uploads', source_bot_id, 'skills')
|
||||||
|
target_skills_path = os.path.join('projects', 'uploads', target_bot_id, 'skills')
|
||||||
|
|
||||||
|
if os.path.exists(source_skills_path):
|
||||||
|
# 如果目标目录已存在,先删除
|
||||||
|
if os.path.exists(target_skills_path):
|
||||||
|
shutil.rmtree(target_skills_path)
|
||||||
|
|
||||||
|
# 复制整个 skills 文件夹
|
||||||
|
shutil.copytree(source_skills_path, target_skills_path)
|
||||||
|
logger.info(f"Copied skills folder from {source_bot_id} to {target_bot_id}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.info(f"Source skills folder not found: {source_skills_path}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to copy skills folder: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
# ============== Admin 配置 ==============
|
# ============== Admin 配置 ==============
|
||||||
ADMIN_USERNAME = "admin"
|
ADMIN_USERNAME = "admin"
|
||||||
ADMIN_PASSWORD = "Admin123" # 生产环境应使用环境变量
|
ADMIN_PASSWORD = "Admin123" # 生产环境应使用环境变量
|
||||||
@ -370,6 +405,8 @@ class BotResponse(BaseModel):
|
|||||||
bot_id: str
|
bot_id: str
|
||||||
is_owner: bool = False
|
is_owner: bool = False
|
||||||
is_shared: bool = False
|
is_shared: bool = False
|
||||||
|
is_published: bool = False # 是否发布到广场
|
||||||
|
copied_from: Optional[str] = None # 复制来源的bot id
|
||||||
owner: Optional[dict] = None # {id, username}
|
owner: Optional[dict] = None # {id, username}
|
||||||
role: Optional[str] = None # 'viewer', 'editor', None for owner
|
role: Optional[str] = None # 'viewer', 'editor', None for owner
|
||||||
shared_at: Optional[str] = None
|
shared_at: Optional[str] = None
|
||||||
@ -394,6 +431,7 @@ class BotSettingsUpdate(BaseModel):
|
|||||||
enable_thinking: Optional[bool] = None
|
enable_thinking: Optional[bool] = None
|
||||||
tool_response: Optional[bool] = None
|
tool_response: Optional[bool] = None
|
||||||
skills: Optional[str] = None
|
skills: Optional[str] = None
|
||||||
|
is_published: Optional[bool] = None # 是否发布到广场
|
||||||
|
|
||||||
|
|
||||||
class ModelInfo(BaseModel):
|
class ModelInfo(BaseModel):
|
||||||
@ -421,9 +459,31 @@ class BotSettingsResponse(BaseModel):
|
|||||||
enable_thinking: bool
|
enable_thinking: bool
|
||||||
tool_response: bool
|
tool_response: bool
|
||||||
skills: Optional[str]
|
skills: Optional[str]
|
||||||
|
is_published: bool = False # 是否发布到广场
|
||||||
|
copied_from: Optional[str] = None # 复制来源的bot id
|
||||||
updated_at: str
|
updated_at: str
|
||||||
|
|
||||||
|
|
||||||
|
# --- 广场相关 ---
|
||||||
|
class MarketplaceBotResponse(BaseModel):
|
||||||
|
"""广场 Bot 响应(公开信息)"""
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
avatar_url: Optional[str] = None
|
||||||
|
owner_name: Optional[str] = None
|
||||||
|
suggestions: Optional[List[str]] = None
|
||||||
|
copy_count: int = 0 # 被复制次数
|
||||||
|
created_at: str
|
||||||
|
updated_at: str
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceListResponse(BaseModel):
|
||||||
|
"""广场列表响应"""
|
||||||
|
bots: List[MarketplaceBotResponse]
|
||||||
|
total: int
|
||||||
|
|
||||||
|
|
||||||
# --- 会话相关 ---
|
# --- 会话相关 ---
|
||||||
class SessionCreate(BaseModel):
|
class SessionCreate(BaseModel):
|
||||||
"""创建会话请求"""
|
"""创建会话请求"""
|
||||||
@ -802,6 +862,58 @@ async def migrate_bot_settings_to_jsonb():
|
|||||||
logger.info("Settings column already exists, skipping migration")
|
logger.info("Settings column already exists, skipping migration")
|
||||||
|
|
||||||
|
|
||||||
|
async def migrate_add_marketplace_fields():
|
||||||
|
"""
|
||||||
|
添加智能体广场相关字段到 agent_bots 表
|
||||||
|
"""
|
||||||
|
pool = get_db_pool_manager().pool
|
||||||
|
|
||||||
|
async with pool.connection() as conn:
|
||||||
|
async with conn.cursor() as cursor:
|
||||||
|
# 1. 添加 is_published 字段
|
||||||
|
await cursor.execute("""
|
||||||
|
SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'agent_bots' AND column_name = 'is_published'
|
||||||
|
""")
|
||||||
|
has_is_published = await cursor.fetchone()
|
||||||
|
|
||||||
|
if not has_is_published:
|
||||||
|
logger.info("Adding is_published column to agent_bots table")
|
||||||
|
await cursor.execute("""
|
||||||
|
ALTER TABLE agent_bots
|
||||||
|
ADD COLUMN is_published BOOLEAN DEFAULT FALSE
|
||||||
|
""")
|
||||||
|
# 创建部分索引,只索引发布的 bots
|
||||||
|
await cursor.execute("""
|
||||||
|
CREATE INDEX idx_agent_bots_is_published
|
||||||
|
ON agent_bots(is_published) WHERE is_published = TRUE
|
||||||
|
""")
|
||||||
|
logger.info("is_published column added successfully")
|
||||||
|
|
||||||
|
# 2. 添加 copied_from 字段
|
||||||
|
await cursor.execute("""
|
||||||
|
SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'agent_bots' AND column_name = 'copied_from'
|
||||||
|
""")
|
||||||
|
has_copied_from = await cursor.fetchone()
|
||||||
|
|
||||||
|
if not has_copied_from:
|
||||||
|
logger.info("Adding copied_from column to agent_bots table")
|
||||||
|
await cursor.execute("""
|
||||||
|
ALTER TABLE agent_bots
|
||||||
|
ADD COLUMN copied_from UUID REFERENCES agent_bots(id) ON DELETE SET NULL
|
||||||
|
""")
|
||||||
|
await cursor.execute("""
|
||||||
|
CREATE INDEX idx_agent_bots_copied_from ON agent_bots(copied_from)
|
||||||
|
""")
|
||||||
|
logger.info("copied_from column added successfully")
|
||||||
|
|
||||||
|
await conn.commit()
|
||||||
|
logger.info("Marketplace fields migration completed")
|
||||||
|
|
||||||
|
|
||||||
async def init_bot_manager_tables():
|
async def init_bot_manager_tables():
|
||||||
"""
|
"""
|
||||||
初始化 Bot Manager 相关的所有数据库表
|
初始化 Bot Manager 相关的所有数据库表
|
||||||
@ -813,6 +925,8 @@ async def init_bot_manager_tables():
|
|||||||
await migrate_bot_settings_to_jsonb()
|
await migrate_bot_settings_to_jsonb()
|
||||||
# 2. User 和 shares 迁移
|
# 2. User 和 shares 迁移
|
||||||
await migrate_bot_owner_and_shares()
|
await migrate_bot_owner_and_shares()
|
||||||
|
# 3. Marketplace 字段迁移
|
||||||
|
await migrate_add_marketplace_fields()
|
||||||
|
|
||||||
# SQL 表创建语句
|
# SQL 表创建语句
|
||||||
tables_sql = [
|
tables_sql = [
|
||||||
@ -1205,7 +1319,8 @@ async def get_bots(authorization: Optional[str] = Header(None)):
|
|||||||
# 管理员可以看到所有 Bot
|
# 管理员可以看到所有 Bot
|
||||||
await cursor.execute("""
|
await cursor.execute("""
|
||||||
SELECT b.id, b.name, b.bot_id, b.created_at, b.updated_at, b.settings,
|
SELECT 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,
|
||||||
|
b.is_published, b.copied_from
|
||||||
FROM agent_bots b
|
FROM agent_bots b
|
||||||
LEFT JOIN agent_user u ON b.owner_id = u.id
|
LEFT JOIN agent_user u ON b.owner_id = u.id
|
||||||
ORDER BY b.created_at DESC
|
ORDER BY b.created_at DESC
|
||||||
@ -1219,6 +1334,8 @@ async def get_bots(authorization: Optional[str] = Header(None)):
|
|||||||
bot_id=str(row[0]), # bot_id 也指向主键 id
|
bot_id=str(row[0]), # bot_id 也指向主键 id
|
||||||
is_owner=True,
|
is_owner=True,
|
||||||
is_shared=False,
|
is_shared=False,
|
||||||
|
is_published=row[8] if row[8] else False,
|
||||||
|
copied_from=str(row[9]) if row[9] else None,
|
||||||
owner={"id": str(row[6]), "username": row[7]} if row[6] else None,
|
owner={"id": str(row[6]), "username": row[7]} if row[6] else None,
|
||||||
role=None,
|
role=None,
|
||||||
description=row[5].get('description') if row[5] else None,
|
description=row[5].get('description') if row[5] else None,
|
||||||
@ -1234,7 +1351,8 @@ async def get_bots(authorization: Optional[str] = Header(None)):
|
|||||||
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,
|
||||||
s.role, s.shared_at, s.expires_at
|
s.role, s.shared_at, s.expires_at,
|
||||||
|
b.is_published, b.copied_from
|
||||||
FROM agent_bots b
|
FROM agent_bots b
|
||||||
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
|
||||||
@ -1253,6 +1371,8 @@ async def get_bots(authorization: Optional[str] = Header(None)):
|
|||||||
bot_id=str(row[0]), # bot_id 也指向主键 id
|
bot_id=str(row[0]), # bot_id 也指向主键 id
|
||||||
is_owner=(row[6] is not None and str(row[6]) == user_id),
|
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_shared=(row[6] is not None and str(row[6]) != user_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,
|
owner={"id": str(row[6]), "username": row[7]} if row[6] is not None else None,
|
||||||
role=row[8] if row[8] is not None else None,
|
role=row[8] if row[8] is not None else None,
|
||||||
shared_at=datetime_to_str(row[9]) if row[9] is not None else None,
|
shared_at=datetime_to_str(row[9]) if row[9] is not None else None,
|
||||||
@ -1486,7 +1606,7 @@ async def get_bot_settings(bot_uuid: str, authorization: Optional[str] = Header(
|
|||||||
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, settings, updated_at
|
SELECT id, settings, updated_at, is_published, copied_from
|
||||||
FROM agent_bots
|
FROM agent_bots
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
""", (bot_uuid,))
|
""", (bot_uuid,))
|
||||||
@ -1495,7 +1615,7 @@ async def get_bot_settings(bot_uuid: str, authorization: Optional[str] = Header(
|
|||||||
if not row:
|
if not row:
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
raise HTTPException(status_code=404, detail="Bot not found")
|
||||||
|
|
||||||
bot_id, settings_json, updated_at = row
|
bot_id, settings_json, updated_at, is_published, copied_from = row
|
||||||
settings = settings_json if settings_json else {}
|
settings = settings_json if settings_json else {}
|
||||||
|
|
||||||
# 获取关联的模型信息
|
# 获取关联的模型信息
|
||||||
@ -1538,6 +1658,8 @@ async def get_bot_settings(bot_uuid: str, authorization: Optional[str] = Header(
|
|||||||
enable_thinking=settings.get('enable_thinking', False),
|
enable_thinking=settings.get('enable_thinking', False),
|
||||||
tool_response=settings.get('tool_response', False),
|
tool_response=settings.get('tool_response', False),
|
||||||
skills=settings.get('skills'),
|
skills=settings.get('skills'),
|
||||||
|
is_published=is_published if is_published else False,
|
||||||
|
copied_from=str(copied_from) if copied_from else None,
|
||||||
updated_at=datetime_to_str(updated_at)
|
updated_at=datetime_to_str(updated_at)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1617,7 +1739,10 @@ async def update_bot_settings(
|
|||||||
if request.skills is not None:
|
if request.skills is not None:
|
||||||
update_json['skills'] = request.skills
|
update_json['skills'] = request.skills
|
||||||
|
|
||||||
if not update_json:
|
# is_published 是表字段,不在 settings JSON 中
|
||||||
|
need_update_published = request.is_published is not None
|
||||||
|
|
||||||
|
if not update_json and not need_update_published:
|
||||||
raise HTTPException(status_code=400, detail="No fields to update")
|
raise HTTPException(status_code=400, detail="No fields to update")
|
||||||
|
|
||||||
async with pool.connection() as conn:
|
async with pool.connection() as conn:
|
||||||
@ -1632,12 +1757,19 @@ async def update_bot_settings(
|
|||||||
existing_settings = row[1] if row[1] else {}
|
existing_settings = row[1] if row[1] else {}
|
||||||
existing_settings.update(update_json)
|
existing_settings.update(update_json)
|
||||||
|
|
||||||
# 更新设置
|
# 更新设置和is_published字段
|
||||||
await cursor.execute("""
|
if need_update_published:
|
||||||
UPDATE agent_bots
|
await cursor.execute("""
|
||||||
SET settings = %s, updated_at = NOW()
|
UPDATE agent_bots
|
||||||
WHERE id = %s
|
SET settings = %s, is_published = %s, updated_at = NOW()
|
||||||
""", (json.dumps(existing_settings), bot_uuid))
|
WHERE id = %s
|
||||||
|
""", (json.dumps(existing_settings), request.is_published, bot_uuid))
|
||||||
|
else:
|
||||||
|
await cursor.execute("""
|
||||||
|
UPDATE agent_bots
|
||||||
|
SET settings = %s, updated_at = NOW()
|
||||||
|
WHERE id = %s
|
||||||
|
""", (json.dumps(existing_settings), bot_uuid))
|
||||||
|
|
||||||
await conn.commit()
|
await conn.commit()
|
||||||
|
|
||||||
@ -2985,3 +3117,411 @@ async def remove_bot_share(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 智能体广场 API ==============
|
||||||
|
|
||||||
|
@router.get("/api/v1/marketplace/bots", response_model=MarketplaceListResponse)
|
||||||
|
async def get_marketplace_bots(
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 20,
|
||||||
|
search: str = "",
|
||||||
|
authorization: Optional[str] = Header(None)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
获取广场智能体列表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: 页码(从1开始)
|
||||||
|
page_size: 每页数量
|
||||||
|
search: 搜索关键词(名称/描述)
|
||||||
|
authorization: Bearer token(可选,用于判断是否已登录)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MarketplaceListResponse: 广场智能体列表
|
||||||
|
"""
|
||||||
|
# 不强制要求登录,但如果有 token 则验证
|
||||||
|
user_valid, _, _ = await verify_user_auth(authorization)
|
||||||
|
|
||||||
|
pool = get_db_pool_manager().pool
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
|
||||||
|
async with pool.connection() as conn:
|
||||||
|
async with conn.cursor() as cursor:
|
||||||
|
# 构建搜索条件
|
||||||
|
search_condition = ""
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if search:
|
||||||
|
search_condition = "AND (b.name ILIKE %s OR b.settings->>'description' ILIKE %s)"
|
||||||
|
search_param = f"%{search}%"
|
||||||
|
params.extend([search_param, search_param])
|
||||||
|
|
||||||
|
# 获取总数
|
||||||
|
count_query = f"""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM agent_bots b
|
||||||
|
WHERE b.is_published = TRUE
|
||||||
|
{search_condition}
|
||||||
|
"""
|
||||||
|
await cursor.execute(count_query, params)
|
||||||
|
total = (await cursor.fetchone())[0]
|
||||||
|
|
||||||
|
# 获取列表
|
||||||
|
list_query = f"""
|
||||||
|
SELECT b.id, b.name, b.settings, b.created_at, b.updated_at,
|
||||||
|
u.username as owner_name
|
||||||
|
FROM agent_bots b
|
||||||
|
LEFT JOIN agent_user u ON b.owner_id = u.id
|
||||||
|
WHERE b.is_published = TRUE
|
||||||
|
{search_condition}
|
||||||
|
ORDER BY b.updated_at DESC
|
||||||
|
LIMIT %s OFFSET %s
|
||||||
|
"""
|
||||||
|
params.extend([page_size, offset])
|
||||||
|
await cursor.execute(list_query, params)
|
||||||
|
rows = await cursor.fetchall()
|
||||||
|
|
||||||
|
bots = []
|
||||||
|
for row in rows:
|
||||||
|
settings = row[2] if row[2] else {}
|
||||||
|
# 计算被复制次数
|
||||||
|
await cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM agent_bots WHERE copied_from = %s
|
||||||
|
""", (row[0],))
|
||||||
|
copy_count = (await cursor.fetchone())[0]
|
||||||
|
|
||||||
|
bots.append(MarketplaceBotResponse(
|
||||||
|
id=str(row[0]),
|
||||||
|
name=row[1],
|
||||||
|
description=settings.get('description'),
|
||||||
|
avatar_url=settings.get('avatar_url'),
|
||||||
|
owner_name=row[5],
|
||||||
|
suggestions=settings.get('suggestions'),
|
||||||
|
copy_count=copy_count,
|
||||||
|
created_at=datetime_to_str(row[3]),
|
||||||
|
updated_at=datetime_to_str(row[4])
|
||||||
|
))
|
||||||
|
|
||||||
|
return MarketplaceListResponse(bots=bots, total=total)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/marketplace/bots/{bot_uuid}", response_model=MarketplaceBotResponse)
|
||||||
|
async def get_marketplace_bot_detail(
|
||||||
|
bot_uuid: str,
|
||||||
|
authorization: Optional[str] = Header(None)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
获取广场智能体详情
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot_uuid: Bot UUID
|
||||||
|
authorization: Bearer token(可选)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MarketplaceBotResponse: 智能体公开信息
|
||||||
|
"""
|
||||||
|
pool = get_db_pool_manager().pool
|
||||||
|
|
||||||
|
async with pool.connection() as conn:
|
||||||
|
async with conn.cursor() as cursor:
|
||||||
|
await cursor.execute("""
|
||||||
|
SELECT b.id, b.name, b.settings, b.created_at, b.updated_at,
|
||||||
|
u.username as owner_name, b.is_published
|
||||||
|
FROM agent_bots b
|
||||||
|
LEFT JOIN agent_user u ON b.owner_id = u.id
|
||||||
|
WHERE b.id = %s
|
||||||
|
""", (bot_uuid,))
|
||||||
|
row = await cursor.fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="Bot not found")
|
||||||
|
|
||||||
|
if not row[6]: # is_published
|
||||||
|
raise HTTPException(status_code=404, detail="Bot not found in marketplace")
|
||||||
|
|
||||||
|
settings = row[2] if row[2] else {}
|
||||||
|
|
||||||
|
# 计算被复制次数
|
||||||
|
await cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM agent_bots WHERE copied_from = %s
|
||||||
|
""", (bot_uuid,))
|
||||||
|
copy_count = (await cursor.fetchone())[0]
|
||||||
|
|
||||||
|
return MarketplaceBotResponse(
|
||||||
|
id=str(row[0]),
|
||||||
|
name=row[1],
|
||||||
|
description=settings.get('description'),
|
||||||
|
avatar_url=settings.get('avatar_url'),
|
||||||
|
owner_name=row[5],
|
||||||
|
suggestions=settings.get('suggestions'),
|
||||||
|
copy_count=copy_count,
|
||||||
|
created_at=datetime_to_str(row[3]),
|
||||||
|
updated_at=datetime_to_str(row[4])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v1/marketplace/bots/{bot_uuid}/copy", response_model=BotResponse)
|
||||||
|
async def copy_marketplace_bot(
|
||||||
|
bot_uuid: str,
|
||||||
|
authorization: Optional[str] = Header(None)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
复制广场智能体到个人管理
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot_uuid: 要复制的 Bot UUID
|
||||||
|
authorization: Bearer token
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BotResponse: 新创建的 Bot 信息
|
||||||
|
"""
|
||||||
|
user_valid, user_id, user_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:
|
||||||
|
# 获取原始 Bot 信息
|
||||||
|
await cursor.execute("""
|
||||||
|
SELECT id, name, settings, is_published
|
||||||
|
FROM agent_bots
|
||||||
|
WHERE id = %s AND is_published = TRUE
|
||||||
|
""", (bot_uuid,))
|
||||||
|
original = await cursor.fetchone()
|
||||||
|
|
||||||
|
if not original:
|
||||||
|
raise HTTPException(status_code=404, detail="Bot not found in marketplace")
|
||||||
|
|
||||||
|
original_id, original_name, original_settings, _ = original
|
||||||
|
settings = original_settings if original_settings else {}
|
||||||
|
|
||||||
|
# 创建新 Bot(名称加"副本"后缀)
|
||||||
|
new_name = f"{original_name} (副本)"
|
||||||
|
new_bot_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# 只复制部分设置(不复制 system_prompt, MCP配置等)
|
||||||
|
new_settings = {
|
||||||
|
'language': settings.get('language', 'zh'),
|
||||||
|
'avatar_url': settings.get('avatar_url'),
|
||||||
|
'description': settings.get('description'),
|
||||||
|
'suggestions': settings.get('suggestions'),
|
||||||
|
'dataset_ids': settings.get('dataset_ids'),
|
||||||
|
# 不复制的设置:
|
||||||
|
# 'model_id', 'system_prompt', 'enable_memori', 'enable_thinking', 'tool_response', 'skills'
|
||||||
|
'enable_memori': False,
|
||||||
|
'enable_thinking': False,
|
||||||
|
'tool_response': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 插入新 Bot
|
||||||
|
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_row = await cursor.fetchone()
|
||||||
|
new_id, created_at, updated_at = new_row
|
||||||
|
|
||||||
|
await conn.commit()
|
||||||
|
|
||||||
|
# 复制 skills 文件夹
|
||||||
|
copy_skills_folder(str(original_id), str(new_id))
|
||||||
|
|
||||||
|
return BotResponse(
|
||||||
|
id=str(new_id),
|
||||||
|
name=new_name,
|
||||||
|
bot_id=new_bot_id,
|
||||||
|
is_owner=True,
|
||||||
|
is_shared=False,
|
||||||
|
is_published=False,
|
||||||
|
copied_from=str(original_id),
|
||||||
|
owner={"id": str(user_id), "username": user_username},
|
||||||
|
role=None,
|
||||||
|
description=new_settings.get('description'),
|
||||||
|
avatar_url=new_settings.get('avatar_url'),
|
||||||
|
created_at=datetime_to_str(created_at),
|
||||||
|
updated_at=datetime_to_str(updated_at)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/api/v1/bots/{bot_uuid}/publish", response_model=SuccessResponse)
|
||||||
|
async def toggle_bot_publication(
|
||||||
|
bot_uuid: str,
|
||||||
|
authorization: Optional[str] = Header(None)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
切换智能体发布状态(仅所有者可操作)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot_uuid: Bot UUID
|
||||||
|
authorization: Bearer token
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
SuccessResponse: 操作结果
|
||||||
|
"""
|
||||||
|
user_valid, user_id, user_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 id, is_published FROM agent_bots WHERE id = %s AND owner_id = %s
|
||||||
|
""", (bot_uuid, user_id))
|
||||||
|
row = await cursor.fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Only bot owner can toggle publication status"
|
||||||
|
)
|
||||||
|
|
||||||
|
current_status = row[1] if row[1] else False
|
||||||
|
new_status = not current_status
|
||||||
|
|
||||||
|
# 更新状态
|
||||||
|
await cursor.execute("""
|
||||||
|
UPDATE agent_bots
|
||||||
|
SET is_published = %s, updated_at = NOW()
|
||||||
|
WHERE id = %s
|
||||||
|
""", (new_status, bot_uuid))
|
||||||
|
|
||||||
|
await conn.commit()
|
||||||
|
|
||||||
|
action = "发布到" if new_status else "取消发布"
|
||||||
|
return SuccessResponse(
|
||||||
|
success=True,
|
||||||
|
message=f"Bot {action} marketplace successfully"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v1/bots/{bot_uuid}/sync-from-source", response_model=SuccessResponse)
|
||||||
|
async def sync_bot_from_source(
|
||||||
|
bot_uuid: str,
|
||||||
|
authorization: Optional[str] = Header(None)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
从原始智能体同步配置(仅限从广场复制的智能体)
|
||||||
|
|
||||||
|
同步以下配置:
|
||||||
|
- 系统提示词
|
||||||
|
- MCP 服务器配置
|
||||||
|
- 技能配置
|
||||||
|
- skills 文件夹
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot_uuid: Bot UUID
|
||||||
|
authorization: Bearer token
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
SuccessResponse: 操作结果
|
||||||
|
"""
|
||||||
|
user_valid, user_id, user_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:
|
||||||
|
# 获取当前 Bot 信息
|
||||||
|
await cursor.execute("""
|
||||||
|
SELECT id, copied_from, settings, owner_id
|
||||||
|
FROM agent_bots
|
||||||
|
WHERE id = %s
|
||||||
|
""", (bot_uuid,))
|
||||||
|
current_bot = await cursor.fetchone()
|
||||||
|
|
||||||
|
if not current_bot:
|
||||||
|
raise HTTPException(status_code=404, detail="Bot not found")
|
||||||
|
|
||||||
|
current_id, copied_from, current_settings, owner_id = current_bot
|
||||||
|
|
||||||
|
# 检查是否是从广场复制的
|
||||||
|
if not copied_from:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="This bot is not copied from marketplace"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检查是否是所有者
|
||||||
|
if str(owner_id) != str(user_id):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Only bot owner can sync from source"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取原始 Bot 信息
|
||||||
|
await cursor.execute("""
|
||||||
|
SELECT id, settings
|
||||||
|
FROM agent_bots
|
||||||
|
WHERE id = %s AND is_published = TRUE
|
||||||
|
""", (copied_from,))
|
||||||
|
source_bot = await cursor.fetchone()
|
||||||
|
|
||||||
|
if not source_bot:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Source bot not found or not published"
|
||||||
|
)
|
||||||
|
|
||||||
|
source_id, source_settings = source_bot
|
||||||
|
source_settings = source_settings if source_settings else {}
|
||||||
|
current_settings = current_settings if current_settings else {}
|
||||||
|
|
||||||
|
# 同步配置:系统提示词、MCP、skill
|
||||||
|
current_settings['system_prompt'] = source_settings.get('system_prompt')
|
||||||
|
current_settings['skills'] = source_settings.get('skills')
|
||||||
|
|
||||||
|
# 更新当前 Bot 的设置
|
||||||
|
await cursor.execute("""
|
||||||
|
UPDATE agent_bots
|
||||||
|
SET settings = %s, updated_at = NOW()
|
||||||
|
WHERE id = %s
|
||||||
|
""", (json.dumps(current_settings), bot_uuid))
|
||||||
|
|
||||||
|
# 同步 MCP 服务器配置
|
||||||
|
await cursor.execute("""
|
||||||
|
DELETE FROM agent_mcp_servers WHERE bot_id = %s
|
||||||
|
""", (bot_uuid,))
|
||||||
|
|
||||||
|
await cursor.execute("""
|
||||||
|
SELECT name, type, config, enabled
|
||||||
|
FROM agent_mcp_servers
|
||||||
|
WHERE bot_id = %s
|
||||||
|
""", (copied_from,))
|
||||||
|
source_mcp_servers = await cursor.fetchall()
|
||||||
|
|
||||||
|
for server in source_mcp_servers:
|
||||||
|
server_name, server_type, server_config, server_enabled = server
|
||||||
|
await cursor.execute("""
|
||||||
|
INSERT INTO agent_mcp_servers (bot_id, name, type, config, enabled)
|
||||||
|
VALUES (%s, %s, %s, %s, %s)
|
||||||
|
""", (bot_uuid, server_name, server_type, json.dumps(server_config), server_enabled))
|
||||||
|
|
||||||
|
await conn.commit()
|
||||||
|
|
||||||
|
# 复制 skills 文件夹
|
||||||
|
copy_skills_folder(str(copied_from), str(bot_uuid))
|
||||||
|
|
||||||
|
return SuccessResponse(
|
||||||
|
success=True,
|
||||||
|
message="Bot synced from source successfully"
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
BIN
suggestion-chips-dark.png
Normal file
BIN
suggestion-chips-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
BIN
suggestion-chips-light.png
Normal file
BIN
suggestion-chips-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
Loading…
Reference in New Issue
Block a user