fix: 修复接口错误 - _loads未定义+重新注册ecosystem路由

1. repositories.py: 添加 _loads 函数定义(之前从 hall_repositories 隐式依赖导致 NameError)
   - 修复 /cert/types 返回 500 错误
2. main.py: 重新注册 rbac_ecosystem 路由(通知/信用/任务/合同/争议等基础功能)
   - 修复 /me/credit 返回 404 错误
3. rbac_ecosystem.py: 将已移除的 service 角色统一替换为 opc_member
   - _ANY 元组移除 service
   - 所有 require_roles 中的 service 替换为 opc_member
This commit is contained in:
Pine
2026-09-03 11:05:37 +08:00
parent cb1492f2d6
commit ab52d4013d
3 changed files with 152 additions and 38 deletions
+8 -8
View File
@@ -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:
+140 -30
View File
@@ -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)
+4
View File
@@ -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)