feat(server): 新增附件关联/关联报销草稿后台任务与申请位置语义

- attachment_association_jobs:从票据夹批量关联附件到报销单,识别城市/日期并创建明细项,内存态 job 跟踪
- linked_reimbursement_draft_jobs:基于申请单异步生成关联报销草稿,调用 Orchestrator 编排,区分 succeeded/failed 终态
- application_location_semantics:抽取差旅出发/到达城市、判断具体地址/业务动作等位置语义,供申请单校验复用
- router 注册两个 job 端点,新增对应 job/语义单元测试
This commit is contained in:
caoxiaozhu
2026-06-24 10:42:05 +08:00
parent d4ff79f326
commit 332f77389d
10 changed files with 1830 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
from __future__ import annotations
from datetime import datetime
from pydantic import BaseModel, Field, field_validator
class AttachmentAssociationJobCreate(BaseModel):
receipt_ids: list[str] = Field(default_factory=list, description="票据夹持久化票据 ID。")
prompt: str = Field(default="", max_length=1000, description="用户发送时的上下文说明。")
conversation_id: str = Field(default="", max_length=120, description="前端会话 ID用于状态恢复。")
@field_validator("receipt_ids")
@classmethod
def validate_receipt_ids(cls, value: list[str]) -> list[str]:
receipt_ids = [
str(item or "").strip()
for item in list(value or [])
if str(item or "").strip()
]
if not receipt_ids:
raise ValueError("请先完成附件 OCR 识别,再发起自动关联。")
return list(dict.fromkeys(receipt_ids))
class AttachmentAssociationJobRead(BaseModel):
job_id: str
status: str
message: str = ""
receipt_ids: list[str] = Field(default_factory=list)
claim_id: str = ""
claim_no: str = ""
uploaded_count: int = 0
skipped_count: int = 0
error: str = ""
prompt: str = ""
conversation_id: str = ""
created_at: datetime
updated_at: datetime

View File

@@ -0,0 +1,32 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
from pydantic import BaseModel, Field, field_validator
class LinkedReimbursementDraftJobCreate(BaseModel):
message: str = Field(min_length=1, max_length=3000, description="生成报销草稿的原始助手请求。")
context_json: dict[str, Any] = Field(default_factory=dict, description="复用 Orchestrator 的上下文。")
conversation_id: str = Field(default="", max_length=120, description="前端会话 ID用于状态恢复。")
@field_validator("message")
@classmethod
def validate_message(cls, value: str) -> str:
normalized = str(value or "").strip()
if not normalized:
raise ValueError("请先选择要关联的申请单。")
return normalized
class LinkedReimbursementDraftJobRead(BaseModel):
job_id: str
status: str
message: str = ""
error: str = ""
run_id: str = ""
conversation_id: str = ""
draft_payload: dict[str, Any] | None = None
created_at: datetime
updated_at: datetime