refactor: Repository 重构(agent_config → agent)+ 新增 agent_router

- 删除旧的 agent_config_repository,新增 agent_repository
- 更新 models_business.py 中 Agent 模型
- 新增 agent_router,将 agent 配置管理从 chat_router 拆分
- 删除废弃的迁移文件 20260518_0001_agent_config_uid.py
- 更新 lifespan、manager.py 中的 Agent 表结构迁移
This commit is contained in:
Wenjie Zhang 2026-05-24 00:45:52 +08:00
parent 01a052de8a
commit 20ac022cf3
8 changed files with 679 additions and 845 deletions

View File

@ -1,265 +0,0 @@
from __future__ import annotations
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.storage.postgres.models_business import AgentConfig
from yuxi.utils.datetime_utils import utc_now_naive
# 默认配置名称
DEFAULT_CONFIG_NAME = "初始配置"
def _merge_skill_slugs(current_slugs: object, new_slugs: list[str]) -> list[str]:
merged: list[str] = []
seen: set[str] = set()
for value in [*(current_slugs if isinstance(current_slugs, list) else []), *new_slugs]:
if not isinstance(value, str):
continue
slug = value.strip()
if not slug or slug in seen:
continue
seen.add(slug)
merged.append(slug)
return merged
class AgentConfigRepository:
def __init__(self, db_session: AsyncSession):
self.db = db_session
async def list_by_user_agent(self, *, uid: str, agent_id: str) -> list[AgentConfig]:
result = await self.db.execute(
select(AgentConfig)
.where(AgentConfig.uid == uid, AgentConfig.agent_id == agent_id)
.order_by(AgentConfig.is_default.desc(), AgentConfig.id.asc())
)
return list(result.scalars().all())
async def get_by_id(self, config_id: int) -> AgentConfig | None:
result = await self.db.execute(select(AgentConfig).where(AgentConfig.id == config_id))
return result.scalar_one_or_none()
async def _get_by_id_for_update(self, config_id: int) -> AgentConfig | None:
result = await self.db.execute(select(AgentConfig).where(AgentConfig.id == config_id).with_for_update())
return result.scalar_one_or_none()
async def set_default(self, *, config: AgentConfig, updated_by: str | None = None) -> AgentConfig:
now = utc_now_naive()
# 先清空该用户+智能体的所有默认配置
await self.db.execute(
update(AgentConfig)
.where(
AgentConfig.uid == config.uid,
AgentConfig.agent_id == config.agent_id,
)
.values(is_default=False, updated_at=now, updated_by=updated_by)
)
# 再设置目标配置为默认
config.is_default = True
config.updated_at = now
config.updated_by = updated_by
await self.db.commit()
await self.db.refresh(config)
return config
async def get_default(self, *, uid: str, agent_id: str) -> AgentConfig | None:
result = await self.db.execute(
select(AgentConfig).where(
AgentConfig.uid == uid,
AgentConfig.agent_id == agent_id,
AgentConfig.is_default.is_(True),
)
)
return result.scalar_one_or_none()
async def get_or_create_default(
self,
*,
uid: str,
agent_id: str,
created_by: str | None = None,
) -> AgentConfig:
existing = await self.get_default(uid=uid, agent_id=agent_id)
if existing:
return existing
items = await self.list_by_user_agent(uid=uid, agent_id=agent_id)
if items:
return items[0]
config = AgentConfig(
uid=uid,
agent_id=agent_id,
name=DEFAULT_CONFIG_NAME,
description=None,
icon=None,
pics=[],
examples=[],
config_json={},
is_default=True,
created_by=created_by,
updated_by=created_by,
created_at=utc_now_naive(),
updated_at=utc_now_naive(),
)
self.db.add(config)
await self.db.commit()
await self.db.refresh(config)
return config
async def _name_exists(self, *, uid: str, agent_id: str, name: str, exclude_id: int | None) -> bool:
stmt = select(AgentConfig.id).where(
AgentConfig.uid == uid,
AgentConfig.agent_id == agent_id,
AgentConfig.name == name,
)
if exclude_id is not None:
stmt = stmt.where(AgentConfig.id != exclude_id)
result = await self.db.execute(stmt)
return result.scalar_one_or_none() is not None
async def ensure_unique_name(
self,
*,
uid: str,
agent_id: str,
desired_name: str,
exclude_id: int | None = None,
) -> str:
candidate = desired_name.strip() or "未命名配置"
if not await self._name_exists(uid=uid, agent_id=agent_id, name=candidate, exclude_id=exclude_id):
return candidate
base = f"{candidate}-副本"
if not await self._name_exists(uid=uid, agent_id=agent_id, name=base, exclude_id=exclude_id):
return base
idx = 2
while True:
candidate2 = f"{base}{idx}"
if not await self._name_exists(uid=uid, agent_id=agent_id, name=candidate2, exclude_id=exclude_id):
return candidate2
idx += 1
async def create(
self,
*,
uid: str,
agent_id: str,
name: str,
description: str | None = None,
icon: str | None = None,
pics: list[str] | None = None,
examples: list[str] | None = None,
config_json: dict | None = None,
is_default: bool = False,
created_by: str | None = None,
) -> AgentConfig:
unique_name = await self.ensure_unique_name(
uid=uid,
agent_id=agent_id,
desired_name=name,
exclude_id=None,
)
config = AgentConfig(
uid=uid,
agent_id=agent_id,
name=unique_name,
description=description,
icon=icon,
pics=pics or [],
examples=examples or [],
config_json=config_json or {},
is_default=False,
created_by=created_by,
updated_by=created_by,
created_at=utc_now_naive(),
updated_at=utc_now_naive(),
)
self.db.add(config)
await self.db.commit()
await self.db.refresh(config)
if is_default:
return await self.set_default(config=config, updated_by=created_by)
return config
async def update(
self,
config: AgentConfig,
*,
name: str | None = None,
description: str | None = None,
icon: str | None = None,
pics: list[str] | None = None,
examples: list[str] | None = None,
config_json: dict | None = None,
updated_by: str | None = None,
) -> AgentConfig:
if name is not None:
config.name = await self.ensure_unique_name(
uid=config.uid,
agent_id=config.agent_id,
desired_name=name,
exclude_id=config.id,
)
if description is not None:
config.description = description
if icon is not None:
config.icon = icon
if pics is not None:
config.pics = pics
if examples is not None:
config.examples = examples
if config_json is not None:
config.config_json = config_json
config.updated_by = updated_by
config.updated_at = utc_now_naive()
await self.db.commit()
await self.db.refresh(config)
return config
async def delete(self, *, config: AgentConfig, updated_by: str | None = None) -> None:
uid = config.uid
agent_id = config.agent_id
was_default = bool(config.is_default)
await self.db.delete(config)
await self.db.commit()
remaining = await self.list_by_user_agent(uid=uid, agent_id=agent_id)
if not remaining:
await self.get_or_create_default(uid=uid, agent_id=agent_id, created_by=updated_by)
return
if was_default:
await self.set_default(config=remaining[0], updated_by=updated_by)
async def add_skills_to_config_json(self, *, agent_config_id: int, new_slugs: list[str]) -> bool:
"""在 config_json.context.skills 中追加 skills并保持顺序去重。
Args:
agent_config_id: AgentConfig ID
new_slugs: 要追加的技能 slug 列表会自动去重
Returns:
是否找到并更新了配置
"""
config = await self._get_by_id_for_update(agent_config_id)
if not config:
return False
config_json = dict(config.config_json or {})
context = dict(config_json.get("context") or {})
context["skills"] = _merge_skill_slugs(context.get("skills"), new_slugs)
config_json["context"] = context
config.config_json = config_json
config.updated_at = utc_now_naive()
await self.db.commit()
await self.db.refresh(config)
return True

View File

@ -0,0 +1,324 @@
from __future__ import annotations
import re
import uuid
from typing import Any
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.agents.buildin import agent_manager
from yuxi.storage.postgres.models_business import Agent, User
from yuxi.utils.datetime_utils import utc_now_naive
DEFAULT_AGENT_SLUG = "default-chatbot"
DEFAULT_AGENT_NAME = "智能助手"
DEFAULT_AGENT_BACKEND_ID = "ChatbotAgent"
DEFAULT_SHARE_CONFIG = {"access_level": "global", "department_ids": [], "user_uids": []}
ACCESS_LEVELS = {"global", "department", "user"}
ADMIN_ROLES = {"admin", "superadmin"}
def is_builtin_agent(agent: Agent) -> bool:
return agent.slug == DEFAULT_AGENT_SLUG
def _normalize_department_ids(department_ids: list | None) -> list[int]:
return [int(department_id) for department_id in department_ids or []]
def _normalize_user_uids(user_uids: list | None) -> list[str]:
return [uid for uid in (str(uid).strip() for uid in user_uids or []) if uid]
def normalize_agent_share_config(
share_config: dict | None,
*,
user_uid: str | None = None,
department_id: int | str | None = None,
force_private: bool = False,
) -> dict:
if force_private:
if not user_uid:
raise ValueError("私有智能体必须绑定创建用户")
return {"access_level": "user", "department_ids": [], "user_uids": [str(user_uid)]}
config = share_config or {}
access_level = config.get("access_level") or "global"
if access_level not in ACCESS_LEVELS:
raise ValueError("无效的智能体权限等级")
if access_level == "global":
return DEFAULT_SHARE_CONFIG.copy()
if access_level == "department":
department_ids = _normalize_department_ids(config.get("department_ids"))
if department_id is not None:
department_ids.append(int(department_id))
department_ids = sorted(set(department_ids))
if not department_ids:
raise ValueError("部门共享至少需要选择一个部门")
return {"access_level": "department", "department_ids": department_ids, "user_uids": []}
user_uids = _normalize_user_uids(config.get("user_uids"))
if user_uid:
user_uids.append(str(user_uid))
user_uids = sorted(set(user_uids))
if not user_uids:
raise ValueError("指定人可访问至少需要选择一个用户")
return {"access_level": "user", "department_ids": [], "user_uids": user_uids}
def user_can_access_agent(user: User, agent: Agent) -> bool:
if user.role == "superadmin":
return True
user_uid = str(user.uid)
if agent.created_by == user_uid:
return True
share_config = agent.share_config or DEFAULT_SHARE_CONFIG.copy()
access_level = share_config.get("access_level")
if access_level == "global":
return True
if access_level == "department":
if user.department_id is None:
return False
try:
return int(user.department_id) in [int(value) for value in share_config.get("department_ids") or []]
except (TypeError, ValueError):
return False
if access_level == "user":
return user_uid in (share_config.get("user_uids") or [])
return False
def user_can_manage_agent(user: User, agent: Agent) -> bool:
return user.role in ADMIN_ROLES or agent.created_by == str(user.uid)
def _slugify(value: str | None) -> str:
base = re.sub(r"[^a-zA-Z0-9_-]+", "-", (value or "").strip().lower()).strip("-")
return base[:56] or f"agent-{uuid.uuid4().hex[:12]}"
class AgentRepository:
def __init__(self, db_session: AsyncSession):
self.db = db_session
async def ensure_default_agent(self, *, created_by: str | None = None) -> Agent:
agent = await self.get_by_slug(DEFAULT_AGENT_SLUG)
if agent:
needs_update = False
if agent.share_config != DEFAULT_SHARE_CONFIG:
agent.share_config = DEFAULT_SHARE_CONFIG.copy()
needs_update = True
if not agent.is_default:
return await self.set_default(agent=agent, updated_by=created_by)
if needs_update:
agent.updated_by = created_by
agent.updated_at = utc_now_naive()
await self.db.commit()
await self.db.refresh(agent)
return agent
agent = Agent(
slug=DEFAULT_AGENT_SLUG,
backend_id=DEFAULT_AGENT_BACKEND_ID,
name=DEFAULT_AGENT_NAME,
description=None,
icon=None,
pics=[],
config_json={"context": {}},
share_config=DEFAULT_SHARE_CONFIG.copy(),
is_default=True,
created_by=created_by,
updated_by=created_by,
created_at=utc_now_naive(),
updated_at=utc_now_naive(),
)
self.db.add(agent)
await self.db.commit()
await self.db.refresh(agent)
return agent
async def list_visible(self, *, user: User) -> list[Agent]:
result = await self.db.execute(select(Agent).order_by(Agent.is_default.desc(), Agent.id.asc()))
agents = list(result.scalars().all())
if user.role == "superadmin":
return agents
return [agent for agent in agents if user_can_access_agent(user, agent)]
async def get_by_slug(self, slug: str) -> Agent | None:
result = await self.db.execute(select(Agent).where(Agent.slug == slug))
return result.scalar_one_or_none()
async def get_visible_by_slug(self, *, slug: str, user: User) -> Agent | None:
agent = await self.get_by_slug(slug)
if agent and user_can_access_agent(user, agent):
return agent
return None
async def get_default(self) -> Agent | None:
result = await self.db.execute(select(Agent).where(Agent.is_default.is_(True)))
return result.scalar_one_or_none()
async def set_default(self, *, agent: Agent, updated_by: str | None = None) -> Agent:
if not is_builtin_agent(agent):
raise ValueError("默认智能体已固定为内置智能助手")
share_config = agent.share_config or DEFAULT_SHARE_CONFIG.copy()
if share_config.get("access_level") != "global":
raise ValueError("内置智能体必须全局共享")
now = utc_now_naive()
await self.db.execute(update(Agent).where(Agent.is_default.is_(True)).values(is_default=False, updated_at=now))
agent.is_default = True
agent.updated_by = updated_by
agent.updated_at = now
await self.db.commit()
await self.db.refresh(agent)
return agent
async def _slug_exists(self, slug: str) -> bool:
result = await self.db.execute(select(Agent.id).where(Agent.slug == slug))
return result.scalar_one_or_none() is not None
async def _unique_slug(self, desired: str | None, name: str) -> str:
base = _slugify(desired or name)
candidate = base
idx = 2
while await self._slug_exists(candidate):
suffix = f"-{idx}"
candidate = f"{base[: 80 - len(suffix)]}{suffix}"
idx += 1
return candidate
async def create(
self,
*,
name: str,
backend_id: str,
slug: str | None = None,
description: str | None = None,
icon: str | None = None,
pics: list[str] | None = None,
config_json: dict | None = None,
share_config: dict | None = None,
is_default: bool = False,
created_by: str | None = None,
creator: User | None = None,
) -> Agent:
normalized_share_config = normalize_agent_share_config(
share_config,
user_uid=str(creator.uid) if creator else created_by,
department_id=creator.department_id if creator else None,
force_private=bool(creator and creator.role not in ADMIN_ROLES),
)
if is_default and normalized_share_config.get("access_level") != "global":
raise ValueError("默认智能体必须全局共享")
agent = Agent(
slug=await self._unique_slug(slug, name),
backend_id=backend_id,
name=name.strip() or "未命名智能体",
description=description,
icon=icon,
pics=pics or [],
config_json=config_json or {"context": {}},
share_config=normalized_share_config,
is_default=False,
created_by=created_by,
updated_by=created_by,
created_at=utc_now_naive(),
updated_at=utc_now_naive(),
)
self.db.add(agent)
await self.db.commit()
await self.db.refresh(agent)
if is_default:
return await self.set_default(agent=agent, updated_by=created_by)
return agent
async def update(
self,
agent: Agent,
*,
name: str | None = None,
description: str | None = None,
icon: str | None = None,
pics: list[str] | None = None,
config_json: dict | None = None,
share_config: dict | None = None,
updated_by: str | None = None,
updater: User | None = None,
) -> Agent:
if name is not None:
agent.name = name.strip() or "未命名智能体"
if description is not None:
agent.description = description
if icon is not None:
agent.icon = icon
if pics is not None:
agent.pics = pics
if config_json is not None:
agent.config_json = config_json
if share_config is not None:
if is_builtin_agent(agent):
agent.share_config = DEFAULT_SHARE_CONFIG.copy()
else:
normalized_share_config = normalize_agent_share_config(
share_config,
user_uid=str(updater.uid) if updater else updated_by,
department_id=updater.department_id if updater else None,
force_private=bool(updater and updater.role not in ADMIN_ROLES),
)
agent.share_config = normalized_share_config
agent.updated_by = updated_by
agent.updated_at = utc_now_naive()
await self.db.commit()
await self.db.refresh(agent)
return agent
async def delete(self, *, agent: Agent) -> None:
await self.db.delete(agent)
await self.db.commit()
async def serialize(
self,
agent: Agent,
*,
user: User,
include_configurable_items: bool = False,
backend_info_cache: dict[tuple[str, bool, str], dict] | None = None,
) -> dict[str, Any]:
data = agent.to_dict()
data["can_manage"] = user_can_manage_agent(user, agent)
data["is_builtin"] = is_builtin_agent(agent)
data["permission_locked"] = is_builtin_agent(agent)
backend = agent_manager.get_agent(agent.backend_id)
if backend:
cache_key = (agent.backend_id, include_configurable_items, user.role)
backend_info = backend_info_cache.get(cache_key) if backend_info_cache is not None else None
if backend_info is None:
backend_info = await backend.get_info(
include_configurable_items=include_configurable_items,
user_role=user.role,
db=self.db if include_configurable_items else None,
user=user if include_configurable_items else None,
)
if backend_info_cache is not None:
backend_info_cache[cache_key] = backend_info
data["capabilities"] = backend_info.get("capabilities", [])
data["metadata"] = backend_info.get("metadata", {})
if include_configurable_items:
data["configurable_items"] = backend_info.get("configurable_items", {})
else:
data["capabilities"] = []
data["metadata"] = {}
if include_configurable_items:
data["configurable_items"] = {}
return data

View File

@ -1,136 +0,0 @@
"""Move agent configs from department scope to user scope.
Revision ID: 20260518_0001
Revises:
Create Date: 2026-05-18
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
revision = "20260518_0001"
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column("agent_configs", sa.Column("uid", sa.String(), nullable=True))
op.execute(
"""
UPDATE agent_configs ac
SET uid = u.uid
FROM users u
WHERE ac.uid IS NULL
AND ac.created_by ~ '^[0-9]+$'
AND u.id = ac.created_by::integer
"""
)
op.execute(
"""
UPDATE agent_configs ac
SET uid = u.uid
FROM users u
WHERE ac.uid IS NULL
AND ac.created_by = u.uid
"""
)
op.execute(
"""
UPDATE agent_configs ac
SET uid = (
SELECT u.uid
FROM users u
WHERE u.department_id = ac.department_id
AND u.is_deleted = 0
ORDER BY
CASE WHEN u.role = 'superadmin' THEN 0 WHEN u.role = 'admin' THEN 1 ELSE 2 END,
u.id ASC
LIMIT 1
)
WHERE ac.uid IS NULL
"""
)
op.execute("DELETE FROM agent_configs WHERE uid IS NULL")
op.execute("DROP INDEX IF EXISTS uq_agent_configs_department_agent_default")
op.execute("DROP INDEX IF EXISTS ix_agent_configs_department_id")
op.drop_constraint("uq_agent_configs_department_agent_name", "agent_configs", type_="unique")
op.drop_constraint("agent_configs_department_id_fkey", "agent_configs", type_="foreignkey")
op.drop_column("agent_configs", "department_id")
op.alter_column("agent_configs", "uid", nullable=False)
op.execute(
"""
WITH ranked AS (
SELECT id, ROW_NUMBER() OVER (PARTITION BY uid, agent_id, name ORDER BY id) AS rn
FROM agent_configs
)
UPDATE agent_configs ac
SET name = LEFT(ac.name, 90) || '-' || ac.id::text
FROM ranked
WHERE ac.id = ranked.id AND ranked.rn > 1
"""
)
op.execute(
"""
WITH ranked AS (
SELECT id, ROW_NUMBER() OVER (PARTITION BY uid, agent_id ORDER BY id) AS rn
FROM agent_configs
WHERE is_default IS TRUE
)
UPDATE agent_configs ac
SET is_default = FALSE
FROM ranked
WHERE ac.id = ranked.id AND ranked.rn > 1
"""
)
op.create_foreign_key("agent_configs_uid_fkey", "agent_configs", "users", ["uid"], ["uid"])
op.create_unique_constraint("uq_agent_configs_uid_agent_name", "agent_configs", ["uid", "agent_id", "name"])
op.create_index("ix_agent_configs_uid", "agent_configs", ["uid"])
op.create_index(
"uq_agent_configs_uid_agent_default",
"agent_configs",
["uid", "agent_id"],
unique=True,
postgresql_where=sa.text("is_default IS TRUE"),
)
def downgrade() -> None:
op.add_column("agent_configs", sa.Column("department_id", sa.Integer(), nullable=True))
op.execute(
"""
UPDATE agent_configs ac
SET department_id = u.department_id
FROM users u
WHERE ac.uid = u.uid
"""
)
op.execute("DELETE FROM agent_configs WHERE department_id IS NULL")
op.execute("DROP INDEX IF EXISTS uq_agent_configs_uid_agent_default")
op.execute("DROP INDEX IF EXISTS ix_agent_configs_uid")
op.drop_constraint("uq_agent_configs_uid_agent_name", "agent_configs", type_="unique")
op.drop_constraint("agent_configs_uid_fkey", "agent_configs", type_="foreignkey")
op.drop_column("agent_configs", "uid")
op.alter_column("agent_configs", "department_id", nullable=False)
op.create_foreign_key("agent_configs_department_id_fkey", "agent_configs", "departments", ["department_id"], ["id"])
op.create_unique_constraint(
"uq_agent_configs_department_agent_name",
"agent_configs",
["department_id", "agent_id", "name"],
)
op.create_index("ix_agent_configs_department_id", "agent_configs", ["department_id"])
op.create_index(
"uq_agent_configs_department_agent_default",
"agent_configs",
["department_id", "agent_id"],
unique=True,
postgresql_where=sa.text("is_default IS TRUE"),
)

View File

@ -14,7 +14,6 @@ from sqlalchemy import (
Integer,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
@ -154,52 +153,44 @@ class AgentEnv(Base):
}
class AgentConfig(Base):
"""智能体配置(按用户隔离,多份可切换)"""
class Agent(Base):
"""用户可管理、可授权、可切换的智能体。"""
__tablename__ = "agent_configs"
__tablename__ = "agents"
id = Column(Integer, primary_key=True, autoincrement=True)
uid = Column(String, ForeignKey("users.uid"), nullable=False, index=True)
agent_id = Column(String(64), nullable=False, index=True)
slug = Column(String(80), nullable=False, unique=True, index=True)
backend_id = Column(String(64), nullable=False, index=True)
name = Column(String(100), nullable=False)
description = Column(String(255), nullable=True)
description = Column(Text, nullable=True)
icon = Column(String(255), nullable=True)
pics = Column(JSON, nullable=False, default=list)
examples = Column(JSON, nullable=False, default=list)
config_json = Column(JSON, nullable=False, default=dict)
share_config = Column(JSON, nullable=False, default=dict)
is_default = Column(Boolean, nullable=False, default=False, index=True)
created_by = Column(String(64), nullable=True)
created_by = Column(String(64), nullable=True, index=True)
updated_by = Column(String(64), nullable=True)
created_at = Column(DateTime, default=utc_now_naive)
updated_at = Column(DateTime, default=utc_now_naive, onupdate=utc_now_naive)
__table_args__ = (
UniqueConstraint("uid", "agent_id", "name", name="uq_agent_configs_uid_agent_name"),
Index(
"uq_agent_configs_uid_agent_default",
"uid",
"agent_id",
unique=True,
postgresql_where=is_default.is_(True),
),
)
__table_args__ = (Index("uq_agents_default", "is_default", unique=True, postgresql_where=is_default.is_(True)),)
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"uid": self.uid,
"agent_id": self.agent_id,
"slug": self.slug,
"agent_id": self.slug,
"backend_id": self.backend_id,
"name": self.name,
"description": self.description,
"icon": self.icon,
"pics": self.pics or [],
"examples": self.examples or [],
"config_json": self.config_json or {},
"share_config": self.share_config or {},
"is_default": bool(self.is_default),
"created_by": self.created_by,
"updated_by": self.updated_by,

View File

@ -3,6 +3,7 @@ import os
from fastapi import APIRouter
from server.routers.auth_router import auth
from server.routers.agent_router import agent_router
from server.routers.chat_router import chat
from server.routers.dashboard_router import dashboard
from server.routers.auth_dept_router import department
@ -25,7 +26,8 @@ router = APIRouter()
# 基础系统接口:健康检查、配置、认证与聊天主链路。
router.include_router(system) # /api/system/* 系统状态与全局配置
router.include_router(auth) # /api/auth/* 登录与用户信息
router.include_router(chat) # /api/chat/* 对话、消息流、运行态
router.include_router(agent_router) # /api/agent/* 智能体管理与运行态
router.include_router(chat) # /api/chat/* 对话线程、消息历史与附件
# 管理与工作台接口:后台任务、权限域以及工具体系配置。
router.include_router(dashboard) # /api/dashboard/* 仪表盘聚合数据

View File

@ -0,0 +1,328 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from server.routers.auth_router import get_admin_user
from server.utils.auth_middleware import get_db, get_required_user
from yuxi.agents.buildin import agent_manager
from yuxi.agents.context import filter_config_by_role
from yuxi.repositories.agent_repository import AgentRepository, is_builtin_agent, user_can_access_agent, user_can_manage_agent
from yuxi.services.agent_run_service import (
cancel_agent_run_view,
create_agent_run_view,
get_active_run_by_thread,
get_agent_run_view,
stream_agent_run_events,
)
from yuxi.services.chat_service import agent_chat, stream_agent_chat
from yuxi.storage.postgres.models_business import User
from yuxi.utils.logging_config import logger
agent_router = APIRouter(prefix="/agent", tags=["agent"])
class AgentCreate(BaseModel):
name: str
backend_id: str = "ChatbotAgent"
slug: str | None = None
description: str | None = None
icon: str | None = None
pics: list[str] | None = None
config_json: dict | None = None
share_config: dict | None = None
set_default: bool = False
class AgentUpdate(BaseModel):
name: str | None = None
description: str | None = None
icon: str | None = None
pics: list[str] | None = None
config_json: dict | None = None
share_config: dict | None = None
class AgentRunCreate(BaseModel):
query: str = Field(..., description="用户输入的问题")
agent_id: str = Field(..., description="智能体 ID")
thread_id: str = Field(..., description="会话线程 ID")
meta: dict = Field(default_factory=dict, description="可选,请求追踪信息,例如 request_id")
image_content: str | None = Field(None, description="可选base64 图片内容")
class AgentChatRequest(BaseModel):
query: str = Field(..., description="用户输入的问题")
agent_id: str = Field(..., description="智能体 ID")
thread_id: str | None = Field(None, description="可选,会话线程 ID不传则自动创建")
meta: dict = Field(default_factory=dict, description="可选,请求追踪信息,例如 request_id")
image_content: str | None = Field(None, description="可选base64 图片内容")
def _backend_info(info: dict) -> dict:
data = dict(info)
data["backend_id"] = data.pop("id", None)
data["type"] = "agent_backend"
return data
def _filter_agent_config_json(backend_id: str, config_json: dict | None, role: str | None) -> dict:
backend = agent_manager.get_agent(backend_id)
context_schema = backend.context_schema if backend else None
return filter_config_by_role(config_json or {}, role, context_schema=context_schema)
async def _serialize_agent(
repo: AgentRepository,
item,
user: User,
*,
include_configurable_items: bool = False,
backend_info_cache: dict[tuple[str, bool, str], dict] | None = None,
) -> dict:
data = await repo.serialize(
item,
user=user,
include_configurable_items=include_configurable_items,
backend_info_cache=backend_info_cache,
)
data["config_json"] = _filter_agent_config_json(item.backend_id, data.get("config_json"), user.role)
return data
@agent_router.get("/backends")
async def list_agent_backends(current_user: User = Depends(get_required_user)):
infos = await agent_manager.get_agents_info(include_configurable_items=False)
return {"backends": [_backend_info(info) for info in infos]}
@agent_router.get("/backends/{backend_id}")
async def get_agent_backend(
backend_id: str,
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
backend = agent_manager.get_agent(backend_id)
if not backend:
raise HTTPException(status_code=404, detail=f"智能体后端 {backend_id} 不存在")
return _backend_info(await backend.get_info(user_role=current_user.role, db=db, user=current_user))
@agent_router.get("")
async def list_agents(current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db)):
repo = AgentRepository(db)
await repo.ensure_default_agent()
items = await repo.list_visible(user=current_user)
backend_info_cache: dict[tuple[str, bool, str], dict] = {}
agents = [
await _serialize_agent(repo, item, current_user, backend_info_cache=backend_info_cache) for item in items
]
return {"agents": agents}
@agent_router.get("/default")
async def get_default_agent(current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db)):
repo = AgentRepository(db)
item = await repo.ensure_default_agent()
if not item or not user_can_access_agent(current_user, item):
raise HTTPException(status_code=404, detail="默认智能体不可访问")
return {"agent": await _serialize_agent(repo, item, current_user, include_configurable_items=True)}
@agent_router.post("")
async def create_agent(
payload: AgentCreate, current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)
):
if not agent_manager.get_agent(payload.backend_id):
raise HTTPException(status_code=404, detail=f"智能体后端 {payload.backend_id} 不存在")
if payload.set_default:
raise HTTPException(status_code=422, detail="默认智能体已固定为内置智能助手")
repo = AgentRepository(db)
try:
item = await repo.create(
name=payload.name,
slug=payload.slug,
backend_id=payload.backend_id,
description=payload.description,
icon=payload.icon,
pics=payload.pics,
config_json=_filter_agent_config_json(payload.backend_id, payload.config_json, current_user.role),
share_config=payload.share_config,
is_default=payload.set_default,
created_by=str(current_user.uid),
creator=current_user,
)
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
return {"agent": await _serialize_agent(repo, item, current_user, include_configurable_items=True)}
@agent_router.get("/{agent_id}")
async def get_agent(agent_id: str, current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db)):
repo = AgentRepository(db)
item = await repo.get_visible_by_slug(slug=agent_id, user=current_user)
if not item:
raise HTTPException(status_code=404, detail="智能体不存在")
return {"agent": await _serialize_agent(repo, item, current_user, include_configurable_items=True)}
@agent_router.put("/{agent_id}")
async def update_agent(
agent_id: str,
payload: AgentUpdate,
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
repo = AgentRepository(db)
item = await repo.get_visible_by_slug(slug=agent_id, user=current_user)
if not item:
raise HTTPException(status_code=404, detail="智能体不存在")
if not user_can_manage_agent(current_user, item):
raise HTTPException(status_code=403, detail="不能编辑非自己创建的智能体")
try:
fields_set = getattr(payload, "model_fields_set", getattr(payload, "__fields_set__", set()))
if "description" in fields_set and payload.description is None:
item.description = None
if "icon" in fields_set and payload.icon is None:
item.icon = None
updated = await repo.update(
item,
name=payload.name,
description=payload.description,
icon=payload.icon,
pics=payload.pics,
config_json=_filter_agent_config_json(item.backend_id, payload.config_json, current_user.role)
if payload.config_json is not None
else None,
share_config=payload.share_config,
updated_by=str(current_user.uid),
updater=current_user,
)
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
return {"agent": await _serialize_agent(repo, updated, current_user, include_configurable_items=True)}
@agent_router.delete("/{agent_id}")
async def delete_agent(
agent_id: str, current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db)
):
repo = AgentRepository(db)
item = await repo.get_visible_by_slug(slug=agent_id, user=current_user)
if not item:
raise HTTPException(status_code=404, detail="智能体不存在")
if not user_can_manage_agent(current_user, item):
raise HTTPException(status_code=403, detail="不能删除非自己创建的智能体")
if is_builtin_agent(item):
raise HTTPException(status_code=409, detail="内置智能体不能删除")
await repo.delete(agent=item)
return {"success": True}
@agent_router.post("/{agent_id}/set_default")
async def set_agent_default(
agent_id: str,
current_user: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
repo = AgentRepository(db)
item = await repo.get_by_slug(agent_id)
if not item:
raise HTTPException(status_code=404, detail="智能体不存在")
try:
updated = await repo.set_default(agent=item, updated_by=str(current_user.uid))
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
return {"agent": await _serialize_agent(repo, updated, current_user, include_configurable_items=True)}
@agent_router.post("/chat")
async def chat_agent(
payload: AgentChatRequest,
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
logger.info(f"query: {payload.query}, agent_id: {payload.agent_id}, meta: {payload.meta}")
return StreamingResponse(
stream_agent_chat(
query=payload.query,
agent_id=payload.agent_id,
thread_id=payload.thread_id,
meta=dict(payload.meta or {}),
image_content=payload.image_content,
current_user=current_user,
db=db,
),
media_type="application/json",
)
@agent_router.post("/chat/sync")
async def chat_agent_sync(
payload: AgentChatRequest,
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
return await agent_chat(
query=payload.query,
agent_id=payload.agent_id,
thread_id=payload.thread_id,
meta=dict(payload.meta or {}),
image_content=payload.image_content,
current_user=current_user,
db=db,
)
@agent_router.post("/runs")
async def create_agent_run(
payload: AgentRunCreate,
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
return await create_agent_run_view(
query=payload.query,
agent_id=payload.agent_id,
thread_id=payload.thread_id,
meta=dict(payload.meta or {}),
image_content=payload.image_content,
current_uid=str(current_user.uid),
db=db,
)
@agent_router.get("/runs/{run_id}")
async def get_agent_run(
run_id: str, current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db)
):
return await get_agent_run_view(run_id=run_id, current_uid=str(current_user.uid), db=db)
@agent_router.post("/runs/{run_id}/cancel")
async def cancel_agent_run(
run_id: str, current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db)
):
return await cancel_agent_run_view(run_id=run_id, current_uid=str(current_user.uid), db=db)
@agent_router.get("/runs/{run_id}/events")
async def stream_run_events(run_id: str, after_seq: str = "0-0", current_user: User = Depends(get_required_user)):
return StreamingResponse(
stream_agent_run_events(run_id=run_id, after_seq=after_seq, current_uid=str(current_user.uid)),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"},
)
@agent_router.get("/thread/{thread_id}/active_run")
async def get_thread_active_run(
thread_id: str,
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
return await get_active_run_by_thread(thread_id=thread_id, current_uid=str(current_user.uid), db=db)

View File

@ -9,20 +9,10 @@ from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.storage.postgres.models_business import User
from server.routers.auth_router import get_admin_user
from server.utils.auth_middleware import get_current_user, get_db, get_required_user
from server.utils.auth_middleware import get_db, get_required_user
from yuxi import config as conf
from yuxi.agents.context import filter_config_by_role
from yuxi.agents.buildin import agent_manager
from yuxi.models import select_model
from yuxi.services.chat_service import agent_chat, get_agent_state_view, stream_agent_chat, stream_agent_resume
from yuxi.services.agent_run_service import (
cancel_agent_run_view,
create_agent_run_view,
get_active_run_by_thread,
get_agent_run_view,
stream_agent_run_events,
)
from yuxi.services.chat_service import get_agent_state_view, stream_agent_resume
from yuxi.repositories.conversation_repository import ConversationRepository
from yuxi.services.conversation_service import (
create_thread_view,
@ -41,7 +31,6 @@ from yuxi.services.thread_files_service import (
save_thread_artifact_to_workspace_view,
)
from yuxi.services.feedback_service import get_message_feedback_view, submit_message_feedback_view
from yuxi.repositories.agent_config_repository import AgentConfigRepository
from yuxi.utils.logging_config import logger
from yuxi.utils.image_processor import process_uploaded_image
from yuxi.utils.paths import VIRTUAL_PATH_PREFIX
@ -63,111 +52,9 @@ class ImageUploadResponse(BaseModel):
error: str | None = None
class AgentConfigCreate(BaseModel):
name: str
description: str | None = None
icon: str | None = None
pics: list[str] | None = None
examples: list[str] | None = None
config_json: dict | None = None
set_default: bool = False
class AgentConfigUpdate(BaseModel):
name: str | None = None
description: str | None = None
icon: str | None = None
pics: list[str] | None = None
examples: list[str] | None = None
config_json: dict | None = None
class AgentRunCreate(BaseModel):
query: str = Field(..., description="用户输入的问题")
agent_config_id: int = Field(..., description="智能体配置 ID后端将据此解析 agent_id 和运行时 context")
thread_id: str = Field(..., description="会话线程 ID")
meta: dict = Field(default_factory=dict, description="可选,请求追踪信息,例如 request_id")
image_content: str | None = Field(None, description="可选base64 图片内容")
class AgentChatRequest(BaseModel):
query: str = Field(..., description="用户输入的问题")
agent_config_id: int = Field(..., description="智能体配置 ID后端将据此解析 agent_id 和运行时 context")
thread_id: str | None = Field(None, description="可选,会话线程 ID不传则自动创建")
meta: dict = Field(default_factory=dict, description="可选,请求追踪信息,例如 request_id")
image_content: str | None = Field(None, description="可选base64 图片内容")
chat = APIRouter(prefix="/chat", tags=["chat"])
async def get_config_user(user: User | None = Depends(get_current_user)) -> User:
if user is None:
raise HTTPException(status_code=401, detail="请登录后再访问", headers={"WWW-Authenticate": "Bearer"})
return user
def _filter_agent_config_json(agent_id: str, config_json: dict | None, role: str | None) -> dict:
agent = agent_manager.get_agent(agent_id)
context_schema = agent.context_schema if agent else None
return filter_config_by_role(config_json or {}, role, context_schema=context_schema)
def _serialize_agent_config(item, role: str | None) -> dict:
data = item.to_dict()
data["config_json"] = _filter_agent_config_json(item.agent_id, data.get("config_json"), role)
return data
# =============================================================================
# > === 智能体管理分组 ===
# =============================================================================
@chat.get("/default_agent")
async def get_default_agent(current_user: User = Depends(get_required_user)):
"""获取默认智能体ID需要登录"""
try:
default_agent_id = conf.default_agent_id
# 如果没有设置默认智能体,尝试获取第一个可用的智能体
if not default_agent_id:
agents = await agent_manager.get_agents_info(include_configurable_items=False)
if agents:
default_agent_id = agents[0].get("id", "")
return {"default_agent_id": default_agent_id}
except Exception as e:
logger.error(f"获取默认智能体出错: {e}")
raise HTTPException(status_code=500, detail=f"获取默认智能体出错: {str(e)}")
@chat.post("/set_default_agent")
async def set_default_agent(request_data: dict = Body(...), current_user=Depends(get_admin_user)):
"""设置默认智能体ID (仅管理员)"""
try:
agent_id = request_data.get("agent_id")
if not agent_id:
raise HTTPException(status_code=422, detail="缺少必需的 agent_id 字段")
# 验证智能体是否存在
agents = await agent_manager.get_agents_info(include_configurable_items=False)
agent_ids = [agent.get("id", "") for agent in agents]
if agent_id not in agent_ids:
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
# 设置默认智能体ID
conf.default_agent_id = agent_id
# 保存配置
conf.save()
return {"success": True, "default_agent_id": agent_id}
except HTTPException as he:
raise he
except Exception as e:
logger.error(f"设置默认智能体出错: {e}")
raise HTTPException(status_code=500, detail=f"设置默认智能体出错: {str(e)}")
@chat.post("/call")
async def call(query: str = Body(...), meta: dict = Body(None), current_user: User = Depends(get_required_user)):
"""调用模型进行简单问答(需要登录)"""
@ -185,305 +72,11 @@ async def call(query: str = Body(...), meta: dict = Body(None), current_user: Us
return {"response": response.content, "request_id": meta["request_id"]}
@chat.get("/agent")
async def get_agent(current_user: User = Depends(get_required_user)):
"""获取所有可用智能体的基本信息(需要登录)"""
agents_info = await agent_manager.get_agents_info(include_configurable_items=False)
return {"agents": agents_info}
@chat.get("/agent/{agent_id}")
async def get_single_agent(
agent_id: str,
current_user: User = Depends(get_config_user),
db: AsyncSession = Depends(get_db),
):
"""获取指定智能体的完整信息(包含配置选项)(需要登录)"""
try:
# 检查智能体是否存在
if not (agent := agent_manager.get_agent(agent_id)):
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
# 获取智能体的完整信息(包含 configurable_items
agent_info = await agent.get_info(user_role=current_user.role, db=db, user=current_user)
return agent_info
except HTTPException:
raise
except Exception as e:
logger.error(f"获取智能体 {agent_id} 信息出错: {e}")
raise HTTPException(status_code=500, detail=f"获取智能体信息出错: {str(e)}")
@chat.get("/agent/{agent_id}/configs")
async def list_agent_configs(
agent_id: str,
current_user: User = Depends(get_config_user),
db: AsyncSession = Depends(get_db),
):
if not agent_manager.get_agent(agent_id):
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
repo = AgentConfigRepository(db)
uid = str(current_user.uid)
items = await repo.list_by_user_agent(uid=uid, agent_id=agent_id)
if not items:
await repo.get_or_create_default(
uid=uid,
agent_id=agent_id,
created_by=uid,
)
items = await repo.list_by_user_agent(uid=uid, agent_id=agent_id)
configs = [
{
"id": item.id,
"name": item.name,
"description": item.description,
"icon": item.icon,
"pics": item.pics or [],
"examples": item.examples or [],
"is_default": bool(item.is_default),
}
for item in items
]
return {"configs": configs}
@chat.get("/agent/{agent_id}/configs/{config_id}")
async def get_agent_config_profile(
agent_id: str,
config_id: int,
current_user: User = Depends(get_config_user),
db: AsyncSession = Depends(get_db),
):
if not agent_manager.get_agent(agent_id):
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
repo = AgentConfigRepository(db)
item = await repo.get_by_id(config_id)
if not item or item.agent_id != agent_id or item.uid != str(current_user.uid):
raise HTTPException(status_code=404, detail="配置不存在")
return {"config": _serialize_agent_config(item, current_user.role)}
@chat.post("/agent/{agent_id}/configs")
async def create_agent_config_profile(
agent_id: str,
payload: AgentConfigCreate,
current_user: User = Depends(get_config_user),
db: AsyncSession = Depends(get_db),
):
if not agent_manager.get_agent(agent_id):
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
repo = AgentConfigRepository(db)
uid = str(current_user.uid)
item = await repo.create(
uid=uid,
agent_id=agent_id,
name=payload.name,
description=payload.description,
icon=payload.icon,
pics=payload.pics,
examples=payload.examples,
config_json=_filter_agent_config_json(agent_id, payload.config_json, current_user.role),
is_default=payload.set_default,
created_by=uid,
)
return {"config": _serialize_agent_config(item, current_user.role)}
@chat.put("/agent/{agent_id}/configs/{config_id}")
async def update_agent_config_profile(
agent_id: str,
config_id: int,
payload: AgentConfigUpdate,
current_user: User = Depends(get_config_user),
db: AsyncSession = Depends(get_db),
):
if not agent_manager.get_agent(agent_id):
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
repo = AgentConfigRepository(db)
item = await repo.get_by_id(config_id)
if not item or item.agent_id != agent_id or item.uid != str(current_user.uid):
raise HTTPException(status_code=404, detail="配置不存在")
updated = await repo.update(
item,
name=payload.name,
description=payload.description,
icon=payload.icon,
pics=payload.pics,
examples=payload.examples,
config_json=_filter_agent_config_json(agent_id, payload.config_json, current_user.role)
if payload.config_json is not None
else None,
updated_by=str(current_user.uid),
)
return {"config": _serialize_agent_config(updated, current_user.role)}
@chat.post("/agent/{agent_id}/configs/{config_id}/set_default")
async def set_agent_config_default(
agent_id: str,
config_id: int,
current_user: User = Depends(get_config_user),
db: AsyncSession = Depends(get_db),
):
if not agent_manager.get_agent(agent_id):
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
repo = AgentConfigRepository(db)
item = await repo.get_by_id(config_id)
if not item or item.agent_id != agent_id or item.uid != str(current_user.uid):
raise HTTPException(status_code=404, detail="配置不存在")
updated = await repo.set_default(config=item, updated_by=str(current_user.uid))
return {"config": _serialize_agent_config(updated, current_user.role)}
@chat.delete("/agent/{agent_id}/configs/{config_id}")
async def delete_agent_config_profile(
agent_id: str,
config_id: int,
current_user: User = Depends(get_config_user),
db: AsyncSession = Depends(get_db),
):
if not agent_manager.get_agent(agent_id):
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
repo = AgentConfigRepository(db)
item = await repo.get_by_id(config_id)
if not item or item.agent_id != agent_id or item.uid != str(current_user.uid):
raise HTTPException(status_code=404, detail="配置不存在")
await repo.delete(config=item, updated_by=str(current_user.uid))
return {"success": True}
@chat.post("/agent")
async def chat_agent(
payload: AgentChatRequest,
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
"""使用特定智能体进行对话(需要登录)"""
logger.info(f"query: {payload.query}, agent_config_id: {payload.agent_config_id}, meta: {payload.meta}")
# 查看图片内容
logger.info(f"image_content present: {payload.image_content is not None}")
if payload.image_content:
logger.info(f"image_content length: {len(payload.image_content)}")
logger.info(f"image_content preview: {payload.image_content[:50]}...")
return StreamingResponse(
stream_agent_chat(
query=payload.query,
agent_config_id=payload.agent_config_id,
thread_id=payload.thread_id,
meta=dict(payload.meta or {}),
image_content=payload.image_content,
current_user=current_user,
db=db,
),
media_type="application/json",
)
@chat.post("/agent/sync")
async def chat_agent_sync(
payload: AgentChatRequest,
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
"""使用特定智能体进行非流式对话(需要登录)"""
logger.info(f"[sync] query: {payload.query}, agent_config_id: {payload.agent_config_id}, meta: {payload.meta}")
logger.info(f"[sync] image_content present: {payload.image_content is not None}")
if payload.image_content:
logger.info(f"[sync] image_content length: {len(payload.image_content)}")
return await agent_chat(
query=payload.query,
agent_config_id=payload.agent_config_id,
thread_id=payload.thread_id,
meta=dict(payload.meta or {}),
image_content=payload.image_content,
current_user=current_user,
db=db,
)
@chat.post("/runs")
async def create_agent_run(
payload: AgentRunCreate,
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
"""创建异步 run 任务并入队(需要登录)"""
return await create_agent_run_view(
query=payload.query,
agent_config_id=payload.agent_config_id,
thread_id=payload.thread_id,
meta=dict(payload.meta or {}),
image_content=payload.image_content,
current_uid=str(current_user.uid),
db=db,
)
@chat.get("/runs/{run_id}")
async def get_agent_run(
run_id: str,
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
"""获取 run 状态(需要登录)"""
return await get_agent_run_view(run_id=run_id, current_uid=str(current_user.uid), db=db)
@chat.post("/runs/{run_id}/cancel")
async def cancel_agent_run(
run_id: str,
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
"""取消 run需要登录"""
return await cancel_agent_run_view(run_id=run_id, current_uid=str(current_user.uid), db=db)
@chat.get("/runs/{run_id}/events")
async def stream_run_events(
run_id: str,
after_seq: str = Query("0-0"),
current_user: User = Depends(get_required_user),
):
"""SSE 拉取 run 事件(需要登录)"""
return StreamingResponse(
stream_agent_run_events(
run_id=run_id,
after_seq=after_seq,
current_uid=str(current_user.uid),
),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
@chat.post("/thread/{thread_id}/resume")
async def resume_thread_chat(
thread_id: str,
approved: bool | None = Body(None),
answer: dict | None = Body(None),
config: dict = Body({}),
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
@ -566,11 +159,9 @@ async def resume_thread_chat(
meta["request_id"] = str(uuid.uuid4())
return StreamingResponse(
stream_agent_resume(
agent_id=agent_id,
thread_id=thread_id,
resume_input=resume_input,
meta=meta,
config=config,
current_user=current_user,
db=db,
),
@ -578,16 +169,6 @@ async def resume_thread_chat(
)
@chat.get("/thread/{thread_id}/active_run")
async def get_thread_active_run(
thread_id: str,
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
"""获取当前会话活跃 run需要登录"""
return await get_active_run_by_thread(thread_id=thread_id, current_uid=str(current_user.uid), db=db)
@chat.get("/thread/{thread_id}/history")
async def get_thread_history(
thread_id: str, current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db)
@ -657,6 +238,7 @@ class AttachmentResponse(BaseModel):
original_path: str | None = None
original_artifact_url: str | None = None
minio_url: str | None = None
request_id: str | None = None
class AttachmentLimits(BaseModel):

View File

@ -34,6 +34,14 @@ async def lifespan(app: FastAPI):
except Exception as e:
logger.error(f"Failed to ensure builtin MCP servers during startup: {e}")
try:
from yuxi.repositories.agent_repository import AgentRepository
async with pg_manager.get_async_session_context() as session:
await AgentRepository(session).ensure_default_agent()
except Exception as e:
logger.error(f"Failed to ensure default agent during startup: {e}")
# 初始化内置模型供应商配置
try:
async with pg_manager.get_async_session_context() as session: