Files
kefu/deploy/review-picker-20260918/picker_class.py
T
2026-09-21 10:34:06 +08:00

290 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class ReviewAssistantPicker(QDialog):
recipientsLoaded = Signal(object)
recipientValidated = Signal(object)
def __init__(self, account, parent=None, *, current_recipient_id=""):
super().__init__(parent)
self.account = str(account)
self.current_recipient_id = str(current_recipient_id or "")
self.recipient = None
self._serial = 0
self._offset = 0
self._closed = False
self._validating = False
self._validation_id = ""
self._filter = "sendable"
self._has_more = False
self.setWindowTitle("选择医疗助理私人微信")
self.resize(740, 640)
self.setMinimumSize(620, 560)
layout = QVBoxLayout(self)
layout.setSpacing(9)
note = QLabel("点击联系人查看详情,再确认设为医疗助理。找不到时,先刷新「微信消息库」,再刷新此列表。")
note.setWordWrap(True)
layout.addWidget(note)
self.configured = QLabel()
self.configured.setWordWrap(True)
self.configured.setTextFormat(Qt.TextFormat.PlainText)
self.configured.setText("当前已设助理:" + self.current_recipient_id if self.current_recipient_id else "当前尚未设置医疗助理")
layout.addWidget(self.configured)
search = QHBoxLayout()
self.query = QLineEdit()
self.query.setPlaceholderText("搜索备注、昵称或联系编号")
self.query.setMaxLength(100)
search.addWidget(self.query, 1)
self.refresh = QPushButton("刷新列表")
self.refresh.setToolTip("仅重读本机已缓存的联系人。请先在微信消息库刷新。")
search.addWidget(self.refresh)
layout.addLayout(search)
filters = QHBoxLayout()
self.filter_buttons = {}
for key, title in (("sendable", "可发送"), ("directory", "通讯录未关联"), ("all", "全部")):
button = QPushButton(title)
button.setCheckable(True)
button.setChecked(key == self._filter)
button.setStyleSheet("QPushButton:checked { background: #E7F0FF; color: #1557C0; border: 1px solid #578CDE; font-weight: 600; }")
button.clicked.connect(lambda _checked=False, value=key: self._set_filter(value))
self.filter_buttons[key] = button
filters.addWidget(button)
filters.addStretch(1)
self.order = QComboBox()
self.order.addItem("最近联系优先", "recent")
self.order.addItem("按名称排序", "name")
self.order.setAccessibleName("联系人排序")
filters.addWidget(self.order)
layout.addLayout(filters)
self.contacts = QListWidget()
self.contacts.setObjectName("ReviewAssistantContacts")
self.contacts.setWordWrap(False)
self.contacts.setTextElideMode(Qt.TextElideMode.ElideRight)
self.contacts.setSpacing(3)
self.contacts.setMinimumHeight(140)
self.contacts.setStyleSheet("""
QListWidget#ReviewAssistantContacts { background: #FFFFFF; border: 1px solid #D8E2F0; border-radius: 8px; padding: 4px; }
QListWidget#ReviewAssistantContacts::item { color: #263954; padding: 7px 10px; border: 1px solid transparent; border-radius: 6px; }
QListWidget#ReviewAssistantContacts::item:hover { background: #F0F5FF; }
QListWidget#ReviewAssistantContacts::item:selected,
QListWidget#ReviewAssistantContacts::item:selected:active,
QListWidget#ReviewAssistantContacts::item:selected:!active { color: #FFFFFF; background: #2563CB; border: 1px solid #1550B1; }
""")
layout.addWidget(self.contacts, 1)
self.status = QLabel("正在只读加载联系人…")
self.status.setWordWrap(True)
layout.addWidget(self.status)
details = QFrame()
details.setObjectName("ReviewAssistantDetails")
details.setStyleSheet("QFrame#ReviewAssistantDetails { background: #F3F7FE; border: 1px solid #D8E2F0; border-radius: 8px; }")
detail_layout = QVBoxLayout(details)
detail_layout.setSpacing(3)
self.details_name = QLabel()
self.details_name.setStyleSheet("font-weight: 600; color: #174EA8;")
self.details_identity = QLabel()
self.details_last_time = QLabel()
self.details_reason = QLabel()
for label in (self.details_name, self.details_identity, self.details_last_time, self.details_reason):
label.setWordWrap(True)
label.setTextFormat(Qt.TextFormat.PlainText)
label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
detail_layout.addWidget(label)
layout.addWidget(details)
buttons = QHBoxLayout()
self.previous = QPushButton("上一页")
self.next = QPushButton("下一页")
self.choose = QPushButton("设为医疗助理")
self.choose.setStyleSheet("QPushButton:enabled { background: #2563CB; color: #FFFFFF; border: 1px solid #1550B1; border-radius: 5px; padding: 7px 14px; }")
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(self._query_changed)
self.order.currentIndexChanged.connect(lambda _index: self._load(reset=True))
self.contacts.currentItemChanged.connect(self._selection_changed)
self.refresh.clicked.connect(lambda: self._load(reset=True))
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)
self._selection_changed(None)
QTimer.singleShot(0, self, self._load)
def _finished(self, _result):
self._closed = True
self._search_timer.stop()
@staticmethod
def _display_name(row):
if row.get("nameKnown") is False:
return str(row.get("displayName") or ("未读取昵称 · 尾号" + str(row.get("id") or "")[-4:]))
return str(row.get("displayName") or row.get("name") or "未读取昵称")
def _item_text(self, row, *, selected=False):
state = "可设为助理" if row.get("selectable", True) else "通讯录 · 尚未关联可发送会话"
if str(row.get("id") or "") == self.current_recipient_id:
state = "已设为助理 · " + state
return ("✓ " if selected else "") + self._display_name(row) + "\n" + state
def _selection_changed(self, item, old=None):
if old is not None:
old_row = old.data(Qt.ItemDataRole.UserRole)
if old_row:
old.setText(self._item_text(old_row))
row = item.data(Qt.ItemDataRole.UserRole) if item is not None else None
can_choose = bool(row) and row.get("selectable", True) and not self._validating
self.choose.setEnabled(bool(can_choose))
if not row:
self.details_name.setText("尚未选择联系人")
self.details_identity.setText("点击上方联系人,在这里核对完整编号。")
self.details_last_time.setText("")
self.details_reason.setText("")
return
item.setText(self._item_text(row, selected=True))
name = self._display_name(row)
self.details_name.setText("当前查看:" + name)
identity = str(row.get("identityLabel") or row.get("id") or "—")
conversation_id = str(row.get("id") or "")
self.details_identity.setText("联系编号:" + identity + ("\n会话标识:" + conversation_id if row.get("selectable", True) and conversation_id not in identity else ""))
self.details_last_time.setText("最近联系:" + str(row.get("lastTime") or "暂无已关联的会话记录"))
if row.get("selectable", True):
self.details_reason.setText("此联系人已设为医疗助理。" if conversation_id == self.current_recipient_id else "已关联可发送的私人微信单聊。核对后,点击「设为医疗助理」。")
else:
self.details_reason.setText("暂不能设为助理:" + str(row.get("unavailableReason") or "尚无可发送的私人微信会话,请建立单聊并刷新消息库。"))
def _query_changed(self, _text):
if self._closed or self._validating:
return
self._serial += 1
self.contacts.clear()
self._selection_changed(None)
self.previous.setEnabled(False)
self.next.setEnabled(False)
self.status.setText("正在等待搜索…")
self._search_timer.start()
def _set_filter(self, value):
if self._closed or self._validating:
return
self._filter = value
for key, button in self.filter_buttons.items():
button.setChecked(key == value)
self._load(reset=True)
def _page(self, delta):
if self._closed or self._validating:
return
self._offset = max(0, self._offset + delta)
self._load()
def _load(self, *, reset=False):
if self._closed or self._validating:
return
self._search_timer.stop()
if reset:
self._offset = 0
self._serial += 1
serial, query, offset = self._serial, self.query.text(), self._offset
status, order = self._filter, str(self.order.currentData() or "recent")
self.choose.setEnabled(False)
self.previous.setEnabled(False)
self.next.setEnabled(False)
self.refresh.setEnabled(False)
self.contacts.clear()
self._selection_changed(None)
self.status.setText("正在只读加载联系人…")
def work():
try:
from review_assistant_contacts import list_recipients
result = list_recipients(self.account, query, 50, offset, status=status, order=order)
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
self.refresh.setEnabled(True)
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(self._item_text(row))
item.setData(Qt.ItemDataRole.UserRole, dict(row))
item.setToolTip(self._display_name(row))
self.contacts.addItem(item)
if str(row.get("id") or "") == self.current_recipient_id:
self.configured.setText("当前已设助理:" + self._display_name(row) + " · " + self.current_recipient_id)
self._has_more = bool(result.get("has_more"))
self.previous.setEnabled(self._offset > 0)
self.next.setEnabled(self._has_more)
total = int(result.get("total") or 0)
counts = {
"sendable": int(result.get("all_selectable_total", result.get("selectable_total", total)) or 0),
"directory": int(result.get("all_contact_total", result.get("contact_total", 0)) or 0),
"all": int(result.get("all_total", total) or 0),
}
for key, title in (("sendable", "可发送"), ("directory", "通讯录未关联"), ("all", "全部")):
self.filter_buttons[key].setText(f"{title} ({counts[key]})")
if total:
self.status.setText(f"共 {total} 位 · 第 {self._offset + 1}–{self._offset + self.contacts.count()} 位;点击联系人查看详情。")
elif self._filter == "sendable" and counts["directory"]:
self.status.setText("没有可发送的匹配联系人。可切换「通讯录未关联」查看,再建立单聊并刷新消息库。")
else:
self.status.setText("没有匹配联系人。可尝试「全部」、搜索备注或编号,或刷新微信消息库。")
def _choose(self):
item = self.contacts.currentItem()
if item is None or self._closed or self._validating:
return
row = item.data(Qt.ItemDataRole.UserRole)
if not row or not row.get("selectable", True):
self.choose.setEnabled(False)
return
self._validating = True
self._validation_id = str(row.get("id") or "")
self.choose.setEnabled(False)
for widget in (self.query, self.contacts, self.order, self.previous, self.next, self.refresh, *self.filter_buttons.values()):
widget.setEnabled(False)
self.status.setText("正在复核当前企微账号和所选联系人…")
self._search_timer.stop()
self._serial += 1
serial, recipient_id = self._serial, self._validation_id
def work():
try:
from review_assistant_contacts import validate_recipient
result = validate_recipient(self.account, recipient_id)
except Exception as exc:
result = {"ok": False, "reason": str(exc)}
result["_serial"] = serial
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 or result.get("_serial", self._serial) != self._serial:
return
self._validating = False
recipient = result.get("recipient") or {}
if result.get("ok") and result.get("account_id") == self.account and str(recipient.get("id") or "") == self._validation_id:
self.recipient = recipient
self.accept()
else:
self.status.setText(str(result.get("reason") or "联系人核验失败,请重新读取当前账号"))
for widget in (self.query, self.contacts, self.order, self.refresh, *self.filter_buttons.values()):
widget.setEnabled(True)
self.previous.setEnabled(self._offset > 0)
self.next.setEnabled(self._has_more)
self._selection_changed(self.contacts.currentItem())