This commit is contained in:
Your Name
2026-08-14 14:37:30 +08:00
parent 21790e35f4
commit 18c15d1262
117 changed files with 28157 additions and 8080 deletions
+299
View File
@@ -11,6 +11,7 @@ from doctor_workstation.core.errors import ApiProtocolError
from doctor_workstation.core.models import Appointment, Consultation, PageResult, Prescription
from doctor_workstation.services.mock_repository import DemoDoctorRepository
from doctor_workstation.services.repository import (
DIAGNOSIS_AI_PERMISSIONS,
PRESCRIPTION_LIBRARY_PERMISSIONS,
PRESCRIPTION_PERMISSIONS,
RemoteDoctorRepository,
@@ -64,6 +65,30 @@ class RecordingClient:
return {"id": int((params or {}).get("id", 0)), "patient_name": "测试患者"}
if endpoint == "tcm.prescription/getByAppointment":
return {}
if endpoint == "tcm.prescriptionLibrary/aiReports":
return {
"prescription_id": int((params or {}).get("id", 0)),
"prescription_name": "疏肝健脾基础方",
"formula_type": "主方",
"reports": [],
"missing_model_keys": ["qwen", "openai"],
"can_view": True,
"can_refresh": True,
"can_edit": True,
"capabilities": {"can_view": True, "can_refresh": True, "can_edit": True},
}
if endpoint == "tcm.diagnosis/aiReports":
return {
"diagnosis_id": int((params or {}).get("id", 0)),
"patient_name": "林晓岚",
"case_summary": "临床诊断:肝郁脾虚证",
"reports": [],
"missing_model_keys": ["qwen", "openai"],
"can_view": True,
"can_refresh": True,
"can_edit": True,
"capabilities": {"can_view": True, "can_refresh": True, "can_edit": True},
}
if endpoint == "doctor.appointment/availableSlots":
return {"slots": [{"time": "09:00", "available": True}]}
if endpoint == "tcm.prescriptionOrder/paidPayOrders":
@@ -79,6 +104,78 @@ class RecordingClient:
return {"id": 88}
if endpoint == "tcm.diagnosis/startCall":
return {"call_record_id": 901}
if endpoint == "tcm.prescriptionLibrary/generateAiReports":
return {
"prescription_id": int(body.get("id", 0)),
"reports": [],
"can_refresh": True,
"can_edit": True,
"status": "success",
}
if endpoint == "tcm.prescriptionLibrary/editAiReport":
return {
"prescription_id": int(body.get("id", 0)),
"report": {
"report_id": int(body.get("report_id", 0)),
"model_key": "qwen",
"content": body.get("content", ""),
},
"can_edit": True,
"can_refresh": True,
}
if endpoint == "tcm.diagnosis/generateAiReports":
return {
"diagnosis_id": int(body.get("id", 0)),
"reports": [],
"can_refresh": True,
"can_edit": True,
"status": "success",
}
if endpoint == "tcm.diagnosis/editAiReport":
return {
"diagnosis_id": int(body.get("id", 0)),
"report": {
"report_id": int(body.get("report_id", 0)),
"model_key": "qwen",
"content": body.get("content", ""),
},
"can_edit": True,
"can_refresh": True,
}
if endpoint == "tcm.diagnosis/aiAssistant":
return {
"diagnosis_id": int(body.get("id", 0)),
"answer": "服务端分析结果",
"model_key": "qwen",
"task": body.get("task"),
}
if endpoint == "tcm.diagnosis/aiAnalysis":
model = str(body.get("model") or "")
if model == "openai":
return {
"diagnosis_advice": "2 型糖尿病,需结合客观检查复核",
"risk_assessment": [
{"label": "用药安全风险", "level": "medium"},
{"label": "肾功能风险", "level": "low"},
],
"treatment_advice": "复核近期检查趋势并评估联合用药安全性。",
"model_key": "openai",
"model_label": "OpenAI",
"model_name": "gpt-5.2",
"generated_at": "2026-08-14 10:31:00",
}
return {
"diagnosis_advice": "2 型糖尿病,血糖控制不佳",
"risk_assessment": [
{"label": "高血糖风险", "level": "high"},
{"label": "心血管风险", "level": "medium"},
],
"treatment_advice": "复核用药依从性并安排糖化血红蛋白检查。",
"model_key": "qwen",
"model_label": "千问",
"model_name": "qwen3.6-35b",
"generated_at": "2026-08-14 10:30:00",
}
return {"ok": True}
@@ -395,9 +492,211 @@ def test_canonical_prescription_permissions_match_routed_views() -> None:
"read": "wcf.prescription/read",
"update": "wcf.prescription/edit",
"delete": "wcf.prescription/delete",
"ai_reports": "tcm.prescriptionLibrary/aiReports",
"generate_ai_reports": "tcm.prescriptionLibrary/generateAiReports",
"edit_ai_report": "tcm.prescriptionLibrary/editAiReport",
}
assert PRESCRIPTION_PERMISSIONS["delete"] == "cf.prescription/del"
assert PRESCRIPTION_PERMISSIONS["patch_patient"] == "tcm.prescription/patchPatient"
assert DIAGNOSIS_AI_PERMISSIONS == {
"ai_reports": "tcm.diagnosis/aiReports",
"generate_ai_reports": "tcm.diagnosis/generateAiReports",
"edit_ai_report": "tcm.diagnosis/editAiReport",
"analysis": "tcm.diagnosis/aiAnalysis",
"assistant": "tcm.diagnosis/aiAssistant",
}
def test_remote_prescription_library_ai_report_endpoints() -> None:
"""AI interpretation uses the same adminapi contract as the Vue library page."""
client = RecordingClient()
repository = RemoteDoctorRepository(client)
listed = repository.list_prescription_template_ai_reports(701)
generated = repository.generate_prescription_template_ai_reports(701)
edited = repository.edit_prescription_template_ai_report(
701, report_id=9, content='{"summary":"演示"}'
)
assert listed["prescription_id"] == 701
assert generated["status"] == "success"
assert edited["report"]["report_id"] == 9
assert client.get_calls[-1] == (
"tcm.prescriptionLibrary/aiReports",
{"id": 701},
)
assert (
"tcm.prescriptionLibrary/generateAiReports",
{"id": 701},
) in client.post_calls
assert (
"tcm.prescriptionLibrary/editAiReport",
{"id": 701, "report_id": 9, "content": '{"summary":"演示"}'},
) in client.post_calls
def test_remote_diagnosis_ai_report_endpoints() -> None:
"""Patient-profile AI reports use the diagnosis adminapi contract."""
client = RecordingClient()
repository = RemoteDoctorRepository(client)
listed = repository.list_diagnosis_ai_reports(501)
generated = repository.generate_diagnosis_ai_reports(501)
edited = repository.edit_diagnosis_ai_report(
501, report_id=9, content='{"summary":"演示"}'
)
assert listed["diagnosis_id"] == 501
assert generated["status"] == "success"
assert edited["report"]["report_id"] == 9
assert client.get_calls[-1] == (
"tcm.diagnosis/aiReports",
{"id": 501},
)
assert (
"tcm.diagnosis/generateAiReports",
{"id": 501},
) in client.post_calls
assert (
"tcm.diagnosis/editAiReport",
{"id": 501, "report_id": 9, "content": '{"summary":"演示"}'},
) in client.post_calls
def test_remote_diagnosis_ai_assistant_uses_first_party_endpoint_only() -> None:
class TimeoutRecordingClient(RecordingClient):
def __init__(self) -> None:
super().__init__()
self.timeouts: list[float | None] = []
def post(
self,
endpoint: str,
payload: dict[str, Any] | None = None,
*,
timeout: float | None = None,
) -> Any:
self.timeouts.append(timeout)
return super().post(endpoint, payload)
client = TimeoutRecordingClient()
repository = RemoteDoctorRepository(client)
result = repository.analyze_diagnosis_ai(
501,
"请给出用药调整建议",
task="medication_review",
)
assert result["answer"] == "服务端分析结果"
assert client.post_calls == [
(
"tcm.diagnosis/aiAssistant",
{"id": 501, "prompt": "请给出用药调整建议", "task": "medication_review"},
)
]
body = client.post_calls[0][1]
assert not ({"key", "api_key", "base_url", "provider", "model"} & body.keys())
assert client.timeouts == [105.0]
def test_remote_diagnosis_ai_analysis_uses_exact_post_contract() -> None:
"""The legacy default is qwen, followed by an explicit OpenAI request."""
class TimeoutRecordingClient(RecordingClient):
def __init__(self) -> None:
super().__init__()
self.timeouts: list[float | None] = []
def post(
self,
endpoint: str,
payload: dict[str, Any] | None = None,
*,
timeout: float | None = None,
) -> Any:
self.timeouts.append(timeout)
return super().post(endpoint, payload)
client = TimeoutRecordingClient()
repository = RemoteDoctorRepository(client)
qwen_result = repository.get_diagnosis_ai_analysis(501)
openai_result = repository.get_diagnosis_ai_analysis(501, model="openai")
assert client.post_calls == [
("tcm.diagnosis/aiAnalysis", {"id": 501, "model": "qwen"}),
("tcm.diagnosis/aiAnalysis", {"id": 501, "model": "openai"}),
]
assert client.timeouts == [105.0, 105.0]
assert qwen_result == {
"diagnosis_advice": "2 型糖尿病,血糖控制不佳",
"risk_assessment": [
{"label": "高血糖风险", "level": "high"},
{"label": "心血管风险", "level": "medium"},
],
"treatment_advice": "复核用药依从性并安排糖化血红蛋白检查。",
"model_key": "qwen",
"model_label": "千问",
"model_name": "qwen3.6-35b",
"generated_at": "2026-08-14 10:30:00",
}
assert openai_result == {
"diagnosis_advice": "2 型糖尿病,需结合客观检查复核",
"risk_assessment": [
{"label": "用药安全风险", "level": "medium"},
{"label": "肾功能风险", "level": "low"},
],
"treatment_advice": "复核近期检查趋势并评估联合用药安全性。",
"model_key": "openai",
"model_label": "OpenAI",
"model_name": "gpt-5.2",
"generated_at": "2026-08-14 10:31:00",
}
calls_before_validation = list(client.post_calls)
timeouts_before_validation = list(client.timeouts)
with pytest.raises(ValueError, match="qwen or openai"):
repository.get_diagnosis_ai_analysis(501, model="invalid") # type: ignore[arg-type]
with pytest.raises(ValueError, match="positive"):
repository.get_diagnosis_ai_analysis(0)
assert client.post_calls == calls_before_validation
assert client.timeouts == timeouts_before_validation
def test_demo_diagnosis_ai_analysis_matches_structured_contract() -> None:
repository = DemoDoctorRepository(today=date(2026, 8, 14))
qwen_result = repository.get_diagnosis_ai_analysis(501)
openai_result = repository.get_diagnosis_ai_analysis(501, model="openai")
expected_keys = {
"diagnosis_advice",
"risk_assessment",
"treatment_advice",
"model_key",
"model_label",
"model_name",
"generated_at",
}
for result in (qwen_result, openai_result):
assert set(result) == expected_keys
assert "肝郁脾虚证" in result["diagnosis_advice"]
assert result["treatment_advice"]
assert result["risk_assessment"]
assert all(
set(item) == {"label", "level"}
and item["level"] in {"high", "medium", "low"}
for item in result["risk_assessment"]
)
assert (qwen_result["model_key"], qwen_result["model_label"]) == ("qwen", "千问")
assert (openai_result["model_key"], openai_result["model_label"]) == (
"openai",
"OpenAI",
)
assert qwen_result["model_name"] != openai_result["model_name"]
assert qwen_result["diagnosis_advice"] != openai_result["diagnosis_advice"]
assert qwen_result["treatment_advice"] != openai_result["treatment_advice"]
def test_demo_mutates_prescriptions_orders_and_patient_workspaces() -> None: