AI审批助手:让流程在你打开之前就已思考完毕

审批场景和聊天场景有本质区别——审批是流程驱动的,事件由系统产生,处理人由流程指定。现有的 AI 工具都是”人主动问、AI 被动答”,但审批需要的是反过来的:流程产生事件后,AI 主动分析,人在打开页面时直接看到结果。
这篇文章介绍一个完整的审批 AI 自动分析方案:后台轮询预分析 + 浏览器扩展秒开展示。所有代码和配置均已开源,你可以直接拿去适配自己的审批平台。
· · ·
一、产品哲学:服务流程,不服务人
市面上大多数 AI 助手是”服务人”的——你问它,它回答。你贴一段文字,它帮你分析。所有交互起点是你主动触发。
但审批场景不一样。审批是流程驱动的:事件发生在流程中,处理人也由流程指定。审批单不是”我想看就看”的内容,而是”到你手里你必须看”的任务。
所以核心设计理念是反过来:
不是「人打开页面 → 点击 AI 按钮 → 等分析结果」。而是「流程产生审批单 → AI 自动分析 → 缓存结果 → 人打开页面秒出」。
人进来看到的,不是一个”请点击分析”的按钮,而是一个已经写好的分析报告,上面还标着”3 分钟前已分析完毕”。
· · ·
二、系统架构全景
先看整体架构。系统由四个运行时组件构成,各司其职:
┌─────────────────────────────────────────────────────┐│ 审批平台 API ││ (你公司的 OA / BPM / 审批系统) │└────────────┬────────────────────────────┬────────────┘ │ │ ① 轮询拉取 ② 页面访问 (每30秒一次) (用户打开审批单) │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────────┐ │ watcher.py │ │ Chrome Extension │ │ 后台常驻进程 │ │ content_script.js │ │ │ │ │ │ 发现新审批单 │ │ 提取页面上下文 │ │ 提取详情数据 │ │ 校验处理人身份 │ │ 匹配Skill规则 │ │ 查缓存 / 实时触发 │ └────────┬────────┘ └──────────┬──────────┘ │ │ ③ 调用分析 ④ 展示结果 │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────────┐ │ bridge.py │ │ panel.html/js │ │ HTTP :39877 │ │ 侧边栏面板 │ │ │ │ │ │ 转发到QoderCLI │ │ SSE流式/缓存秒开 │ │ SSE流式回传 │ │ Typewriter效果 │ └────────┬────────┘ └─────────────────────┘ │ ⑤ AI推理 │ ▼ ┌─────────────────┐ │ qodercli │ │ 本地AI引擎 │ │ │ │ Skill路由: │ │ 报价审批分析 │ │ 方案评估 │ │ 财报解读 ... │ └────────┬────────┘ │ ⑥ 分析结果 │ ▼ ┌─────────────────┐ │ SQLite 缓存 │ │ HTTP :39878 │ │ │ │ /result/{id} │ │ /stats │ └─────────────────┘
▲ 六步数据流:从审批平台到用户面板,全链路自动完成
· · ·
三、四大先进设计
这套系统不是简单的”API + Chrome 插件”,它有四个设计点值得拿出来说。
先进一:缓存先行(Cache-First),不是调用先行
传统的 AI 集成思路是”用户点击→调 API→等结果”。这条路在审批场景走不通——审批动辄十几页附件,AI 分析可能需要 30~60 秒。让审批人等一分钟?不可能。
所以设计了一个 Watcher 后台常驻进程,它在用户打开页面之前就已经跑完了分析:
# watcher.py 核心循环(基于实际代码)class WatcherEngine: def run_once(self): # ① 轮询审批平台 API items = fetch_approval_list(self.config) for item in items: bid = item['id'] # ② 去重:查 SQLite 是否已分析 cur = self.db.execute( 'SELECT bill_id FROM approvals WHERE bill_id = ?', (bid,)) if cur.fetchone(): continue # 已处理,跳过 # ③ 获取审批详情(表单字段) context = fetch_approval_detail(self.config, bid) # ④ 智能匹配 Skill(URL模式 + 关键词) skill = match_skill(self.config, item.get('url', ''), context) # 报价单 → 报价审批分析 # 合同 → 方案评估 # 财报 → 财报解读 # ⑤ 写入 pending,调用 Qoder AI self.db.execute('''INSERT INTO approvals (bill_id, url, title, skill, status, created_at) VALUES (?, ?, ?, ?, 'pending', ?)''', (bid, item['url'], item['title'], skill, now)) result = call_qoder_analysis(skill, context) # ⑥ 更新分析结果 self.db.execute('''UPDATE approvals SET result_text = ?, status = 'done', analyzed_at = ? WHERE bill_id = ?''', (result, datetime.now().isoformat(), bid)) self.db.commit()
当用户终于打开审批页面时,Chrome 扩展做的第一件事不是调 AI,而是先查缓存:
// content_script.js — 缓存优先策略(基于实际代码)async function executeAutoTrigger(match) { const { rule } = match; const ctx = extractPageContext(); // 提取 billId、url、title // ★ 先查后台 watcher 缓存 const cached = await checkWatcherCache(ctx.billId); if (cached) { console.log('★ 缓存秒开:', ctx.billId, cached.skill); recordTrigger(ctx.billId, ctx.url, rule); openPanel('auto'); setTimeout(() => sendCachedResult(cached), 500); return; // 不走实时分析 } // 缓存未命中 → 去重 → 实时分析 if (!checkDedup(ctx.billId, ctx.url, rule)) return; recordTrigger(ctx.billId, ctx.url, rule); openPanel('auto'); // panel 加载后通过 bridge.py 触发实时 SSE 分析 sendToBackground({ type: 'AUTO_TRIGGER', data: { skill: rule.skill, context: ctx } });}
💡 设计要点:缓存命中的时候,面板显示”⚡ 缓存命中 · Skill「报价审批分析」· 3分钟前已分析”,用户看到的是毫秒级加载。只有 Watcher 覆盖不到的新类型审批单,才会走实时分析通路。
先进二:三层处理人校验
审批流程有明确的处理人概念——一个审批单到张三手里,李四打开看只是”浏览”,不应该触发任何 AI 分析。但大多数”自动触发”方案都忽略了这个校验。
我设计了三层校验,层层递进:
// content_script.js — 三策略提取当前处理人(基于实际代码)function extractPageHandler() { const bodyText = (document.body?.innerText || ''); // 策略① CSS选择器:从配置中读取 selector 列表 for (const sel of handlerVerifyConfig.selectors) { try { const el = document.querySelector(sel); if (el) { const text = (el.innerText || el.textContent || '').trim(); if (text && text.length < 30) { return text; // "张三" } } } catch (_) {} } // 策略② 正则匹配:从配置中读取 pattern 列表 for (const pattern of handlerVerifyConfig.textPatterns) { try { const m = bodyText.match(new RegExp(pattern, 'i')); if (m && m[1]) { const name = m[1].trim(); if (name && name.length < 20) return name; } } catch (_) {} } // 策略③ 按钮兜底:配置中指定的审批操作按钮可见 → 当前用户是处理人 for (const sel of handlerVerifyConfig.buttonSelectors) { if (sel.includes(':contains(')) { const text = sel.match(/:contains\('(.+?)'\)/)?.[1] || ''; const buttons = document.querySelectorAll( 'button, a, .btn, [role="button"]'); for (const btn of buttons) { if ((btn.innerText || '').includes(text) && btn.offsetParent !== null) { return '__BUTTON_DETECTED__'; } } } } return null;}
拿到处理人后,和配置的用户名做匹配:
// content_script.js — 处理人校验(基于实际代码)function isCurrentHandler() { const handler = extractPageHandler(); if (!handler) return { isHandler: false, extractedName: null, reason: '未检测到处理人信息' }; if (handler === '__BUTTON_DETECTED__') return { isHandler: true, extractedName: '按钮检测', reason: '审批操作按钮可见' }; // 名称匹配(支持别名,含 .filter(Boolean) 过滤空值) const names = [currentUser.userName, ...currentUser.aliases] .map(n => n.toLowerCase().trim()).filter(Boolean); const lowerHandler = handler.toLowerCase(); const matched = names.some(name => lowerHandler.includes(name) || name.includes(lowerHandler)); return matched ? { isHandler: true, extractedName: handler, reason: '名称匹配' } : { isHandler: false, extractedName: handler, reason: `页面处理人「${handler}」不是当前用户「${currentUser.userName}」` };}
在触发链路的最前面调用:
async function executeAutoTrigger(match) { // ★ 处理人校验:不是当前处理人 → 直接跳过 const check = isCurrentHandler(); if (!check.isHandler) { console.log('⊘ 跳过:', check.reason); return; // 静默退出,不弹面板 } // ... 后续缓存查询和AI分析 ...}
⚠️ 为什么这个很重要:没有这个校验,同事打开你的审批单看热闹,也会触发一次 AI 分析(消耗 Token、浪费算力)。更关键的是,如果分析结果面板自动弹出,会干扰非处理人的浏览体验。
先进三:事件驱动 + 多层去重
审批页面往往是 SPA(单页应用),URL 可能不变但内容已切换。同时页面 DOM 可能多次渲染,同一个审批单可能被重复触发。这需要一套健壮的事件监听 + 去重机制:
// content_script.js — 自动触发引擎(基于实际代码)// ① URL 变化监听(SPA路由切换)function startUrlWatcher() { let lastUrl = location.href; new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; onPageChanged(); // URL变了→重新匹配规则 } }).observe(document, { subtree: true, childList: true });}// ② DOM 内容监听(审批数据异步加载)function startDomWatcher() { new MutationObserver(() => { const match = matchTriggerRule(); if (match) executeAutoTrigger(match); }).observe(document.body, { childList: true, subtree: true });}// ③ 多层去重(真实实现)function checkDedup(billId, url, rule) { const now = Date.now(); // 层1:全局冷却窗口 if (now - lastAutoTriggerTime < cooldownMs) { return false; } // 层2:单号去重(5分钟窗口) if (billId && billId !== 'UNKNOWN') { const billKey = 'bill:' + billId + ':' + (rule.skill || rule.name); if (dedupCache[billKey] && (now - dedupCache[billKey] < dedupWindowMs)) { return false; } } // 层3:URL + Skill 去重(SPA同页面不重复触发) const urlKey = 'url:' + url + ':' + (rule.skill || rule.name); if (triggeredUrls.has(urlKey)) { return false; } return true;}
先进四:数据不出本地
这是整套方案最”反潮流”的地方——所有 AI 推理在用户自己的电脑上完成。审批单数据不发送到任何云端服务。
审批平台 API → watcher.py → bridge.py → qodercli (本地AI) ↑ │ │ 数据全在用户本机 │ │ SQLite 缓存也在本地磁盘 │ └──────────────────────────────────────────────┘
对比主流方案:
┌──────────────┬──────────────┬──────────────┐│ 方案 │ 数据流向 │ 隐私风险 │├──────────────┼──────────────┼──────────────┤│ ChatGPT插件 │ 审批数据→云端│ 高(涉密) ││ Copilot │ 上下文→云端 │ 中 ││ 本方案 │ 全在本地 │ 无 │└──────────────┴──────────────┴──────────────┘
这对处理合同、报价、人事审批等敏感数据尤为重要。
· · ·
四、Skill 路由引擎:什么单子配什么分析
不同类型的审批需要不同的分析思路。报价单要看价格合理性、历史对比、汇率影响。合同要看条款风险、违约责任。简历要看匹配度、关键经验。所以设计了一套规则引擎,支持 URL 模式匹配 + 关键词匹配:
// config/auto-trigger-rules.json — Skill 路由规则(基于实际配置){ "rules": [ { "id": "price-approval", "name": "报价审批自动分析", "triggers": [ { "type": "urlContains", "value": "/price-approval" }, { "type": "keyword", "keywords": ["报价审批","报价单","折扣率","销售报价"], "minHits": 2 } ], "skill": "报价审批分析" }, { "id": "contract-review", "triggers": [ { "type": "urlContains", "value": "/contract" }, { "type": "keyword", "keywords": ["合同","甲方","乙方","协议","违约责任"], "minHits": 2 } ], "skill": "方案评估" }, { "id": "financial-statement", "triggers": [ { "type": "keyword", "keywords": ["利润表","损益表","资产负债表","现金流量表"], "minHits": 1 } ], "skill": "财报解读" }, { "id": "resume-screening", "triggers": [ { "type": "keyword", "keywords": ["简历","求职","应聘","工作经历","教育背景"], "minHits": 2 } ], "skill": "简历筛选助手" } ], "defaultSkill": "审批快速分析", "cooldownMs": 30000, "dedupWindowMs": 300000}
每条规则包含触发条件(URL 匹配 OR 关键词命中)和目标 Skill。未匹配到的走默认 Skill。规则是纯 JSON 配置,加新审批类型不需要改代码,只加一条 JSON 记录即可。
· · ·
五、如何搭建你自己的?
整个系统部署只涉及三个东西跑起来:
第一步:配置审批平台 API
编辑 watcher/config.json,填上你公司审批系统的 API 地址和 Token:
{ "user": { "userId": "zhangsan", "userName": "张三", "userNameAliases": ["Zhang San"] }, "apis": { "list": { "url": "https://your-oa.com/api/approvals/pending", "headers": { "Authorization": "Bearer YOUR_TOKEN" }, "responseMapping": { "itemsPath": "data.list", "idField": "id", "handlerField": "currentHandler" } }, "detail": { "urlPattern": "https://your-oa.com/api/approvals/{id}", "responseMapping": { "fieldsPath": "formData", "handlerField": "currentHandler" } } }}
💡 关键:responseMapping 用点路径(如 data.list)描述 API 返回 JSON 中审批列表的位置。换一个审批平台只需改这几行配置,watcher 代码不用动。
第二步:配置处理人校验
打开你审批平台的任意一个审批页面,按 F12 查看 DOM 结构,找到”当前处理人”对应的 HTML。比如你的平台是这样的:
<div class="approval-info"> <span class="label">当前处理人</span> <span class="value user-name">张三</span></div>
那就把 .user-name 填到 config/auto-trigger-rules.json:
{ "currentUser": { "userId": "zhangsan", "userName": "张三", "userNameAliases": ["Zhang San"] }, "handlerVerification": { "enabled": true, "selectors": [".user-name", ".approval-handler"], "textPatterns": [ "当前处理人[::]*\\s*(\\S+)", "审批人[::]*\\s*(\\S+)" ] }}
第三步:启动服务
# 终端1:启动 Qoder 桥接服务(在原 James超级助理 项目目录下)cd ../james-super-assistant && python bridge.py# → HTTP 服务启动在 :39877# 终端2:启动后台 Watcher(在 approval-auto-agent 目录下)cd watcher && python watcher.py# → 轮询服务启动,结果 HTTP :39878# Chrome:加载扩展(开发者模式 → 加载已解压的扩展 → 选项目根目录)
第四步:打开审批页面
正常使用审批系统即可。当审批单到你手里时,右侧自动弹出面板,分析结果已经在那里了。
· · ·
六、成本与收益
┌──────────────┬─────────────────────────────┐│ 指标 │ 数据 │├──────────────┼─────────────────────────────┤│ 打开到结果 │ 缓存命中 <100ms ││ │ 实时分析 15~60s ││ 预分析延迟 │ 审批单创建后 ~30s 完成 ││ 隐私 │ 全程本地,0 数据上传 ││ 扩展性 │ 新审批类型仅加JSON配置 ││ 运行成本 │ 本地算力,无 API 费用 │└──────────────┴─────────────────────────────────┘
· · ·
总结
整套方案的核心思路:用后台轮询把 AI 分析前置,用缓存先行把等待时间归零,用三层校验确保只在处理人打开时触发。
· · ·
夜雨聆风