Files
kefu/deploy/review-visibility-20260916/frontend_patch.py
T
2026-09-21 10:34:06 +08:00

144 lines
11 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.
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)
? `<button class="btn blue review-approve" data-action="approve-task" data-key="${esc(task.key)}">${icon("send")}通过并发送</button>`
: "";
}
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 `<div class="review-hint task-review-hint" role="status">${icon("shield")}<div><strong>${title}</strong><p>${esc(reason)}</p><p>${help}</p></div></div>`;
}
function taskAnswer(task){
const answer = String(task.answer || "");
return `<div class="answer-block${answer.trim() ? "" : " answer-empty"}">${answer.trim() ? esc(answer).replace(/\n/g,"<br>") : task.needsHuman || task.awaitingReview ? "暂无可审核回复,请先人工处理原始消息。" : "尚未生成回复"}</div>`;
}
function reviewSummary(){
const m = STATE.metrics || {};
const items = [["待审核", "review", "shield"], ["需人工", "human", "user"]];
return `<div class="review-summary" aria-label="人工处理任务">${items.map(([label,key,glyph])=>`<button class="review-summary-item ${Number(m[key]) > 0 ? "has-pending" : ""}" data-action="open-review-queue" data-key="${label}">${icon(glyph)}<span>${label}</span><b data-live-metric="${key}">${esc(m[key] || 0)}</b><span class="review-summary-link">查看任务 ›</span></button>`).join("")}</div>`;
}
'''
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}</section>`;', '${foreignDraftHint}</section>`;')
s = replace_once(s, '<div class="subhead">AI 回复预览</div>${answer}', '${reviewHint}<div class="subhead">AI 回复预览</div>${answer}')
s = replace_once(s, '<div class="subhead">复制的聊天内容</div><div class="copy-block">${messages}', '<div class="subhead">来源消息</div><div class="copy-block">${messages}')
s = replace_once(s, 'const hint = archive ? "新的客户会话会自动出现在这里" : "所有客户消息均已完成";', 'const hint = archive ? "新的客户会话会自动出现在这里" : (STATE.queueFilter && STATE.queueFilter !== "全部" ? "当前筛选下没有任务,可切换其他状态查看。" : "新的任务会显示在这里。");')
s = replace_once(s, '<div class="queue-row ${active?"active":""}" data-action=', '<div class="queue-row ${active?"active":""} ${!archive && p.status === "待审核" ? "review-pending" : !archive && p.status === "需人工" ? "human-pending" : ""}" data-action=')
start = s.index('function queueDetailPanel(){')
end = s.index('\nfunction schedulePanel(){', start)
block = s[start:end]
block = replace_once(block, ' const message = (task.messages && task.messages[0]) || {time:"", text:"队列为空时,选择任务后会在这里显示来源消息。"};', ''' const messages = (task.messages || []).map(message=>`<div class="message-line"><time>${esc(message.time || "")}</time><span>${esc(message.text || "")}</span></div>`).join("") || `<div class="message-line"><span>暂无来源消息</span></div>`;
const retryButton = !task.awaitingReview && !task.needsHuman ? `<button class="btn blue" data-action="retry-task" data-key="${esc(task.key || "")}">${icon("refresh")}立即重试</button>` : "";''')
block = replace_once(block, 'return `<section class="panel panel-pad"><div class="panel-title"><h2>任务详情</h2></div><div class="detail-stats">', 'return `<section class="panel panel-pad queue-task-detail"><div class="panel-title"><h2>任务详情</h2></div><div class="detail-stats">')
block = replace_once(block, '<div class="copy-block"><div class="message-line"><time>${esc(message.time || "")}</time><span>${esc(message.text || "")}</span></div></div>', '<div class="copy-block">${messages}</div>${taskReviewHint(task)}<div class="subhead">AI 回复预览</div>${taskAnswer(task)}')
block = replace_once(block, '<div class="task-actions" style="margin-top:17px;justify-content:center">', '<div class="task-actions">${taskApprovalButton(task)}')
# One retry button remains in rendered template after retryButton declaration.
old = '<button class="btn blue" data-action="retry-task" data-key="${esc(task.key || "")}">${icon("refresh")}立即重试</button>'
pos = block.rindex(old)
block = block[:pos] + '${retryButton}' + block[pos+len(old):]
s = s[:start] + block + s[end:]
s = replace_once(s, '${liveRibbonAuto()}<div class="auto-grid">', '${liveRibbonAuto()}${reviewSummary()}<div class="auto-grid">')
s = replace_once(s, '${liveRibbonQueue()}<div class="queue-view-grid">', '${liveRibbonQueue()}${reviewSummary()}<div class="queue-view-grid">')
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,'</head>', ' <link rel="stylesheet" href="console-review.css?v=review1">\n</head>')
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))