更新
This commit is contained in:
@@ -2696,11 +2696,15 @@ class DemoDoctorRepository:
|
||||
) -> dict[str, Any]:
|
||||
"""Return the demo archive in the server response envelope shape."""
|
||||
|
||||
with self._lock:
|
||||
consultation = self._find_consultation(diagnosis_id)
|
||||
return deepcopy(
|
||||
{
|
||||
"lists": self._im_messages.get(diagnosis_id, []),
|
||||
with self._lock:
|
||||
consultation = self._find_consultation(diagnosis_id)
|
||||
rows = [
|
||||
{**row, "diagnosis_id": diagnosis_id}
|
||||
for row in self._im_messages.get(diagnosis_id, [])
|
||||
]
|
||||
return deepcopy(
|
||||
{
|
||||
"lists": rows,
|
||||
"patient_im_id": f"patient_{consultation.patient_id}",
|
||||
"patient_name": consultation.patient_name,
|
||||
"doctor_accounts_queried": [] if only_archived else ["doctor_1001"],
|
||||
|
||||
@@ -2192,7 +2192,9 @@ class AiConsultDialog(QDialog):
|
||||
"messages", {"value": None, "error": "未返回资料。"}
|
||||
)
|
||||
message_rows, rejected_messages = _owned_rows(
|
||||
message_section.get("value"), self.diagnosis_id
|
||||
message_section.get("value"),
|
||||
self.diagnosis_id,
|
||||
require_owner=True,
|
||||
)
|
||||
message_section["value"] = message_rows
|
||||
if rejected_messages:
|
||||
|
||||
@@ -1575,9 +1575,17 @@ class AppointmentsPage(QWidget):
|
||||
)
|
||||
present_diagnosis_ai_report(self.repository, self.permissions, self, payload)
|
||||
|
||||
def _open_ai_consult(self) -> None:
|
||||
def open_selected_ai_consult(self) -> bool:
|
||||
"""Open the diagnosis-scoped assistant for the selected appointment."""
|
||||
|
||||
row = self._current_row()
|
||||
diagnosis_id = _diagnosis_id(row)
|
||||
if (
|
||||
row is None
|
||||
or diagnosis_id <= 0
|
||||
or not can_open_ai_consult(self.permissions)
|
||||
):
|
||||
return False
|
||||
present_ai_consult(
|
||||
self.repository,
|
||||
self.permissions,
|
||||
@@ -1587,6 +1595,11 @@ class AppointmentsPage(QWidget):
|
||||
seed=row,
|
||||
source_title="问诊列表",
|
||||
)
|
||||
return True
|
||||
|
||||
def _open_ai_consult(self) -> None:
|
||||
if not self.open_selected_ai_consult():
|
||||
show_toast(self, "请先选择一条有关联诊单的问诊记录。", "warning")
|
||||
|
||||
def _request_video(self) -> None:
|
||||
if not _canonical_allowed(
|
||||
|
||||
@@ -2977,9 +2977,17 @@ class ConsultationsPage(QWidget):
|
||||
return
|
||||
self._diagnosis_dialog.open_view_only(diagnosis_id, seed=record)
|
||||
|
||||
def _open_ai_consult(self) -> None:
|
||||
def open_selected_ai_consult(self) -> bool:
|
||||
"""Open the assistant with the selected diagnosis as its authority key."""
|
||||
|
||||
record = self.table.current_data()
|
||||
diagnosis_id = _as_int(first_value(record, "diagnosis_id", "id", default=0))
|
||||
if (
|
||||
record is None
|
||||
or diagnosis_id <= 0
|
||||
or not can_open_ai_consult(self.permissions)
|
||||
):
|
||||
return False
|
||||
present_ai_consult(
|
||||
self.repository,
|
||||
self.permissions,
|
||||
@@ -2989,8 +2997,13 @@ class ConsultationsPage(QWidget):
|
||||
first_value(record, "source_patient_id", "patient_id", default=0)
|
||||
),
|
||||
seed=record,
|
||||
source_title="患者列表",
|
||||
source_title="问诊列表",
|
||||
)
|
||||
return True
|
||||
|
||||
def _open_ai_consult(self) -> None:
|
||||
if not self.open_selected_ai_consult():
|
||||
show_toast(self, "请先选择一条有效诊单。", "warning")
|
||||
|
||||
def _open_edit(self) -> None:
|
||||
if not _canonical_allowed(self.permissions, "tcm.diagnosis/edit"):
|
||||
|
||||
@@ -2740,6 +2740,24 @@ class PatientsPage(QWidget):
|
||||
source_title="我的患者",
|
||||
)
|
||||
|
||||
def open_selected_ai_consult(self) -> bool:
|
||||
"""Open AI chat for the selected row in the active patient workspace."""
|
||||
|
||||
if not can_open_ai_consult(self.permissions):
|
||||
return False
|
||||
workspace = self.tabs.currentWidget()
|
||||
row = None
|
||||
for table_name in ("table", "queue_table"):
|
||||
table = getattr(workspace, table_name, None)
|
||||
current_data = getattr(table, "current_data", None)
|
||||
if callable(current_data):
|
||||
row = current_data()
|
||||
break
|
||||
if row is None or self._diagnosis_id(row) <= 0:
|
||||
return False
|
||||
self._open_ai_consult(row)
|
||||
return True
|
||||
|
||||
def _open_order_diagnosis(self, row: Any) -> None:
|
||||
editable = _canonical_allowed(self.permissions, "tcm.diagnosis/edit")
|
||||
readable = _canonical_allowed(self.permissions, "tcm.diagnosis/readonlyDetail")
|
||||
|
||||
@@ -8647,17 +8647,14 @@ class ReceptionPage(QWidget):
|
||||
task=diagnosis_ai_task(prompt),
|
||||
)
|
||||
|
||||
def _open_ai_consult(self) -> None:
|
||||
if not self._can_ai_assistant:
|
||||
show_toast(self, "当前账号没有使用 AI 问诊助手的权限。", "danger")
|
||||
return
|
||||
if self._detail_loading:
|
||||
show_toast(self, "患者病历仍在加载,请稍候。", "warning")
|
||||
return
|
||||
def open_selected_ai_consult(self) -> bool:
|
||||
"""Open AI chat only when the reception selection has a diagnosis."""
|
||||
|
||||
if not self._can_ai_assistant or self._detail_loading:
|
||||
return False
|
||||
context = self._selection_context()
|
||||
if context is None or context[2] is None:
|
||||
show_toast(self, "当前患者缺少诊单编号。", "danger")
|
||||
return
|
||||
return False
|
||||
seed = self._selected_detail or self._selected_record or {}
|
||||
present_ai_consult(
|
||||
self.repository,
|
||||
@@ -8668,6 +8665,17 @@ class ReceptionPage(QWidget):
|
||||
seed=seed,
|
||||
source_title="接诊台",
|
||||
)
|
||||
return True
|
||||
|
||||
def _open_ai_consult(self) -> None:
|
||||
if self.open_selected_ai_consult():
|
||||
return
|
||||
if not self._can_ai_assistant:
|
||||
show_toast(self, "当前账号没有使用 AI 问诊助手的权限。", "danger")
|
||||
elif self._detail_loading:
|
||||
show_toast(self, "患者病历仍在加载,请稍候。", "warning")
|
||||
else:
|
||||
show_toast(self, "请先选择一位有诊单的患者。", "warning")
|
||||
|
||||
def _open_ai_report(
|
||||
self,
|
||||
|
||||
@@ -40,6 +40,7 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from .dialogs.ai_consult import can_open_ai_consult
|
||||
from .dialogs.local_audio_queue import LocalAudioQueueDialog
|
||||
from .pages import (
|
||||
AppointmentsPage,
|
||||
@@ -872,6 +873,7 @@ class ShellWindow(QMainWindow):
|
||||
if session_permissions is not None
|
||||
else get_value(self.current_user, "permissions", None)
|
||||
)
|
||||
self._can_ai_assistant = can_open_ai_consult(self.permissions)
|
||||
session_menu = get_value(self.session, "menu", None)
|
||||
self.menu = (
|
||||
session_menu if session_menu is not None else get_value(session, "menu", [])
|
||||
@@ -1116,6 +1118,7 @@ class ShellWindow(QMainWindow):
|
||||
outer_assistant.setContentsMargins(13, 0, 13, 0)
|
||||
outer_assistant.addWidget(self.assistant_card)
|
||||
layout.addLayout(outer_assistant)
|
||||
self.assistant_card.setVisible(self._can_ai_assistant)
|
||||
|
||||
self.upload_settings_button = QPushButton("设置 ›", sidebar)
|
||||
self.upload_settings_button.setObjectName("ShellSettingsButton")
|
||||
@@ -1288,6 +1291,7 @@ class ShellWindow(QMainWindow):
|
||||
self.ai_top_button.setAccessibleName("AI 助手")
|
||||
self.ai_top_button.clicked.connect(self._open_ai_assistant)
|
||||
layout.addWidget(self.ai_top_button)
|
||||
self.ai_top_button.setVisible(self._can_ai_assistant)
|
||||
|
||||
self.notification_button = _PaintedIconButton(
|
||||
"notification", size=38, parent=topbar
|
||||
@@ -1456,7 +1460,9 @@ class ShellWindow(QMainWindow):
|
||||
else self._expanded_sidebar_width()
|
||||
)
|
||||
self.brand_copy.setVisible(not self._sidebar_collapsed)
|
||||
self.assistant_card.setVisible(not self._sidebar_collapsed)
|
||||
self.assistant_card.setVisible(
|
||||
self._can_ai_assistant and not self._sidebar_collapsed
|
||||
)
|
||||
self.upload_settings_button.setVisible(not self._sidebar_collapsed)
|
||||
margins = (7, 14, 7, 0) if self._sidebar_collapsed else (12, 19, 12, 0)
|
||||
self.nav_layout.setContentsMargins(*margins)
|
||||
@@ -1542,12 +1548,35 @@ class ShellWindow(QMainWindow):
|
||||
show_toast(self, f"正在当前页面搜索“{query}”。", "info")
|
||||
|
||||
def _open_ai_assistant(self) -> None:
|
||||
self.navigate("reception")
|
||||
if not self._can_ai_assistant:
|
||||
show_toast(self, "当前账号没有使用 AI 问诊助手的权限。", "danger")
|
||||
return
|
||||
|
||||
current_page = self.stack.currentWidget()
|
||||
opener = getattr(current_page, "open_selected_ai_consult", None)
|
||||
if callable(opener) and bool(opener()):
|
||||
return
|
||||
|
||||
reception = self.pages.get("reception")
|
||||
if reception is None or not self.navigate("reception"):
|
||||
show_toast(
|
||||
self,
|
||||
"当前账号没有可用的接诊台;请从问诊列表或患者列表选择诊单后使用 AI 分析。",
|
||||
"warning",
|
||||
4800,
|
||||
)
|
||||
return
|
||||
|
||||
if reception is not current_page:
|
||||
reception_opener = getattr(reception, "open_selected_ai_consult", None)
|
||||
if callable(reception_opener) and bool(reception_opener()):
|
||||
return
|
||||
|
||||
show_toast(
|
||||
self,
|
||||
"已进入接诊台;选择患者后可查看 AI 报告与辅助分析。",
|
||||
"请先在接诊台选择一位有诊单的患者,再点击“开始对话”。",
|
||||
"info",
|
||||
3600,
|
||||
4200,
|
||||
)
|
||||
|
||||
def _open_local_audio_settings(self) -> None:
|
||||
|
||||
@@ -710,6 +710,36 @@ def test_ownerless_notes_prescriptions_and_tracking_rows_fail_closed_as_warning(
|
||||
dialog.close()
|
||||
|
||||
|
||||
def test_ownerless_im_messages_fail_closed_before_chat_rendering(
|
||||
application: QApplication,
|
||||
immediate_async: None,
|
||||
) -> None:
|
||||
class OwnerlessMessageRepository(WorkspaceRepository):
|
||||
def list_im_chat_messages(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
*,
|
||||
only_archived: bool = True,
|
||||
) -> list[dict[str, Any]]:
|
||||
del diagnosis_id, only_archived
|
||||
return [
|
||||
{
|
||||
"msg_id": "ownerless-message",
|
||||
"msg_type": "text",
|
||||
"text": "不应展示的无归属会话",
|
||||
"is_from_doctor": False,
|
||||
}
|
||||
]
|
||||
|
||||
dialog = _open_dialog(application, OwnerlessMessageRepository())
|
||||
chat_text = "\n".join(
|
||||
label.text() for label in dialog.chat_host.findChildren(QLabel)
|
||||
)
|
||||
|
||||
assert "不应展示的无归属会话" not in chat_text
|
||||
dialog.close()
|
||||
|
||||
|
||||
def test_tracking_response_without_diagnosis_owner_is_filtered_without_retry(
|
||||
application: QApplication,
|
||||
immediate_async: None,
|
||||
|
||||
@@ -33,10 +33,16 @@ class _ShellPageDouble(QWidget):
|
||||
self.current_user = current_user
|
||||
self.refresh_count = 0
|
||||
self.show_count = 0
|
||||
self.ai_context_available = False
|
||||
self.ai_open_count = 0
|
||||
|
||||
def refresh(self) -> None:
|
||||
self.refresh_count += 1
|
||||
|
||||
def open_selected_ai_consult(self) -> bool:
|
||||
self.ai_open_count += 1
|
||||
return self.ai_context_available
|
||||
|
||||
def showEvent(self, event: Any) -> None: # noqa: N802 - Qt virtual
|
||||
super().showEvent(event)
|
||||
self.show_count += 1
|
||||
@@ -143,7 +149,8 @@ def shell_window(
|
||||
"user": {"name": "陈医生", "department_name": "中医门诊", "role_ids": [1]},
|
||||
"demo_mode": True,
|
||||
},
|
||||
permissions={item.permissions[0] for item in navigation},
|
||||
permissions={item.permissions[0] for item in navigation}
|
||||
| {"tcm.diagnosis/aiAssistant"},
|
||||
)
|
||||
window.show()
|
||||
application.processEvents()
|
||||
@@ -255,6 +262,90 @@ def test_reference_shell_has_integrated_search_ai_card_and_window_controls(
|
||||
assert "在线" in shell_window.assistant_status.text()
|
||||
|
||||
|
||||
def test_shell_ai_entry_opens_the_current_selected_diagnosis(
|
||||
application: QApplication,
|
||||
shell_window: ShellWindow,
|
||||
) -> None:
|
||||
current = shell_window.pages["appointments"]
|
||||
assert isinstance(current, _ShellPageDouble)
|
||||
current.ai_context_available = True
|
||||
|
||||
shell_window.assistant_button.click()
|
||||
application.processEvents()
|
||||
|
||||
assert current.ai_open_count == 1
|
||||
assert shell_window.stack.currentWidget() is current
|
||||
|
||||
|
||||
def test_shell_ai_entry_falls_back_to_reception_and_opens_its_selection(
|
||||
application: QApplication,
|
||||
shell_window: ShellWindow,
|
||||
) -> None:
|
||||
appointments = shell_window.pages["appointments"]
|
||||
reception = shell_window.pages["reception"]
|
||||
assert isinstance(appointments, _ShellPageDouble)
|
||||
assert isinstance(reception, _ShellPageDouble)
|
||||
reception.ai_context_available = True
|
||||
|
||||
shell_window.ai_top_button.click()
|
||||
application.processEvents()
|
||||
|
||||
assert appointments.ai_open_count == 1
|
||||
assert reception.ai_open_count == 1
|
||||
assert shell_window.stack.currentWidget() is reception
|
||||
|
||||
|
||||
def test_shell_ai_entry_on_reception_opens_chat_instead_of_noop(
|
||||
application: QApplication,
|
||||
shell_window: ShellWindow,
|
||||
) -> None:
|
||||
reception = shell_window.pages["reception"]
|
||||
assert isinstance(reception, _ShellPageDouble)
|
||||
assert shell_window.navigate("reception")
|
||||
reception.ai_context_available = True
|
||||
|
||||
shell_window.ai_top_button.click()
|
||||
application.processEvents()
|
||||
|
||||
assert reception.ai_open_count == 1
|
||||
assert shell_window.stack.currentWidget() is reception
|
||||
|
||||
|
||||
def test_shell_hides_global_ai_entries_without_ai_permission(
|
||||
application: QApplication,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
navigation = [
|
||||
NavigationItem(
|
||||
"appointments",
|
||||
"问诊列表",
|
||||
"号",
|
||||
_ShellPageDouble,
|
||||
("doctor.appointment/lists",),
|
||||
)
|
||||
]
|
||||
monkeypatch.setattr(
|
||||
shell_module,
|
||||
"_resolve_navigation",
|
||||
lambda _menu, _permissions, *, demo_mode: [
|
||||
(item, item.title) for item in navigation
|
||||
],
|
||||
)
|
||||
window = ShellWindow(
|
||||
object(),
|
||||
{"user": {"name": "无 AI 权限医生"}, "demo_mode": True},
|
||||
permissions={"doctor.appointment/lists"},
|
||||
)
|
||||
window.show()
|
||||
application.processEvents()
|
||||
|
||||
assert window.assistant_card.isHidden()
|
||||
assert window.ai_top_button.isHidden()
|
||||
|
||||
window.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_shell_settings_opens_global_local_audio_upload_manager(
|
||||
application: QApplication,
|
||||
shell_window: ShellWindow,
|
||||
|
||||
Reference in New Issue
Block a user