from pathlib import Path import os, shutil ROOTS = (Path('C:/wechat_rpa'), Path('C:/kefu/wechat_rpa')) def edit(root, name, transform): path = root / name old = path.read_text(encoding='utf-8-sig') new = transform(old) if new == old: raise RuntimeError(f'No change: {path}') backup = root / 'backups/review-badges-20260917' / name backup.parent.mkdir(parents=True, exist_ok=True) if not backup.exists(): shutil.copy2(path, backup) tmp = path.with_name(path.name + '.review-edit') tmp.write_text(new, encoding='utf-8') os.replace(tmp, path) def replace(s, old, new): if s.count(old) != 1: raise RuntimeError(f'Expected once: {old[:100]} ({s.count(old)})') return s.replace(old, new, 1) def gui(s): s = replace(s, 'from review_state import task_review_state', 'from review_state import task_review_state, pending_attention_counts') pos = s.index('class QueuePage(') s = s[:pos] + replace(s[pos:], ' logMessage = Signal(str, str)', ' attentionChanged = Signal(int, int)\n logMessage = Signal(str, str)') s = replace(s, ' def _refresh_queue(self) -> None:\n', ' def _load_pending_states(self):\n """Refresh counts without rebuilding hidden queue widgets."""\n') demo = ' self._showing_demo_tasks = not rows and "--qt-smoke-test" in sys.argv\n' s = replace(s, demo, '') marker = ' self.pending_states = {\n str(key): dict(state) for key, state in rows if isinstance(state, dict)\n }\n' s = replace(s, marker, marker + ''' counts = pending_attention_counts(self.pending_states) self.attentionChanged.emit(counts['review'], counts['human']) return rows def _refresh_queue(self) -> None: rows = self._load_pending_states() self._showing_demo_tasks = not rows and "--qt-smoke-test" in sys.argv ''') s = replace(s, ' self.label = label\n', ' self.label = label\n self.attention_count = 0\n') needle = ' @staticmethod\n def _line(painter: QPainter, *points: tuple[float, float]) -> None:' s = replace(s, needle, ''' def set_attention_counts(self, review: int, human: int) -> None: self.attention_count = max(0, review) + max(0, human) detail = f"待处理 {self.attention_count} 个会话(待审核 {review},需人工 {human})" self.setToolTip(detail if self.attention_count else "暂无待审核或需人工处理的会话") self.setAccessibleName(f"{self.label},{detail}" if self.attention_count else self.label) self.update() ''' + needle) needle = ' painter.drawText(label_rect, Qt.AlignHCenter | Qt.AlignVCenter, self.label)\n' s = replace(s, needle, needle + ''' if self.attention_count: badge_text = str(self.attention_count) if self.attention_count <= 99 else "99+" painter.setFont(_paint_font(ui_px(11), QFont.Weight.Bold)) height = ui_px(20) width = max(height, painter.fontMetrics().horizontalAdvance(badge_text) + ui_px(10)) right = min(self.width() - ui_px(2), int(cx + ui_px(32))) badge = QRect(right - width, ui_px(2), width, height) painter.setPen(QPen(QColor("#FFFFFF"), ui_px(2))) painter.setBrush(QColor("#E6374F")) painter.drawRoundedRect(badge, height / 2, height / 2) painter.setPen(QColor("#FFFFFF")) painter.drawText(badge, Qt.AlignCenter, badge_text) ''') needle = ' def select(self, index: int) -> None:\n' s = replace(s, needle, ''' def set_attention_counts(self, review: int, human: int) -> None: for button in self.nav_buttons[:2]: button.set_attention_counts(review, human) ''' + needle) needle = ' self.queue_page.logMessage.connect(self.append_log)\n' s = replace(s, needle, needle + ''' self.queue_page.attentionChanged.connect(self.sidebar.set_attention_counts) counts = pending_attention_counts(getattr(self.queue_page, "pending_states", {})) self.sidebar.set_attention_counts(counts['review'], counts['human']) ''') needle = ' "queueFilter": getattr(self.queue_page, "active_task_filter", "全部") or "全部",\n' s = replace(s, needle, needle + ' "attention": pending_attention_counts(getattr(self.queue_page, "pending_states", {})),\n') s = replace(s, ' review_states = [task_review_state(value) for value in (getattr(self.queue_page, "pending_states", {}) or {}).values()]', ' attention = pending_attention_counts(getattr(self.queue_page, "pending_states", {}))') s = replace(s, 'str(sum(value["awaitingReview"] and not value["needsHuman"] for value in review_states))', 'str(attention["review"])') s = replace(s, 'str(sum(value["needsHuman"] for value in review_states))', 'str(attention["human"])') start = s.index(' def _refresh_queue_page(self) -> None:') end = s.index('\n def ', start + 5) section = s[start:end] section = replace(section, ' self._schedule_console_push()', ''' else: self.queue_page._load_pending_states() self._schedule_console_push()''') section = section.replace('队列页开着的时候,让它跟着机器人一起动。', '所有页面更新侧栏计数;仅在队列可见时重建队列内容。') s = s[:start] + section + s[end:] return s def html(s): for page in ('auto', 'queue'): start = s.index('data-view="' + page + '"') end = s.index('', start) part = s[start:end] part = replace(part, '', '') s = s[:start] + part + s[end:] return s def js(s): s = replace(s, 'function syncOrb(){', '''function syncAttentionBadges(){ const counts = STATE.attention || STATE.metrics || {}; const review = Math.max(0, Math.trunc(Number(counts.review) || 0)); const human = Math.max(0, Math.trunc(Number(counts.human) || 0)); const total = review + human; const detail = total ? `待处理 ${total} 个会话(待审核 ${review},需人工 ${human})` : "暂无待审核或需人工处理的会话"; [["auto", "自动回复"], ["queue", "任务队列"]].forEach(([view, label]) => { const button = document.querySelector(`.nav-btn[data-view="${view}"]`); const badge = button && button.querySelector(".nav-attention-badge"); if (!badge) return; badge.hidden = total === 0; badge.textContent = total > 99 ? "99+" : String(total); button.title = detail; button.setAttribute("aria-label", `${label},${detail}`); }); } function syncOrb(){ syncAttentionBadges();''') return s for root in ROOTS: edit(root, 'review_state.py', lambda s:s + ''' def pending_attention_counts(pending): """Count conversations, regardless of the visible queue filter or page.""" review = human = 0 for state in (pending or {}).values(): if not isinstance(state, dict): continue flags = task_review_state(state) if flags['needsHuman']: human += 1 elif flags['awaitingReview']: review += 1 return {'review': review, 'human': human, 'total': review + human} ''') edit(root, 'wechat_gui_qt.py', gui) edit(root, 'assets/ui/zhenyang-ai-console.html', html) edit(root, 'assets/ui/console-app.js', js) edit(root, 'assets/ui/console-review.css', lambda s:s + ''' /* Counts remain legible in logical CSS pixels at every display scale. */ #stage .nav-icon { position:relative; } #stage .nav-attention-badge { position:absolute; top:-4px; right:-9px; z-index:2; display:flex; align-items:center; justify-content:center; box-sizing:border-box; min-width:22px; height:22px; padding:0 5px; border:2px solid white; border-radius:999px; background:#e6374f; color:white; font-size:11px; font-weight:700; line-height:18px; font-variant-numeric:tabular-nums; pointer-events:none; } #stage .nav-attention-badge[hidden] { display:none; } ''') print('Patched badges:', root)