--- C:\kefu\wechat_rpa\backups\review-assistant-20260917\wechat_gui_qt.py +++ C:\kefu\wechat_rpa\wechat_gui_qt.py @@ -64,6 +64,7 @@ QLayout, QLineEdit, QListWidget, + QListWidgetItem, QListWidgetItem, QMainWindow, QMenu, @@ -8928,6 +8929,26 @@ self.host.settings_page._emit_save() self.host._push_console_state() + @Slot() + def refreshReviewAssistant(self) -> None: + self.host._refresh_review_assistant() + + @Slot() + def chooseReviewAssistant(self) -> None: + self.host._choose_review_assistant() + + @Slot(str, result=str) + def setReviewAssistantEnabled(self, payload: str) -> str: + try: + value = json.loads(payload) + if not isinstance(value, dict) or type(value.get("enabled")) is not bool: + raise ValueError("提醒开关格式无效") + result = self.host._save_review_assistant( + str(value.get("expected_account") or ""), enabled=value["enabled"]) + except (ValueError, TypeError) as exc: + result = {"ok": False, "message": str(exc)} + return json.dumps(result, ensure_ascii=False) + @Slot(str, result=str) def saveAutomationSettings(self, payload: str) -> str: try: @@ -9291,8 +9312,145 @@ target.activateWindow() +class ReviewAssistantPicker(QDialog): + recipientsLoaded = Signal(object) + recipientValidated = Signal(object) + + def __init__(self, account, parent=None): + super().__init__(parent) + self.account = str(account) + self.recipient = None + self._serial = 0 + self._offset = 0 + self._closed = False + self.setWindowTitle("选择医疗助理私人微信") + self.resize(660, 500) + layout = QVBoxLayout(self) + note = QLabel("仅列出当前企微账号已有的私人微信单聊。请核对联系人及联系编号,同名联系人不会自动合并。") + note.setWordWrap(True) + layout.addWidget(note) + self.query = QLineEdit() + self.query.setPlaceholderText("搜索联系人昵称") + self.query.setMaxLength(100) + layout.addWidget(self.query) + self.contacts = QListWidget() + layout.addWidget(self.contacts, 1) + self.status = QLabel("正在只读加载联系人…") + self.status.setWordWrap(True) + layout.addWidget(self.status) + buttons = QHBoxLayout() + self.previous = QPushButton("上一页") + self.next = QPushButton("下一页") + self.choose = QPushButton("选择此联系人") + self.choose.setEnabled(False) + cancel = QPushButton("取消") + for widget in (self.previous, self.next, self.choose, cancel): + buttons.addWidget(widget) + layout.addLayout(buttons) + self._search_timer = QTimer(self) + self._search_timer.setSingleShot(True) + self._search_timer.setInterval(250) + self._search_timer.timeout.connect(lambda: self._load(reset=True)) + self.query.textChanged.connect(lambda _text: self._search_timer.start()) + self.contacts.currentItemChanged.connect(lambda item, _old: self.choose.setEnabled(item is not None)) + self.previous.clicked.connect(lambda: self._page(-50)) + self.next.clicked.connect(lambda: self._page(50)) + self.choose.clicked.connect(self._choose) + cancel.clicked.connect(self.reject) + self.finished.connect(self._finished) + self.recipientsLoaded.connect(self._loaded) + self.recipientValidated.connect(self._validated) + QTimer.singleShot(0, self, self._load) + + def _finished(self, _result): + self._closed = True + self._search_timer.stop() + + def _page(self, delta): + self._offset = max(0, self._offset + delta) + self._load() + + def _load(self, *, reset=False): + if self._closed: + return + if reset: + self._offset = 0 + self._serial += 1 + serial, query, offset = self._serial, self.query.text(), self._offset + self.choose.setEnabled(False) + self.previous.setEnabled(False) + self.next.setEnabled(False) + self.contacts.clear() + self.status.setText("正在只读加载联系人…") + def work(): + try: + from review_assistant_contacts import list_recipients + result = list_recipients(self.account, query, 50, offset) + except Exception as exc: + result = {"ready": False, "reason": str(exc), "items": []} + result["_serial"] = serial + try: + self.recipientsLoaded.emit(result) + except RuntimeError: + pass + threading.Thread(target=work, name="review-assistant-contacts", daemon=True).start() + + def _loaded(self, result): + if self._closed or result.get("_serial") != self._serial: + return + if not result.get("ready") or str(result.get("account_id")) != self.account: + self.status.setText(str(result.get("reason") or "当前企微账号已变化,请重新选择")) + return + for row in result.get("items", []): + item = QListWidgetItem(f"{row['name']} · {row['identityLabel']}\n最近联系:{row.get('lastTime') or '—'}") + item.setData(Qt.ItemDataRole.UserRole, dict(row)) + self.contacts.addItem(item) + self.previous.setEnabled(self._offset > 0) + self.next.setEnabled(bool(result.get("has_more"))) + total = int(result.get("total") or 0) + self.status.setText(f"共 {total} 个匹配的私人微信单聊;请选择并核对收件人。" if total else "没有匹配的已有私人微信单聊。请先与助理建立会话并刷新消息库。") + + def _choose(self): + item = self.contacts.currentItem() + if item is None or self._closed: + return + row = item.data(Qt.ItemDataRole.UserRole) + self.choose.setEnabled(False) + self.query.setEnabled(False) + self.contacts.setEnabled(False) + self.previous.setEnabled(False) + self.next.setEnabled(False) + self.status.setText("正在复核当前企微账号和所选联系人…") + self._search_timer.stop() + self._serial += 1 + def work(): + try: + from review_assistant_contacts import validate_recipient + result = validate_recipient(self.account, str(row.get("id") or "")) + except Exception as exc: + result = {"ok": False, "reason": str(exc)} + try: + self.recipientValidated.emit(result) + except RuntimeError: + pass + threading.Thread(target=work, name="review-assistant-recipient-check", daemon=True).start() + + def _validated(self, result): + if self._closed: + return + if result.get("ok") and result.get("account_id") == self.account: + self.recipient = result["recipient"] + self.accept() + else: + self.status.setText(str(result.get("reason") or "联系人核验失败,请重新读取当前账号")) + self.query.setEnabled(True) + self.contacts.setEnabled(True) + self.choose.setEnabled(self.contacts.currentItem() is not None) + + class MainWindow(QMainWindow): desktopSessionChecked = Signal(str, object) + reviewAssistantUpdated = Signal(object) wechatDatabaseLoaded = Signal(object) def _set_account_menu_title(self, account: dict | None = None) -> None: @@ -9627,6 +9785,7 @@ self.persona_page.saved.connect(self._persona_saved) self.capsule.expandRequested.connect(self.expand_console) self.capsule.stopRequested.connect(self.stop_monitoring) + self.reviewAssistantUpdated.connect(self._review_assistant_updated) self.wechatDatabaseLoaded.connect(self._wechat_database_loaded) @staticmethod @@ -9658,6 +9817,7 @@ "notify_send_failed": True, "notify_timeout": True, "notify_manual": True, + "review_assistant_accounts": {}, "notify_disconnect": True, } @@ -9711,13 +9871,14 @@ merged.update(engine or {}) self.save_runtime_settings(merged) - def save_runtime_settings(self, settings: dict | None = None) -> None: + def save_runtime_settings(self, settings: dict | None = None) -> bool: previous_auto_launch = bool( getattr(self, "runtime_settings", {}).get("auto_launch", False) ) merged = dict(self._runtime_defaults()) merged.update(getattr(self, "runtime_settings", {}) or {}) merged.update(settings or self.settings_page.values()) + previous_settings = getattr(self, "runtime_settings", {}) self.runtime_settings = merged try: temporary = APP_SETTINGS_FILE.with_suffix(".json.tmp") @@ -9732,6 +9893,8 @@ # 什么都不会看到——"点了没反应"的一半原因就在这里。 self._broadcast_save_result(False, f"保存失败:{exc}") self.append_log(f"运行参数保存失败:{exc}", "err") + self.runtime_settings = previous_settings + return False else: if previous_auto_launch != bool(self.runtime_settings.get("auto_launch", False)): self._sync_auto_launch(bool(self.runtime_settings.get("auto_launch", False))) @@ -9748,6 +9911,7 @@ self.runtime_settings["foreign_draft_autosend_minutes"] )) self._broadcast_save_result(True, "设置已保存并生效") + return True def _broadcast_save_result(self, ok: bool, message: str) -> None: """把保存结果同时告诉所有共用这份运行参数的页面。""" @@ -10925,6 +11089,146 @@ }) return cleaned + def _review_assistant_available(self) -> tuple[bool, str]: + if "PROTOCOL" not in globals(): + return False, "当前版本不支持医疗助理审核提醒" + return True, "" + + def _review_assistant_console_state(self) -> dict: + from review_assistant_contacts import notification_preview + available, reason = self._review_assistant_available() + state = dict(getattr(self, "_review_assistant_state", {}) or {}) + account = str(state.get("account_id") or "") if available else "" + settings = self.runtime_settings.get("review_assistant_accounts") or {} + config = settings.get(account, {}) if isinstance(settings, dict) else {} + if not isinstance(config, dict): + config = {} + recipient_id = str(config.get("recipient_conv_id") or "") + return {"supported": available, "accountId": account, + "accountName": str(state.get("account_name") or account), + "ready": available and bool(state.get("ready")), + "busy": bool(getattr(self, "_review_assistant_busy", False)), + "reason": reason or str(state.get("reason") or ("点击读取当前账号后选择助理私人微信" if not account else "")), + "message": str(state.get("message") or ""), + "notification": dict(state.get("notification") or {}), + "enabled": config.get("enabled") is True, + "recipient": {"id": recipient_id, "name": str(config.get("recipient_name") or ""), + "label": f"私人微信 · 联系编号 {recipient_id[2:]}" if recipient_id else ""}, + "preview": notification_preview(state.get("account_name") or account or "当前企微账号")} + + def _refresh_review_assistant(self) -> None: + available, reason = self._review_assistant_available() + if not available: + self._review_assistant_state = {"ready": False, "reason": reason} + self._push_console_state(force=True) + return + if getattr(self, "_review_assistant_busy", False): + return + self._review_assistant_busy = True + self._review_assistant_serial = getattr(self, "_review_assistant_serial", 0) + 1 + serial = self._review_assistant_serial + self._schedule_console_push(urgent=True) + def work(): + try: + from review_assistant_contacts import active_context, list_recipients + result = active_context() + if result.get("ready"): + result = list_recipients(result["account_id"], limit=1) + if result.get("ready"): + from review_assistant import notification_summary + result["notification"] = notification_summary(SCRIPT_DIR, result["account_id"]) + except Exception as exc: + result = {"ready": False, "reason": str(exc)} + result.update(_kind="refresh", _serial=serial) + try: + self.reviewAssistantUpdated.emit(result) + except RuntimeError: + pass + threading.Thread(target=work, name="review-assistant-account", daemon=True).start() + + def _choose_review_assistant(self) -> None: + state = self._review_assistant_console_state() + if not state["ready"] or state["busy"]: + self._refresh_review_assistant() + return + expected = state["accountId"] + dialog = ReviewAssistantPicker(expected, self) + try: + if dialog.exec() == QDialog.DialogCode.Accepted and dialog.recipient: + self._save_review_assistant(expected, recipient=dialog.recipient) + finally: + dialog.deleteLater() + + def _save_review_assistant(self, expected_account: str, *, enabled=None, recipient=None) -> dict: + state = self._review_assistant_console_state() + if not state["ready"] or state["busy"] or not expected_account or state["accountId"] != expected_account: + return {"ok": False, "message": "当前账号未核验或正在处理,请重新读取当前账号"} + accounts = self.runtime_settings.get("review_assistant_accounts") or {} + previous = dict(accounts.get(expected_account) or {}) if isinstance(accounts, dict) else {} + target = str((recipient or {}).get("id") or previous.get("recipient_conv_id") or "") + if (enabled is True or recipient is not None) and not target: + return {"ok": False, "message": "请先选择助理私人微信联系人"} + self._review_assistant_busy = True + self._review_assistant_serial = getattr(self, "_review_assistant_serial", 0) + 1 + serial = self._review_assistant_serial + self._schedule_console_push(urgent=True) + def work(): + try: + from review_assistant_contacts import active_context, validate_recipient + if enabled is False and recipient is None: + context = active_context() + result = {"ok": context.get("ready") and context.get("account_id") == expected_account, + "reason": context.get("reason") or "当前企业微信账号已变化,请重新读取", "recipient": None} + else: + result = validate_recipient(expected_account, target) + except Exception as exc: + result = {"ok": False, "reason": str(exc)} + result.update(_kind="save", _serial=serial, expected_account=expected_account, + requested_enabled=enabled, replace_recipient=recipient is not None) + try: + self.reviewAssistantUpdated.emit(result) + except RuntimeError: + pass + threading.Thread(target=work, name="review-assistant-save", daemon=True).start() + return {"ok": True, "message": "正在核对当前企微账号与收件人,保存结果将显示在提醒设置中"} + + @Slot(object) + def _review_assistant_updated(self, result) -> None: + if result.get("_serial") != getattr(self, "_review_assistant_serial", 0): + return + self._review_assistant_busy = False + if getattr(self, "_exit_requested", False): + return + available, reason = self._review_assistant_available() + if not available: + self._review_assistant_state = {"ready": False, "reason": reason} + elif result.get("_kind") == "refresh": + self._review_assistant_state = {key: result.get(key) for key in ("account_id", "account_name", "ready", "reason", "notification")} + elif not result.get("ok"): + self._review_assistant_state = {"ready": False, "reason": result.get("reason") or "联系人核验失败,请重新读取账号"} + self.append_log("[审核提醒] " + self._review_assistant_state["reason"], "warn") + else: + expected = str(result.get("expected_account") or "") + if expected != str(getattr(self, "_review_assistant_state", {}).get("account_id") or ""): + return + settings = self.runtime_settings.get("review_assistant_accounts") or {} + accounts = dict(settings) if isinstance(settings, dict) else {} + config = dict(accounts.get(expected) or {}) + recipient = result.get("recipient") + changed = bool(recipient and recipient["id"] != config.get("recipient_conv_id")) + if recipient: + config.update(recipient_conv_id=recipient["id"], recipient_name=recipient["name"], recipient_kind="微信客户") + requested = result.get("requested_enabled") + config["enabled"] = bool(requested) if requested is not None else (False if changed else config.get("enabled") is True) + accounts[expected] = config + if self.save_runtime_settings({"review_assistant_accounts": accounts}) is False: + self._review_assistant_state["message"] = "保存失败,原有提醒配置保持不变,请重试" + else: + self._review_assistant_state["message"] = ("已选择助理私人微信;开启提醒后生效" if changed else + ("当前账号的医疗助理审核提醒已开启" if config["enabled"] else "当前账号的医疗助理审核提醒已关闭")) + self.append_log("[审核提醒] " + self._review_assistant_state["message"], "ok") + self._push_console_state(force=True) + def _console_automation_state(self, wecom) -> dict: settings = self.settings_page.values() connected = bool(wecom) if isinstance(wecom, bool) else bool((wecom or {}).get("connected")) @@ -10937,6 +11241,7 @@ except Exception: timeout = 120 return { + "reviewAssistant": self._review_assistant_console_state(), "poll": settings.get("poll_interval"), "batch": settings.get("message_batch_window_seconds"), "delay": settings.get("send_delay_seconds"),