fix: persist visitor handover tickets anonymously
This commit is contained in:
@@ -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:
|
||||
"""非转人工回答不得污染管理员待处理队列。"""
|
||||
|
||||
Reference in New Issue
Block a user