144 lines
11 KiB
Python
144 lines
11 KiB
Python
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))
|