from pathlib import Path import hashlib import json import re import shutil ROOTS = [Path('C:/wechat_rpa'), Path('C:/kefu/wechat_rpa')] HELPERS = r'''function taskCanApprove(task){ return !!(task && task.key && task.awaitingReview && task.canApprove === true && !task.needsHuman && String(task.answer || "").trim()); } function taskApprovalButton(task){ return taskCanApprove(task) ? `` : ""; } function taskReviewHint(task){ if (!task || (!task.awaitingReview && !task.needsHuman)) return ""; const canApprove = taskCanApprove(task); const title = canApprove ? "待人工审核" : "需人工处理"; const reason = task.reviewReason || (canApprove ? "当前采用审核后发送模式。" : "当前任务无法自动发送,需要人工确认。" ); const help = canApprove ? task.isProtocol ? "协议回复草稿已保存在本应用。核对下方回复后,点击「通过并发送」;也可以转人工处理。" : "核对下方回复后,点击「通过并发送」;也可以在企业微信中确认草稿并手动发送。" : "此任务没有可直接通过发送的回复。请在企业微信查看原始消息并人工处理;可在这里转人工或取消任务。"; return `
${icon("shield")}
${title}

${esc(reason)}

${help}

`; } function taskAnswer(task){ const answer = String(task.answer || ""); return `
${answer.trim() ? esc(answer).replace(/\n/g,"
") : task.needsHuman || task.awaitingReview ? "暂无可审核回复,请先人工处理原始消息。" : "尚未生成回复"}
`; } function reviewSummary(){ const m = STATE.metrics || {}; const items = [["待审核", "review", "shield"], ["需人工", "human", "user"]]; return `
${items.map(([label,key,glyph])=>``).join("")}
`; } ''' CSS = '''/* Keep review actions visible in both embedded console versions. */ #stage .review-summary { display:flex; flex-wrap:wrap; gap:12px; margin:0 0 18px; } #stage .review-summary-item { display:flex; align-items:center; gap:10px; padding:12px 16px; min-height:48px; border:1px solid #d4deed; border-radius:14px; color:#435572; background:#f5f8ff; font:inherit; cursor:pointer; } #stage .review-summary-item svg { width:18px; height:18px; flex:none; } #stage .review-summary-item b { font-size:20px; } #stage .review-summary-item.has-pending { background:#fff5df; color:#875600; border-color:#e5bd64; } #stage .review-summary-link { font-size:12px; margin-left:12px; } #stage .queue-filters { flex-wrap:wrap; } #stage .queue-filters .filter-pill { padding:0 12px; } #stage .queue-row.review-pending .q-copy > span, #stage .queue-row.human-pending .q-copy > span { color:#9b6000; font-weight:700; } #stage .queue-row.review-pending .q-copy i, #stage .queue-row.human-pending .q-copy i { background:#df9300; } #stage .task-review-hint { margin:12px 0; } #stage .task-review-hint > div { min-width:0; } #stage .task-review-hint p { margin:5px 0 0; line-height:1.6; overflow-wrap:anywhere; } #stage .task-review-hint strong { font-size:14px; color:#875600; } #stage .answer-empty { color:var(--muted); } #stage .queue-task-detail .answer-block { max-height:320px; overflow:auto; white-space:normal; overflow-wrap:anywhere; } #stage .queue-task-detail .task-actions { margin-top:17px; flex-wrap:wrap; } #stage .review-approve { min-height:38px; } #stage .task-card { min-height:0; } ''' def replace_once(text, old, new): assert text.count(old) == 1, (old[:100], text.count(old)) return text.replace(old, new, 1) def patch_js(s): s = replace_once(s, 'function currentTaskCard(){', HELPERS + 'function currentTaskCard(){') start = s.index(' const answer = task.answer ?', s.index('function currentTaskCard(){')) end = s.index(' // 输入框里有人打的字', start) s = s[:start] + ' const answer = taskAnswer(task);\n const approveBtn = taskApprovalButton(task);\n const reviewHint = taskReviewHint(task);\n' + s[end:] s = replace_once(s, '${reviewHint}${foreignDraftHint}`;', '${foreignDraftHint}`;') s = replace_once(s, '
AI 回复预览
${answer}', '${reviewHint}
AI 回复预览
${answer}') s = replace_once(s, '
复制的聊天内容
${messages}', '
来源消息
${messages}') s = replace_once(s, 'const hint = archive ? "新的客户会话会自动出现在这里" : "所有客户消息均已完成";', 'const hint = archive ? "新的客户会话会自动出现在这里" : (STATE.queueFilter && STATE.queueFilter !== "全部" ? "当前筛选下没有任务,可切换其他状态查看。" : "新的任务会显示在这里。");') s = replace_once(s, '
`
${esc(message.text || "")}
`).join("") || `
暂无来源消息
`; const retryButton = !task.awaitingReview && !task.needsHuman ? `` : "";''') block = replace_once(block, 'return `

任务详情

', 'return `

任务详情

') block = replace_once(block, '
${esc(message.text || "")}
', '
${messages}
${taskReviewHint(task)}
AI 回复预览
${taskAnswer(task)}') block = replace_once(block, '
', '
${taskApprovalButton(task)}') # One retry button remains in rendered template after retryButton declaration. old = '' pos = block.rindex(old) block = block[:pos] + '${retryButton}' + block[pos+len(old):] s = s[:start] + block + s[end:] s = replace_once(s, '${liveRibbonAuto()}
', '${liveRibbonAuto()}${reviewSummary()}
') s = replace_once(s, '${liveRibbonQueue()}
', '${liveRibbonQueue()}${reviewSummary()}
') s = replace_once(s, 'const pills = ["全部","处理中","等待中","待重试"]', 'const pills = ["全部","待审核","需人工","处理中","等待中","待重试"]') # Preserve the demo screen, update only the live view's description. pos = s.index('queueView = function(){') s = s[:pos] + s[pos:].replace('查看排队、处理中与待重试的自动回复任务', '查看待审核、需人工处理及自动回复任务', 1) s = replace_once(s, ' answer: state.task.answer,', ''' answer: state.task.answer, awaitingReview: state.task.awaitingReview, canApprove: state.task.canApprove, needsHuman: state.task.needsHuman, reviewReason: state.task.reviewReason, isProtocol: state.task.isProtocol, foreignDraft: state.task.foreignDraft,''') s = replace_once(s, 'const metricModes = [Number(metrics.failed) > 0, Number(metrics.warnings) > 0];', 'const metricModes = [Number(metrics.failed) > 0, Number(metrics.warnings) > 0, Number(metrics.review) || 0, Number(metrics.human) || 0];') s = replace_once(s, ' "approve-task": () => callBridge("approveTask", key),', ''' "approve-task": () => { if (!taskCanApprove(STATE.task) || STATE.task.key !== key) return toast("任务状态已变化,请重新选择待审核任务。"); callBridge("approveTask", key); }, "open-review-queue": () => { STATE.queueFilter = key; if (STATE.view !== "queue") { if (typeof flushEdits === "function") flushEdits(); window.__pendingNavigation = "queue"; window.__navigationSerial = (window.__navigationSerial || 0) + 1; render("queue"); callBridge("navigate", "queue"); } callBridge("setQueueFilter", key); },''') return s results=[] for root in ROOTS: backup=root/'backups/review-visibility-20260916/frontend' for relative in ['assets/ui/console-app.js','assets/ui/zhenyang-ai-console.html']: src=root/relative; dest=backup/relative assert not dest.exists(), f'Backup exists: {dest}' dest.parent.mkdir(parents=True, exist_ok=True); shutil.copy2(src,dest) js=root/'assets/ui/console-app.js' js.write_text(patch_js(js.read_text(encoding='utf-8')),encoding='utf-8',newline='\n') html=root/'assets/ui/zhenyang-ai-console.html' text=html.read_text(encoding='utf-8') text,n=re.subn(r'console-app\.js\?v=[^"\s]+', 'console-app.js?v=review1', text) assert n==1 text=replace_once(text,'', ' \n') html.write_text(text,encoding='utf-8',newline='\n') (root/'assets/ui/console-review.css').write_text(CSS,encoding='utf-8',newline='\n') results.append({'root':str(root),'files':[{'path':p,'sha256':hashlib.sha256((root/p).read_bytes()).hexdigest()} for p in ['assets/ui/console-app.js','assets/ui/zhenyang-ai-console.html','assets/ui/console-review.css']]}) report=Path('C:/kefu/deploy/review-visibility-20260916/frontend-files.json') report.write_text(json.dumps(results,ensure_ascii=False,indent=2),encoding='utf-8') print(json.dumps(results,ensure_ascii=False))