355 lines
17 KiB
Diff
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,
|