更新
This commit is contained in:
@@ -7,5 +7,5 @@ __version__ = "1.4.2"
|
||||
|
||||
# 调试模式开启时,登录页显示“演示模式”和“服务器设置”。
|
||||
# 正式发布请保持 False;此时程序只使用下面配置的线上域名。
|
||||
DEBUG_MODE = False
|
||||
DEBUG_MODE = True
|
||||
ONLINE_API_BASE_URL = "https://admin.zhenyangtang.com.cn"
|
||||
|
||||
@@ -412,7 +412,8 @@ class ApplicationController(QObject):
|
||||
self.current_repository: Any = None
|
||||
self.current_demo_mode = self.debug_mode and config.demo_mode
|
||||
self.video_calls: dict[str, Any] = {}
|
||||
self.video_pending: dict[str, object] = {}
|
||||
self.video_pending: dict[str, object] = {}
|
||||
self._pending_im_request: tuple[str, object] | None = None
|
||||
self.demo_video_dialogs: dict[str, DemoVideoDialog] = {}
|
||||
self._video_preview_state: dict[str, Any] | None = None
|
||||
self._video_preview_generation = 0
|
||||
@@ -725,7 +726,8 @@ class ApplicationController(QObject):
|
||||
call.close()
|
||||
self._wait_for_video_lifecycle(calls, timeout=1.25)
|
||||
self.video_calls.clear()
|
||||
self.video_pending.clear()
|
||||
self.video_pending.clear()
|
||||
self._pending_im_request = None
|
||||
for dialog in self.demo_video_dialogs.values():
|
||||
dialog.close()
|
||||
self.demo_video_dialogs.clear()
|
||||
@@ -746,7 +748,8 @@ class ApplicationController(QObject):
|
||||
if parent is None or self.current_repository is None:
|
||||
return
|
||||
patient_id = payload.get("patient_id")
|
||||
diagnosis_id = payload.get("diagnosis_id")
|
||||
diagnosis_id = payload.get("diagnosis_id")
|
||||
appointment_id = int(payload.get("appointment_id") or 0)
|
||||
patient_name = str(payload.get("patient_name") or "患者")
|
||||
open_im = str(payload.get("mode") or "video").lower() == "im"
|
||||
fallback_record = payload.get("record")
|
||||
@@ -754,8 +757,16 @@ class ApplicationController(QObject):
|
||||
show_toast(parent, "患者或诊单信息不完整,无法发起视频。", "danger", 4200)
|
||||
return
|
||||
|
||||
call_key = str(diagnosis_id)
|
||||
existing_call = self.video_calls.get(call_key)
|
||||
call_key = f"{diagnosis_id}:{appointment_id}"
|
||||
if open_im and self._pending_im_request is not None:
|
||||
pending_key, pending_marker = self._pending_im_request
|
||||
if pending_key != call_key:
|
||||
# Retire a previous selection before any early return, including
|
||||
# when the selected IM window is already open.
|
||||
if self.video_pending.get(pending_key) is pending_marker:
|
||||
self.video_pending.pop(pending_key, None)
|
||||
self._pending_im_request = None
|
||||
existing_call = self.video_calls.get(call_key)
|
||||
if open_im and existing_call is not None and getattr(existing_call, "open_im", False):
|
||||
qt_window = getattr(existing_call, "qt_window", None)
|
||||
if qt_window is not None:
|
||||
@@ -801,13 +812,18 @@ class ApplicationController(QObject):
|
||||
)
|
||||
repository = self.current_repository
|
||||
marker = object()
|
||||
self.video_pending[call_key] = marker
|
||||
self.video_pending[call_key] = marker
|
||||
if open_im:
|
||||
self._pending_im_request = (call_key, marker)
|
||||
|
||||
def get_video_context() -> tuple[Any, dict[str, str]]:
|
||||
ticket = repository.get_call_ticket(
|
||||
patient_id=int(patient_id),
|
||||
diagnosis_id=int(diagnosis_id),
|
||||
)
|
||||
diagnosis_id=int(diagnosis_id),
|
||||
appointment_id=appointment_id,
|
||||
)
|
||||
if appointment_id and int(ticket.raw.get("appointment_id") or 0) != appointment_id:
|
||||
raise ValueError("通话凭证与本次挂号不匹配,请重新打开聊天窗口。")
|
||||
detail: Any = {}
|
||||
detail_loader = getattr(repository, "patient_detail", None)
|
||||
if callable(detail_loader):
|
||||
@@ -868,10 +884,12 @@ class ApplicationController(QObject):
|
||||
parent: QWidget,
|
||||
error: Exception,
|
||||
) -> None:
|
||||
if self.video_pending.get(call_key) is not marker:
|
||||
return
|
||||
self.video_pending.pop(call_key, None)
|
||||
if self.shell_window is parent:
|
||||
if self.video_pending.get(call_key) is not marker:
|
||||
return
|
||||
self.video_pending.pop(call_key, None)
|
||||
if self._pending_im_request == (call_key, marker):
|
||||
self._pending_im_request = None
|
||||
if self.shell_window is parent:
|
||||
show_toast(
|
||||
parent,
|
||||
f"视频准备失败:{friendly_error(error)}",
|
||||
@@ -892,9 +910,11 @@ class ApplicationController(QObject):
|
||||
patient_name: str = "患者",
|
||||
patient_case: Mapping[str, Any] | None = None,
|
||||
) -> None:
|
||||
if self.video_pending.get(call_key) is not marker:
|
||||
return
|
||||
self.video_pending.pop(call_key, None)
|
||||
if self.video_pending.get(call_key) is not marker:
|
||||
return
|
||||
self.video_pending.pop(call_key, None)
|
||||
if self._pending_im_request == (call_key, marker):
|
||||
self._pending_im_request = None
|
||||
if (
|
||||
self.shell_window is None
|
||||
or self.current_repository is not repository
|
||||
@@ -919,8 +939,8 @@ class ApplicationController(QObject):
|
||||
open_im=open_im,
|
||||
patient_name=patient_name,
|
||||
patient_case=patient_case,
|
||||
on_open_diagnosis=lambda current_id=diagnosis_id: (
|
||||
self._open_video_diagnosis(current_id)
|
||||
on_open_diagnosis=lambda current_id=diagnosis_id, current_key=call_key: (
|
||||
self._open_video_diagnosis(current_id, call_key=current_key)
|
||||
),
|
||||
)
|
||||
except Exception as error:
|
||||
@@ -942,7 +962,7 @@ class ApplicationController(QObject):
|
||||
)
|
||||
)
|
||||
|
||||
def _open_video_diagnosis(self, diagnosis_id: Any) -> None:
|
||||
def _open_video_diagnosis(self, diagnosis_id: Any, *, call_key: str) -> None:
|
||||
"""Open the diagnosis while keeping its live video visible as a preview."""
|
||||
|
||||
shell = self.shell_window
|
||||
@@ -951,7 +971,7 @@ class ApplicationController(QObject):
|
||||
dialog = shell.open_diagnosis_by_id(diagnosis_id, modeless=True)
|
||||
if dialog is None:
|
||||
return
|
||||
call = self.video_calls.get(str(diagnosis_id))
|
||||
call = self.video_calls.get(call_key)
|
||||
video_window = getattr(call, "qt_window", None)
|
||||
if video_window is not None:
|
||||
self._show_video_preview(video_window, dialog)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Appointment medium labels, separate from first-visit/follow-up diagnosis type."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
APPOINTMENT_MODES = (("视频问诊", "video"), ("图文问诊", "text"))
|
||||
|
||||
|
||||
def appointment_type_description(value: Any) -> str:
|
||||
if value is None or isinstance(value, str) and not value.strip():
|
||||
return "视频问诊"
|
||||
return {"video": "视频问诊", "text": "图文问诊", "phone": "电话问诊"}.get(str(value), "未知")
|
||||
|
||||
|
||||
def can_appointment_video(value: Any) -> bool:
|
||||
"""UI hint only; actual call permission must come from the server ticket."""
|
||||
return value is None or isinstance(value, str) and (not value.strip() or value == "video")
|
||||
|
||||
|
||||
def appointment_type_value(record: Any) -> Any:
|
||||
"""Read appointment rows and diagnosis-list projections without visit-type aliases."""
|
||||
def read(value: Any, key: str) -> Any:
|
||||
if isinstance(value, Mapping):
|
||||
return value.get(key)
|
||||
result = getattr(value, key, None)
|
||||
raw = getattr(value, "raw", None)
|
||||
return raw.get(key) if result is None and isinstance(raw, Mapping) else result
|
||||
|
||||
appointment_id = read(record, "appointment_id") or read(record, "latest_appointment_id")
|
||||
rows = read(record, "appointments")
|
||||
if isinstance(rows, (list, tuple)) and rows:
|
||||
if appointment_id:
|
||||
for row in rows:
|
||||
if str(read(row, "id")) == str(appointment_id):
|
||||
return read(row, "appointment_type")
|
||||
elif len(rows) == 1:
|
||||
return read(rows[0], "appointment_type")
|
||||
else:
|
||||
return "unknown"
|
||||
for key in ("appointment_type", "latest_appointment_type"):
|
||||
value = read(record, key)
|
||||
if value is not None and value != "":
|
||||
return value
|
||||
for key in ("appointment", "latest_appointment"):
|
||||
nested = read(record, key)
|
||||
if nested is not None:
|
||||
return read(nested, "appointment_type")
|
||||
return None
|
||||
@@ -3022,7 +3022,7 @@ class DemoDoctorRepository:
|
||||
raise ValueError("order_no is required")
|
||||
return {"qrcode_url": f"https://demo.invalid/qrcode/order/{clean}.png"}
|
||||
|
||||
def get_call_ticket(self, patient_id: int, diagnosis_id: int) -> CallTicket:
|
||||
def get_call_ticket(self, patient_id: int, diagnosis_id: int, *, appointment_id: int = 0) -> CallTicket:
|
||||
"""Return non-production placeholder credentials for UI demonstration."""
|
||||
|
||||
with self._lock:
|
||||
@@ -3036,21 +3036,22 @@ class DemoDoctorRepository:
|
||||
assistant_id="assistant_2001",
|
||||
diagnosis_id=diagnosis_id,
|
||||
is_lochost_vod=False,
|
||||
raw={"demo": True},
|
||||
raw={"demo": True, "appointment_id": appointment_id},
|
||||
)
|
||||
|
||||
def start_call(
|
||||
self, diagnosis_id: int, patient_id: int, *, call_type: int = 2
|
||||
self, diagnosis_id: int, patient_id: int, *, call_type: int = 2, appointment_id: int = 0
|
||||
) -> dict[str, Any]:
|
||||
"""Create a mutable demo call record."""
|
||||
|
||||
with self._lock:
|
||||
self.get_call_ticket(patient_id, diagnosis_id)
|
||||
self.get_call_ticket(patient_id, diagnosis_id, appointment_id=appointment_id)
|
||||
record = {
|
||||
"id": self._next_call_id,
|
||||
"diagnosis_id": diagnosis_id,
|
||||
"patient_id": patient_id,
|
||||
"call_type": call_type,
|
||||
"call_type": call_type,
|
||||
"appointment_id": appointment_id,
|
||||
"status": "ringing",
|
||||
"room_id": "",
|
||||
}
|
||||
|
||||
@@ -712,10 +712,10 @@ class DoctorRepository(Protocol):
|
||||
def generate_order_qrcode(self, order_no: str) -> dict[str, Any]:
|
||||
"""Generate the payment mini-program QR code for a generic order."""
|
||||
|
||||
def get_call_ticket(self, patient_id: int, diagnosis_id: int) -> CallTicket:
|
||||
def get_call_ticket(self, patient_id: int, diagnosis_id: int, *, appointment_id: int = 0) -> CallTicket:
|
||||
"""Return short-lived call credentials."""
|
||||
|
||||
def start_call(self, diagnosis_id: int, patient_id: int, *, call_type: int = 2) -> Any:
|
||||
def start_call(self, diagnosis_id: int, patient_id: int, *, call_type: int = 2, appointment_id: int = 0) -> Any:
|
||||
"""Create a call record."""
|
||||
|
||||
def end_call(
|
||||
@@ -2629,22 +2629,51 @@ class RemoteDoctorRepository:
|
||||
)
|
||||
return result
|
||||
|
||||
def get_call_ticket(self, patient_id: int, diagnosis_id: int) -> CallTicket:
|
||||
def get_call_ticket(self, patient_id: int, diagnosis_id: int, *, appointment_id: int = 0) -> CallTicket:
|
||||
"""Obtain short-lived Tencent credentials for a consultation call."""
|
||||
|
||||
result = _require_mapping(
|
||||
self.client.post(
|
||||
"tcm.diagnosis/getCallSignature",
|
||||
{"patient_id": patient_id, "diagnosis_id": diagnosis_id},
|
||||
result = _require_mapping(
|
||||
self.client.post(
|
||||
"tcm.diagnosis/getCallSignature",
|
||||
{"patient_id": patient_id, "diagnosis_id": diagnosis_id, "appointment_id": appointment_id},
|
||||
),
|
||||
"tcm.diagnosis/getCallSignature",
|
||||
)
|
||||
ticket = CallTicket.from_dict(result)
|
||||
"tcm.diagnosis/getCallSignature",
|
||||
)
|
||||
policy_fields = {
|
||||
"appointment_id", "appointmentId", "appointment_type", "appointment_type_desc",
|
||||
"can_video_call", "can_audio_call", "call_disabled_reason",
|
||||
}
|
||||
if appointment_id > 0 and not policy_fields.intersection(result):
|
||||
# Older signature endpoints authenticate the patient/diagnosis but
|
||||
# do not echo the appointment. Keep the requested chat context;
|
||||
# never treat it as server authorization for an audio/video call.
|
||||
if (
|
||||
str(result.get("diagnosis_id", "")).strip() != str(diagnosis_id)
|
||||
or str(result.get("patient_id", "")).strip() != str(patient_id)
|
||||
or result.get("patientUserId") != f"patient_{patient_id}"
|
||||
):
|
||||
raise ValueError("聊天凭证与当前患者或诊单不匹配,请重新打开聊天窗口。")
|
||||
result = dict(result)
|
||||
result.update(
|
||||
appointment_id=appointment_id,
|
||||
can_video_call=False,
|
||||
can_audio_call=False,
|
||||
call_disabled_reason="暂未获取到本次挂号的通话权限,仍可图文聊天",
|
||||
)
|
||||
if appointment_id > 0:
|
||||
returned_id = result.get("appointment_id")
|
||||
if (
|
||||
isinstance(returned_id, bool)
|
||||
or not isinstance(returned_id, (int, str))
|
||||
or str(returned_id).strip() != str(appointment_id)
|
||||
):
|
||||
raise ValueError("通话凭证与本次挂号不匹配,请重新打开聊天窗口。")
|
||||
ticket = CallTicket.from_dict(result)
|
||||
if ticket.diagnosis_id is None:
|
||||
ticket.diagnosis_id = diagnosis_id
|
||||
return ticket
|
||||
|
||||
def start_call(self, diagnosis_id: int, patient_id: int, *, call_type: int = 2) -> Any:
|
||||
def start_call(self, diagnosis_id: int, patient_id: int, *, call_type: int = 2, appointment_id: int = 0) -> Any:
|
||||
"""Create the server-side call record before ringing participants."""
|
||||
|
||||
payload = self.client.post(
|
||||
@@ -2652,7 +2681,8 @@ class RemoteDoctorRepository:
|
||||
{
|
||||
"diagnosis_id": diagnosis_id,
|
||||
"patient_id": patient_id,
|
||||
"call_type": call_type,
|
||||
"call_type": call_type,
|
||||
"appointment_id": appointment_id,
|
||||
},
|
||||
)
|
||||
if not isinstance(payload, Mapping):
|
||||
|
||||
@@ -42,7 +42,8 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from .widgets import (
|
||||
from ..core.appointment_modes import APPOINTMENT_MODES
|
||||
from .widgets import (
|
||||
display_text,
|
||||
first_value,
|
||||
friendly_error,
|
||||
@@ -972,13 +973,24 @@ class AppointmentDrawer(QDialog):
|
||||
type_container = QWidget()
|
||||
type_layout = QHBoxLayout(type_container)
|
||||
type_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.appointment_type_radio = QRadioButton("视频问诊")
|
||||
self.appointment_type_radio.setChecked(True)
|
||||
type_layout.addWidget(self.appointment_type_radio)
|
||||
self.appointment_type_radio = QRadioButton("视频问诊")
|
||||
self.appointment_type_radio.setChecked(True)
|
||||
type_layout.addWidget(self.appointment_type_radio)
|
||||
self.text_appointment_type_radio = QRadioButton("图文问诊")
|
||||
type_layout.addWidget(self.text_appointment_type_radio)
|
||||
self.appointment_type_group = QButtonGroup(self)
|
||||
self.appointment_type_group.addButton(self.appointment_type_radio, 0)
|
||||
self.appointment_type_group.addButton(self.text_appointment_type_radio, 1)
|
||||
type_layout.addStretch(1)
|
||||
self._add_form_row("预约类型:", type_container)
|
||||
self.appointment_type = QComboBox(self)
|
||||
self.appointment_type.addItem("视频问诊", "video")
|
||||
for label, value in APPOINTMENT_MODES:
|
||||
self.appointment_type.addItem(label, value)
|
||||
self.appointment_type_group.idClicked.connect(self.appointment_type.setCurrentIndex)
|
||||
self.appointment_type.currentIndexChanged.connect(
|
||||
lambda index: self.appointment_type_group.button(index).setChecked(True)
|
||||
if self.appointment_type_group.button(index) is not None else None
|
||||
)
|
||||
self.appointment_type.hide()
|
||||
|
||||
patient_container = QWidget()
|
||||
|
||||
@@ -26,9 +26,9 @@ from PySide6.QtCore import (
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QAction,
|
||||
QColor,
|
||||
QFont,
|
||||
QFontMetrics,
|
||||
QColor,
|
||||
QFont,
|
||||
QFontMetrics,
|
||||
QIcon,
|
||||
QLinearGradient,
|
||||
QMouseEvent,
|
||||
@@ -61,8 +61,13 @@ from PySide6.QtWidgets import (
|
||||
QWidgetItem,
|
||||
)
|
||||
|
||||
from . import icons
|
||||
from .reception_style import TECH_BLUE, body_family, heading_family
|
||||
from ..core.appointment_modes import (
|
||||
appointment_type_description,
|
||||
appointment_type_value,
|
||||
can_appointment_video,
|
||||
)
|
||||
from . import icons
|
||||
from .reception_style import TECH_BLUE, body_family, heading_family
|
||||
from .widgets import display_text, first_value, gender_text, get_value
|
||||
|
||||
PRIMARY = QColor("#4F63D9")
|
||||
@@ -112,7 +117,7 @@ def _blue_appointment_text(record: Any, appointment: Any) -> tuple[str, str]:
|
||||
This returns display strings without mutating either repository record.
|
||||
"""
|
||||
|
||||
current_id = _as_int(first_value(record, "appointment_id", default=0))
|
||||
current_id = _as_int(first_value(record, "appointment_id", "latest_appointment_id", default=0))
|
||||
appointment_id = _as_int(first_value(appointment, "id", "appointment_id", default=0))
|
||||
current = current_id > 0 and appointment_id == current_id
|
||||
doctor = first_value(appointment, "doctor_name", "appointment_doctor_name")
|
||||
@@ -130,6 +135,11 @@ def _blue_appointment_text(record: Any, appointment: Any) -> tuple[str, str]:
|
||||
when = time_text if date_text and date_text in time_text else " ".join(
|
||||
part for part in (date_text, time_text) if part
|
||||
)
|
||||
mode = appointment_type_value(appointment)
|
||||
if current and get_value(appointment, "appointment_type", None) is None:
|
||||
mode = appointment_type_value(record)
|
||||
if appointment_id > 0:
|
||||
when = f"{when} · {appointment_type_description(mode)}"
|
||||
return display_text(doctor, "—"), when
|
||||
|
||||
|
||||
@@ -297,7 +307,8 @@ def _appointments(record: Any) -> list[Any]:
|
||||
return [
|
||||
{
|
||||
"id": first_value(record, "appointment_id"),
|
||||
"status": first_value(record, "appointment_status"),
|
||||
"status": first_value(record, "appointment_status"),
|
||||
"appointment_type": appointment_type_value(record),
|
||||
"doctor_name": first_value(
|
||||
record, "appointment_doctor_name", "doctor_name", default=""
|
||||
),
|
||||
@@ -392,7 +403,8 @@ def _video_ids_complete(record: Any) -> bool:
|
||||
appointment_id = _as_int(
|
||||
first_value(
|
||||
record,
|
||||
"appointment_id",
|
||||
"appointment_id",
|
||||
"latest_appointment_id",
|
||||
default=first_value(
|
||||
_appointments(record)[0] if _appointments(record) else None,
|
||||
"id",
|
||||
@@ -770,7 +782,8 @@ class DiagnosisTableModel(QAbstractTableModel):
|
||||
part
|
||||
for part in (
|
||||
display_text(first_value(apt, "doctor_name"), "-"),
|
||||
display_text(first_value(apt, "time_text", "appointment_time"), "-"),
|
||||
display_text(first_value(apt, "time_text", "appointment_time"), "-"),
|
||||
appointment_type_description(appointment_type_value(apt)),
|
||||
)
|
||||
if part
|
||||
)
|
||||
@@ -1776,12 +1789,14 @@ class DiagnosisTableHost(QFrame):
|
||||
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
video_capable = self.action_policy.get("video_call", False)
|
||||
call_state = video_call_state(record)
|
||||
if video_capable and _appointment_active(record) and call_state == "live":
|
||||
button = QToolButton(host)
|
||||
button.setText("进入视频问诊")
|
||||
is_text = appointment_type_value(record) == "text"
|
||||
if video_capable and _appointment_active(record) and (is_text or call_state == "live"):
|
||||
button = QToolButton(host)
|
||||
button.setText("图文沟通" if is_text else "进入视频问诊")
|
||||
button.setProperty("rowLink", "primary")
|
||||
button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
button.setToolTip("医生已发起视频会话,点击进入(将使用摄像头和麦克风)")
|
||||
button.setToolTip("发送文字、图片和文件;不支持音视频通话" if is_text
|
||||
else "医生已发起视频会话,点击进入(将使用摄像头和麦克风)")
|
||||
button.setEnabled(_video_ids_complete(record))
|
||||
if not button.isEnabled():
|
||||
button.setToolTip("患者、诊单或挂号标识不完整,无法进入视频问诊")
|
||||
@@ -1900,7 +1915,8 @@ class DiagnosisTableHost(QFrame):
|
||||
"cancel_assign", item
|
||||
),
|
||||
)
|
||||
if self.action_policy.get("video_qr", False) and _appointment_active(record):
|
||||
if (self.action_policy.get("video_qr", False) and _appointment_active(record)
|
||||
and can_appointment_video(appointment_type_value(record))):
|
||||
_add_menu_action(
|
||||
menu,
|
||||
"视频二维码",
|
||||
|
||||
@@ -33,6 +33,7 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from ...core.appointment_modes import appointment_type_description, appointment_type_value
|
||||
from ..diagnosis_drawer import (
|
||||
DIAGNOSIS_QSS,
|
||||
CaseGrid,
|
||||
@@ -3589,7 +3590,7 @@ class DiagnosisDialog(QDialog):
|
||||
first_value(row, "assistant_name"),
|
||||
first_value(row, "appointment_date"),
|
||||
first_value(row, "appointment_time", "period"),
|
||||
first_value(row, "appointment_type_text", "appointment_type"),
|
||||
appointment_type_description(appointment_type_value(row)),
|
||||
" / ".join(
|
||||
part
|
||||
for part in (
|
||||
|
||||
@@ -10,7 +10,7 @@ from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from PySide6.QtCore import QDate, Qt, QTimer, QUrl, Signal
|
||||
from PySide6.QtGui import QBrush, QColor, QDesktopServices, QFont, QPixmap
|
||||
from PySide6.QtGui import QBrush, QColor, QDesktopServices, QFont, QPixmap
|
||||
from PySide6.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QButtonGroup,
|
||||
@@ -36,8 +36,13 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from ..appointments_style import appointments_stylesheet
|
||||
from ..dialogs import DiagnosisDialog
|
||||
from ...core.appointment_modes import (
|
||||
appointment_type_description,
|
||||
appointment_type_value,
|
||||
can_appointment_video,
|
||||
)
|
||||
from ..appointments_style import appointments_stylesheet
|
||||
from ..dialogs import DiagnosisDialog
|
||||
from ..dialogs.ai_consult import can_open_ai_consult, present_ai_consult
|
||||
from ..dialogs.prescription import (
|
||||
PrescriptionDetailDialog,
|
||||
@@ -49,11 +54,11 @@ from ..dialogs.prescription_ai import (
|
||||
can_open_diagnosis_ai_report,
|
||||
present_diagnosis_ai_report,
|
||||
)
|
||||
from ..filter_disclosure import FilterDisclosure
|
||||
from ..icons import icon
|
||||
from ..infinite_list import InfiniteList
|
||||
from ..reception_style import heading_family
|
||||
from ..theme import mark_business_dialog
|
||||
from ..filter_disclosure import FilterDisclosure
|
||||
from ..icons import icon
|
||||
from ..infinite_list import InfiniteList
|
||||
from ..reception_style import heading_family
|
||||
from ..theme import mark_business_dialog
|
||||
from ..widgets import (
|
||||
MessageBanner,
|
||||
PageHeader,
|
||||
@@ -312,7 +317,8 @@ def _appointment_info_cell(_value: Any, row: Any) -> str:
|
||||
channel = display_text(
|
||||
first_value(row, "channel_name", "channel_source_name", "source_name"), "—"
|
||||
)
|
||||
return f"{status} {doctor}\n{date_text} {time_text}\n最近渠道:{channel}"
|
||||
mode = appointment_type_description(appointment_type_value(row))
|
||||
return f"{status} {doctor}\n{date_text} {time_text} · {mode}\n最近渠道:{channel}"
|
||||
|
||||
|
||||
def _revisit_cell(_value: Any, row: Any) -> str:
|
||||
@@ -1273,7 +1279,16 @@ class AppointmentsPage(QWidget):
|
||||
host,
|
||||
)
|
||||
date_label.setProperty("tableAppointmentMeta", True)
|
||||
layout.addWidget(date_label)
|
||||
date_row = QHBoxLayout()
|
||||
date_row.setContentsMargins(0, 0, 0, 0)
|
||||
date_row.setSpacing(6)
|
||||
date_row.addWidget(date_label)
|
||||
mode_label = QLabel(appointment_type_description(appointment_type_value(row)), host)
|
||||
mode_label.setObjectName("AppointmentModeLabel")
|
||||
mode_label.setProperty("tableAppointmentMeta", True)
|
||||
date_row.addWidget(mode_label)
|
||||
date_row.addStretch(1)
|
||||
layout.addLayout(date_row)
|
||||
channel = display_text(
|
||||
first_value(row, "channel_name", "channel_source_name", "source_name"),
|
||||
"—",
|
||||
@@ -1310,7 +1325,8 @@ class AppointmentsPage(QWidget):
|
||||
layout.setContentsMargins(5, 0, 5, 0)
|
||||
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
button = QPushButton("IM 问诊", host)
|
||||
is_text = appointment_type_value(row) == "text"
|
||||
button = QPushButton("图文沟通" if is_text else "IM 问诊", host)
|
||||
button.setObjectName("AppointmentImConsultButton")
|
||||
button.setProperty("appointmentImAction", True)
|
||||
patient_name = display_text(first_value(row, "patient_name"), "患者")
|
||||
@@ -1338,7 +1354,8 @@ class AppointmentsPage(QWidget):
|
||||
elif status_error:
|
||||
tooltip = status_error
|
||||
else:
|
||||
tooltip = "打开患者 IM,可发送消息并从会话中发起视频"
|
||||
tooltip = ("打开图文问诊,可发送文字、图片和文件;不支持音视频通话"
|
||||
if is_text else "打开患者 IM,可发送消息;通话能力由本次挂号决定")
|
||||
button.setToolTip(tooltip)
|
||||
button.clicked.connect(
|
||||
lambda _checked=False, source=row: self._run_video_row_action(
|
||||
@@ -1425,8 +1442,9 @@ class AppointmentsPage(QWidget):
|
||||
status = _status_value(row) if has_row else 0
|
||||
not_completed = has_row and status != 3
|
||||
self.edit_button.setEnabled(has_row and _diagnosis_id(row) > 0)
|
||||
self.qr_button.setEnabled(
|
||||
not_completed
|
||||
self.qr_button.setEnabled(
|
||||
not_completed
|
||||
and can_appointment_video(appointment_type_value(row))
|
||||
and _video_patient_id(row) > 0
|
||||
and _as_int(first_value(row, "doctor_id", default=0)) > 0
|
||||
)
|
||||
@@ -1448,8 +1466,9 @@ class AppointmentsPage(QWidget):
|
||||
)
|
||||
self.cancel_button.setEnabled(has_row and status == 1)
|
||||
self.toolbar_edit_button.setEnabled(has_row and _diagnosis_id(row) > 0)
|
||||
self.toolbar_qr_button.setEnabled(
|
||||
not_completed
|
||||
self.toolbar_qr_button.setEnabled(
|
||||
not_completed
|
||||
and can_appointment_video(appointment_type_value(row))
|
||||
and _video_patient_id(row) > 0
|
||||
and _as_int(first_value(row, "doctor_id", default=0)) > 0
|
||||
)
|
||||
@@ -1537,7 +1556,8 @@ class AppointmentsPage(QWidget):
|
||||
self.video_requested.emit(
|
||||
{
|
||||
"source": "appointments",
|
||||
"appointment_id": appointment_id,
|
||||
"appointment_id": appointment_id,
|
||||
"appointment_type": appointment_type_value(row),
|
||||
"patient_id": patient_id,
|
||||
"diagnosis_id": diagnosis_id,
|
||||
"patient_name": first_value(row, "patient_name", default="患者"),
|
||||
@@ -1559,9 +1579,12 @@ class AppointmentsPage(QWidget):
|
||||
if not all(callable(getattr(self.repository, name, None)) for name in required_methods):
|
||||
show_toast(self, "当前仓库未提供视频二维码能力。", "warning")
|
||||
return
|
||||
row = self._current_row()
|
||||
if row is None or _status_value(row) == 3:
|
||||
return
|
||||
row = self._current_row()
|
||||
if row is None or _status_value(row) == 3:
|
||||
return
|
||||
if not can_appointment_video(appointment_type_value(row)):
|
||||
show_toast(self, "本次挂号不支持视频问诊二维码。", "warning")
|
||||
return
|
||||
diagnosis_id = _diagnosis_id(row)
|
||||
patient_id = _video_patient_id(row)
|
||||
doctor_id = _as_int(first_value(row, "doctor_id", default=0))
|
||||
@@ -2214,7 +2237,7 @@ class AppointmentsPage(QWidget):
|
||||
("时段", period_text),
|
||||
(
|
||||
"预约类型",
|
||||
first_value(detail, "appointment_type_desc", "appointment_type_text"),
|
||||
appointment_type_description(appointment_type_value(detail)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -38,7 +38,8 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from .. import icons
|
||||
from ...core.appointment_modes import appointment_type_value, can_appointment_video
|
||||
from .. import icons
|
||||
from ..consultations_style import consultations_stylesheet
|
||||
from ..diagnosis_index_widgets import (
|
||||
DiagnosisChip,
|
||||
@@ -226,7 +227,7 @@ def _appointment_status(record: Any) -> Any:
|
||||
|
||||
|
||||
def _appointment_id(record: Any) -> int:
|
||||
value = first_value(record, "appointment_id", default=None)
|
||||
value = first_value(record, "appointment_id", "latest_appointment_id", default=None)
|
||||
if value is None:
|
||||
value = first_value(get_value(record, "latest_appointment", None), "id", default=None)
|
||||
if value is None:
|
||||
@@ -289,7 +290,8 @@ def _video_payload(record: Any) -> dict[str, Any]:
|
||||
|
||||
return {
|
||||
"source": "consultations",
|
||||
"appointment_id": _appointment_id(record),
|
||||
"appointment_id": _appointment_id(record),
|
||||
"appointment_type": appointment_type_value(record),
|
||||
"patient_id": first_value(record, "patient_id", "source_patient_id"),
|
||||
"diagnosis_id": first_value(record, "diagnosis_id", "id"),
|
||||
"patient_name": first_value(record, "patient_name", default="患者"),
|
||||
@@ -2655,7 +2657,11 @@ class ConsultationsPage(QWidget):
|
||||
or ""
|
||||
).strip()
|
||||
|
||||
def _request_video_qr(self) -> None:
|
||||
def _request_video_qr(self) -> None:
|
||||
record = self.table.current_data()
|
||||
if record is not None and not can_appointment_video(appointment_type_value(record)):
|
||||
show_toast(self, "本次挂号不支持视频问诊二维码。", "warning")
|
||||
return
|
||||
self._request_qr(
|
||||
capability="video_qr",
|
||||
permission="tcm.diagnosis/videoQr",
|
||||
@@ -3050,11 +3056,11 @@ class ConsultationsPage(QWidget):
|
||||
self.video_button.setEnabled(
|
||||
has_record
|
||||
and is_video_available(record)
|
||||
and video_call_is_live(record)
|
||||
and (appointment_type_value(record) == "text" or video_call_is_live(record))
|
||||
and valid_ids
|
||||
)
|
||||
self.call_toolbar_button.setEnabled(self.video_button.isEnabled())
|
||||
self.video_qr_toolbar_button.setEnabled(has_record)
|
||||
self.video_qr_toolbar_button.setEnabled(has_record and can_appointment_video(appointment_type_value(record)))
|
||||
self.complete_toolbar_button.setEnabled(has_record)
|
||||
self.case_toolbar_button.setEnabled(has_record)
|
||||
self.prescription_toolbar_button.setEnabled(has_record and not self._prescription_busy)
|
||||
@@ -3498,7 +3504,7 @@ class ConsultationsPage(QWidget):
|
||||
if record is None or not is_video_available(record):
|
||||
self.banner.show_message("仅当前“已预约”的挂号可进入视频问诊。", "warning")
|
||||
return
|
||||
if not video_call_is_live(record):
|
||||
if appointment_type_value(record) != "text" and not video_call_is_live(record):
|
||||
self.banner.show_message("医生尚未发起视频会话,请等待会话开始。", "warning")
|
||||
return
|
||||
payload = _video_payload(record)
|
||||
|
||||
@@ -43,13 +43,18 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from ...core.appointment_modes import (
|
||||
APPOINTMENT_MODES,
|
||||
appointment_type_description,
|
||||
appointment_type_value,
|
||||
)
|
||||
from .. import icons
|
||||
from ..appointment_drawer import AppointmentDrawer
|
||||
from ..dialogs import DiagnosisDialog, present_ai_consult, present_order_detail
|
||||
from ..dialogs.ai_consult import can_open_ai_consult
|
||||
from ..dialogs.prescription import PrescriptionOrderListDialog
|
||||
from ..filter_disclosure import FilterDisclosure
|
||||
from ..infinite_list import InfiniteList
|
||||
from ..dialogs.prescription import PrescriptionOrderListDialog
|
||||
from ..filter_disclosure import FilterDisclosure
|
||||
from ..infinite_list import InfiniteList
|
||||
from ..patient_orders_style import patient_orders_stylesheet
|
||||
from ..patient_progress_style import patient_progress_stylesheet
|
||||
from ..patients_style import patient_list_stylesheet, patients_chrome_stylesheet
|
||||
@@ -499,7 +504,8 @@ class _LegacyAppointmentDialog(QDialog):
|
||||
form = QFormLayout()
|
||||
form.setVerticalSpacing(10)
|
||||
self.appointment_type = QComboBox()
|
||||
self.appointment_type.addItem("视频问诊", "video")
|
||||
for label, value in APPOINTMENT_MODES:
|
||||
self.appointment_type.addItem(label, value)
|
||||
form.addRow("预约类型 *", self.appointment_type)
|
||||
self.channel_source = QComboBox()
|
||||
self.channel_source.addItem("正在加载渠道…", "")
|
||||
@@ -1367,12 +1373,14 @@ class _PatientInfoDelegate(QStyledItemDelegate):
|
||||
painter.setBrush(QColor(fill))
|
||||
painter.drawRoundedRect(badge, 3, 3)
|
||||
draw(status, badge.toRect(), color, True)
|
||||
elif index.column() == 4:
|
||||
value = display_text(first_value(row, "appointment_time_text"))
|
||||
date_text, separator, time_text = value.partition(" ")
|
||||
if separator and time_text:
|
||||
draw(date_text, top)
|
||||
draw(time_text, bottom)
|
||||
elif index.column() == 4:
|
||||
value = display_text(first_value(row, "appointment_time_text"))
|
||||
date_text, separator, time_text = value.partition(" ")
|
||||
if separator and time_text:
|
||||
draw(date_text, top)
|
||||
has_appointment = _as_int(first_value(row, "appointment_id", default=0)) > 0
|
||||
mode = appointment_type_description(appointment_type_value(row)) if has_appointment else ""
|
||||
draw(f"{time_text} · {mode}" if mode else time_text, bottom)
|
||||
else:
|
||||
draw(value, rect)
|
||||
else:
|
||||
@@ -3005,7 +3013,7 @@ class PatientProgressWorkspace(QWidget):
|
||||
"appointment_time",
|
||||
"预约时间",
|
||||
95,
|
||||
lambda value, _row: display_text(value)[:5],
|
||||
lambda value, row: f"{display_text(value)[:5]} · {appointment_type_description(appointment_type_value(row))}",
|
||||
),
|
||||
TableColumn(
|
||||
"ahead_count",
|
||||
|
||||
@@ -73,6 +73,7 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from ...core.appointment_modes import appointment_type_description, appointment_type_value
|
||||
from .. import icons
|
||||
from ..diagnosis_drawer import DailyRecordPanel
|
||||
from ..diagnosis_editors import FlowLayout
|
||||
@@ -7824,6 +7825,7 @@ class ReceptionPage(QWidget):
|
||||
meta_parts = [f"{gender} · {display_text(age)}岁", phone]
|
||||
if visit_id not in (None, ""):
|
||||
meta_parts.append(f"就诊号:{display_text(visit_id)}")
|
||||
meta_parts.append(appointment_type_description(appointment_type_value(appointment)))
|
||||
self.patient_meta_label.setText(" | ".join(meta_parts))
|
||||
self.patient_meta_label.setToolTip(" · ".join(condition_parts))
|
||||
status_number = (
|
||||
@@ -7866,15 +7868,7 @@ class ReceptionPage(QWidget):
|
||||
display_text(first_value(appointment, "assistant_name"))
|
||||
)
|
||||
self.appointment_labels["type"].setText(
|
||||
display_text(
|
||||
first_value(
|
||||
appointment,
|
||||
"appointment_type_text",
|
||||
"type_text",
|
||||
"appointment_type",
|
||||
"type",
|
||||
)
|
||||
)
|
||||
appointment_type_description(appointment_type_value(appointment))
|
||||
)
|
||||
self.appointment_labels["channel"].setText(
|
||||
display_text(
|
||||
@@ -8928,6 +8922,12 @@ class ReceptionPage(QWidget):
|
||||
self.video_button.setEnabled(
|
||||
appointment_id is not None and diagnosis_id is not None and patient_id is not None
|
||||
)
|
||||
is_text = appointment_type_value(appointment) == "text"
|
||||
self.video_button.setText("图文沟通" if is_text else "IM 问诊")
|
||||
self.video_button.setToolTip(
|
||||
"发送文字、图片和文件;图文问诊不支持音视频通话" if is_text
|
||||
else "打开患者 IM,会话通话能力由本次挂号决定"
|
||||
)
|
||||
self.edit_button.setEnabled(diagnosis_id is not None)
|
||||
report_enabled = (
|
||||
self._can_ai_report
|
||||
@@ -9684,6 +9684,7 @@ class ReceptionPage(QWidget):
|
||||
payload = {
|
||||
"source": "reception",
|
||||
"appointment_id": appointment_id,
|
||||
"appointment_type": appointment_type_value(appointment),
|
||||
"patient_id": patient_id,
|
||||
"diagnosis_id": diagnosis_id,
|
||||
"patient_name": first_value(
|
||||
|
||||
@@ -85,6 +85,18 @@ def _normalized_key(key: Any) -> str:
|
||||
return "".join(character for character in str(key).lower() if character.isalnum())
|
||||
|
||||
|
||||
def _appointment_id(value: Any) -> int:
|
||||
if value is None:
|
||||
return 0
|
||||
try:
|
||||
parsed = int(value)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise VideoTicketError("appointment_id must be a nonnegative integer") from exc
|
||||
if isinstance(value, bool) or parsed < 0 or str(value) != str(parsed):
|
||||
raise VideoTicketError("appointment_id must be a nonnegative integer")
|
||||
return parsed
|
||||
|
||||
|
||||
_FORBIDDEN_SECRET_KEYS = {"sdksecret", "sdksecretkey", "secretkey"}
|
||||
|
||||
|
||||
@@ -138,7 +150,8 @@ def _ticket_mapping(ticket: Any) -> Mapping[str, Any]:
|
||||
if hasattr(ticket, attribute_name)
|
||||
}
|
||||
if adapted:
|
||||
return adapted
|
||||
# Preserve server policy and identities; model attributes alone omit them.
|
||||
return {**(dict(raw) if isinstance(raw, Mapping) else {}), **adapted}
|
||||
raise VideoTicketError("backend ticket must be a mapping or call-ticket object")
|
||||
|
||||
|
||||
@@ -199,8 +212,15 @@ class VideoCallRequest:
|
||||
patient_id: Identifier | None = None
|
||||
call_record_id: Identifier | None = None
|
||||
backend_mode: BackendMode = BackendMode.EMBEDDED
|
||||
appointment_id: int = 0
|
||||
appointment_type: Any = None
|
||||
appointment_type_desc: str = ""
|
||||
can_video_call: bool = False
|
||||
can_audio_call: bool = False
|
||||
call_disabled_reason: str = ""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "appointment_id", _appointment_id(self.appointment_id))
|
||||
object.__setattr__(self, "sdk_app_id", _sdk_app_id(self.sdk_app_id))
|
||||
object.__setattr__(self, "user_id", _non_empty_string(self.user_id, "userID"))
|
||||
object.__setattr__(self, "user_sig", _non_empty_string(self.user_sig, "userSig"))
|
||||
@@ -257,6 +277,13 @@ class VideoCallRequest:
|
||||
"userSig": self.user_sig,
|
||||
"targetUserId": self.target_user_id,
|
||||
"diagnosisId": self.diagnosis_id,
|
||||
"patientId": self.patient_id,
|
||||
"appointmentId": self.appointment_id,
|
||||
"appointment_type": self.appointment_type,
|
||||
"appointment_type_desc": self.appointment_type_desc,
|
||||
"can_video_call": self.can_video_call is True,
|
||||
"can_audio_call": self.can_audio_call is True,
|
||||
"call_disabled_reason": self.call_disabled_reason,
|
||||
}
|
||||
|
||||
def safe_log_context(self) -> dict[str, Any]:
|
||||
@@ -340,6 +367,12 @@ def normalize_backend_ticket(
|
||||
diagnosis_id=normalized_diagnosis,
|
||||
patient_id=normalized_patient,
|
||||
call_record_id=payload_call_record,
|
||||
appointment_id=_appointment_id(payload.get("appointment_id", 0)),
|
||||
appointment_type=payload.get("appointment_type"),
|
||||
appointment_type_desc=str(payload.get("appointment_type_desc") or ""),
|
||||
can_video_call=payload.get("can_video_call") is True,
|
||||
can_audio_call=payload.get("can_audio_call") is True,
|
||||
call_disabled_reason=str(payload.get("call_disabled_reason") or ""),
|
||||
backend_mode=BackendMode.parse(backend_mode),
|
||||
)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from .launcher import VideoCallRequest
|
||||
from .launcher import VideoCallRequest, normalize_backend_ticket
|
||||
|
||||
ResultT = TypeVar("ResultT")
|
||||
|
||||
@@ -288,7 +288,29 @@ class OrderedCallLifecycle:
|
||||
with self._lock:
|
||||
return self.bound_room_id or self._claimed_room_id
|
||||
|
||||
def start(self) -> Future[bool]:
|
||||
def refresh_call_policy(self) -> Future[dict[str, Any]]:
|
||||
"""Fetch policy for this exact immutable consultation on the worker."""
|
||||
def operation() -> dict[str, Any]:
|
||||
ticket = self.repository.get_call_ticket(
|
||||
patient_id=self.request.patient_id,
|
||||
diagnosis_id=self.request.diagnosis_id,
|
||||
appointment_id=self.request.appointment_id,
|
||||
)
|
||||
refreshed = normalize_backend_ticket(
|
||||
ticket, patient_id=self.request.patient_id,
|
||||
diagnosis_id=self.request.diagnosis_id,
|
||||
)
|
||||
if (refreshed.appointment_id != self.request.appointment_id
|
||||
or refreshed.target_user_id != self.request.target_user_id
|
||||
or refreshed.user_id != self.request.user_id
|
||||
or refreshed.sdk_app_id != self.request.sdk_app_id):
|
||||
raise ValueError("当前问诊已变更,请重新打开聊天窗口")
|
||||
return refreshed.to_web_config()
|
||||
return self._worker.submit("refresh_policy", operation)
|
||||
|
||||
def start(self, *, call_type: int = 2) -> Future[bool]:
|
||||
if type(call_type) is not int or call_type not in (1, 2):
|
||||
raise ValueError("call_type must be 1 or 2")
|
||||
with self._lock:
|
||||
if self._start_future is not None:
|
||||
return self._start_future
|
||||
@@ -297,7 +319,8 @@ class OrderedCallLifecycle:
|
||||
raise ValueError("video repository does not implement start_call")
|
||||
payload: dict[str, Any] = {
|
||||
"diagnosis_id": self.request.diagnosis_id,
|
||||
"call_type": 2,
|
||||
"call_type": call_type,
|
||||
"appointment_id": self.request.appointment_id,
|
||||
}
|
||||
if self.request.patient_id is not None:
|
||||
payload["patient_id"] = self.request.patient_id
|
||||
|
||||
@@ -279,6 +279,7 @@ if WEBENGINE_AVAILABLE: # pragma: no cover - GUI behavior needs an integration
|
||||
call_ended = Signal(str) # type: ignore[misc]
|
||||
call_error = Signal(str) # type: ignore[misc]
|
||||
_start_completed = Signal(bool) # type: ignore[misc]
|
||||
_policy_completed = Signal(str, object) # type: ignore[misc]
|
||||
_room_completed = Signal(str, bool, str) # type: ignore[misc]
|
||||
_screenshot_completed = Signal(bool, str) # type: ignore[misc]
|
||||
_transcription_completed = Signal(str, str, str, bool, str) # type: ignore[misc]
|
||||
@@ -369,6 +370,7 @@ if WEBENGINE_AVAILABLE: # pragma: no cover - GUI behavior needs an integration
|
||||
self._connect_permissions()
|
||||
|
||||
self._start_completed.connect(self._on_lifecycle_started)
|
||||
self._policy_completed.connect(self._on_policy_completed)
|
||||
self._room_completed.connect(self._on_room_completed)
|
||||
self._screenshot_completed.connect(self._on_screenshot_completed)
|
||||
self._transcription_completed.connect(self._on_transcription_completed)
|
||||
@@ -509,7 +511,10 @@ if WEBENGINE_AVAILABLE: # pragma: no cover - GUI behavior needs an integration
|
||||
QTimer.singleShot(0, self._open_diagnosis_safely)
|
||||
return
|
||||
if event == "call-start-request":
|
||||
self._start_call_cycle()
|
||||
self._start_call_cycle(call_type=message.get("callType", 2))
|
||||
return
|
||||
if event == "call-policy-request":
|
||||
self._refresh_call_policy(str(message.get("requestId") or ""))
|
||||
return
|
||||
if event == "screenshot":
|
||||
self._save_screenshot(str(message.get("dataUrl") or ""))
|
||||
@@ -934,16 +939,48 @@ if WEBENGINE_AVAILABLE: # pragma: no cover - GUI behavior needs an integration
|
||||
"本次本地录音未正常结束,文件已保留以便排查。",
|
||||
)
|
||||
|
||||
def _start_call_cycle(self) -> None:
|
||||
if self._closing or self._start_requested:
|
||||
return
|
||||
def _prepare_call_cycle(self) -> None:
|
||||
if self._call_cycle_closed:
|
||||
self.lifecycle = self._lifecycle_factory()
|
||||
self._lifecycles.append(self.lifecycle)
|
||||
self._call_cycle_closed = False
|
||||
|
||||
def _refresh_call_policy(self, request_id: str) -> None:
|
||||
if not request_id or self._closing or self._shutdown_requested:
|
||||
return
|
||||
self._prepare_call_cycle()
|
||||
def completed(future: Future[dict[str, Any]]) -> None:
|
||||
try:
|
||||
policy = future.result()
|
||||
except Exception:
|
||||
policy = {"call_disabled_reason": "无法确认本次挂号通话权限,请重新打开聊天窗口"}
|
||||
with suppress(RuntimeError):
|
||||
self._policy_completed.emit(request_id, policy)
|
||||
try:
|
||||
self.lifecycle.refresh_call_policy().add_done_callback(completed)
|
||||
except Exception:
|
||||
self._on_policy_completed(request_id, {})
|
||||
|
||||
def _on_policy_completed(self, request_id: str, policy: dict[str, Any]) -> None:
|
||||
if self._closing or self._shutdown_requested or self._released:
|
||||
return
|
||||
# Send policy only; refreshed credentials never enter logs or notices.
|
||||
payload = {key: policy.get(key) for key in (
|
||||
"appointmentId", "appointment_type", "can_video_call", "can_audio_call",
|
||||
"call_disabled_reason", "appointment_type_desc",
|
||||
)}
|
||||
self._page.runJavaScript(
|
||||
"window.doctorConsultation?.callPolicyResult?.("
|
||||
+ json.dumps(request_id) + "," + json.dumps(payload, ensure_ascii=True) + ");"
|
||||
)
|
||||
|
||||
def _start_call_cycle(self, *, call_type: int = 2) -> None:
|
||||
if self._closing or self._shutdown_requested or self._start_requested:
|
||||
return
|
||||
self._prepare_call_cycle()
|
||||
self._start_requested = True
|
||||
try:
|
||||
future = self.lifecycle.start()
|
||||
future = self.lifecycle.start(call_type=call_type)
|
||||
except Exception:
|
||||
self._start_requested = False
|
||||
self._on_lifecycle_started(False)
|
||||
@@ -1149,8 +1186,8 @@ if WEBENGINE_AVAILABLE: # pragma: no cover - GUI behavior needs an integration
|
||||
self._closing = True
|
||||
self._media_active = False
|
||||
self._shutdown_timer.stop()
|
||||
if self._start_requested and not self._call_cycle_closed:
|
||||
self.lifecycle.end(self._close_reason)
|
||||
# Also retire policy-only workers for IM sessions without a live call.
|
||||
self.lifecycle.end(self._close_reason)
|
||||
self._abort_local_audio_recording("")
|
||||
self._release_webengine()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user