fix: persist visitor handover tickets anonymously

This commit is contained in:
张胜宇
2026-09-11 11:57:48 +08:00
parent 9220b7b47c
commit 0059701509
2 changed files with 40 additions and 4 deletions
+9 -2
View File
@@ -10,6 +10,7 @@ from app.core.errors import RunLeaseLostError
from app.model.audit import InteractionAudit
from app.model.conversation import ConversationMessage
from app.model.platform import AgentRun, DomainEventOutbox, HandoverTicket, RequestIdempotency
from app.model.risk import RiskUser
class AgentPersistenceService:
@@ -55,10 +56,15 @@ class AgentPersistenceService:
# 转人工只接受已完成治理决策的结果;工单与回复绑定,便于管理员回看上下文。
handover_ticket: HandoverTicket | None = None
if result.result.transfer_required:
# 访客令牌的 subject 只是匿名会话标识,不对应 sys_user;工单归属字段
# 可为空,避免把匿名 ID 当成正式用户外键,同时保留 session_id 供后台追踪。
ticket_customer_id = await self.session.scalar(
select(RiskUser.id).where(RiskUser.id == run.user_id)
)
handover_ticket = HandoverTicket(
ticket_no=f"ticket-{uuid4().hex[:24]}",
session_id=run.session_id,
customer_id=run.user_id,
customer_id=ticket_customer_id,
source_agent=run.agent_type,
source_message_id=message.id,
intent=(result.result.intent.intent if result.result.intent else None),
@@ -84,7 +90,8 @@ class AgentPersistenceService:
run.updated_at = now
run.locked_until = None
self.session.add(InteractionAudit(
actor_type="agent", actor_id=run.user_id, target_customer_id=run.user_id,
actor_type="agent", actor_id=run.user_id,
target_customer_id=handover_ticket.customer_id if handover_ticket else run.user_id,
session_id=run.session_id, portal="agent", action_type="agent.run_completed",
detail={"run_id": run_id, "result_message_id": message.id}, created_at=now,
))
@@ -26,12 +26,14 @@ class FakeSession:
self.run = run
self.added: list[Any] = []
self.executed: list[Any] = []
self._scalar_calls = 0
def begin(self) -> FakeTransaction:
return FakeTransaction()
async def scalar(self, statement: object) -> AgentRun:
return self.run
async def scalar(self, statement: object) -> object:
self._scalar_calls += 1
return self.run if self._scalar_calls == 1 else self.run.user_id
def add(self, item: Any) -> None:
self.added.append(item)
@@ -105,6 +107,33 @@ async def test_transfer_required_result_creates_pending_ticket_and_outbox_event(
assert transfer_event.payload["ticket_no"] == tickets[0].ticket_no
class VisitorSession(FakeSession):
"""模拟访客运行:运行记录存在,但不存在对应的正式 sys_user。"""
def __init__(self, run: AgentRun) -> None:
super().__init__(run)
async def scalar(self, statement: object) -> object:
self._scalar_calls += 1
if self._scalar_calls == 1:
return self.run
return None
@pytest.mark.asyncio
async def test_visitor_transfer_ticket_does_not_use_anonymous_id_as_customer_fk() -> None:
"""访客转人工写匿名工单,customer_id 必须为空而不是随机访客 ID。"""
session = VisitorSession(queued_run())
await AgentPersistenceService(session).complete_run(
"run-transfer-1", result(transfer_required=True), memory_extraction_requested=False
)
tickets = added_of(session.added, HandoverTicket)
assert len(tickets) == 1
assert tickets[0].customer_id is None
@pytest.mark.asyncio
async def test_normal_result_does_not_create_handover_ticket_or_event() -> None:
"""非转人工回答不得污染管理员待处理队列。"""