申购单文件夹自动分拣与调度
适用场景
用户把多种类型的申购单(零件BOM、电气件、紧固件、外购件、零散物料)以及可能的物料数据库,
全部放进同一个文件夹,希望我自动分辨类型、调用正确的「处理」skill 把编码填回去,
再(经确认后)调用正确的「处理」skill 生成金蝶导入模板。
四类各自有独立的「处理」skill(含匹配+导入),本 skill 不重复实现匹配/导入逻辑,而是调度它们:
| 类型 | 处理 skill(含匹配+导入) | 匹配字段 |
|---|---|---|
| 零件BOM | 零件BOM处理 |
名称+规格+品牌+材质+表面处理(五字段精确) |
| 电气件 | 电气件处理 |
品名+规格+品牌+单位(四字段精确) |
| 紧固件 | 紧固件处理 |
品名+规格+材质+表面处理(含表面模糊) |
| 外购件 | scripts/match_waibuy.py(本 skill 内置) |
名称+规格+品牌(三字段精确,命中回填编码,未命中标「未匹配」提示) |
| 零散物料 | 零散物料导入 |
提取编码列为空的行,A/B/C 系列新建 |
> 注:匹配/导入脚本已内嵌在各「处理」skill 的 scripts/ 内(自包含,不再依赖独立匹配/导入 skill)。本 skill 通过加载对应「处理」skill 并运行其 scripts/ 下脚本完成调度。
> 💡 单一类型快捷入口:如果用户只给了一份某类型的申购单(不是混装文件夹),不必走本分拣流程,
> 直接调用对应的「匹配+导入一体」skill 即可一键完成 匹配→(确认)→导入→(金蝶确认后)回填:
> - 零件BOM → 零件BOM处理
> - 电气件 → 电气件处理
> - 紧固件 → 紧固件处理
> 这些一体 skill 已内建三道确认门禁(匹配预览、导入生成、金蝶后回填),与本流程安全规则一致。
> 本分拣 skill 仍专用于多种类型混在一个文件夹的统一调度场景。
核心约束(务必遵守)
> 🔴🔴 桌面只留最终文件,备份/证据不留桌面(2026-09-16 用户明确要求)
> 用户原话:「以后桌面上我只想看到最终的 你备份的和证据都帮我删除」。
> - 禁止把 .bak.xlsx、.污染留存.xlsx、*备份*、*证据* 留在桌面等用户可见目录。
> - 备份/证据放工作区临时目录(D:\ai\<session>\,_ 前缀),收尾时自行删除,不必等用户开口。
> - 任务结束桌面应只剩:用户原始文件 + 最终交付件 + 物料表。
> - 删前先做完整性核对(如「编码→物料表反查名称」),确认最终件完好再删。
> - 删除须走回收站(可恢复)。实测可用/不可用:
> · ❌ send2trash —— 含空格/中文长路径报 ProcessLookupError: [Errno 3]
> · ❌ PowerShell Add-Type / New-Object -ComObject —— 被安全策略拦截
> · ✅ Python ctypes 调 shell32.SHFileOperationW(FO_DELETE + FOF_ALLOWUNDO)
> · ✅ [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile($p,'OnlyErrorDialogs','SendToRecycleBin')
> 验证入站:读 C:\$Recycle.Bin\<SID>\$I*(UTF-16 含原路径);shell 里写 $Recycle.Bin 的 $ 会被转义,用 Python/PS 直接构造。
> ⚠️ 导入模板共用一个 导入模板.xlsx(D:\莫佩琪\msl\导入模板.xlsx,由 _shared/paths.py 管理,可用 MSL_DIR 环境变量覆盖)。
> 零件BOM / 电气件 / 紧固件 / 零散物料 四个导入流程同时导入会互相覆盖。
> 因此:生成导入模板这一步绝对不能自动串着跑,必须停下来等用户逐类确认。
> ⚠️ 匹配可全自动(只是把编码填回申购单原文件,原地改,安全)。但默认先 --dry-run 预览匹配率,用户确认后再真写。
> ⚠️ 编码回填遵循「确认后回填」:导入模板上传金蝶、用户确认全部成功后,才把新编码回填进申购单空编码列;未确认绝不填。
> ⚠️ 子 skill 版本更新自动生效:本 skill 是「瘦路由」,不复制匹配/导入逻辑,只做分类与调度。实际匹配/导入由我加载对应「处理」skill 的最新 SKILL.md 与 scripts/ 现场执行(脚本已内嵌在各处理 skill 内,自包含),每次都从磁盘读最新版。因此任一处理 skill 升级后,下次分拣自动使用新版本,无需改动本 skill。唯一注意:若某处理 skill 重命名了脚本文件名或大幅改动 CLI 参数,本 SKILL.md 里对该脚本名的引用需同步更新;但执行时我以加载到的处理 skill 实际说明为准,会自行校正。
> 📦 本 skill 自包含(2026-09-29 起):除调度外,match_waibuy.py(外购件匹配/回填)自带一份
> in-skill _shared/,与另外 4 个「处理」skill 同等的自包含约束(母本 skills/_shared/ 为单一真源,
> 由 python sync_shared.py 同步、tests/test_shared_in_sync.py 逐字节守护)。
> 之所以必须自带:此前本 skill 的 match_waibuy.py 靠 sys.path 复用仓库根 _shared/,
> 本地能跑,但单独打包发布到 skillhub 后安装目录里没有 _shared/ → ModuleNotFoundError
> → 发布出去的「申购单分拣」直接不可用。现导入写法已统一为 from _shared.xxx import ...(行为零变化)。
> 🔴🔴 备注列绝不做信息筛选(2026-09-12 用户两次明确要求,通用铁律,适用全部 5 类):
> 申购单「备注」列 = 普通备注列,只由 _shared/normalize.build_remark() 按原规则处理(重量前缀 + 原备注 + 表尾备注),
> 不得对备注内容做任何筛选/判别/分流,更不得把备注内容写进模板其他列(尤其金蝶「表面处理」字段)。
> 申购单若只有备注列、无独立表面处理列 → 模板表面处理列留空。禁止为备注列新增 strip_weight_prefix /
> looks_like_surface / clean_surface 之类「从备注推表面处理」的逻辑(曾试做,已全部回滚)。
> 用户原话:「备注列的信息不加入表面处理」/「申购单上的信息都在备注,备注你只要按照你以前的规则处理就行,不用筛选填到模板的表面处理列或者别的列」。
前置条件
- Python 环境:
C:\Users\mpqtc\.workbuddy\binaries\python\envs\default\Scripts\python.exe;依赖见本 skill 目录requirements.txt(openpyxl>=3.0,<4、xlrd>=1.2.0,<2.0) - 执行方式:脚本调用一律用 Python 绝对路径,形如
cd "<skill目录>" && PYTHONIOENCODING=utf-8 "<python绝对路径>" "scripts/xxx.py" ...;
文件操作(备份/拷贝/删除/列目录)优先交给 Python(shutil、ctypes.SHFileOperationW走回收站);
读文件用 Read 工具、查文件用 Glob/Grep(工具选择规范,与 shell 是否可用无关)。
> ⚠️ 2026-09-29 订正:此前记录的「本机 bash 工具残缺,ls/cp/mkdir/head/tail一律报
>command not found、PowerShell 输出可能为空」(2026-09-20 实测)已过时 —— 2026-09-29 实测这些命令
> 均恢复正常可用,管道(| head)也正常。故ls/cp/mkdir与管道可以直接用,不必再一律绕行 Python。
> 自检口诀:命令报command not found或 PowerShell 无输出 → 才改走 Python 绝对路径,别反复重试 shell。 - 四个处理 skill 已安装:
零件BOM处理/电气件处理/紧固件处理/零散物料导入需位于~/.workbuddy/skills/下(本 skill 只做分类与调度,实际匹配/导入由对应 skill 的脚本执行,每次从磁盘读最新版) - 共用的导入模板:
D:\莫佩琪\msl\导入模板.xlsx(金蝶 71 列格式;MSL_DIR环境变量可覆盖) - 待分拣文件:多类申购单放在同一目录(如桌面),
.xls/.xlsx;不支持.xlsb - 物料表:金蝶导出
物料_*.xlsx,自动探测顺序 = 桌面(Desktop/桌面/ OneDrive 重定向)优先 →MSL_DIR
工作流
Step 1:扫描 + 分类
用本 skill 的 scripts/classify.py 扫描文件夹:
PYTHONIOENCODING=utf-8 python "scripts/classify.py" "<文件夹路径>" [--material 物料表路径] [--json]
脚本输出一份分类报告:文件名 / 类型 / 置信度 / 判定理由,并列出检测到的物料候选。
- 类型取值:
物料数据库/零件BOM/电气件/紧固件/外购件/零散物料/⚠未知 - 置信度
low或类型为unknown的文件,必须让用户确认类型,不要瞎猜。
Step 2:用户确认分类
把分类报告呈现给用户。若有 unknown / low 或用户认为分错的,先纠正再继续。
(用户一般会把文件命名成 电气申购单 / 紧固件申购单 / 零件BOM / 零散物料,文件名权重最高。)
Step 3:逐份跑匹配(先 dry-run 预览)
对每个申购单(不含物料数据库、不含零散物料),按其类型加载对应「处理」skill,先 --dry-run 预览:
- 零件BOM → 加载
零件BOM处理skill,跑scripts/match_bom.py <申购单> <物料表> <输出.xlsx> --dry-run - 电气件 → 加载
电气件处理skill,跑scripts/match_electrical.py <申购单> <物料表> <输出.xlsx> --dry-run - 紧固件 → 加载
紧固件处理skill: - 表头有「品牌」列 →
scripts/match.py <申购单> <物料表> <输出.xlsx> --with-brand --dry-run(五字段) - 无品牌列 →
scripts/match.py <申购单> <物料表> <输出.xlsx> --dry-run(四字段) - 外购件 → 跑
scripts/match_waibuy.py <申购单> <物料表> [--dry-run] [--all-sheets](名称+规格+品牌 三字段精确匹配,命中回填编码、未命中在「匹配详情」列标「未匹配」)
> 🔎 外购件「疑似复用候选」报告(v1.3.0 新增,只报告不写入):外购件清单的品牌列常写「淘宝」(购买渠道而非真品牌),
> 而物料库里同物料的品牌字段多为空 → 三字段精确匹配落空。脚本会在汇总后附一段
> 「疑似复用候选」:列出未匹配行中在物料库已有「同名同规格」旧码(仅品牌不同)的项及其候选编码。
> 脚本绝不自动采用,须人工判断是复用旧码还是建新码(防重复建码,对齐 M26016 事故教训)。
> 自检口诀:外购件一批未匹配里若有多条「同名同规格但品牌空」→ 先看疑似复用候选,别直接建新码。
> 🔴 多 sheet 申购单必须加 --all-sheets(4 个匹配脚本口径已统一,2026-09-21)
> 申购单常把多页做成多个工作表(外购件如 ZMSH0001-…外购件清单.xlsx 含 清单01~清单04;
> 电气件如 ZMSH001电气采购单.xlsx 含 清单1+清单2)。
> 4 个匹配脚本(match_waibuy.py / match_bom.py / match.py / match_electrical.py)现在行为一致:
> 默认只处理第一张表,但只要工作簿含多 sheet 就会打印 ⚠️ 警告并提示 --all-sheets(不再静默跳过)。
> - 多 sheet 单据:加 --all-sheets 一次跑全部表;或 --sheet <表名> 只跑指定表。
> - ⚠️ 加 --all-sheets 前先确认每张表都是真清单 —— 申购单里可能夹带历史草稿表
> (实测 M26016-M01…紧固件申购单.xlsx 活动表是真清单 3 行,另带 2 张 330+ 行的历史草稿)。
> 看到 ⚠️ 警告别条件反射加 --all-sheets。
> - 复核阶段 audit.py(≥v1.4)已遍历全部 sheet,不会漏审。
> 自检口诀:① 文件名带「采购单/清单」但只有几十行、页脚写「第 1 页 共 2 页」→ 先看 sheetnames,别只跑活动表;
> ② 命中率异常低 / 未匹配条数明显偏少 → 先看是不是多 sheet 只跑了一个。
> 锁定测试:tests/test_multisheet.py。
- 零散物料 → 无匹配步骤,跳过(直接进入导入)
> 第三参 <输出.xlsx> 与子 skill 命令保持一致(dry-run 预览时不写文件,可填 <申购单> 表示原地修改路径)。各匹配脚本默认原地修改源文件。
> 💡 备注栏合并自动取消(2026-08-11 起):各「处理」skill 在匹配/生成前会自动跑 _shared/unmerge_column.py,取消备注栏(按表头定位)的纵向合并并填充内容到每一格(因匹配脚本写备注时只解除合并、只写左上角,先取消合并才能避免次行变空)。本分拣流程无需额外处理,调度各处理 skill 时即自动生效。已无合并则幂等跳过。
把每份单的匹配成功数 / 未匹配数汇总给用户看。
Step 4:预览后直接写回编码(无需确认)
按 2026-07-20 规则「匹配写回不需要确认」,Step 3 的 --dry-run 预览后直接去掉 --dry-run 重跑各匹配脚本,原地填回编码到申购单,不再单独停下问确认。
> 注意:匹配脚本默认原地修改源文件(.xls 不支持原地改,需先另存 .xlsx)。写入前提醒用户关闭 Excel。
Step 5:收尾强制复核(防漏处理,必须执行)
> ⚠️ 每次分拣,写回编码后、确认导入前,必须跑 scripts/audit.py 做一次全量复核。
> 它的作用是:扫描分类出的每一份申购单,检查编码列是否真的被填过——若发现某份编码列全空、无任何「未匹配」标记、且有真实数据行(即匹配脚本根本没跑过),立即报 UNPROCESSED 并 exit(1),禁止收尾。
> 这正是本次(M26013 零件BOM 被漏处理)这类问题的根因:分类列了 N 份,执行时凭记忆少跑了 1 份,最后没对账。
PYTHONIOENCODING=utf-8 python "scripts/audit.py" "<文件夹路径>" [--material 物料表路径]
audit.py 输出每份申购单的完成度:
| 状态 | 含义 | 处理 |
|---|---|---|
DONE |
全部数据行已填编码 | ✅ 通过 |
NEEDS_IMPORT |
有「未匹配」行(需新建编码) | 正常,进入 Step 6 确认导入 |
UNPROCESSED |
编码列全空且无任何标记、有真实数据行 → 匹配脚本没跑过 | ❌ 必须补跑对应匹配 skill,再回来复核 |
PARTIAL_EMPTY |
部分数据行既无编码也无标记 | ❌ 需核对,可能漏处理 |
> 📌 v1.4 起遍历全部 sheet:多 sheet 申购单(如 ZMSH001 的 清单1+清单2)各表独立审计后汇总计数,
> 任一表不过即不过。同时表头行改为按「标识列表头最多的行」定位,页脚行(第 N 页 共 M 页)用正则识别。
> 这三处修复共同消除了「PARTIAL_EMPTY 假警报」(实测误报 11 行)。
复核不过(出现 UNPROCESSED / PARTIAL_EMPTY)绝不许进入 Step 6 导入确认,必须先补齐处理、再次跑 audit.py 直到全绿(全部 DONE/NEEDS_IMPORT)。
> 🔴 DONE 也可能是假绿 —— 维度声明损坏(2026-09-15 加固,audit v1.3)
> audit.py 曾用 read_only=True 加载工作簿。read_only 模式下 openpyxl 信任 xlsx 里声明的
> dimension,而金蝶导出的表有 declared dimension = A1:A1 的毛病 —— 实测物料表
> ws.max_row 读出 1、实际 28983 行。若某份申购单也带此毛病,主循环
> range(hr+1, ws.max_row+1) 一行都不遍历 → filled=unmatched=unmarked_empty=0
> → 命中 DONE(空数据文件)分支,把「一份都没复核」伪装成「复核通过」。
> 已改为非 read_only 加载(max_row 由实际解析到的单元格推导,天然免疫)。
> 申购单通常几百行,全量加载代价可忽略;物料数据库已被 classify 过滤、不进 audit。
> 已实测无效的绕法,勿再试:
> · ws.reset_dimensions() —— 只把 max_row/max_column 置 None 不重算,detect_code_column 里 range(1, None+1) 直接 TypeError;
> · ws.calculate_dimension(force=True) —— read_only 下仍回读声明值 'A1:A1';
> · read_only 下 iter_rows() 计数 —— 大表上直接卡死(曾在此 28983 行表上被 SIGTERM)。
> 自查口诀:见到 DONE 但 已填 列是 0、而这份单子明明有数据行 → 先怀疑维度声明,别直接放行。
Step 6:停下,确认导入模板生成(关键门禁)
汇总所有申购单的「未匹配 / 需新建」情况,列出哪几类还有未匹配行:
零件BOM:12 行未匹配(需新建)
电气件:0 行未匹配(无需导入)
紧固件:5 行未匹配(需新建)
外购件:3 行未匹配(需另行处理,由用户走「零散物料导入」skill 建码,本流程不自动导入)
零散物料:20 行(全部新建)
停下来问用户:是否生成导入模板?哪几类要生成?
> 注:外购件未匹配行不纳入本流程的导入模板——外购件=申购单,其未命中物料由用户另行用「零散物料导入」skill 建码,
> 不与零件BOM/电气/紧固件的导入模板混用。
>
> 🔁 但「金蝶确认后回填」外购件走自己的脚本:scripts/match_waibuy.py <外购件清单> --backfill [--all-sheets]
> ——不要用「零散物料导入」的 generate.py --backfill:它只认标准 14 列零散物料表,喂外购件清单会列位错乱并崩溃
> (实测 ValueError: int('6SV21008B'))。外购件清单的表结构知识只在本 skill 的 match_waibuy.py 里,回填理应由它承担。
>
> 🔀 例外:用户明确要求「把外购件未匹配行并入当前导入模板」时(2026-09-18 实践) —— 可以并,做法仍是临时中间表法:
> ① 按用户指定的「大类 + 中类」建 14 列标准零散物料临时表(1序号 2大类 3中类 4品名 5规格 6品牌 7单位 8数量 9交期 10备注 11物料编码 12电气件 13机台编号 14工程代号,编码列留空、单位统一);
> ② 跑 零散物料导入/scripts/generate.py <临时表> <物料表> --append --header-row 1 --data-start-row 2;
> ③ 临时表用完立刻走回收站清理。
> 中类由用户拍板(本例 9 行铭牌/LOGO转印贴/警示标识全归 A.002),不要自己猜;并入后仍然只出一份共用 导入模板.xlsx。
> 并入 ≠ 回填:回填照旧走 match_waibuy.py --backfill --all-sheets。
Step 7:合并生成导入模板(用户确认后)
⚠️ 共用一个 导入模板.xlsx。按用户确认的类型,合并进同一个文件。
生成模式:默认「新建部分(全新)模式」 —— 每次生成的导入文件只含本次新建的物料行,原历史数据行不带入文件(原数据在每次生成前的自动备份里,可恢复,并非丢失)。
- 第一类用默认全新模式生成(只含该类新建部分、清除原历史、序号与分组编码从旧模板+物料表最大值接下去、生成前自动备份旧模板);
- 其余类接
--append并入同一文件(同属本次新建,非历史累积,不会重复传金蝶); - 绝对不要跨批次用
--append累积历史数据 —— 那样会把已导入金蝶的旧物料重新带进新文件,导致重复导入。 - 不同类别用不同系列(A/B/C),不会冲突。
> 每次真正写入前,generate.py 自动把当前 导入模板.xlsx(含全部原历史行,即「老文档」)备份到 导入模板备份/ 目录(tag 写前),写后再留一份 写后 快照。即使新文件只含新建部分,老数据也始终可恢复。
各「处理」skill 的调用方式见其各自的 SKILL.md。零散物料导入直接处理申购单(提取编码列为空的行)。
Step 8:金蝶导入 + 确认后回填
- 用户把合并后的
导入模板.xlsx上传金蝶。 - 用户自行判断金蝶导入是否全部成功。
- 用户确认全部成功 → 各「处理」skill 的
--backfill模式(或按导入模板读映射)把新编码回填进申购单空编码列。
- 外购件 →申购单分拣/scripts/match_waibuy.py <外购件清单.xlsx> --backfill --all-sheets
(读共用导入模板取本次新建编码;只填空、不覆盖已有编码;回填后「匹配详情」保持「未匹配」)。
- ✅--backfill现已遵守--dry-run(2026-09-21 修复 / 2026-09-29 复核):
4 个 generate 均把dry_run透传给_shared/dedup_backfill.backfill(),其内部(if dry_run:,约 L221)
只逐组打印「将写入 N 行」而不落盘,并有tests/test_backfill_dry_run.py(文件逐字节不变)守护。
→ 现在可以用--backfill --dry-run安全预览回填。
> 📌 历史(勿再照抄旧结论):2026-09-20 曾实测--backfill --dry-run仍写源文件
> (字节数 17330 → 17338、md5 改变);该脚枪已于 2026-09-21 体检批次堵住。回填终究是写源文件的操作,
> 保持先备份的习惯依然稳妥,但不必再因此放弃 dry-run 预览。
- 回填后「匹配详情」列保持原值不变(如仍为「未匹配」),只填编码列 —— 属既定行为,不是漏回填。 - 用户指出部分失败 → 不回填(或仅回填确认成功的部分)。
🔁 换新物料表「重跑匹配」回填(2026-09-18 实践,用户常用姿势)
用户的金蝶习惯是:导入成功后重新导出一份 物料_*.xlsx 丢到桌面,然后说「新物料表,重新匹配」。
这条路径等价于回填(而且比 --backfill 更强:顺带把历史上其他漏匹配的行一起补上),做法:
- 先判定导入是否真成功:读新表,检查本轮新建码(各系列起止区间)是否全部存在,且表行数 ≈ 旧表 + 本轮新建数。
两头都对 → 导入成功,可以重跑匹配;否则先问用户。 - 在副本上跑(不要直接在桌面文件上跑):桌面 11 份文件 copy 到工作区
_r2/work,同时留一份_r2/bak;
按类型分别跑match_bom.py(零件BOM)/match.py(紧固件,无品牌列别加--with-brand)/match_waibuy.py --all-sheets(外购件)。
paths.get_material_path()会自动取桌面最新的物料_*.xlsx,但显式传路径更稳。 - 逐单元格 diff
workvsbak,只看三类变化:
-空 → 值:新填编码 = 本轮战果;
-值 → 空:🔴 回归! 已有编码被清掉(默认match_bom.py/match.py会覆盖/清空不匹配行的旧码);
-值 → 新值:改写,要逐个看。
外购件那 9 行的未匹配 → 空属既定行为(match_waibuy.py命中后把「匹配详情」置空),不算丢数据。
- ⚠️ 判回归只看「编码列」(零件BOM/ZMSH=P,紧固件=T)——只要编码列没有值 → 空,就是安全的。
其他列(尤其A序号列)的值 → 空 / 值 → 新值很可能是插行位移的表象:
match_bom.py在匹配到「新物料 + 加工件」时会补插喷塑费行 F.001.00001(本轮首次匹配时未匹配→不插,
换新表重跑匹配成功才补插),插行后下方整段下移 + 触发序号顺延重编(见match_bom.py序号重编段:
保留首条数据行原起始序号往后顺延),于是bak与work的 A 列逐行对不上。
自检口诀:值→空落在 A 列且同文件出现「喷塑费」新行 → 是行位移不是丢数据;只认编码列。 - diff 干净(无「值 → 空」落在编码列)才写回桌面;写回前
md5比对确认一致,再走回收站清工作区。 - 收尾跑
audit.py <文件夹> --material <新表>,要求全DONE、未匹配 0。
> ⚠️ 「重跑匹配」前不必加 --preserve-codes:不加才是全量重匹配(旧码被新表复核一遍,正合此意);
> 只有当用户在两次运行之间手工填过编码时才需要 --preserve-codes 保护。
> ⚠️ 喷塑费行不会重复插:match_bom.py 有「已存在则跳过」判定,可放心重跑。
> ⚠️ 模板要清:重跑匹配成功后,D:\莫佩琪\msl\导入模板.xlsx 里那批码已经导入过金蝶,
> 留着有被再次上传→重复导入的风险,需提醒用户清空(下次生成模板时「全新模式」也会覆盖它)。
物料表来源(重要:别把零散物料清单当物料表)
- 真正的物料数据库只有金蝶导出的
物料_*.xlsx;编码*.xls/BIANMA/副本编码/基础物料/
ERP清单·加工件清单都是零散物料清单(预编码列表),要走「零散物料导入」skill,不能当物料表。 - 获取顺序:
classify.py --material <路径>显式传入 → 否则自动探测桌面或MSL_DIR下最新物料_*.xlsx。
> 📎 完整判定细节(含 classify.py 的金蝶管理列强约束、多份物料表如何推荐、外购件归类变更)见
> references/material-source-rules.md。
常见坑位
- 不要自动跑导入:Step 5 必须停下确认,否则四类导入相互覆盖。
- low / unknown 必须人工确认:分类器是基于文件名+表头特征的启发式,对命名不规范或表头异常的文件会判错。
- 零散物料无匹配 skill:它本身就是「未编码行全部新建」,所以直接从 Step 5 进导入。
- .xls 不支持原地改:先提示用户另存为 .xlsx,再跑匹配。
- 未匹配别急着建新码:紧固件常把强度等级(如
8.8级)误填进材质列导致 0 命中,先核对,勿据此造重复物料。
高频坑(每条 = 一句话现象 + 自检口诀;完整复盘见 references/incidents.md)
- 🔴 紧固件四字段 0 命中的第二种根因:物料库把材质/表面处理语境写进了「名称」字段(2026-09-19)
自检口诀:紧固件 dry-run 报0/N(N 远大于 0)→ 先看物料库名称是否内嵌材质/表面处理语境。 - 「焊接」文件名会误导零件BOM:焊接设备采购单(表头含大类/中类/单位,实为电气件)会被"焊接"关键词带偏。分类器已对「设备/采购单」优先判电气,但"焊接追加申购单"这类表头像电气件的仍会标
low冲突请你确认,别直接信文件名。 - 🔴 零件BOM 喷塑费行「批次」会被表头纵向合并泄漏(2026-09-17,match_bom ≥1.9.13 已修)
自检口诀:喷塑费行 B 列(区域/批次列)出现「区域/批次」等表头词 → 表头合并泄漏。 - 🔴 零件BOM「修改版」新增 C16 列会挤走编码列(2026-09-14,match_bom ≥1.9.12 已修)
自检口诀:dry-run 显示STANDARD(Col16)但 C16 表头既不是空也不是「物料编码」→ 立刻查会不会覆盖数据。 - 🔴 同图号「表面处理措辞差异」会造成重复建码(2026-09-14)
自检口诀:生成新码前按「规格型号」反查物料库——同规格已有码就先问是不是措辞差异。 - 同名申购单桌面 vs msl 归档可能不是同一版:判断"是否已处理过"不能只看文件名,要比对明细行与列结构;重跑前先备份(
match_bom.py不自动 .bak)。 - 外购件清单 = 申购单(不是零散物料):外购件清单由本 skill 的
scripts/match_waibuy.py处理(匹配名称+规格+品牌);零散物料走「零散物料导入」skill,两者不混放同一分拣文件夹。 - 🔴 外购件清单「回填」必须用
match_waibuy.py --backfill,不能用零散物料导入的generate.py --backfill(2026-09-14)
自检口诀:回填外购件报int()或列位对不上 → 用错脚本了。 - 🔴 多页外购件清单的「页脚行」曾被误标「未匹配」(2026-09-14 已修)
自检口诀:外购件未匹配条数比实际缺码行多 → 看有没有页脚行混进了统计。 - 外购件清单的「品牌」列常写的是淘宝链接:品牌会被归一成「淘宝」,回填时「名称+规格+品牌」精确键对不上。
--backfill因此设计了「名称+规格」兜底(仅当该键唯一才采用;多条候选则跳过告警,绝不猜)。 - 🔴 外购件清单末尾的「公司名整幅合并行」曾被误标「未匹配」(2026-09-16,match_waibuy ≥1.3.0 已修)
自检口诀:外购件「未匹配」数比人工数的缺码行多 1 → 看最后一行是不是公司名横幅。 - 🔴 物料表重复脏数据全量打印会淹没匹配结果(2026-09-16 已修)
自检口诀:匹配输出出现十万字符级刷屏 → 看是否把重复组全量列出了。 - 🔴 多 sheet 申购单的回填会跨表串写(按行号写错表,覆盖正确编码)(2026-09-16,dedup_backfill ≥1.2.0 已修)
自检口诀:回填日志必须出现全部 sheet 名(只出现一个就说明其余页没走对分支);回填后必做「编码→物料表反查名称」逐行校验(串写不报错、日志还显示"成功")。 - 🔴 多 sheet 申购单:匹配与复核必须「双管齐下」(2026-09-16 实测漏一页)
自检口诀:文件名带「采购单/清单」但只有几十行、页脚写「第 1 页 共 2 页」→ 先查 sheetnames,别只跑活动表。 - 🔴 audit 的
PARTIAL_EMPTY报数对不上时先查「表头行是否取错」(2026-09-16 实测误报 11 行)
自检口诀:audit 报PARTIAL_EMPTY但与人工数的缺码行对不上 → 看表头行取的是第几行。 - 同名文件多份只计一次:同一文件在多个目录出现时,校验脚本会去重(只计一次)并报告所有重复路径,避免重复计数。
- 紧固件处理 skill 目录名已统一为「紧固件处理」:目录名与显示名一致;运行其脚本用
紧固件处理/scripts/...路径。 - 🔴 文件名弱词「设备」会把紧固件抢成电气件(2026-09-18,classify ≥1.5.0 已修)
自检口诀:文件名同时出现「设备」和「紧固/螺丝/螺栓」→ 必须判紧固件,「设备」不算弱证据。 - 🔴 申购单可能附带「历史草稿」工作表(2026-09-18)
自检口诀:数据行数对不上 max_row → 先查 sheetnames 有没有草稿/废弃页;有草稿表就别加--all-sheets。 - 🔴 零件BOM「无区域/批次列」变体(2026-09-18)
自检口诀:dry-run 里「表头认列 XX 自动修正」= 变体适配生效,属正常,无需手工加--surface-col。 - 🔴 外购件匹配对「已填编码」幂等保留 → 换新物料表「重跑」不会刷新已填行(2026-09-12)
自检口诀:重跑后「命中 0 / 保留 N」且期望有变化 → 目标行被幂等保护锁住了,须先清空该行编码再跑。 - ✅ 多 sheet 外购件清单的 audit 覆盖已修(2026-09-16 起):
audit.py≥v1.4 已遍历全部 sheet。⚠️ 但匹配阶段仍需--all-sheets(默认只处理活动表,不加会打印 ⚠️ 提示)。
维护与回归测试
本 skill 的分类器与表尾识别已纳入仓库级回归测试(<skills_dir>/tests/),改动后必跑:
cd ~/.workbuddy/skills
PYTHONIOENCODING=utf-8 python -m pytest tests -q
与本次直接相关的两组:
| 测试 | 守护 |
|---|---|
tests/test_classify_rules.py |
classify.py 的 score_type() 判定 —— 含「misc 词条误吞零件BOM/电气件/紧固件」(2026-08-14 修复项)、金蝶物料表与零散清单的区分、标题强信号 |
tests/test_waibuy_tail_rows.py |
match_waibuy.py 的表尾行识别 —— 页码页脚、A:M 公司名横幅、表尾关键词、5 列阈值边界 |
另有三项跨 skill 门禁,改版本号或改 _shared/ 后必查:
| 测试 | 守护 |
|---|---|
tests/test_version_drift.py |
SKILL.md version == 主脚本 __version__,且同 skill 内所有脚本版本一致 |
tests/test_shared_in_sync.py |
5 份 in-skill _shared/ 与母本逐字节一致(改完先跑 python sync_shared.py) |
tests/test_pyflakes_clean.py |
全部 scripts/ + 母本 _shared/ 零 pyflakes 告警 |
tests/test_multisheet.py |
4 个匹配脚本的多 sheet 口径一致(默认只跑第一张表但必打 ⚠️ 警告、--all-sheets、--sheet 定向) |
tests/test_doc_references.py |
每个 references/*.md 都被 SKILL.md 引用(防孤儿参考);incidents.md 存在且被索引 |
> 📁 事故复盘已归档:本文档「常见坑位」只留「现象 + 自检口诀」,
> 完整根因与修复过程见 references/incidents.md(按日期分节,可搜关键词)。
> ⚠️ 升版本号时别只改主脚本:申购单分拣 的 classify.py/audit.py/match_waibuy.py
> 三脚本版本必须一起升(1.4.0 起统一),否则 test_version_drift 直接失败。
> 详见 tests/README.md 的完整清单与维护注意事项。