153 lines
8.0 KiB
Python
153 lines
8.0 KiB
Python
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('</button>', start)
|
||
part = s[start:end]
|
||
part = replace(part, '</svg></span>', '</svg><span class="nav-attention-badge" hidden aria-hidden="true"></span></span>')
|
||
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)
|