更新bug
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user