主题
ERP 录屏分析与 PRD 流水线
本章目标:
- 看懂
demoo_service这个独立 Flask 服务在 A-CDM 里的定位:它不在 deer-flow Agent 链路上,而是 ERP 顾问的"录屏反编译工作站"- 吃透
erp_flow四阶段流水线(Stage 1 提取 → Stage 2 合并 → Stage 2.5 纪要 → Stage 2.6 去重),尤其是金蝶云星瀚 Selector 语义化页面类型识别这套核心算法- 弄清两条录制采集路径(反向代理 vs Chrome 扩展直采)与共享 DB / Nextcloud 的集成边界
TL;DR
demoo_service 是一个独立的 Flask 单体(server.py,app = Flask(__name__),demoo_service/server.py:57),跟 deer-flow 的 LangGraph Agent 完全解耦,服务于"企业核心经营系统(ERP)反编译"这件事:把顾问对金蝶云星瀚 / 华为 MetaERP 的浏览器操作录屏,反向工程出业务单据结构与 PRD 输入。核心是 erp_flow 包的四阶段无监督流水线——Stage 1 CompactExtractor 用金蝶 kd-cq-* Selector 语义化分类把每个页面识别成 query_list / form_detail / dashboard_home 等类型并提取字段,Stage 2 ContextMerger 按 formId 跨文件合并去重,Stage 2.5 TranscriptLoader 加载会议纪要,Stage 2.6 ContextDeduplicator 生成最终 final_context.json(Stage 3 LLM 生成 PRD 已在代码里禁用)。录制有两条路径:proxy_recorder.py 反向代理注入 JS(零插件)和 Chrome 扩展 v1.22 直采。产物经 nextcloud_uploader.py 归档,元数据经 db.py 写入与 acdm-backend 共享的 platform-db acdm.recording_result 表。
Overview(为什么需要一条"录屏反编译"流水线)
ERP 实施咨询有个老大难问题:甲方现有系统(老 ERP)没有文档,或文档严重过期,顾问要做"现状分析"只能靠肉眼看顾问操作系统。传统做法是顾问一边点系统一边手写《现状分析》《单据字段表》,几百个字段纯人肉誊抄,既慢又错。
直觉做法是"录个屏给 AI 看视频",但视频对 LLM 是黑盒:看不到 DOM 结构、控件类型、字段是否必填、表格有几列。真正有价值的信息全在前端 DOM 里。
demoo_service 的答案分两步:
- 采集结构化操作日志:不是录视频,而是在浏览器里注入 recorder JS,把每次 click/input 时的完整
form_state(所有form_fields的 selector / className / current_value)抓成 JSON。 - Selector 语义化反编译:金蝶云星瀚前端的 className 是语义化的(
kd-cq-field、kd-cq-org、kd-cq-querypanel-item),不是哈希乱码。erp_flow利用这个特征,纯代码(不依赖 LLM)就能把一段操作流还原成"这是个查询列表页,有这些筛选条件;那是个表单详情页,有这些字段,字段类型是组织选择器"。
这就是为什么它是一条确定性流水线而非 Agent:页面类型识别、控件类型推断、跨文件去重全是规则驱动,结果可复现、零 token 成本。LLM 生成 PRD(Stage 3)反而是可选的、目前禁用的尾段。
Architecture
demoo_service 是个传统 Flask 单体,自带前端静态资源(Vue SPA /erp),核心 100+ 路由集中在 server.py。它跟 deer-flow Agent 不共享进程、不共享框架,仅通过两个集成点跟 A-CDM 主链路交汇:① Nextcloud(产物归档到 129项目 共享目录)② platform-db acdm schema 的 recording_result 表(与 acdm-backend 共库)。
| 组件 | 职责 | 入口文件 | Source |
|---|---|---|---|
Flask server.py | HTTP 路由总线、录屏分析编排、Vue SPA 托管 | server.py | demoo_service/server.py:57-61 |
proxy_recorder Blueprint | 零插件反向代理录制(注入 recorder JS) | proxy_recorder.py | demoo_service/proxy_recorder.py:56-181 |
| Chrome 扩展 v1.22 | 直采录制(目标系统原域名,无需代理) | chrome_extension/manifest.json | demoo_service/chrome_extension/manifest.json:1-42 |
erp_flow.ERPPRDPipeline | 四阶段流水线主编排器 | erp_flow/pipeline.py | demoo_service/erp_flow/pipeline.py:49-206 |
CompactExtractor | Stage 1:单文件 Selector 语义化提取 | erp_flow/stages/extractor.py | demoo_service/erp_flow/stages/extractor.py:125-450 |
detect_page_type | 页面类型识别(规则阈值算法) | erp_flow/stages/page_type_detector.py | demoo_service/erp_flow/stages/page_type_detector.py:36-144 |
ContextMerger | Stage 2:按 formId 跨文件合并 | erp_flow/stages/merger.py | demoo_service/erp_flow/stages/merger.py:18-231 |
TranscriptLoader | Stage 2.5:加载清洗版/原始稿纪要 | erp_flow/stages/transcript.py | demoo_service/erp_flow/stages/transcript.py:14-124 |
ContextDeduplicator | Stage 2.6:生成 final_context.json | erp_flow/stages/deduplicator.py | demoo_service/erp_flow/stages/deduplicator.py:19-483 |
PRDGenerator | Stage 3:四段式 LLM 生成 PRD(禁用) | erp_flow/stages/generator.py | demoo_service/erp_flow/stages/generator.py:25-87 |
db.upsert_recording_result | 写共享 platform-db | db.py | demoo_service/db.py:24-141 |
nextcloud_uploader | WebDAV 归档录屏与解析产物 | nextcloud_uploader.py | demoo_service/nextcloud_uploader.py:46-131 |
注意 server.py 里有两条并存的分析路径,不要混淆:
- 旧路径
/api/start_analysis→_run_analysis→BusinessModelingEngine(server.py:462-549):基于screen_parser/business_modeling的"四大模型"(数据/行为/规则/流程)反编译,是 README 描述的"基础反编译回溯"功能。 - 新路径
/api/v2/recordings/<filename>/parse→erp_flow.ERPPRDPipeline(server.py:2397-2449):本章重点,Selector 语义化流水线,产出final_context.json+ Markdown。
Components / Subsystems
Stage 1:CompactExtractor —— Selector 语义化提取
职责:把单个录屏 JSON 文件解析成结构化页面快照列表。
关键类:CompactExtractor 在 demoo_service/erp_flow/stages/extractor.py:125。它是个主协调器,自身只管流程,具体提取委托给 7 个拆分模块(page_type_detector / query_list_extractor / form_detail_extractor / page_context_extractor / click_action_extractor / form_analyzer),extractor.py:26-51 一次性 import。
关键方法:
load()(extractor.py:155):读 JSON,兼容三种格式(裸数组 /{operations:[]}/ 单个{action_type}),过滤掉console操作。_extract_page_snapshots()(extractor.py:188):核心循环。对每个 operation,click 操作走extract_click_action,非 click 操作先detect_page_type(form_fields)识别类型,过滤掉 login 和 dashboard_home(extractor.py:211-212,这两类对反编译无价值),再按类型分流提取:query_list走查询条件 + 搜索框 + 列表表格,form_detail走表单字段 + 分录表格,其他走通用兜底。run()(extractor.py:435):load → identify_forms → analyze_forms → extract_flow → deduplicate_forms → to_compact,产出ExtractionResult。
实现要点 —— 截图时间戳关联:_load_screenshot_index()(extractor.py:538)扫描与 JSON 同级的 snapshots/ 目录,解析文件名 erp_operations_{业务路径}_{YYYYMMDD}_{HHMMSS}.png 拿到截图时间戳;_associate_screenshot()(extractor.py:565)用 ±10 秒容差(settings.screenshot_match_tolerance,config/settings.py:74)给每个快照配最近的截图。这是把操作日志和视觉证据对齐的巧妙处。
页面类型识别(本章核心算法)
职责:零 LLM,纯规则把一个页面归类为 query_list / form_detail / dashboard_home / login / unknown。
关键函数:detect_page_type(form_fields) 在 demoo_service/erp_flow/stages/page_type_detector.py:36。
算法分两步:先扫一遍 form_fields,按 selector / className 统计九类特征计数(查询面板数、表单字段数、ID 选择器数、表格行数、Tab 数、卡片数等),其中 ID 选择器会排除工具栏按钮(EXCLUDED_ID_SELECTORS + 所有 #tbl* 前缀,page_type_detector.py:24-33);然后按优先级从高到低判定:
python
# 摘自 page_type_detector.py:113-144(节选判定逻辑)
if has_login_url: # 1 登录页最高优先
return "login"
if query_panel_count >= PAGE_TYPE_THRESHOLDS["query_panel_min"]: # 2 ≥3 个查询面板 → 列表页
return "query_list"
if (form_field_count >= PAGE_TYPE_THRESHOLDS["form_field_min"] or # 3 ≥10 字段 或
id_selector_count >= PAGE_TYPE_THRESHOLDS["id_selector_min"]): # ≥5 ID选择器 → 详情页
return "form_detail"
if (table_row_count >= PAGE_TYPE_THRESHOLDS["table_row_min"] and # 4 ≥5 表格行且无表单 → 列表页
id_selector_count < PAGE_TYPE_THRESHOLDS["id_selector_min"]):
return "query_list"
if table_header_count >= 3 and (tree_menu_count >= 1 or toolbar_count >= 2): # 4.5 无查询面板的列表工作区
return "query_list"这套阈值在 config/constants.py:289-294(PAGE_TYPE_THRESHOLDS),特征选择器集合在 constants.py:66-79(QUERY_PANEL_SELECTORS / FORM_FIELD_SELECTORS)。设计权衡:为什么用阈值计数而不是单一选择器命中?因为金蝶页面经常混排(列表页里嵌一两个筛选字段、详情页里有汇总表格),单选择器命中会误判;用"哪类特征压倒性多数"+ 优先级兜底更鲁棒。这也是为什么 README 列的页面类型识别条件是"≥N 个"而非"存在"。
Stage 2:ContextMerger —— 跨文件合并去重
职责:把多个 ExtractionResult 按页面身份合并成一份 MergedContext。
关键类:ContextMerger 在 demoo_service/erp_flow/stages/merger.py:18。
去重键设计(_merge_page_snapshots,merger.py:71-126):优先从 URL 提取 formId= 作为稳定标识(merger.py:97-99),组合键 = formId|page_type;没有 formId 时退回 hash(url)|page_type。click_action 类型不参与合并直接保留(它是离散操作证据,合并会丢信息,merger.py:85-88)。同键多页面走 _merge_similar_pages(merger.py:128):query_list 合并 query_conditions / entry_tables / query_actions,form_detail 按 field_id 合并 form_fields,都带 occurrence_count 计数。FILTERED_TITLES(constants.py:176-178)里的标题直接跳过。
Stage 2.5:TranscriptLoader —— 会议纪要加载
职责:把顾问访谈的"口水稿"作为业务规则补充喂给后续(原本给 Stage 3 LLM)。
关键类:TranscriptLoader 在 demoo_service/erp_flow/stages/transcript.py:14。_find_transcript_files()(transcript.py:68)扫 口水稿/ 子目录 + 根目录的 口水稿*.md / 纪要*.md,按清洗版 > 纪要 > 原始稿 > 其他排序(transcript.py:90-99)。清洗版整文喂入;原始稿只抽 ## 内容分析 到 ## 发言统计 之间的摘要段(_extract_content_analysis,transcript.py:103-108),避免把冗长逐字稿全塞进去。
Stage 2.6:ContextDeduplicator —— 生成 final_context.json
职责:对 merged_context.json 做二次去重并附加流程语义,产出 final_context.json(下游/前端真正消费的文件)。
关键类:ContextDeduplicator 在 demoo_service/erp_flow/stages/deduplicator.py:19。它按 page_tabs.text 分组、page_type 分类(deduplicator.py:38-46),关键决策:同一页若既有 query_list 又有 form_detail,合并成 form_detail 并同时保留 query_conditions 与 form_fields(_merge_query_and_form,deduplicator.py:220-268)——因为顾问常常先在列表页搜、再点进详情页,两者其实是同一业务对象的两面。同组内多快照按"丰富度"(query_conditions 数 > form_fields 数 > 表格有数据)取最丰的一个(_dedupe_form_detail,deduplicator.py:375-397)。最后给每个快照算 flow_order 和人话 flow_action(如"进入「付款申请」(12个字段)"、"填写表单-点击「保存」",deduplicator.py:91-184)。
Stage 3:PRDGenerator(已禁用)
关键类:PRDGenerator 在 demoo_service/erp_flow/stages/generator.py:25。设计是四段式 LLM 调用:Part 1 业务背景/SOP/规则 → 3.1.x 按页面分块生成字段表(section_chunk_size=42 字段/次,config/settings.py:55)→ Part 4 RBAC/上线标准。但 pipeline.py:172-200 把整个 Stage 3 注释掉了,run() 在 Stage 2.6 后直接返回成功并打印 [INFO] Stage 3 (LLM生成PRD) 已禁用(pipeline.py:203)。generator.py:7-9 文件头也明确标注禁用。所以当前流水线产出止于 final_context.json,PRD 文档由 server.py 的 _generate_markdown_from_context(server.py:2123)以纯模板方式从 context 渲染,不走 LLM。
Data Flow / 控制流
下面是前端点"解析录屏"到产物落库的完整时序(新路径 /api/v2/recordings/<filename>/parse):
逐步说明:
- 前端 POST
scenario_name+project_id(server.py:2400-2401)。 - 在
erp_analysis_results/recordings和Config.SCREEN_RECORDINGS_DIR两处定位录屏,带路径前缀校验防穿越(server.py:2404-2416)。 project_id请求体优先,缺省从录屏metadata.project_id兜底(server.py:2418-2427)——因为db.upsert_recording_result强制要求非空 project_id(db.py:51-52)。- 创建临时目录,把录屏拷进去(流水线要求文件在 project_dir 根,
server.py:2429-2436)。 ERPPRDPipeline(project_dir=tmp, skip_llm=True).run()(server.py:2445-2449)——skip_llm本来控制 Stage 3,但 Stage 3 已禁用故无实际差异。- 读
final_context.json(server.py:2454-2460),_generate_markdown_from_context渲染人类可读 MD。 - JSON + MD 固定上传到
129项目/ba-materials/playwright(server.py:2483-2490,注释明确"不随 project_id 变化",业务约定)。 upsert_recording_result写acdm.recording_result,以session_uuid为冲突键 UPSERT,status='markdowned',created_by取X-User-Sub/X-Forwarded-User头(server.py:2520-2536,db.py:88-104)。DB 失败不阻断,以db_error字段返回(server.py:2538-2544)。finally清理临时目录(server.py:2561-2564)。
速查表
erp_flow 四阶段一览
| 阶段 | 类 / 函数 | 输入 | 输出 | 状态 | Source |
|---|---|---|---|---|---|
| Stage 1 | CompactExtractor.run() | 单个录屏 JSON | *_stage1.json (ExtractionResult) | 启用 | demoo_service/erp_flow/stages/extractor.py:435-450 |
| Stage 2 | ContextMerger.merge() | N 个 ExtractionResult | merged_context.json | 启用 | demoo_service/erp_flow/stages/merger.py:24-69 |
| Stage 2.5 | TranscriptLoader.load() | 口水稿/*.md | 纪要补充文本 | 启用 | demoo_service/erp_flow/stages/transcript.py:32-66 |
| Stage 2.6 | dedupe_merged_context() | merged_context.json | final_context.json | 启用 | demoo_service/erp_flow/stages/deduplicator.py:469-483 |
| Stage 3 | PRDGenerator.generate() | final_context + 纪要 | *_PRD.md | 禁用 | demoo_service/erp_flow/pipeline.py:172-200 |
页面类型判定速查
| 页面类型 | 判定条件(优先级降序) | 后续处理 | Source |
|---|---|---|---|
login | URL 含 login | 跳过(extractor 过滤) | demoo_service/erp_flow/stages/page_type_detector.py:113-114 |
query_list | 查询面板 ≥3;或表格行 ≥5 且 ID选择器 <5;或表头≥3+树菜单/工具栏 | 提取 query_conditions / search_box / 列表表格 | demoo_service/erp_flow/stages/page_type_detector.py:117-132 |
form_detail | 表单字段 ≥10 或 ID选择器 ≥5;或 ID选择器 ≥3 兜底 | 提取 form_fields / metrics / entry_tables | demoo_service/erp_flow/stages/page_type_detector.py:121-142 |
dashboard_home | 卡片≥2 或 Tab≥3,且无查询面板/表单 | 跳过(extractor 过滤) | demoo_service/erp_flow/stages/page_type_detector.py:135-138 |
unknown | 其他 | 通用兜底提取 | demoo_service/erp_flow/stages/page_type_detector.py:144 |
控件类型映射(8 种,constants.py:19-28)
| className | 控件类型 | className | 控件类型 |
|---|---|---|---|
kd-cq-checkbox | checkbox | kd-cq-combo | combo |
kd-cq-number | number | kd-cq-basedata | basedata |
kd-cq-text | text | kd-cq-org | org |
kd-cq-date | date | kd-cq-largeText | largeText |
Source: demoo_service/erp_flow/config/constants.py:19-28
两条录制采集路径对比
| 维度 | 反向代理 proxy_recorder | Chrome 扩展 v1.22 |
|---|---|---|
| 用户安装 | 零插件,访问 /rec/sid/target/* | 需装 CRX |
| 域名 | 经我方代理域名(改 SSO redirect 参数兜白名单) | 目标系统原域名直采 |
| 注入方式 | 服务端把 recorder_inject.js 注进每个 HTML | content.js (document_start) |
| 事件回传 | POST /api/proxy-rec/events 批量 | 同一组 /api/proxy-rec/* 端点 |
| 落盘 | proxy_rec_stop 写 erp_operations_*.json + 截图 zip | 同 |
| Source | demoo_service/proxy_recorder.py:130-366 | demoo_service/chrome_extension/manifest.json:13-41 |
Configuration
| Config | 默认值 | 含义 | 影响 | Source |
|---|---|---|---|---|
ERP_LLM_API_URL | 腾讯 lkeap anthropic 网关 | Stage 3 LLM 端点 | Stage 3 禁用时无效 | demoo_service/erp_flow/config/settings.py:39-44 |
ERP_LLM_API_KEY | 空 | Stage 3 LLM key | 同上 | demoo_service/erp_flow/config/settings.py:45-47 |
section_chunk_size | 42 | 单次 LLM 调用字段数上限 | Stage 3 分块 | demoo_service/erp_flow/config/settings.py:55 |
screenshot_match_tolerance | 10 | 截图-快照时间匹配容差(秒) | Stage 1 截图关联精度 | demoo_service/erp_flow/config/settings.py:74 |
max_flow_steps | 60 | operation_flow 最多保留步数 | Stage 1 输出体积 | demoo_service/erp_flow/config/settings.py:65 |
DATABASE_URL | 空 | 共享 platform-db DSN | 空则跳过 DB 写入(不报错) | demoo_service/config.py:146 |
DATABASE_SCHEMA | acdm | 与 acdm-backend 共用 schema | recording_result 表前缀 | demoo_service/config.py:147, demoo_service/db.py:72-73 |
NEXTCLOUD_USERNAME/PASSWORD | 空 | WebDAV 凭据 | 空则跳过归档(不报错) | demoo_service/config.py:151-152 |
NEXTCLOUD_URL | team.r7.chinasoftinc.com | NC 基址 | 录屏/产物归档目标 | demoo_service/config.py:150 |
安全相关:db.py:51-52 强制 project_id 非空,否则 raise ValueError——这是数据隔离边界,防止录屏结果落到错误项目;server.py:2411 路径前缀校验 startswith(rec_dir.resolve()) 防目录穿越。proxy_recorder.py:588-591 主动剥离上游 CSP / X-Frame-Options 头(否则注入脚本无法执行),这是录制能工作的必要妥协,仅限内网录制场景。
Common Pitfalls / 实战 Tips
- Stage 3 是禁用的:任何"为什么 PRD 没生成 / LLM 没调用"的疑问,根因都是
pipeline.py:172-200注释块。当前 PRD 输入靠final_context.json+server.py:2123模板渲染,不是 LLM 产物。 project_id必须有值:/api/v2/.../parse若请求体和录屏 metadata 都没有project_id,db.upsert_recording_result会raise ValueError(db.py:51-52),整个请求 500。录制时务必让前端带project_id(proxy_recorder.py:152在 start 时存入 metadata)。- login / dashboard_home 会被丢弃:
extractor.py:211-212主动continue跳过这两类页面,所以final_context.json里看不到首页/登录页是预期行为,不是 bug。 - NC 上传路径写死 129 项目:解析产物固定进
129项目/ba-materials/playwright(server.py:2483-2484),录屏源文件进129项目/录屏(proxy_recorder.py:346),与传入的project_id无关,这是业务约定,排查"产物去哪了"别按 project_id 找。 - DB / NC 失败不阻断主流程:两者都是"记日志返回 None"(
db.py:138-140、nextcloud_uploader.py:88-93),解析仍会成功返回。响应里db_error非空说明落库失败,需另行补录。 - 截图靠时间戳关联,±10 秒容差:扩展/代理回传的截图文件名时间和操作时间戳偏差超过 10 秒就匹配不上(
config/settings.py:74),录制时系统时钟漂移会导致截图丢关联。
References
demoo_service/erp_flow/pipeline.py:49-206—ERPPRDPipeline四阶段主编排器(Stage 3 禁用块在 172-200)demoo_service/erp_flow/stages/extractor.py:125-595—CompactExtractorStage 1,Selector 语义化提取 + 截图关联demoo_service/erp_flow/stages/page_type_detector.py:36-144— 页面类型识别规则阈值算法(本章核心)demoo_service/erp_flow/stages/merger.py:18-231—ContextMergerStage 2,按 formId 跨文件合并去重demoo_service/erp_flow/stages/deduplicator.py:19-483—ContextDeduplicatorStage 2.6,生成 final_context.json + flow 语义demoo_service/erp_flow/config/constants.py:19-294— Selector 映射 / 页面类型阈值 / 噪声标题等业务常量demoo_service/server.py:2397-2564—/api/v2/recordings/<f>/parse流水线桥接 + Nextcloud + DB 集成demoo_service/proxy_recorder.py:56-366— 反向代理零插件录制(start/events/stop/screenshot + 透明代理)demoo_service/db.py:24-141—upsert_recording_result写共享 platform-db acdm schemademoo_service/nextcloud_uploader.py:46-131— WebDAV 录屏/产物归档助手
Related Pages
| Page | Relationship |
|---|---|
| 项目管理与资源授权 | 本章 recording_result.project_id 关联该章项目实体;DB 同库 acdm schema |
| 可逆DB操作与存储层 | 本章 db.py 写入的 platform-db 由该章统一描述存储层 |
| BA专家报告工作流 | 本章产物落 129项目/ba-materials/playwright,作为该章 BA 报告的输入素材 |
| 知识库与Wiki协编 | 本章产物经 Nextcloud 归档,与该章 NC 集成共用 nextcloud_uploader |
| 系统整体架构 | 本章 demoo_service 是该章描述的独立子服务,非 deer-flow Agent 链路 |
| Caddy网关与生产路由拓扑 | 本章 Flask 服务的对外路由由该章 Caddy 反代描述 |