feat: 新增8BF多智能体会商页面
This commit is contained in:
1 parent
f1e53ef5c0
commit
e70bb4aa6f
10 files changed
+552
-13
No files matched your search
@@ -21,6 +21,8 @@ from datetime import datetime
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from deerflow.runtime.user_context import get_effective_user_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/business-mapping", tags=["business-mapping"])
|
||||
|
||||
@@ -62,6 +64,54 @@ def _get_store(request: Request):
|
||||
return store
|
||||
|
||||
|
||||
def _is_admin(request: Request) -> bool:
|
||||
"""Treat no-auth deployments as the single administrative workspace."""
|
||||
user = getattr(request.state, "user", None)
|
||||
return user is None or getattr(user, "system_role", None) == "admin"
|
||||
|
||||
|
||||
def _current_user_id(request: Request) -> str:
|
||||
user = getattr(request.state, "user", None)
|
||||
if user is not None:
|
||||
return str(user.id)
|
||||
return get_effective_user_id()
|
||||
|
||||
|
||||
async def _validate_public_chain_mapping(
|
||||
request: Request,
|
||||
fields: dict[str, object],
|
||||
) -> None:
|
||||
"""Require globally mapped chains to remain readable by every account."""
|
||||
if "chain_id" not in fields:
|
||||
return
|
||||
|
||||
raw_chain_id = fields["chain_id"]
|
||||
chain_id = raw_chain_id.strip() if isinstance(raw_chain_id, str) else ""
|
||||
if not chain_id:
|
||||
fields["chain_id"] = None
|
||||
fields["chain_title"] = None
|
||||
return
|
||||
|
||||
chain_store = getattr(request.app.state, "roundtable_chain_store", None)
|
||||
if chain_store is None:
|
||||
raise HTTPException(status_code=503, detail="Business chain store not available")
|
||||
|
||||
chain = await chain_store.get_chain(
|
||||
chain_id,
|
||||
_current_user_id(request),
|
||||
is_admin=True,
|
||||
)
|
||||
if chain is None:
|
||||
raise HTTPException(status_code=400, detail="Business chain not found")
|
||||
if not chain.get("is_public"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Global business mappings require a public business chain",
|
||||
)
|
||||
fields["chain_id"] = chain_id
|
||||
fields["chain_title"] = str(chain.get("title") or "") or None
|
||||
|
||||
|
||||
# ── routes ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -78,9 +128,12 @@ async def list_mappings(request: Request) -> BusinessMappingListResponse:
|
||||
async def update_mapping(
|
||||
request: Request, business_code: str, body: BusinessMappingUpdateRequest
|
||||
) -> BusinessMappingResponse:
|
||||
if not _is_admin(request):
|
||||
raise HTTPException(status_code=403, detail="Only administrators can update business mappings")
|
||||
store = _get_store(request)
|
||||
# Only forward fields the caller explicitly sent (distinguish omitted vs null).
|
||||
fields = body.model_dump(exclude_unset=True)
|
||||
await _validate_public_chain_mapping(request, fields)
|
||||
row = await store.update_mapping(business_code, **fields)
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Business mapping not found")
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Authorization and shared-chain invariants for global business mappings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.gateway.routers import business_mapping as business_mapping_router
|
||||
|
||||
|
||||
class _MappingStore:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[tuple[str, dict[str, object]]] = []
|
||||
|
||||
async def update_mapping(self, business_code: str, **fields: object) -> dict[str, object]:
|
||||
self.calls.append((business_code, fields))
|
||||
return {
|
||||
"business_code": business_code,
|
||||
"label": "岗位多智能体会商",
|
||||
"agent_id": None,
|
||||
"agent_name": None,
|
||||
"chain_id": fields.get("chain_id"),
|
||||
"chain_title": fields.get("chain_title"),
|
||||
"sort_order": 3,
|
||||
}
|
||||
|
||||
|
||||
class _ChainStore:
|
||||
def __init__(self, chain: dict[str, object] | None) -> None:
|
||||
self.chain = chain
|
||||
|
||||
async def get_chain(self, chain_id: str, user_id: str, *, is_admin: bool) -> dict[str, object] | None:
|
||||
assert chain_id == "chain-8bf"
|
||||
assert user_id == "admin-id"
|
||||
assert is_admin is True
|
||||
return self.chain
|
||||
|
||||
|
||||
def _request(role: str, chain: dict[str, object] | None = None):
|
||||
return SimpleNamespace(
|
||||
app=SimpleNamespace(
|
||||
state=SimpleNamespace(
|
||||
business_mapping_store=_MappingStore(),
|
||||
roundtable_chain_store=_ChainStore(chain),
|
||||
)
|
||||
),
|
||||
state=SimpleNamespace(user=SimpleNamespace(id="admin-id", system_role=role)),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_only_administrators_can_update_global_business_mappings() -> None:
|
||||
request = _request("user")
|
||||
|
||||
with pytest.raises(HTTPException) as error:
|
||||
await business_mapping_router.update_mapping(
|
||||
request,
|
||||
"8BF",
|
||||
business_mapping_router.BusinessMappingUpdateRequest(chain_id="chain-8bf"),
|
||||
)
|
||||
|
||||
assert error.value.status_code == 403
|
||||
assert request.app.state.business_mapping_store.calls == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_global_mapping_requires_a_public_chain_and_canonicalizes_its_title() -> None:
|
||||
request = _request("admin", {"id": "chain-8bf", "title": "岗位协同链", "is_public": True})
|
||||
|
||||
response = await business_mapping_router.update_mapping(
|
||||
request,
|
||||
"8BF",
|
||||
business_mapping_router.BusinessMappingUpdateRequest(
|
||||
chain_id="chain-8bf",
|
||||
chain_title="客户端伪造的标题",
|
||||
),
|
||||
)
|
||||
|
||||
assert response.chain_id == "chain-8bf"
|
||||
assert response.chain_title == "岗位协同链"
|
||||
assert request.app.state.business_mapping_store.calls == [
|
||||
("8BF", {"chain_id": "chain-8bf", "chain_title": "岗位协同链"})
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_global_mapping_rejects_a_private_chain() -> None:
|
||||
request = _request("admin", {"id": "chain-8bf", "title": "私有链", "is_public": False})
|
||||
|
||||
with pytest.raises(HTTPException) as error:
|
||||
await business_mapping_router.update_mapping(
|
||||
request,
|
||||
"8BF",
|
||||
business_mapping_router.BusinessMappingUpdateRequest(chain_id="chain-8bf"),
|
||||
)
|
||||
|
||||
assert error.value.status_code == 400
|
||||
assert request.app.state.business_mapping_store.calls == []
|
||||
Reference in new issue
Block a user