@whyshu/argent-engine 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/MANIFEST.json +63 -29
  2. package/engine/VERSION +1 -1
  3. package/engine/core/contracts/argent.contracts.json +1 -1
  4. package/engine/scripts/base-bootstrap.sh +2 -1
  5. package/engine/scripts/billing-mode-daily.sh +2 -1
  6. package/engine/scripts/build-engine-package.mjs +5 -2
  7. package/engine/scripts/capability-packs.lock.json +25 -25
  8. package/engine/scripts/check-billing-mode.sh +2 -1
  9. package/engine/scripts/collect-evidence.mjs +42 -0
  10. package/engine/scripts/customer-readiness.sh +2 -1
  11. package/engine/scripts/daily-report.sh +2 -1
  12. package/engine/scripts/dev-window.sh +2 -1
  13. package/engine/scripts/final-check.mjs +43 -0
  14. package/engine/scripts/flow-requirements.json +24 -9
  15. package/engine/scripts/flow-timers-plist.sh +2 -1
  16. package/engine/scripts/flow-timers.sh +3 -1
  17. package/engine/scripts/flow_config.py +2 -1
  18. package/engine/scripts/init-ml.sh +5 -2
  19. package/engine/scripts/invoice-table.sh +2 -1
  20. package/engine/scripts/lib/argent-datadir.sh +18 -0
  21. package/engine/scripts/lib/argent_datadir.py +23 -0
  22. package/engine/scripts/poll-fulfillment.sh +4 -2
  23. package/engine/scripts/poll-invoice.sh +4 -2
  24. package/engine/scripts/provider-select.sh +2 -1
  25. package/engine/scripts/release-argent.sh +1 -1
  26. package/engine/scripts/retire-inventory.mjs +40 -0
  27. package/engine/scripts/run-tests.sh +2 -1
  28. package/engine/scripts/s1-audit.mjs +85 -0
  29. package/engine/scripts/usage-anchor.sh +3 -2
  30. package/engine/scripts/wenyao-browser.sh +3 -2
  31. package/engine/scripts/wenyao-login.sh +3 -2
  32. package/engine/scripts/ziniao-keepalive.sh +2 -1
  33. package/engine/scripts/ziniao-post-upgrade-check.sh +2 -1
  34. package/engine/scripts/ziniao-windows.sh +2 -1
  35. package/engine/skills/README.md +86 -0
  36. package/engine/skills/argent-dev/SKILL.md +41 -0
  37. package/engine/skills/internal.json +37 -0
  38. package/engine/skills/lktop-pm/SKILL.md +35 -0
  39. package/engine/skills/lktopapp-dev/SKILL.md +34 -0
  40. package/engine/skills/ml-config-guide/SKILL.md +50 -0
  41. package/engine/skills/ml-fulfillment-sop/SKILL.md +178 -0
  42. package/engine/skills/ml-fulfillment-sop/references/andes-components.md +69 -0
  43. package/engine/skills/ml-fulfillment-sop/references/cdp-port-mapping.md +35 -0
  44. package/engine/skills/ml-fulfillment-sop/references/feishu-base-fulfillment-schema.md +136 -0
  45. package/engine/skills/ml-fulfillment-sop/references/multi-window-concurrency.md +69 -0
  46. package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +77 -0
  47. package/engine/skills/ml-fulfillment-sop/references/selectors.md +243 -0
  48. package/engine/skills/ml-fulfillment-sop/scripts/fulfillment.js +586 -0
  49. package/engine/skills/ml-fulfillment-sop/scripts/poll-and-execute.sh +271 -0
  50. package/engine/skills/ml-fulfillment-sop/templates/report.md +55 -0
  51. package/engine/skills/ml-invoice/SKILL.md +506 -0
  52. package/engine/skills/ml-invoice/references/pitfalls.md +268 -0
  53. package/engine/skills/ml-invoice/references/portal-flow.md +360 -0
  54. package/engine/skills/ml-invoice/references/template-fill-contract.md +133 -0
  55. package/engine/skills/ml-invoice-xzh/SKILL.md +133 -0
  56. package/engine/skills/ml-invoice-xzh/references/pitfalls.md +230 -0
  57. package/engine/skills/ml-store-daily-report/SKILL.md +224 -0
  58. package/engine/skills/ml-store-daily-report/scripts/README.md +25 -0
  59. package/engine/skills/ml-store-daily-report/scripts/extract.js +691 -0
  60. package/engine/skills/ml-store-daily-report/scripts/generate.js +299 -0
  61. package/engine/skills/pet-dev/SKILL.md +42 -0
  62. package/engine/skills/whyshu-pm/SKILL.md +35 -0
  63. package/engine/skills/whyshu-web-dev/SKILL.md +50 -0
  64. package/engine/version.mjs +1 -1
  65. package/engine/verticals/mercadolibre/flows.json +37 -5
  66. package/package.json +3 -2
  67. package/requirements.txt +5 -0
@@ -0,0 +1,133 @@
1
+ # 填报列契约(由 `scripts/wenyao-template-map.json` 生成,勿手改)
2
+
3
+ 模板真源:**门户下载版**《跨境电商-下单模板.xlsx》(`getTemplateUrl`)。
4
+ 运营版《上海文鳐发票模版.xlsx》仅作「每列怎么填」的说明,**列序不可照抄**(门户版多 `*是否投保`)。
5
+
6
+ - sheet: `Sheet1`|表头行: **7**|数据行: **8**|清底: `9`
7
+ - 模板来源标记: `门户下载《跨境电商-下单模板.xlsx》(~/.dsh/argent/wenyao-downloads/);运营版《上海文鳐发票模版.xlsx》仅作填法说明用`
8
+
9
+ ## 顶部块(第 2 行)
10
+
11
+ | 字段 | 单元格 | 取值 | 说明 |
12
+ |---|---|---|---|
13
+ | 客户名称 | `A2` | `武汉六禾园` | |
14
+ | 下单时间 | `B2` | (空) | 运营确认:非必须,留空 |
15
+ | 预计进仓时间 | `C2` | `__TODAY__`(当前日期) | 运营确认:填当前日期值 |
16
+ | 委托编号 | `D2` | (空) | 免填 |
17
+ | 起运港 | `E2` | `CNYTN` | |
18
+ | 目的港 | `F2` | `USLGB` | |
19
+ | 港前仓 | `G2` | `华南仓` | 2026-09-11 用户确认以运营填的华南仓为准 |
20
+ | 送货国家 | `H2` | `墨西哥` | |
21
+
22
+ ## 明细列(第 8 行,A..AG)
23
+
24
+ | 列 | 表头 | 来源 | 取值/表达式 |
25
+ |---|---|---|---|
26
+ | `A` | 我司关联单号(无需填写) | 常量 | (空) |
27
+ | `B` | 产品中文品名 | 飞书字段 | 飞书 `产品名称` |
28
+ | `C` | 产品英文品名 | LLM 翻译 | LLM:中文品名 → 英文(LLM),如 42-Piece Professional Makeup Brush and Sponge Cosmetic Bag - Brown |
29
+ | `D` | 货件号/唛头 | 飞书字段 | 飞书 `货件号` |
30
+ | `E` | 箱序 | 计算 | `1-{箱数}` |
31
+ | `F` | SKU | 飞书字段 | 飞书 `SKU` |
32
+ | `G` | 卖家ID | 店铺映射 | 店铺 → 卖家ID |
33
+ | `H` | 单箱产品数量 | 计算 | `{数量}/{箱数}` |
34
+ | `I` | 总产品数量 | 飞书字段 | 飞书 `数量` |
35
+ | `J` | 总箱数 | 飞书字段 | 飞书 `箱数` |
36
+ | `K` | 单箱总重 | 飞书字段 | 飞书 `重量` |
37
+ | `L` | 单箱产品净重 | 飞书字段 | 飞书 `重量` |
38
+ | `M` | 货箱总重 | 计算 | `{重量}*{箱数}` |
39
+ | `N` | 货箱长度 | 飞书字段 | 飞书 `长` |
40
+ | `O` | 货箱宽度 | 飞书字段 | 飞书 `宽` |
41
+ | `P` | 货箱高度 | 飞书字段 | 飞书 `高` |
42
+ | `Q` | 单件方数 | 计算 | `{长}*{宽}*{高}/1000000` |
43
+ | `R` | 总方数 | 计算 | `({长}*{宽}*{高}/1000000)*{箱数}` |
44
+ | `S` | 产品材质 | 常量 | `{材质},{材质英文}`(运营示例为『塑料,plastics』⇒ 中英双语。材质英文走**三级解析**:① per-SKU 字典(<数据目录>/lip-sku-dict.json,键=SKU,优先 confirmed:true 的「材质英文」)→ ② 本映射的 `materialEn` 小词典 → ③ 会话传入(`--material-en` / 行内 `材质英文`,政策A 允许 LLM 译)。三级都没命中 ⇒ 填报**阻断**(不落盘)并指路「请翻译成英文后用 --material-en 重跑」;③ 命中时真跑会写回字典标 `confirmed:false`。⛔ HS 编码那条人工红线只管 HS。)|**随行变化**(影子对比器按 `perRow` 放行,判据在填报器) |
45
+ | `T` | 产品用途 | 常量 | `家用` |
46
+ | `U` | 产品品牌 | 常量 | `无` |
47
+ | `V` | 产品型号 | 常量 | `无` |
48
+ | `W` | 产品申报单价 | 常量 | `7` |
49
+ | `X` | 产品申报总价 | 计算 | `7*{数量}` |
50
+ | `Y` | 申报币种 | 常量 | `USD` |
51
+ | `Z` | 产品海关编码 | 飞书字段 | 飞书 `HS编码` |
52
+ | `AA` | 产品图片 | 图片 | 飞书 `产品图`(浮动图片,锚 `AA8`) |
53
+ | `AB` | 是否带电 | 飞书字段 | 飞书 `是否带电` |
54
+ | `AC` | 是否带磁 | 飞书字段 | 飞书 `是否带磁` |
55
+ | `AD` | 危险品 | 常量 | `否` |
56
+ | `AE` | 是否单证报关 | 常量 | `否` |
57
+ | `AF` | 是否投保 | 常量 | `否`(2026-09-11 用户确认填否;门户模板有此列,运营版没有) |
58
+ | `AG` | 仓库编码 | 常量 | `MXRC03`(2026-09-11 用户确认 MXRC03(门户显示 + 鑫中航 mapping 一致;运营说明里的 MXCR03 视为笔误)) |
59
+
60
+ ## 店铺 → 卖家ID 映射
61
+
62
+ 真源:`scripts/ml-store-ids.json`(键=国家-店号;**主账号/子账号共用同一 ML ID**)。
63
+
64
+ | 店铺 | 卖家ID |
65
+ |---|---|
66
+ | 墨西哥-1店 | `2443747608` |
67
+ | 墨西哥-2店 | `2475636269` |
68
+ | 墨西哥-3店 | `2216458475` |
69
+ | 墨西哥-4店 | `3126660693` |
70
+ | 墨西哥-6店 | `3354492092` |
71
+
72
+ 未配置(解析将**响亮失败**、不会顶替):巴西-1店
73
+
74
+ ## 材质中英对照
75
+
76
+ | 中文 | 英文 |
77
+ |---|---|
78
+ | 塑料 | plastics |
79
+ | 牛津布 | oxford fabric |
80
+ | 不锈钢 | stainless steel |
81
+ | 硅胶 | silicone |
82
+ | 玻璃 | glass |
83
+ | 纸 | paper |
84
+ | 木 | wood |
85
+ | 金属 | metal |
86
+
87
+ ## 材质英文(`S` 列)三级解析 —— 与 HS 的分界
88
+
89
+ 合同:`S` = `{材质},{材质英文}`。**材质英文走三级解析**(唯一实现 = 填报器的 `resolve_material_en()`;脚本路径与入口见 `SKILL.md`「材质英文(`S` 列)三级解析」):
90
+
91
+ | 级 | 来源 | 命中后 | 写回字典? |
92
+ |---|---|---|---|
93
+ | ① | per-SKU 字典 `<数据目录>/lip-sku-dict.json`(键 = `SKU`,**优先 `confirmed:true`** 的「材质英文」) | 直接用 | ⛔ 不写(答案已固化) |
94
+ | ② | 本映射的 `materialEn` 通用小词典 | 直接用 | ⛔ 不写 |
95
+ | ③ | 会话传入 `--material-en "<英文>"`(或行内 `材质英文`;**政策A:LLM 译出的可直接用**) | 用它填 `S` 列 | ✅ 写回 |
96
+
97
+ 写回形状(③ 定音时;落到 `<数据目录>/lip-sku-dict.json`):
98
+
99
+ ```json
100
+ {"<SKU>": {"材质英文": "<英文>", "confirmed": false,
101
+ "来源": "LLM 译 <YYYY-MM-DD>(政策A:材质英文可直接用,HS 不动)", "备注": ""}}
102
+ ```
103
+
104
+ - 写回**合并进已有条目**(⛔ 不丢 `品名英文`/`海关编码`);**原子写**(`tmp` + `os.replace`);同 `SKU` 同值 ⇒ **幂等跳过**(不重写、不动 mtime);
105
+ - **⛔ 不覆盖 `confirmed:true`**;字典里已有 `confirmed:true` 条目但缺材质英文 ⇒ 只留痕、**不写回**(不把已确认条目降级成 `confirmed:false`);
106
+ - **同名不同值 ⇒ 冲突交人**:保留字典值、**不写回**、阻断重跑(与 `flow_case` 的「不覆盖 + 记冲突」同一口径);
107
+ - 审计:`<数据目录>/material-en/audit.jsonl`;阶段台账:编排器 P0 行打印的 `log=<路径>`(`run-<货件号>-<时间>.jsonl`,目录名见 `SKILL.md`)。
108
+
109
+ **三级都没命中 ⇒ 阻断**(填报器不落盘、闸门 exit ≠ 0),文案指路:
110
+
111
+ ```
112
+ 材质「<中文材质>」未能在 SKU 字典 / 小词典命中 ⇒ 请翻译成英文后用 --material-en 重跑
113
+ (政策A:材质英文允许 LLM 译;HS 编码仍必须人工维护)
114
+ ```
115
+
116
+ ### ⛔ HS 编码(`Z` 列)不在这条链上
117
+
118
+ HS 编码由**运营/人工维护**:系统**不预填、不写候选、不自动确认**(红线,勿动)。「不预填」那条**只管 HS** —— 把它套到材质英文上,就是 2026-10-12 两条票被误阻的成因。
119
+
120
+ 另有一份**按中文品名**建的翻译缓存 `<数据目录>/lip-product-dict.json`(品名英文/材质英文/HS 候选),与本节这份**按 SKU** 的字典是两回事:本填报链只读 `lip-sku-dict.json`(HS 候选**不预填**进模板,见上一条红线)。
121
+
122
+ ## 已知与运营版的差异
123
+
124
+ - 是否投保
125
+ - 仓库编码列位
126
+
127
+ ## 硬约束
128
+
129
+ - **只写第 2、8 行**;第 5-6 行是门户自带示例(门户要求不删)⇒ 不动;表头/声明/校验/冻结窗格一律不动。
130
+ - **写值前先清空 `A2..H2`**(门户版预填 `CNYTN/USLGB/东莞集货仓/墨西哥` 不得沿用)。
131
+ - **港前仓 = `华南仓`**(覆盖模板自带的 `东莞集货仓`),G2 单元格校验**保留不动**。
132
+ - 方数用**精确值**,不做 Excel 式四舍五入。
133
+ - 产品图为**浮动图片**,需 Pillow。
@@ -0,0 +1,133 @@
1
+ ---
2
+ id: ml-invoice-xzh
3
+ name: ml-invoice-xzh
4
+ title: 鑫中航发票流程(货代门户 · 老链路)
5
+ version: 0.1.0
6
+ category: 产品运营
7
+ tags: [发票, 鑫中航, 货代, 门户, 回写, 大货标签]
8
+ difficulty: ⭐⭐⭐⭐
9
+ triggers:
10
+ - pattern: 鑫中航|发票|货代|大货标签|发票状态|进仓单|箱唛
11
+ - pattern: 106\.13\.216\.234|invoice_stageb|invoice_runner
12
+ description: "鑫中航货代发票流程技能(Argent on DSH)——与文鳐同引擎、不同门户:阶段A 在 **lip 表**里挑「货代公司=鑫中航-美墨 ∧ FULL状态=已完成 ∧ 发票就绪=true」的行生成发票;阶段B 上传鑫中航门户、下载大货标签、回写表字段并流转状态机(待生成→填写中→已上传→标签已下载→已完成)。含掌舵者派发与台账协议(§6)、与文鳐共享的踩坑清单,以及**待客户机核对**的清单。"
13
+ ---
14
+
15
+ ## 口径(2026-09-17 起生效;**权威在注册表** `scripts/flow-requirements.json#flows.xzh-invoice`)
16
+
17
+ > 用户口径:**文鳐与鑫中航同属发票流程、同一张表(lip)** ⇒ 本技能原先写的「另一张 FULL任务表 +
18
+ > `状态=已完成 ∧ 发票状态=待生成`」**已作废**。以下为准:
19
+
20
+ - **数据源**:lip 表 `K8zjbMB9TaGKADsruYRcuKPFnAe / tblj6CBX0JQWDLhh`(多货代共用表),身份 `lip-customer`;
21
+ - **分流**:`routing: 货代公司=鑫中航-美墨`(同表里另有 上海文鳐/允墨/商达/鑫隆/美加运通/冲港/三瀛/鸿泰信 等货代);
22
+ - **开启前提(两个条件都要)**:`FULL状态=已完成`(**不是**「状态」——那是物流状态,lip 表里全为 `在途`)
23
+ **∧ `发票就绪=true`**(运营自认字段填齐的放行勾);
24
+ - **缺口回填(与文鳐同规则)**:`发票错误信息` 是**给运营看的**列 ⇒ **只有运营字段缺口**才
25
+ 写它 + **取消勾选 `发票就绪`**(运营补齐后自己重勾);我方/系统类缺口不写那一列,我方自行解决。
26
+ 工具:`python3 scripts/flow_return_to_ops.py --flow xzh-invoice --case '<票号>' [--yes]`(默认 dry-run);
27
+ - **旧 runner `invoice_runner.py` 已作废**(旧表/旧口径),不要再据它改判据。
28
+
29
+ # 鑫中航发票流程(ml-invoice-xzh)
30
+
31
+ > 与 `ml-invoice`(上海文鳐)是**同引擎的两条货代链**:用例 IR / 分诊 / 认领 / 台账 / 失败回路完全共用,
32
+ > **只有门户适配器与凭据不同**。本技能只写鑫中航特有的部分;通用作业协议见 §6 与 `references/pitfalls.md`。
33
+
34
+ ## 0. 三条不可动摇的边界
35
+
36
+ 1. **门户是对方生产系统**:`http://106.13.216.234:10001`(老 IP 型系统)。登录/上传/下载都留真实痕迹。
37
+ - **口径更新(2026-09-22 用户明示)**:门户访问为**常设授权** —— 定时触发本身即代表用户确认
38
+ (触发文案里的默认动作含「门户上传(幂等)→ 下载大货标签 → 回写」)。
39
+ **只有登录失败/滑块/账号异常等 needs_human 情况才停下发卡片交人**,不要因「每次访问需确认」而停跑。
40
+ - 注意:**每一票走到「已完成」都必须登录门户**——`_finish_with_label` 会先
41
+ `invoice_stageb --download-label`(`login_xinhang()` → `goto /Login/VueLoginView`),
42
+ `download_label()` **没有本地复用/短路**。故「补终态」也必然碰门户。
43
+ 2. **写表必须经用户批准**:`发票文件`/`大货标签`/状态流转都是**生产表写入**。
44
+ - **口径更新(2026-09-18 / 09-22 用户明示)**:定时轮为**真实执行**(生成+上传+回写),
45
+ 原「自动轮只做影子」的旧红线**已撤销**;守卫照旧(写前整行快照 + 写后读回 + 审计 JSONL,
46
+ 白名单由注册表派生 `writeFields ∪ stateFields`)。
47
+ 3. **本机是测试机、生产在客户机**:本机旧轮询已停用(`PAUSE_INVOICE`/`PAUSE_POLL` 存在即停);
48
+ 但**本机 flow-trigger 定时轮在跑真流程**(2026-09-18 起),不要据此拒绝执行。
49
+
50
+ ## 1. 机器角色(先确认你在哪台机)
51
+
52
+ - 本机(chester Mac):开发/测试,只跑**影子/只读**;
53
+ - 客户机(Huangwei):真实跑,凭据与浏览器 profile 都在那边。
54
+
55
+ ## 2. 数据在哪(已核实)
56
+
57
+ | 项 | 值 | 来源 |
58
+ |---|---|---|
59
+ | 源表 | `invoice-mapping.json` → `fullSource`(**引用而非写死**) | 流程注册表 `flows.xzh-invoice.source.baseRef/tableRef` |
60
+ | SKU 主数据 | 同文件 `skuSource` | 同上 |
61
+ | 身份 | lark-cli profile `ml-fulfillment` | 注册表 `source.identity` |
62
+ | 门户凭据 | `~/.dsh/argent/config.json` 的 `xhInvoiceUser` / `xhInvoicePass` | 只放 600 权限文件,**禁止**进仓库/日志 |
63
+ | 浏览器登录态 | Chrome 持久 profile `~/.dsh/argent/chrome-profile-xinhang` | `invoice_stageb.py` |
64
+
65
+ ## 3. 状态机(表字段)
66
+
67
+ `发票状态`:**待生成 → 填写中 → 已上传 → 标签已下载 → 已完成**(失败 ⇒ `失败` + `发票错误信息`);
68
+ 进度文案写 `当前步骤`。幂等靠状态机(非终态才动)。
69
+
70
+ ## 4. 执行脚本(现有实现;逐步迁到新引擎)
71
+
72
+ | 阶段 | 命令 | 作用 |
73
+ |---|---|---|
74
+ | A 生成 | `python3 ~/.argent-dsh/scripts/invoice_runner.py --generate [--record-id <rid>]` | 挑 `货代公司=鑫中航-美墨 ∧ FULL状态=已完成 ∧ 发票就绪=true` 的行,生成发票 xlsx(旧「状态=已完成 ∧ 发票状态=待生成」口径已作废) |
75
+ | B 回写+门户 | `invoice_runner.py --stageb [--record-id <rid>]` | 上传鑫中航门户 → 下载大货标签 → 回写表字段 → 流转状态 |
76
+ | 缺口预检 | `invoice_lip_runner.py --dry-run-pick`(**现存但当前无人调用**) | 缺口分级(SKU 字典缺/HS 空/材质空 ⇒ 待勾选 + 提示),可直接接进分诊 |
77
+
78
+ ## 5. 与文鳐共享的部分(不要重复造)
79
+
80
+ - 用例 IR、分诊、认领+上限、台账、失败回路:**同一个引擎**,命令与文鳐一致(`--flow` 换成 `xzh-invoice`);
81
+ - **材质英文/材质字典口径**(三级解析:per-SKU 字典 `lip-sku-dict.json` → 通用小词典 → 会话传入;
82
+ 冲突交人;**HS 与材质英文的分界**):见 `skills/ml-invoice/SKILL.md` §「材质英文(`S` 列)三级解析」
83
+ —— **同一套判据**,⛔ 本技能不复述细节;
84
+ - 🔴 **口径(2026-10-13,用户拍板)**:**lip 表里填什么就照填入发票** —— 表内「材质」「HS 编码」是运营已维护的结论,
85
+ ⛔ **不得**因"材质类别与 HS 章看起来不一致"(如 材质=不锈钢 而 HS=3924900000 属第 39 章)而**停下等核对**;
86
+ HS 红线只约束「表内为空时不得编造」。有疑问**只在台账/卡片留一句提示,不得阻塞真跑**(详见
87
+ `skills/ml-invoice/SKILL.md` 的「口径」小节)。
88
+ - 踩坑清单:`references/pitfalls.md`(传 `field_id` 而非字段名、附件下载 20008、写后立刻读回会假失败、
89
+ 审计不能只在成功路径写、宽投影会错位等)——与货代无关,直接适用。
90
+
91
+ ## 6. 派发与台账(掌舵者职责)
92
+
93
+ > 完整协议(四步法、§6.1 子代理模板、§6.2 done 证据、§6.3 最短路径、§6.4 会话内问用户)
94
+ > 见 `skills/ml-invoice/SKILL.md` §6 —— **同一套**,只把 `--flow wenyao-invoice` 换成 `--flow xzh-invoice`。
95
+ > ⚠️ **放行额度与上游变化**(2026-10-13,本节**不复述**):运营**改了上游数据** ⇒ 额度自动重置;
96
+ > 只重复勾「发票就绪」不算;「门户尚未生成标签」这类**外部时序**走独立通道(长冷却 = 声明面
97
+ > `externalTiming.cooldownMin`、独立上限,**不占**运营放行额度,⛔ 不要为此去补产品图)
98
+ > —— 四条口径见 `ml-invoice` §6.2.1 的表。
99
+ > 📣 **外部时序阻塞时写进表/卡片的文案**(`invoice_lip_runner.py#_label_wait_text` /
100
+ > `flow_triage.py#external_wait_copy`,两处逐字**同一句**):
101
+ > `系统将在约 <cooldownMin> 分钟后自动再试;你也可以现在人工处理(处理完取消勾选「发票就绪」,
102
+ > 系统就不再自动拾取)` —— 即:要么**等**,要么**现在就自己处理**(处理完**取消勾选**「发票就绪」)。
103
+
104
+ 1. 取本轮计划:`python3 ~/.argent-dsh/scripts/flow_triage.py --flow xzh-invoice --json`
105
+ 2. 判断缺口:`python3 ~/.argent-dsh/scripts/ml_schema_check.py --flow xzh-invoice`(缺字段 ⇒ 落卡等人)
106
+ 3. 在飞上限 2:`flow_ledger.py --inflight --flow xzh-invoice`;未满才对 `to_dispatch` 每票派**一个** subagent,
107
+ 派发即记 `dispatched`;
108
+ 4. 完成后按证据记 `done`(有产物)或 `failed`/`blocked`;
109
+ 5. **失败收口必须写表**(2026-10-09 缺陷 B,与文鳐同一条命令):失败/阻塞**只能**经
110
+ `python3 ~/.argent-dsh/scripts/invoice_fail_closeout.py --flow xzh-invoice --case xzh-invoice__<票号> …`
111
+ 收口 —— 它把「结论 + 动作」**同一串**写进表「发票错误信息」与台账(⛔ 不许只写台账、
112
+ 不许手写 `--error-message`);成功走完用 `--outcome success` 清空该列。详见 `ml-invoice` §6.2.1。
113
+
114
+ **红线(定时/自动轮)**:只读影子;**禁止**门户写操作、禁止写表;真跑由用户批准的人工轮执行。
115
+
116
+ ## 7. 待客户机核对(真跑前必须落实,不猜)
117
+
118
+ 1. **门户步骤**:`106.13.216.234:10001` 的登录/导入/勾选/导出具体路径与选择器。现有实现见
119
+ `invoice_stageb.py`;本技能**尚未固化**这些步骤(此前复制来的 `portal-flow.md` 已删除,避免文鳐口径冒充鑫中航)。
120
+ 2. **回写守卫**:现有 `wenyao-invoice-writeback.mjs` 是**文鳐/lip 表**版(白名单/备份/审计)。鑫中航写的是
121
+ **FULL任务表**(profile `ml-fulfillment`)⇒ 需**复用同一套守卫并换字段白名单**(计划:把守卫抽成可复用件,
122
+ 或先复制一份改白名单)。
123
+ 3. **发票模板契约**:文鳐有 `template-fill-contract.md`(由 `wenyao_skill_contract.py` 生成);
124
+ 鑫中航的模板列映射**未生成**,需按门户下载版模板固化一次。
125
+ 4. **状态取值与表**:`状态`/`发票状态` 的取值域需在客户机实测确认(本机 `FEISHU_*` 表 180 行状态全为中文「已完成」,
126
+ 与旧 poll 过滤器写的 `Pending/运行中` 不一致 —— 见 `flow-requirements.json` 的 `_pickNote`/`_tableNote`)。
127
+
128
+ ## 8. 自检清单(交付前逐条过)
129
+
130
+ - [ ] `python3 ~/.argent-dsh/scripts/flow_config.py --list` 里 `xzh-invoice` 源解析成功(表 id 来自 invoice-mapping.json)
131
+ - [ ] `flow_triage.py --flow xzh-invoice` 能列出候选(或明确为空)
132
+ - [ ] 影子跑不写任何字段、不碰门户
133
+ - [ ] 真写前:写前备份 + 写后读回 + 审计三件套就位(§7.2 落实后)
@@ -0,0 +1,230 @@
1
+ > 来源:与上海文鳐流程(`skills/ml-invoice`)**共用**的踩坑清单 —— 这些坑与货代无关,鑫中航同样适用(`field_id` vs 字段名、附件 20008、写后读回假失败、审计落盘时机、宽投影错位等)。
2
+
3
+ # 踩坑清单(上海文鳐发票流程)
4
+
5
+ 按**出现频率/杀伤力**排序。每条含「现象 / 根因 / 修法 / 证据」。改脚本或排障前先对照本清单。
6
+ **本文是这类教训的第一落点**;`~/.dsh/AGENTS.md` 只留与全局相关的摘要。
7
+
8
+ ---
9
+
10
+ ## 1. 附件上传按「字段名」会偶发 `20008 The user does not exist`(抖动 ~20%)
11
+
12
+ - **现象**:`base +record-upload-attachment --field-id 发票文件` 有时成功、有时返回
13
+ `20008 The user does not exist`(该码会**掩盖真实原因**)。同一命令同一参数,连跑 5 次能挂 1 次。
14
+ - **根因**:传字段**名**时,CLI 先去 `/open-apis/base/v3/bases/<b>/tables/<t>/fields/<name>` 解析成 id;
15
+ 这条路偶发失败,而失败被包装成 20008。传 `field_id` 则直连,不经解析。
16
+ - **修法**:**全程传 `field_id`**。启动时 `+field-list` 建「名 ↔ id」映射,读记录、上传附件、写值都用 id;
17
+ 另给 CLI 调用加有限重试(识别 `20008 / The user does not exist / rate limit`)兜底。
18
+ - **证据**:2026-09-11 P1 首次实写,`--field-id 发票文件` 挂在 20008;换 `--field-id fldvmra3Rp` 立刻成功。
19
+ 改全程 field_id 后 **6/6 稳定**。
20
+ - ⚠️ 同源坑:`+record-download-attachment` 对**用户/运营上传**的附件也报 20008(见 §3)。
21
+
22
+ ---
23
+
24
+ ## 2. `--file` / `--output` 必须是**当前目录内的相对路径**
25
+
26
+ - **现象**:`unsafe file path: --file must be a relative path within the current directory, got "/Users/..."`。
27
+ - **根因**:lark-cli 的安全策略,拒绝把 cwd 之外的绝对路径交给读写文件类 flag。
28
+ - **修法**:`cd` 到文件所在目录,传 `./<basename>`(脚本里即 `cwd = dirname(file)` + `'./' + basename(file)`)。
29
+ 同类约束也适用于 `--output`(下载附件时)。
30
+ - **证据**:P1 第一次 `--yes` 就死在这里,改成 `cwd` + 相对路径后通过。
31
+
32
+ ---
33
+
34
+ ## 3. 附件下载对用户上传的文件报 20008
35
+
36
+ - **现象**:`base +record-download-attachment` 对**运营/用户上传**的附件(如产品图)报
37
+ `20008 The user does not exist`;bot 自己上传的附件能正常下载。raw API `GET .../records/<rec>` 也报同一个码。
38
+ - **修法(实测可行)**:
39
+ ```bash
40
+ lark-cli --profile lip-customer api GET \
41
+ /open-apis/drive/v1/medias/<file_token>/download --as bot --output ./x.png
42
+ ```
43
+ `file_token` 从**窄投影**记录读里拿(`发票文件`/`产品图`/`大货标签` 都是附件数组,元素含 `file_token`/`name`/`size`)。
44
+ - **证据**:成功取回运营上传的 236KB 产品图 PNG。
45
+
46
+ ---
47
+
48
+ ## 4. 宽投影读表会**错位**,单值核验必须窄投影
49
+
50
+ - **现象**:一次取 20+ 列(markdown 格式)时字段错位:同一轮里 `材质/产品图` 读对、**`HS编码` 被读成空**,
51
+ 据此差点误判"缺 HS 不能开跑"。实际 76636923 / 76637122 都有 `HS=9603290090`。
52
+ - **修法**:
53
+ - 单值核验:一次 **≤ 6~8 列**,按**表头名**映射(第一列恒为 `_record_id`)。
54
+ - 要全行:用 `+record-get --format json`,它回显 `fields` 名单 + `field_id_list`,
55
+ **按名 zip** 而不是按位置猜(脚本 `readRecord()` 即此法)。
56
+ - 更稳:直接传 `field_id`。
57
+ - **证据**:用户当场纠正"76636923 这条是有 HS 编码的,你是不是看错了?"
58
+
59
+ ---
60
+
61
+ ## 5. 附件格会**重复上传**,必须幂等
62
+
63
+ - **现象**:同一份文件在 `发票文件` 格里出现两份(同名同大小,两个 `file_token`)。
64
+ - **根因**:`+record-upload-attachment` 是 **append 语义**(追加),不是覆盖;重跑就多一份。
65
+ - **修法**:上传前先读格内附件,**同名 + 同大小**判定为同一份 ⇒ 默认跳过上传;发现重复用
66
+ `+record-remove-attachment --file-token <多余 token> --yes` 清掉,只留一份。要强制重传用 `--replace`。
67
+ - **证据**:首次实写因中途失败重跑,格内变成 2 份,已去重为 1 份(保留 `WYp0bFaq3oBduMx3ZIscxACVnyb`)。
68
+
69
+ ---
70
+
71
+ ## 6. 审计不能只在成功路径写
72
+
73
+ - **现象**:首次 `--yes` 已把附件和状态写进生产表,但在最后一步崩溃 ⇒ **审计里什么都没有**,
74
+ 真值迁移(跳过→填写中)只在备份 JSON 里能看到。
75
+ - **修法**:把写操作包在 try/catch 里,**失败也 append 一条 `ok:false` 的审计行**(含 error 摘要),
76
+ 并提示"写前备份已留存,可按备份回滚"。事后对已发生的静默写入**补记 `note-partial-run`**,不假装没发生。
77
+ - **证据**:`~/.dsh/argent/invoice-writeback/audit.jsonl` 现有 2 行(1 条收口 + 1 条补记)。
78
+
79
+ ---
80
+
81
+ ## 7. 模板真源必须是**门户下载版**,运营版只是说明
82
+
83
+ - **现象**:门户版比运营版**多一列 `*是否投保`** ⇒ `仓库编码` 从运营版的 AF 挪到**门户版 AG**。
84
+ 照运营版列序填就会整行右移。
85
+ - **修法**:列映射以门户下载的《跨境电商-下单模板.xlsx》为准;运营版只用来读"每列怎么填"。
86
+ 映射真源固化在 `scripts/wenyao-template-map.json`(A..AG,33 列)。
87
+ - **证据**:用户明确"上海文鳐模版你应该用门户下载的,而不用运营给的,运营给的只是说明用的"。
88
+
89
+ ---
90
+
91
+ ## 8. 门户版模板的**预填值不能直接用**
92
+
93
+ - **现象**:门户版 `E2/F2/G2/H2` 预填 `CNYTN / USLGB / 东莞集货仓 / 墨西哥`,
94
+ 其中 `港前仓=东莞集货仓` 与运营要求 `华南仓` 冲突;`G2` 还带下拉校验(只列 `东莞集货仓`)。
95
+ - **修法**:**写值前先清空 `A2..H2`**(清底),再写按运营说明算出的值;
96
+ **港前仓写 `华南仓`**、**不管 G2 校验**(保留原校验不动,不删不改)。
97
+ - **证据**:用户"港前仓不用管校验,就填华南仓"。
98
+ - ⚠️ 待观察:门户导入时是否接受 `华南仓`(其下拉只有东莞集货仓)。P2 第一阶段就要验证这一点。
99
+
100
+ ---
101
+
102
+ ## 9. 「只改两行」是硬约束,第 5-6 行是门户自带示例
103
+
104
+ - **现象**:门户模板第 5-6 行自带**填写示例**(含公式),一开始被误以为是需要清理的脏数据。
105
+ - **修法**:**只写第 2 行(顶部块)+ 第 8 行(明细)**;示例行、表头(第 7 行)、声明行、隐藏行、冻结窗格、
106
+ 单元格校验**一律不动**。测试里加"除第 2、8 行外与模板逐格一致"的回归断言。
107
+ - **证据**:用户"第 5-6 行本身就是门户模板提供的示例…门户要求不删那就不用管"。
108
+
109
+ ---
110
+
111
+ ## 10. 插图需要 Pillow,且图片是**浮动图片**
112
+
113
+ - **现象**:`You must install Pillow to fetch image objects`;`@IMAGE@` 占位若被跳过,会把运营的说明文字留在第 8 行。
114
+ - **修法**:Pillow 装进跑脚本的解释器(本机 `/tmp/pw-venv`,uv 管理的需 `--break-system-packages`);
115
+ 插图用**浮动图片锚在 `AA8`**(数据行「\*产品图片」列,不是 cell value);缺 Pillow 时给明确中文报错而不是抛栈。
116
+ - **证据**:`76636923-...xlsx` 最终件 / 图片数 1。
117
+
118
+ ---
119
+
120
+ ## 10b. 图片**必须缩放到装得进单元格**,不要写死像素宽度
121
+
122
+ - **现象**:用户实测反馈"产品图片已经比单元格大,装不下了"。
123
+ - **根因**:原先写死 `max_w = 120` px。而目标格 AA8 实际可用是
124
+ **AA 列宽 13 ≈ 96 px、第 8 行高 40pt ≈ 53 px** ⇒ 120×119 px 明显溢出。
125
+ - **修法**:按**单元格真实几何**等比缩放(只缩不放):
126
+ 列宽→像素 `round(width*7)+5`;行高→像素 `round(pt*96/72)`;留 4% 余量。落盘后断言"图片像素 ≤ 单元格像素"。
127
+ 当前结果:原图 500×496 → **50×50**(高度是瓶颈)。
128
+ - ⚠️ **openpyxl 取列宽必须用 `ws.column_dimensions['AA']`,不能用 `.get('AA')`**:
129
+ 模板 `<cols>` 里有该列,但 `DimensionHolder.get()` 返回 `None`(实测 AA 宽 13 被读成默认 8.43,
130
+ 盒子算小约 1/3,图片被缩过头)。取不到再回落默认值。
131
+ - **证据**:`wenyao-template.test.mjs` §⑤ 落盘断言「图片装得进单元格」+「不是写死的 120px」。
132
+
133
+ ---
134
+
135
+ ## 10c. 门户模板**第 3 行是隐藏行** ⇒ 预览器行号会"少 1",别据此判断行列
136
+
137
+ - **现象**:用户在飞书预览附件时报告"数据行是 #7"、"图片跑到了 A10",而文件实际是**数据在第 8 行、图片锚点 AA8**。
138
+ - **根因**:门户模板 **第 3 行是隐藏行**(**运营版模板没有隐藏行**)。部分查看器把隐藏行折叠后,
139
+ 其后所有行的**显示序号比真实行号小 1**(真实第 8 行显示成第 7 行);图片高约 3 行,下沿又容易被读成第 10 行。
140
+ - **修法**:
141
+ - **判断行列一律以真实行号为准**,别按预览器显示的数字;可疑就用 `openpyxl` 直读单元格核对。
142
+ - 注意:门户版隐藏第 3 行、运营版没有 ⇒ 同样内容在两份文件里"看起来"会差一行。
143
+ - 交叉验证:把飞书里的附件**下载回来**再读(`lark-cli api GET /open-apis/drive/v1/medias/<token>/download`),
144
+ 确认"用户看到的那份"和"我生成的那份"是同一份。
145
+ - **教训**:用户报"位置/行号不对"时,**先把文件本身查清楚(含从远端拉回原件)再改代码**——
146
+ 本次险些把本来正确的 `AA8` 改成错误的 `AA7`(表头行)。
147
+
148
+ ---
149
+
150
+
151
+ ## 10d. 别用"下拉/接口的取值范围"反推后端校验(差点误判港前仓)
152
+
153
+ - **现象**:读到门户 `warehouse/listAll` 只返回 4 个仓(东莞集货仓/义乌/宁波/上海),
154
+ 就断定"没有华南仓 ⇒ 填 `G2=华南仓` 会被拒",还写成了"🔴 冲突待拍板"。
155
+ - **实际**:那 4 个只是**下拉预设选项**;真导入 `G2=华南仓` 的文件,门户**成功 1 票、失败 0 条**。
156
+ - **修法/教训**:
157
+ - 预设取值范围 ≠ 后端白名单,**校验宽松度只能靠真跑一票验证**;
158
+ - 结论要么标注"**实测**",要么标注"**未验证的推测**",不要写成既成事实;
159
+ - 与运营/用户口径冲突时,**先问、再实测**,不要自己替业务拍板。
160
+ - **证据**:2026-09-11 首票导入 → `WY535082 审单中`,`货件号 76636923`、`仓库编码 MXRC03`、`件数 6`。
161
+
162
+ ---
163
+
164
+ ## 10e. 门户导出的文件接不住(0 字节 / 不知去哪了)
165
+
166
+ - **现象**:点「下载」后本地找不到文件;或用 `download.save_as()` 存下来是 **0 字节**。
167
+ - **根因**:
168
+ ① 长驻 Chrome 由外部启动,`Browser.setDownloadBehavior` **设在 page CDP session 上不生效**,必须设**浏览器级**;
169
+ ② `save_as()` 在下载尚未落盘到底层临时文件时就拷贝,拿到空文件。
170
+ - **修法**:浏览器级 `new_browser_cdp_session()` 设下载目录 + `expect_download()` 捕获 +
171
+ **`shutil.copy(download.path(), dest)`**;或直接 `download.url`(`/api/v1/common/file?fileId=…`)自己取字节。
172
+ - **证据**:`唛头20260911180600.zip` 经 `copy(path())` 得到正确 45,838 bytes(与 API 报的 fileSize 一致)。
173
+
174
+ ## 10f. 「导出唛头」是**异步任务**,文件不在工具栏
175
+
176
+ - **现象**:点完「导出唛头」只弹 toast,工具栏没有「导出文件」下载入口。
177
+ - **实际**:文件落在**右上角「导出任务」面板**(按时间倒序,最上面一条即本次);
178
+ ⚠️ 该面板混着大量历史任务(实测 **190 个「下载」按钮**)⇒ 必须核对**时间戳**,别拿错别人的唛头。
179
+ - **证据**:2026-09-11 18:06 那条 → zip 内 `WY535082-76636923_mark.pdf`,6 页 = 6 箱。
180
+
181
+ ---
182
+
183
+ ## 10g. 写后**立刻**读回会"假失败"(飞书 Base 是异步生效的)
184
+
185
+ - **现象**:脚本报 `读回验收:状态=填写中 ❌` 并 exit 1,但**写入其实成功了**;
186
+ 隔几秒再读就是新值。(2026-09-11 收口那次真实踩到。)
187
+ - **根因**:`lark-cli` 文档已写明——**Table 下大多数更新通过异步链路生效,接口成功返回后立即读取可能暂时看不到最新状态**。
188
+ - **修法**:
189
+ - **写完不要立刻断言**,改成**轮询重试**(本脚本:最多 8 次 × 1.5s,直到状态与附件都符合预期);
190
+ - 判定"操作成功"优先用**写入响应**,读回只作**最终验收**;
191
+ - 同类坑:`+record-upload-attachment` 后立刻查 `file_token` 也会拿到 `None`(同一原因,已加重试)。
192
+ - **教训**:**"读回失败"不等于"写入失败"**,别据此重试写入(会重复写);先延迟复读再判断。
193
+
194
+ ---
195
+
196
+ ## 10h. 下载的临时文件会被清掉(别 `copy(download.path())` 当主路径)
197
+
198
+ - **现象**:`shutil.copy(download.path(), dest)` 报 `FileNotFoundError` —— Playwright 的临时文件
199
+ **在拿到 download 对象之后可能已被清理**(竞态)。而 `save_as()` 又实测存成 **0 字节**。
200
+ - **修法(可靠)**:用 `expect_download()` **只为拿稳定的 `download.url`**,然后
201
+ **用页面自身会话 fetch 字节**:`page.evaluate` 里 `fetch(url,{credentials:'include'})` → `arrayBuffer` → base64 → Python `b64decode` 落盘。
202
+ 临时文件还在时才走 `copy(path())` 兜底。
203
+ - **证据**:2026-09-11 首票用 copy 成功、第二票(76637122)就因竞态失败;改为页面取字节后稳定。
204
+
205
+ ## 10i. 「导出任务」面板的遮罩会挡住后续点击
206
+
207
+ - **现象**:下载完接着点「查询」,Playwright 报
208
+ `<div … items> from <div class="wenyao-shell-my-top"> subtree intercepts pointer events`(TimeoutError)。
209
+ - **根因**:导出任务面板留开着,其遮罩层拦截了下方元素的点击。
210
+ - **修法**:封装 `close_overlays(page)`(连按 Esc + 必要时点弹窗 ×),**在查询/点击前先调用**,
211
+ 并对关键按钮加 `click(force=True)` 兜底;用完面板立刻关。
212
+ - **证据**:76637122 续跑时先踩到,加 `close_overlays` 后通过。
213
+
214
+ ---
215
+
216
+ ## 11. `跳过` 必须算作"可开始",否则文鳐行全被挡掉
217
+
218
+ - **现象**:老流程给文鳐行写死 `发票状态=跳过` + `发票错误信息="货代上海文鳐未接入发票自动化(首期仅鑫中航-美墨)"`;
219
+ 若把 `跳过` 当终态,文鳐接入后这批行永远不会被拾取。
220
+ - **修法**:状态机里 `跳过/空/待生成/失败` 均为可开始;只有 `已上传/标签已下载/已完成` 是终态。
221
+ 文鳐待办行 = `FULL状态=已完成 ∧ 发票状态∈{跳过,空}`(实测 **19 行**)。
222
+ - ⚠️ 同时注意:这些行的 `发票错误信息` 是**过时文案**,接入后应清掉(脚本支持 `--clear-error`,属额外字段需用户授权)。
223
+
224
+ ---
225
+
226
+ ## 12. 别在本机做"自动拾取"
227
+
228
+ - **现象**:本机和生产客户机会**抢同一批生产行**,且跨机 `pgrep` 无效、看不出来。
229
+ - **修法**:本机 `~/.dsh/argent/PAUSE_POLL`(FULL)+ `PAUSE_INVOICE`(发票)置位;
230
+ 要试跑就**单独执行 runner**,不要靠 cron。生产在 Huangwei 客户机。