feat: complete multimodal ticket intake

This commit is contained in:
hailin
2026-07-27 18:59:28 +08:00
parent 96452934a3
commit 93065c0a00
39 changed files with 6443 additions and 267 deletions

View File

@@ -1,4 +1,5 @@
import io
from types import SimpleNamespace
import pytest
from django.core.files.uploadedfile import SimpleUploadedFile
@@ -95,6 +96,30 @@ def test_feature_requires_published_ticket_agent(admin_client, db):
assert response.json()["vision_available"] is False
def test_availability_uses_effective_step_vision_model(admin_client, db):
agent = create_ticket_agent()
version = agent.active_version
provider = version.model_deployment.provider
provider.image_egress_approved = True
provider.save(update_fields=["image_egress_approved"])
vision = ModelDeployment.objects.create(
provider=provider,
display_name="步骤视觉模型",
model_name="ticket-vision-model",
capability="vision_chat",
)
binding = version.steps.get()
binding.model_deployment = vision
binding.save(update_fields=["model_deployment"])
response = admin_client.get("/api/ticket-drafts/availability")
assert response.status_code == 200
assert response.json()["available"] is True
assert response.json()["vision_available"] is True
assert response.json()["vision_reason"] == ""
def test_revision_conflict_and_manual_edit(admin_client, db):
create_ticket_agent()
created = admin_client.post(
@@ -145,9 +170,16 @@ def test_confirm_is_idempotent_and_records_accepted(admin_client, category):
assert first.status_code == 200
assert second.status_code == 200
assert first.json()["confirmed_ticket"]["id"] == second.json()["confirmed_ticket"]["id"]
assert first.json()["created_ticket"]["id"] == first.json()["confirmed_ticket"]["id"]
assert first.json()["decision"] == "accepted"
assert Ticket.objects.count() == 1
assert Ticket.objects.get().ai_solution == ""
assert not hasattr(Ticket.objects.get(), "ai_solution")
ticket_id = first.json()["created_ticket"]["id"]
trace = admin_client.get(f"/api/tickets/{ticket_id}/intake-trace")
assert trace.status_code == 200
assert trace.json()["available"] is True
assert trace.json()["status"] == "ticket_created"
assert admin_client.get(f"/api/tickets/{ticket_id}/attachments").json() == []
def test_confirm_records_modified_and_reject_records_rejected(admin_client, category):
@@ -201,13 +233,14 @@ def test_image_upload_validates_content_and_never_audits_source_text(admin_clien
monkeypatch.setattr("apps.tickets.draft_api.put_object", lambda *args: None)
output = io.BytesIO()
Image.new("RGB", (8, 8), color="red").save(output, format="PNG")
upload = SimpleUploadedFile("proof.png", output.getvalue(), content_type="text/plain")
upload = SimpleUploadedFile("proof.png", output.getvalue(), content_type="image/png")
response = admin_client.post(
f"/api/ticket-drafts/{created['public_id']}/images", {"file": upload}
)
assert response.status_code == 200
assert response.json()["images"][0]["mime_type"] == "image/png"
assert response.json()["materials"][0]["scan_status"] == "clean"
snapshots = list(
OperationAuditLog.objects.filter(target_id=created["public_id"]).values_list(
"request_snapshot", flat=True
@@ -224,12 +257,14 @@ def test_text_model_rejects_image_analysis(admin_client, db):
content_type="application/json",
).json()
draft = TicketDraft.objects.get(public_id=created["public_id"])
draft.images.create(
draft.materials.create(
file_name="proof.png",
mime_type="image/png",
file_size=10,
sha256="0" * 64,
storage_path="ticket-drafts/proof.png",
scan_status="clean",
parse_status="not_applicable",
)
response = admin_client.post(
f"/api/ticket-drafts/{created['public_id']}/analyze",
@@ -240,6 +275,38 @@ def test_text_model_rejects_image_analysis(admin_client, db):
assert "不支持图片" in response.json()["detail"]
def test_vision_intake_can_start_with_image_only(admin_client, db, monkeypatch):
create_ticket_agent(capability="vision_chat", approved=True)
created = admin_client.post(
"/api/ticket-drafts",
data={"source_text": ""},
content_type="application/json",
).json()
draft = TicketDraft.objects.get(public_id=created["public_id"])
draft.materials.create(
file_name="proof.png",
mime_type="image/png",
file_size=10,
sha256="0" * 64,
storage_path="ticket-drafts/proof.png",
scan_status="clean",
parse_status="not_applicable",
)
monkeypatch.setattr(
"apps.tickets.draft_api.execute_ticket_draft_analysis.delay",
lambda *_args: SimpleNamespace(id="task-1"),
)
response = admin_client.post(
f"/api/ticket-drafts/{created['public_id']}/analyze",
data={"expected_revision": created["revision"]},
content_type="application/json",
)
assert response.status_code == 200
assert response.json()["status"] == "analyzing"
def test_cleanup_deletes_unconfirmed_details_but_keeps_confirmed_draft(
admin_client, db, monkeypatch
):
@@ -251,7 +318,7 @@ def test_cleanup_deletes_unconfirmed_details_but_keeps_confirmed_draft(
source_text="过期敏感原文",
expires_at=timezone.now(),
)
expired.images.create(
expired.materials.create(
file_name="proof.png",
mime_type="image/png",
file_size=10,