diff --git a/app/api/routers/rbac_ecosystem.py b/app/api/routers/rbac_ecosystem.py index cab49e7..d9138c4 100644 --- a/app/api/routers/rbac_ecosystem.py +++ b/app/api/routers/rbac_ecosystem.py @@ -13,7 +13,7 @@ from ...infrastructure.repositories import Database router = APIRouter(tags=["ecosystem"]) -_ANY = ("opc_member", "carrier", "service", "operator") +_ANY = ("opc_member", "carrier", "operator") # ── 通知中心 ────────────────────────────────────────────────────────────── @@ -51,7 +51,7 @@ async def rate_task( req: RatingRequest, request: Request, db: Database = Depends(get_db), - actor: dict = Depends(require_roles("service", "opc_member")), + actor: dict = Depends(require_roles("opc_member")), ): if not req.to_id: raise HTTPException(status_code=400, detail="缺少被评对象") @@ -66,7 +66,7 @@ async def rate_task( async def get_contract( task_id: str, db: Database = Depends(get_db), - _u: dict = Depends(require_roles("service", "opc_member")), + _u: dict = Depends(require_roles("opc_member")), ): contract = await db.contracts.get_for_task(task_id) if contract is None: @@ -80,7 +80,7 @@ async def sign_contract( req: SignRequest, request: Request, db: Database = Depends(get_db), - actor: dict = Depends(require_roles("service", "opc_member")), + actor: dict = Depends(require_roles("opc_member")), ): from ...services.settlement_service import SettlementService @@ -96,7 +96,7 @@ async def release_escrow( task_id: str, request: Request, db: Database = Depends(get_db), - actor: dict = Depends(require_roles("service")), + actor: dict = Depends(require_roles("opc_member")), ): from ...services.settlement_service import SettlementService @@ -122,7 +122,7 @@ async def create_dispute( req: DisputeRequest, request: Request, db: Database = Depends(get_db), - actor: dict = Depends(require_roles("service", "opc_member")), + actor: dict = Depends(require_roles("opc_member")), ): from ...services.settlement_service import SettlementService @@ -151,7 +151,7 @@ async def resolve_dispute( @router.get("/investor/matches", summary="投资撮合(按偏好推荐)") async def investor_matches( db: Database = Depends(get_db), - user: dict = Depends(require_roles("service")), + user: dict = Depends(require_roles("opc_member")), ): from ...services.settlement_service import SettlementService @@ -164,7 +164,7 @@ async def join_roadshow( roadshow_id: str, request: Request, db: Database = Depends(get_db), - actor: dict = Depends(require_roles("service", "operator", "carrier")), + actor: dict = Depends(require_roles("opc_member", "operator", "carrier")), ): rs = await db.roadshows.get(roadshow_id) if rs is None: diff --git a/app/infrastructure/repositories.py b/app/infrastructure/repositories.py index 2d8e7a1..7e00435 100644 --- a/app/infrastructure/repositories.py +++ b/app/infrastructure/repositories.py @@ -33,6 +33,7 @@ from .models import ( InvestorPreference, Message, Notification, + OfficialAgent, OpcProfile, OpcCertification, ParkAdmission, @@ -75,35 +76,6 @@ USER_TABLE = "users" TOKEN_TABLE = "tokens" AGENTS_TABLE = "agents" -# 内置智能体身份(唯一来源在服务端;本地据此同步 workspace) -DEFAULT_AGENT_ID = "pine_agents_official_001" -QA_AGENT_ID = "pine_agents_official_002" -AGENT_SEED = ( - { - "id": DEFAULT_AGENT_ID, - "name": "小园", - "description": "默认助手,处理和园区、创业、政策等相关工作", - "language": "zh", - "model_name": "", - "template_type": "default", - "deletable": False, - "use_fixed_soul": True, - }, - { - "id": QA_AGENT_ID, - "name": "问答助手", - "description": ( - "内置 PineAgents 设置问答助手,本地配置在 `PINEAGENTS_WORKING_DIR` 下," - "并提供文档。建议在回答前阅读文件;在此工作区外使用绝对路径编写代码。" - ), - "language": "zh", - "model_name": "", - "template_type": "qa", - "deletable": True, - "use_fixed_soul": False, - }, -) - PROFILE_FIELDS = ( "username", "nickname", @@ -138,6 +110,15 @@ def new_id(prefix: str) -> str: return f"{prefix}_{secrets.token_hex(12)}" +def _loads(s: str, default): + """安全解析 JSON 字符串,失败返回默认值。""" + try: + v = json.loads(s or "") + return v if v is not None else default + except Exception: # noqa: BLE001 + return default + + def is_expired(expires_at: str) -> bool: """判断 ``expires_at``(ISO 字符串)是否已过期。""" try: @@ -1159,7 +1140,135 @@ class AgentRepository: a = await self.session.scalar(stmt) if a is None: return False - self.session.delete(a) + await self.session.delete(a) + await self.session.commit() + return True + + +# --------------------------------------------------------------------------- +# 官方预置智能体配置(管理后台可配置,替代硬编码 AGENT_SEED) +# --------------------------------------------------------------------------- +class OfficialAgentRepository: + """官方预置智能体(全局共享,非 per-user)。 + + 官方智能体由运营端管理后台(/admin/agents/official)维护;桌面端经 + ``/agent-bootstrap`` 拉取并按此初始化本地 workspace。 + """ + + def __init__(self, session: AsyncSession): + self.session = session + + @staticmethod + def _to_dict(a: "OfficialAgent") -> dict: + return { + "id": a.id, + "name": a.name, + "description": a.description, + "language": a.language, + "model_name": a.model_name, + "template_type": a.template_type, + "deletable": a.deletable, + "use_fixed_soul": a.use_fixed_soul, + "scope": a.scope or "opc", + "soul": a.soul or "{}", + "profile": a.profile or "{}", + "heartbeat": a.heartbeat or "{}", + "memory": a.memory or "{}", + "enabled": a.enabled, + "sort_order": a.sort_order, + "created_at": a.created_at, + "updated_at": a.updated_at, + } + + async def all(self) -> list[dict]: + rows = await self.session.scalars( + select(OfficialAgent).order_by(OfficialAgent.sort_order, OfficialAgent.created_at), + ) + return [self._to_dict(a) for a in rows] + + async def active(self) -> list[dict]: + """仅返回启用中的官方智能体(/agent-bootstrap 下发用)。""" + rows = await self.session.scalars( + select(OfficialAgent) + .where(OfficialAgent.enabled == True) # noqa: E712 + .order_by(OfficialAgent.sort_order, OfficialAgent.created_at), + ) + return [self._to_dict(a) for a in rows] + + async def get(self, agent_id: str) -> dict | None: + a = await self.session.get(OfficialAgent, agent_id) + return self._to_dict(a) if a else None + + async def create( + self, + agent_id: str, + name: str, + *, + description: str = "", + language: str = "zh", + model_name: str = "", + template_type: str = "common", + deletable: bool = True, + use_fixed_soul: bool = False, + scope: str = "opc", + soul: str = "{}", + profile: str = "{}", + heartbeat: str = "{}", + memory: str = "{}", + enabled: bool = True, + sort_order: int = 0, + ) -> dict: + now = utcnow_iso() + a = OfficialAgent( + id=agent_id, + name=name.strip(), + description=description, + language=language, + model_name=model_name, + template_type=template_type, + deletable=deletable, + use_fixed_soul=use_fixed_soul, + scope=scope, + soul=soul, + profile=profile, + heartbeat=heartbeat, + memory=memory, + enabled=enabled, + sort_order=sort_order, + created_at=now, + updated_at=now, + ) + self.session.add(a) + await self.session.commit() + return self._to_dict(a) + + async def update(self, agent_id: str, fields: dict) -> dict | None: + a = await self.session.get(OfficialAgent, agent_id) + if a is None: + return None + allowed = { + k: fields[k] + for k in fields + if k in ( + "name", "description", "language", "model_name", "template_type", + "deletable", "use_fixed_soul", "scope", + "soul", "profile", "heartbeat", "memory", + "enabled", "sort_order", + ) + } + if not allowed: + return None + for k, v in allowed.items(): + setattr(a, k, v) + a.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(a) + + async def delete(self, agent_id: str) -> bool: + a = await self.session.get(OfficialAgent, agent_id) + if a is None: + return False + await self.session.delete(a) await self.session.commit() return True @@ -2497,6 +2606,7 @@ class Database: self.users = UserRepository(self.session) self.tokens = TokenRepository(self.session) self.agents = AgentRepository(self.session) + self.official_agents = OfficialAgentRepository(self.session) self.roles = RoleRepository(self.session) self.orgs = OrgRepository(self.session) self.regions = RegionRepository(self.session) diff --git a/app/main.py b/app/main.py index 0f159da..4813df1 100644 --- a/app/main.py +++ b/app/main.py @@ -21,11 +21,13 @@ from app.api.routers import rbac_admin as rbac_admin_router from app.api.routers import rbac_hall as rbac_hall_router from app.api.routers import rbac_opc as rbac_opc_router from app.api.routers import rbac_operator as rbac_operator_router +from app.api.routers import rbac_agents as rbac_agents_router from app.api.routers import rbac_public as rbac_public_router from app.api.routers import rbac_org as rbac_org_router from app.api.routers import rbac_portals as rbac_portals_router from app.api.routers import rbac_certifications as rbac_certifications_router from app.api.routers import rbac_training as rbac_training_router +from app.api.routers import rbac_ecosystem as rbac_ecosystem_router from app.api.routers import templates as templates_router from app.api.routers import relay as relay_router from app.pay import routers as pay_router @@ -86,12 +88,14 @@ app.include_router(bootstrap_router.router) app.include_router(rbac_admin_router.router) app.include_router(rbac_opc_router.router) app.include_router(rbac_operator_router.router) +app.include_router(rbac_agents_router.router) app.include_router(rbac_public_router.router) app.include_router(rbac_org_router.router) app.include_router(rbac_portals_router.router) app.include_router(rbac_hall_router.router) app.include_router(rbac_certifications_router.router) app.include_router(rbac_training_router.router) +app.include_router(rbac_ecosystem_router.router) app.include_router(rbac_hall_router.community_router) app.include_router(rbac_hall_router.dm_router) app.include_router(relay_router.router)