Skip to content

ERP 录屏分析与 PRD 流水线

本章目标:

  1. 看懂 demoo_service 这个独立 Flask 服务在 A-CDM 里的定位:它不在 deer-flow Agent 链路上,而是 ERP 顾问的"录屏反编译工作站"
  2. 吃透 erp_flow 四阶段流水线(Stage 1 提取 → Stage 2 合并 → Stage 2.5 纪要 → Stage 2.6 去重),尤其是金蝶云星瀚 Selector 语义化页面类型识别这套核心算法
  3. 弄清两条录制采集路径(反向代理 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 的答案分两步:

  1. 采集结构化操作日志:不是录视频,而是在浏览器里注入 recorder JS,把每次 click/input 时的完整 form_state(所有 form_fields 的 selector / className / current_value)抓成 JSON。
  2. Selector 语义化反编译:金蝶云星瀚前端的 className 是语义化的(kd-cq-fieldkd-cq-orgkd-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.pyHTTP 路由总线、录屏分析编排、Vue SPA 托管server.pydemoo_service/server.py:57-61
proxy_recorder Blueprint零插件反向代理录制(注入 recorder JS)proxy_recorder.pydemoo_service/proxy_recorder.py:56-181
Chrome 扩展 v1.22直采录制(目标系统原域名,无需代理)chrome_extension/manifest.jsondemoo_service/chrome_extension/manifest.json:1-42
erp_flow.ERPPRDPipeline四阶段流水线主编排器erp_flow/pipeline.pydemoo_service/erp_flow/pipeline.py:49-206
CompactExtractorStage 1:单文件 Selector 语义化提取erp_flow/stages/extractor.pydemoo_service/erp_flow/stages/extractor.py:125-450
detect_page_type页面类型识别(规则阈值算法)erp_flow/stages/page_type_detector.pydemoo_service/erp_flow/stages/page_type_detector.py:36-144
ContextMergerStage 2:按 formId 跨文件合并erp_flow/stages/merger.pydemoo_service/erp_flow/stages/merger.py:18-231
TranscriptLoaderStage 2.5:加载清洗版/原始稿纪要erp_flow/stages/transcript.pydemoo_service/erp_flow/stages/transcript.py:14-124
ContextDeduplicatorStage 2.6:生成 final_context.jsonerp_flow/stages/deduplicator.pydemoo_service/erp_flow/stages/deduplicator.py:19-483
PRDGeneratorStage 3:四段式 LLM 生成 PRD(禁用)erp_flow/stages/generator.pydemoo_service/erp_flow/stages/generator.py:25-87
db.upsert_recording_result写共享 platform-dbdb.pydemoo_service/db.py:24-141
nextcloud_uploaderWebDAV 归档录屏与解析产物nextcloud_uploader.pydemoo_service/nextcloud_uploader.py:46-131

注意 server.py 里有两条并存的分析路径,不要混淆:

  • 旧路径 /api/start_analysis_run_analysisBusinessModelingEngine(server.py:462-549):基于 screen_parser / business_modeling 的"四大模型"(数据/行为/规则/流程)反编译,是 README 描述的"基础反编译回溯"功能。
  • 新路径 /api/v2/recordings/<filename>/parseerp_flow.ERPPRDPipeline(server.py:2397-2449):本章重点,Selector 语义化流水线,产出 final_context.json + Markdown。

Components / Subsystems

Stage 1:CompactExtractor —— Selector 语义化提取

职责:把单个录屏 JSON 文件解析成结构化页面快照列表。

关键类:CompactExtractordemoo_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

关键类:ContextMergerdemoo_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_typeclick_action 类型不参与合并直接保留(它是离散操作证据,合并会丢信息,merger.py:85-88)。同键多页面走 _merge_similar_pages(merger.py:128):query_list 合并 query_conditions / entry_tables / query_actions,form_detailfield_id 合并 form_fields,都带 occurrence_count 计数。FILTERED_TITLES(constants.py:176-178)里的标题直接跳过。

Stage 2.5:TranscriptLoader —— 会议纪要加载

职责:把顾问访谈的"口水稿"作为业务规则补充喂给后续(原本给 Stage 3 LLM)。

关键类:TranscriptLoaderdemoo_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(下游/前端真正消费的文件)。

关键类:ContextDeduplicatordemoo_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(已禁用)

关键类:PRDGeneratordemoo_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):

逐步说明:

  1. 前端 POST scenario_name + project_id(server.py:2400-2401)。
  2. erp_analysis_results/recordingsConfig.SCREEN_RECORDINGS_DIR 两处定位录屏,带路径前缀校验防穿越(server.py:2404-2416)。
  3. project_id 请求体优先,缺省从录屏 metadata.project_id 兜底(server.py:2418-2427)——因为 db.upsert_recording_result 强制要求非空 project_id(db.py:51-52)。
  4. 创建临时目录,把录屏拷进去(流水线要求文件在 project_dir 根,server.py:2429-2436)。
  5. ERPPRDPipeline(project_dir=tmp, skip_llm=True).run()(server.py:2445-2449)——skip_llm 本来控制 Stage 3,但 Stage 3 已禁用故无实际差异。
  6. final_context.json(server.py:2454-2460),_generate_markdown_from_context 渲染人类可读 MD。
  7. JSON + MD 固定上传到 129项目/ba-materials/playwright(server.py:2483-2490,注释明确"不随 project_id 变化",业务约定)。
  8. upsert_recording_resultacdm.recording_result,以 session_uuid 为冲突键 UPSERT,status='markdowned',created_byX-User-Sub/X-Forwarded-User 头(server.py:2520-2536,db.py:88-104)。DB 失败不阻断,以 db_error 字段返回(server.py:2538-2544)。
  9. finally 清理临时目录(server.py:2561-2564)。

速查表

erp_flow 四阶段一览

阶段类 / 函数输入输出状态Source
Stage 1CompactExtractor.run()单个录屏 JSON*_stage1.json (ExtractionResult)启用demoo_service/erp_flow/stages/extractor.py:435-450
Stage 2ContextMerger.merge()N 个 ExtractionResultmerged_context.json启用demoo_service/erp_flow/stages/merger.py:24-69
Stage 2.5TranscriptLoader.load()口水稿/*.md纪要补充文本启用demoo_service/erp_flow/stages/transcript.py:32-66
Stage 2.6dedupe_merged_context()merged_context.jsonfinal_context.json启用demoo_service/erp_flow/stages/deduplicator.py:469-483
Stage 3PRDGenerator.generate()final_context + 纪要*_PRD.md禁用demoo_service/erp_flow/pipeline.py:172-200

页面类型判定速查

页面类型判定条件(优先级降序)后续处理Source
loginURL 含 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_tablesdemoo_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-checkboxcheckboxkd-cq-combocombo
kd-cq-numbernumberkd-cq-basedatabasedata
kd-cq-texttextkd-cq-orgorg
kd-cq-datedatekd-cq-largeTextlargeText

Source: demoo_service/erp_flow/config/constants.py:19-28

两条录制采集路径对比

维度反向代理 proxy_recorderChrome 扩展 v1.22
用户安装零插件,访问 /rec/sid/target/*需装 CRX
域名经我方代理域名(改 SSO redirect 参数兜白名单)目标系统原域名直采
注入方式服务端把 recorder_inject.js 注进每个 HTMLcontent.js (document_start)
事件回传POST /api/proxy-rec/events 批量同一组 /api/proxy-rec/* 端点
落盘proxy_rec_stoperp_operations_*.json + 截图 zip
Sourcedemoo_service/proxy_recorder.py:130-366demoo_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_KEYStage 3 LLM key同上demoo_service/erp_flow/config/settings.py:45-47
section_chunk_size42单次 LLM 调用字段数上限Stage 3 分块demoo_service/erp_flow/config/settings.py:55
screenshot_match_tolerance10截图-快照时间匹配容差(秒)Stage 1 截图关联精度demoo_service/erp_flow/config/settings.py:74
max_flow_steps60operation_flow 最多保留步数Stage 1 输出体积demoo_service/erp_flow/config/settings.py:65
DATABASE_URL共享 platform-db DSN空则跳过 DB 写入(不报错)demoo_service/config.py:146
DATABASE_SCHEMAacdm与 acdm-backend 共用 schemarecording_result 表前缀demoo_service/config.py:147, demoo_service/db.py:72-73
NEXTCLOUD_USERNAME/PASSWORDWebDAV 凭据空则跳过归档(不报错)demoo_service/config.py:151-152
NEXTCLOUD_URLteam.r7.chinasoftinc.comNC 基址录屏/产物归档目标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_resultraise 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-140nextcloud_uploader.py:88-93),解析仍会成功返回。响应里 db_error 非空说明落库失败,需另行补录。
  • 截图靠时间戳关联,±10 秒容差:扩展/代理回传的截图文件名时间和操作时间戳偏差超过 10 秒就匹配不上(config/settings.py:74),录制时系统时钟漂移会导致截图丢关联。

References

  • demoo_service/erp_flow/pipeline.py:49-206ERPPRDPipeline 四阶段主编排器(Stage 3 禁用块在 172-200)
  • demoo_service/erp_flow/stages/extractor.py:125-595CompactExtractor Stage 1,Selector 语义化提取 + 截图关联
  • demoo_service/erp_flow/stages/page_type_detector.py:36-144 — 页面类型识别规则阈值算法(本章核心)
  • demoo_service/erp_flow/stages/merger.py:18-231ContextMerger Stage 2,按 formId 跨文件合并去重
  • demoo_service/erp_flow/stages/deduplicator.py:19-483ContextDeduplicator Stage 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-141upsert_recording_result 写共享 platform-db acdm schema
  • demoo_service/nextcloud_uploader.py:46-131 — WebDAV 录屏/产物归档助手
PageRelationship
项目管理与资源授权本章 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 反代描述

公司内部参考 · 由 claude-wiki-gen 基于源码自动生成的二次分析