Files
kefu/deploy/review-assistant-20260917/production-qt.diff
T
2026-09-21 10:34:06 +08:00

355 lines
17 KiB
Diff

--- C:\wechat_rpa\backups\review-assistant-20260917\wechat_gui_qt.py
+++ C:\wechat_rpa\wechat_gui_qt.py
@@ -65,6 +65,7 @@
QLayout,
QLineEdit,
QListWidget,
+ QListWidgetItem,
QListWidgetItem,
QMainWindow,
QMenu,
@@ -8976,6 +8977,26 @@
def exportProtocolReport(self) -> None:
self.host._export_protocol_report()
+ @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:
@@ -9341,8 +9362,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)
protocolProbeLoaded = Signal(object)
@@ -9720,6 +9878,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)
self.protocolProbeLoaded.connect(self._protocol_probe_loaded)
@@ -9753,6 +9912,7 @@
"notify_send_failed": True,
"notify_timeout": True,
"notify_manual": True,
+ "review_assistant_accounts": {},
"notify_disconnect": True,
}
@@ -11143,6 +11303,146 @@
return
self.append_log(f"协议诊断报告已保存:{filename}", "ok")
+ 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"))
@@ -11155,6 +11455,7 @@
except Exception:
timeout = 120
return {
+ "reviewAssistant": self._review_assistant_console_state(),
"transport": getattr(self, "runtime_settings", {}).get("reply_transport", VISUAL),
"protocol": getattr(self, "_protocol_probe", {
"status": "not_checked", "label": "尚未检测", "canSend": False,