更新bug

This commit is contained in:
Your Name
2026-08-20 17:47:14 +08:00
parent 35f91ee37a
commit 5794f60c5d
67 changed files with 9257 additions and 1287 deletions
+175 -71
View File
@@ -2,16 +2,18 @@
from __future__ import annotations
import mimetypes
import re
import time
import mimetypes
import re
import time
from collections.abc import Callable, Iterator, Mapping, Sequence
from contextlib import suppress
from datetime import date
from contextlib import suppress
from datetime import date
from inspect import Parameter, signature
from io import BytesIO
from os import PathLike
from pathlib import Path
from typing import Any, Final, Literal, Protocol
from pathlib import Path
from typing import Any, Final, Literal, Protocol
from uuid import uuid4
from doctor_workstation.core.errors import (
ApiBusinessError,
@@ -631,14 +633,15 @@ class DoctorRepository(Protocol):
) -> Any:
"""Attach one uploaded replay URI to a call record."""
def upload_call_recording(
self,
path: str | PathLike[str],
diagnosis_id: int,
*,
call_record_id: int | None = None,
) -> dict[str, Any]:
"""Upload a local video then attach it to a real call record."""
def upload_call_recording(
self,
path: str | PathLike[str],
diagnosis_id: int,
*,
call_record_id: int | None = None,
mime_type: str = "audio/webm",
) -> dict[str, Any]:
"""Upload a locally captured call recording to a real call record."""
def list_im_chat_messages(
self, diagnosis_id: int, *, only_archived: bool = True
@@ -699,11 +702,19 @@ class DoctorRepository(Protocol):
def start_call(self, diagnosis_id: int, patient_id: int, *, call_type: int = 2) -> Any:
"""Create a call record."""
def end_call(self, diagnosis_id: int) -> Any:
"""End the active diagnosis call."""
def bind_call_room(self, diagnosis_id: int, room_id: str) -> Any:
"""Bind a TRTC room to the active call."""
def end_call(
self, diagnosis_id: int, *, call_record_id: int | str | None = None
) -> Any:
"""End the exact active call when its server identity is available."""
def bind_call_room(
self,
diagnosis_id: int,
room_id: str,
*,
call_record_id: int | str | None = None,
) -> Any:
"""Bind a TRTC room to the exact active call and start cloud recording."""
def start_call_transcription(
self,
@@ -2322,31 +2333,75 @@ class RemoteDoctorRepository:
body["call_record_id"] = call_record_id
return self.client.post("tcm.diagnosis/attachLocalCallRecording", body)
def upload_call_recording(
self,
path: str | PathLike[str],
diagnosis_id: int,
*,
call_record_id: int | None = None,
) -> dict[str, Any]:
"""Perform the same upload/create/attach sequence as ``CallRecordPanel``."""
if diagnosis_id <= 0:
raise ValueError("diagnosis_id must be positive")
# Match the admin component exactly: upload first so a failed upload does
# not leave behind an empty synthetic call record.
file_url = self.upload_material(path, "video")
target_id = call_record_id
if target_id is None:
target_id = int(self.create_manual_call_record(diagnosis_id).get("id") or 0)
if target_id <= 0:
raise ApiProtocolError("tcm.diagnosis/createManualCallRecord returned no id")
self.attach_local_call_recording(diagnosis_id, file_url, call_record_id=target_id)
return {
"diagnosis_id": diagnosis_id,
"call_record_id": target_id,
"file_url": file_url,
}
def upload_call_recording(
self,
path: str | PathLike[str],
diagnosis_id: int,
*,
call_record_id: int | None = None,
mime_type: str = "audio/webm",
) -> dict[str, Any]:
"""Chunk-upload one local audio recording and attach it atomically."""
if diagnosis_id <= 0:
raise ValueError("diagnosis_id must be positive")
if call_record_id is not None and call_record_id <= 0:
raise ValueError("call_record_id must be positive")
recording_path = Path(path).expanduser().resolve()
if not recording_path.is_file():
raise ValueError(f"recording file does not exist: {recording_path}")
file_size = recording_path.stat().st_size
if file_size <= 0:
raise ValueError("recording file must not be empty")
chunk_size = 4 * 1024 * 1024
chunk_total = (file_size + chunk_size - 1) // chunk_size
upload_id = f"local_audio_{uuid4().hex}"
file_name = recording_path.name
clean_mime = str(mime_type or "audio/webm").strip()[:120] or "audio/webm"
final_result: Mapping[str, Any] | None = None
bound_call_record_id = call_record_id
with recording_path.open("rb") as stream:
for chunk_index in range(chunk_total):
content = stream.read(chunk_size)
if not content:
raise ApiProtocolError("local recording ended before all chunks were read")
result = self.client.post_multipart(
"tcm.diagnosis/uploadCallRecording",
files={"file": (file_name, content, clean_mime)},
data={
"diagnosis_id": diagnosis_id,
"call_record_id": bound_call_record_id or 0,
"upload_id": upload_id,
"file_name": file_name,
"file_size": file_size,
"mime_type": clean_mime,
"chunk_index": chunk_index,
"chunk_total": chunk_total,
},
)
final_result = _require_mapping(
result,
"tcm.diagnosis/uploadCallRecording",
)
returned_id = int(final_result.get("call_record_id") or 0)
if returned_id <= 0:
raise ApiProtocolError(
"uploadCallRecording returned no call_record_id"
)
if bound_call_record_id is None:
bound_call_record_id = returned_id
elif returned_id != bound_call_record_id:
raise ApiProtocolError(
"uploadCallRecording returned a different call_record_id"
)
if final_result is None or not bool(final_result.get("completed")):
raise ApiProtocolError("uploadCallRecording did not complete the local audio upload")
file_url = str(final_result.get("file_url") or "").strip()
if not file_url:
raise ApiProtocolError("uploadCallRecording returned no COS file URL")
return dict(final_result)
def list_im_chat_messages(
self, diagnosis_id: int, *, only_archived: bool = True
@@ -2546,20 +2601,53 @@ class RemoteDoctorRepository:
)
return {"call_record_id": call_record_id}
def end_call(self, diagnosis_id: int) -> Any:
"""End the active call/recording associated with a diagnosis."""
return self.client.post("tcm.diagnosis/endCall", {"diagnosis_id": diagnosis_id})
def bind_call_room(self, diagnosis_id: int, room_id: str) -> Any:
"""Bind the actual TRTC room to the active call record."""
if not room_id.strip():
raise ValueError("room_id is required")
return self.client.post(
"tcm.diagnosis/bindCallRoom",
{"diagnosis_id": diagnosis_id, "room_id": room_id.strip()},
)
@staticmethod
def _optional_call_record_id(call_record_id: int | str | None) -> int | str | None:
if call_record_id is None:
return None
if isinstance(call_record_id, bool) or not str(call_record_id).strip():
raise ValueError("call_record_id must be a positive identifier")
try:
normalized = int(str(call_record_id).strip())
except (TypeError, ValueError) as exc:
raise ValueError("call_record_id must be a positive identifier") from exc
if normalized <= 0:
raise ValueError("call_record_id must be a positive identifier")
return normalized
def end_call(
self, diagnosis_id: int, *, call_record_id: int | str | None = None
) -> Any:
"""Stop COS cloud recording and end the exact call record."""
body: dict[str, Any] = {"diagnosis_id": diagnosis_id}
normalized_id = self._optional_call_record_id(call_record_id)
if normalized_id is not None:
body["call_record_id"] = normalized_id
return self.client.post("tcm.diagnosis/endCall", body)
def bind_call_room(
self,
diagnosis_id: int,
room_id: str,
*,
call_record_id: int | str | None = None,
) -> Any:
"""Bind the actual TRTC room and auto-start COS cloud recording."""
if not room_id.strip():
raise ValueError("room_id is required")
body: dict[str, Any] = {
"diagnosis_id": diagnosis_id,
"room_id": room_id.strip(),
}
normalized_id = self._optional_call_record_id(call_record_id)
if normalized_id is not None:
body["call_record_id"] = normalized_id
return self.client.post(
"tcm.diagnosis/bindCallRoom",
body,
)
@staticmethod
def _transcription_identity(
@@ -2767,24 +2855,40 @@ def _require_mapping(value: object, endpoint: str) -> Mapping[str, Any]:
return value
def _client_request(
def _client_request(
client: Any,
method: str,
endpoint: str,
payload: Mapping[str, Any] | None = None,
*,
timeout: float | None = None,
) -> Any:
"""Call get/post, ignoring timeout kwargs that test doubles do not accept."""
fn = getattr(client, method)
params = dict(payload or {})
try:
if timeout is None:
return fn(endpoint, params)
return fn(endpoint, params, timeout=timeout)
except TypeError:
return fn(endpoint, params)
) -> Any:
"""Call get/post once, omitting timeout for legacy test doubles.
Capability is determined before invocation. Catching ``TypeError`` from
the call itself is unsafe for POST because the transport may already have
submitted the write before raising, and a fallback call would duplicate it.
"""
fn = getattr(client, method)
params = dict(payload or {})
if timeout is not None:
try:
parameters = signature(fn).parameters.values()
except (TypeError, ValueError):
parameters = ()
accepts_timeout = any(
parameter.kind is Parameter.VAR_KEYWORD
or (
parameter.name == "timeout"
and parameter.kind
in {Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY}
)
for parameter in parameters
)
if accepts_timeout:
return fn(endpoint, params, timeout=timeout)
return fn(endpoint, params)
def _material_kind(