Skip to content

Releases

v1.6.0 — 装箱出单前预算:PI/CI/PL 同源,不再依赖「PL 必须先跑」

Choose a tag to compare

@Dangooy Dangooy released this 28 Sep 06:58

把装箱计算改成「出单前先算一次」,PI / CI / PL 三份单据从同一份数字取数。

先说人话

以前是「谁先生成谁算数」:CI 上的重量是读 PL 写下来的,所以 PL 必须先跑。这是个没写在任何地方、但一旦被打破就出错的前提。

打破的后果:CI 会退回用一个经验系数估算(净重 × 1.036),而 PL 用的是真算出来的数——两份单据的重量就对不上了,而且没有任何提示。

现在改成:出单前先把装箱算一遍、存好,三份单据都读这一份。 谁先谁后都一样。

实测效果

场景 结果
正常出单 CI 毛重 = 真实算出的 598.00 kg(不再是估算的 590.52)✅
装箱配置写错 拦下,不出任何正式单据,并说明原因 ⚠️ 见下方行为变化
重量信息没填 CI 照常出,但明确提示「这个毛重是估算的」

⚠️ 一处行为变化(升级前请读)

重量齐全、但装箱参数非法(比如每托盘箱数设成 0)时:

  • 以前:PL 报个警告,CI 照样生成(毛重走估算),单据照样发出去
  • 现在:记错误,拦住所有正式单据(PI / PL / CI 都不出)

理由:重量齐全却算不出来,一定是数据异常或代码 bug。出一套数字可能错的单据,比不出更糟——毛重错了在清关和收汇上都是实际问题。报价单不受影响,报错会说明具体原因。

技术细节

  • 装箱计算收敛成单一入口 compute_packing_summary()(缺重量检查 + 参数解析 + 计算)。PL writer 不再自己解析一遍参数——两处各自解析参数正是过去两单分叉的根因之一。
  • 参数优先级明确为:人工补录(review.pallet)> 配置(config.packing)> 模块默认值。
  • 两条路径都接入:主流程与价格回写。少接一处,那条路径上就又会各自算一遍、回到分叉状态。
  • 那条 ×1.036 兜底保留但透明化:不再默默使用,会带标记并由流程提示用户。

新增测试(11 个)

  • 参数优先级(人工补录 > 配置 > 默认值)
  • 预算与 PL writer 算出同一份数(同源的基础)
  • 回写不覆盖 total_qty / 港口等字段
  • 顺序无关性:PL 完全不跑时,CI 仍能拿到真值
  • fail-loud 拦住全部正式单据 / 重量不全时预算正确跳过
  • 估算值被正确标记

同时给两条既有测试补了说明:它们直接调 writer、绕过流程,现在测的是 writer 层契约而非生产路径;生产路径由新测试覆盖。

验证

355 passed(原 344 + 新增 11),覆盖率 82.02% ≥ 门槛 80%,ruff 通过;CI 在 Python 3.11 / 3.12 / 3.13 三个版本上全绿。

详见 CHANGELOG.md。

v1.5.0 — 新增三条出单前门禁:买方 / 条款 / 收款银行不全就不让出单

Choose a tag to compare

@Dangooy Dangooy released this 28 Sep 05:07

新增三道「出单前的门禁」:买方名称、贸易条款、收款银行,任何一项缺失就不让出正式单据。

先说人话

一份 PI 或 CI 发出去,上面买方抬头是空的、或者少了收款账号——这不是排版问题,是客户真的汇不出钱、清关真的会卡。

以前这种问题不会报错,只会默默印出一份有问题的单据。现在改成出单前先检查这三样:

规则 检查什么
R011 买方英文名称有没有
R012 付款 / 交货条款有没有
R013 收款银行名 / 账号 / SWIFT 全不全,SWIFT 格式对不对

任何一项不过 → 拦住不让出单,并告诉你缺什么、去哪个配置键补。

⚠️ 升级提示(请先读)

这三条是拦截级。如果你的配置里这三样有缺失,以前能出的单据现在会被拦下。

这是本次改动的目的,但升级后第一次运行可能遇到「以前能出、现在出不了」。按报错提示到配置中心补齐即可——每条规则都会指出缺什么、去哪补。

只出报价单不受影响:quote_only=True 不触发这三条门禁。报价阶段还没走到正式单据,不该被正式单据的门禁卡住,否则你连报价都出不了。

为什么做成「拦截」而不是「提醒」

这三样和别的字段不同:其他字段错了,后果是内部返工;这三样错了,是出到客户或海关手上才暴露。等客户来问「你们账号呢」,已经晚了。

好消息是它们都能在出单前确定性地检查出来,不需要人工判断——所以没有理由不做。

技术细节

  • 三条规则都先看 model.resolved;未解析时直接放行,否则报价阶段会被卡住(先有鸡还是先有蛋)
  • 规则顺序插在 R003 之后、R004 之前,与「先验单据要素完整性、再验具体字段」的阅读顺序一致
  • 规则总数 10 → 13
  • SWIFT 格式校验复用已有的 config_service.validate_seller,不另造一套

新增测试(10 个)

  • 规则层 5 个:含两个边界——「未走 resolve_entities 的模型不应被误拦」与「SWIFT 格式错也要拦,不只是缺」
  • CLI 层 5 个:验证不只是规则报错,而是确实拦住了落盘(PI / CI 不生成);含 quote-only 不被误伤的边界

验证

344 passed(原 334 + 新增 10),覆盖率 81.96% ≥ 门槛 80%,ruff 通过;CI 在 Python 3.11 / 3.12 / 3.13 三个版本上全绿。

详见 CHANGELOG.md。

v1.4.4 — 单据日期不再跟着运行机器时区走(UTC 服务器上会印错一天)

Choose a tag to compare

@Dangooy Dangooy released this 27 Sep 14:00

修掉一个会让单据印错日期的缺陷,并加固 CI。

先说人话

报价单、PI、CI、装箱单上的日期,之前是「看这台电脑现在几点」。听起来没毛病——问题是这台电脑可能不在你所在的时区。

生产环境常见的是 UTC 服务器(云主机、CI runner 基本都是 UTC)。而中国是 UTC+8、日本是 UTC+9,这意味着:当地凌晨 0 点到早上 8、9 点之间开出来的单据,日期会印成前一天。

这个错很难发现——只在特定时段出现,白天测试永远测不出来,等客户或海关看到才暴露。

现在改成:可以在配置里写明业务时区,日期就跟这台机器在哪、几点没有关系了。不填则保持原样,不改变已有用户的结果。

复现验证

把宿主机冻结在 UTC 2026-07-26 23:30:

单据日期
修复前 26 July 2026 ❌
修复后(Asia/Shanghai) 27 July 2026 ✅

四种时区实测得出不同日期,证明配置真的生效(America/New_York 正确得出 26 日)。

技术细节

  • 修复(understanding/assembler.py):order.date 此前由裸 datetime.now() 生成,取的是运行机器时区。
  • 新增配置项 defaults.business_timezone(IANA 时区名):
    • 配置了 → 日期与运行环境无关,始终是业务所在地的那一天
    • 未配置 → 保持旧的本地时区行为,不静默改变已有用户的结果
    • 非法值 → 响亮失败并指明配置项;静默回退会重新制造正好在修的 bug
  • 初始化向导新增一问引导填写(默认 Asia/Shanghai);内置 config/config.yaml 同步写出该键并说明不配置的后果。
  • 新增 tests/test_business_timezone.py(23 个用例):resolve_business_tz 的三态单测,加上走真实 assemble() 路径的跨日历日断言、未配置时保持旧行为、date_format 不受影响、配置模板与向导均写出该键。

测试把宿主机冻结成一台 UTC 机器,且必须在跨日历日的时刻(UTC 23:30)取值才有判别力——否则开发机本身在亚洲时区时,旧实现也会印出正确日期,测试便恒过。

CI 加固(同批)

  • 覆盖率门槛 80%:此前 CI 只跑 --cov-report=term 把覆盖率打印到日志却不强制,覆盖率可以一路下滑而构建依然是绿的。
  • lint 改为检查全仓库:此前 scripts/ 与 examples/ 下 4 个 Python 文件不被检查,其中 scripts/check_version.py 正是版本一致性检查自身。
  • Actions 升级:actions/checkout v4 → v7、actions/setup-python v5 → v7。旧版 action 依赖的运行时会被逐步淘汰,届时会出现与代码改动无关的构建失败。
  • 矩阵加 fail-fast: false:此前一个 Python 版本失败会直接取消另一个。
  • 矩阵新增 Python 3.13:pyproject.toml 声明 requires-python = ">=3.11",但此前只测到 3.12。

验证

334 passed(原 311 + 新增 23),覆盖率 81.79% ≥ 门槛 80%,ruff 通过;CI 在 Python 3.11 / 3.12 / 3.13 三个版本上全绿。

详见 CHANGELOG.md。

v1.4.3 — 截断/降级可见化 + 吨数量格式 + 欧洲数字(审计遗留清单清零)

Choose a tag to compare

@Dangooy Dangooy released this 17 Aug 06:03

第三批外部审计修复(C 系列)。至此 2026-08 审计记录的全部遗留问题清零。

  • C1 LLM 4000 字符截断不再静默:WARNING + _llm_truncated 标记进 meta
  • C2 解析降级标记不再被 assembler 丢弃:parser_model/review_status/parser_notes 如实反映
  • C3 CI 吨计价数量列改两位小数(与 PI 一致,不再 12,345.6→12,346)
  • C4 欧洲数字格式支持("1.234,56"→1234.56),解析失败打 WARNING

311 测试全绿(296+15);ruff 全过。详见 CHANGELOG 与 docs/adversarial-review-2608-external.md。

v1.4.2 — buyer 匹配碰撞修复 / PL 单据自洽 / 版本一致性检查

Choose a tag to compare

@Dangooy Dangooy released this 16 Aug 09:22

第二批外部审计修复(B 系列,完整记录见 docs/adversarial-review-2608-external.md)。

Fixed

  • buyer 匹配中间剥除碰撞(B1)(understanding/buyer_matcher.py):_core_name 此前从名字任意位置剥法律后缀 token,"Alpha Co Trading Ltd" 中间的 co 被剥后核心名与另一家 "Alpha Trading LLC" 相等,唯一命中即接受——A 客户的单据抬头会写上 B 客户。现改为只剥开头连续段(俄语前缀惯例,如 ООО)+ 结尾连续段(英语后缀链,如 Limited Liability Company),中间的法律词保留。行为变化:部分此前静默匹配通过的输入现在会硬阻断进 review.json 人工确认——错配风险交还给人,这是修复目的。
  • PL 每箱重与净重舍入不自洽(B3)(writers/pl_writer_lite.py、writers/ci_writer.py、validation/cross_doc.py、pipeline/main.py):此前行净重=真实净重、每箱重=round(净重/箱数),100kg 分 3 箱时 33.33×3=99.99≠100,报关算术核查看的正是这组关系。现改为行净重=每箱重×箱数(单据内 KGS/CTN×CTS=NET、Σ行净重=合计 恒成立;与磅秤实重偏差 ≤0.005×箱数 kg,低于称重精度);CI 页脚净重改为优先读 PL 回写的 derived.total_net_weight(与毛重 T1 的同源模式对称),PL 未运行时回退独立累加;新增跨单净重校验 check_ci_pl_net_weight(0.01 容差)接入两处生成流程。

Added

  • 版本一致性检查(B2)(scripts/check_version.py、.github/workflows/ci.yml):CI 新增独立 job,强制 pyproject.toml ↔ .claude-plugin/plugin.json 版本相等、且 CHANGELOG 有对应版本小节,违反即失败。防的是 v1.4.1 收尾时发现的真实事故——plugin.json 落后两个版本无人察觉。
  • 20 个新测试(276 → 296):中间剥除碰撞负样本("Alpha Co/Holdings/Limited Trading")、核心名新语义固化、俄语前缀模糊回归、版本检查脚本(含对仓库自身)、PL 行自洽与整除不变、Excel 单元格级 D×C==F 断言(此前无任何 D 列断言)、CI 读 PL 回写净重与回退、端到端两单净重相等、跨单净重校验矩阵。

v1.4.1 — 外部审计修复:金额与重量口径统一

Choose a tag to compare

@Dangooy Dangooy released this 15 Aug 13:10

第三轮外部审计修复(问题编号 A1-A7,完整审计记录见 docs/adversarial-review-2608-external.md;与 2026-07 的两轮 T 系列审计不重叠)。本轮主题:金额与重量路径上的字段口径一致性、输出校验——这类路径错一个数,落到真实单据上就是直接的金钱或清关风险。

Fixed

  • 吨计价下 PI 重量列单位混淆(A1)(models/order_model.py、writers/pi_writer.py、writers/ci_writer.py、writers/pl_writer_lite.py):weight_kg 缺失时,PI 会把 kg_mpcs(每千件重量)原值写进 "Weight (kgs)" 列,而吨计价的金额公式 =M/1000*N 引用该列——等于用"每千件重"当"总重"算钱;同一行 CI 走 compute_amount 只认 weight_kg 得 0.0,PI 与 CI 尽管共享 amounts.py 单一来源仍会分叉。新增 effective_weight_kg() 统一派生(weight_kg → 单件重×数量 → kg_mpcs×数量/1000 → None,0.0 视为合法重量不被吞掉),PI / CI / PL lite 三处共用同一来源。
  • CI 与 PL 净重口径分裂(A2)(writers/ci_writer.py):CI 净重合计只认 weight_kg,PL 却按三个来源派生——只有 kg_mpcs 的商品,PL 打印真实净重、CI 打 N.W.: 0.00KGS,清关单证对不上。两单现在都走 effective_weight_kg()。
  • LLM 返回无 schema 校验(A3)(understanding/llm_parser.py、understanding/canonicalizer.py、understanding/assembler.py):解析成功的 LLM 输出此前只查一层 isinstance(dict) 就写入缓存——items: null 会让 canonicalizer 崩溃(.get 的默认值对显式 null 不生效)、字符串数字数量会让 assembler 的 sum() 崩溃,坏结果还会被缓存持续污染重跑。新增 _validate_rfq_schema()(结构校验 + 数值字符串规范化(含千分位逗号)+ 负数量拒绝),校验失败走既有的显式降级路径(打标记、不进缓存);下游补 or [] 空值防护。
  • 不可信隔离标签可被闭合逃逸(A4)(understanding/llm_parser.py):询盘内容里出现字面量 </untrusted_document_content> 即可闭合 T4 建立的提示词隔离标签,让其后的伪指令逃出隔离区。_strip_untrusted_tags() 现于包裹前剥离内容中的标签字面量。
  • price_unit/currency 为显式 null 时 PI 崩溃、CI 渲染 "None"(A5)(understanding/assembler.py、writers/pi_writer.py、writers/ci_writer.py):.get() 的默认值不覆盖显式 null(LLM 返回 "price_unit": null 即可触达),PI 的 .upper() 直接 AttributeError;CI 无币种兜底会打出 None/ FOB PRICE、SAY: NONE … ONLY.。根因在 assembler 修复(or 兜底),writer 层再加第二道防线,与 quote_writer 口径一致。
  • PL 空订单 / 除零无守卫(A7)(writers/pl_writer_lite.py):items 为空会静默产出"1 个托盘、毛重=托盘自重"的空货装箱单;cartons_per_pallet=0 除零崩溃。现在入口处双守卫,均以 ValueError 拒绝生成。

Changed

  • 报价单 Amount 列改为活公式(A6)(writers/quote_writer.py):非重量模式下表头写着 "Amount(币种)"、数据列却固定写 weight_kg——金额从未被计算,列恒为空(有重量数据时甚至显示公斤数)。现在复用 amounts.amount_formula()(=数量×单价/=数量/1000×单价`),销售填价后金额即时算出;吨计价缺重量数据时诚实留空。
  • 新增 27 个测试(249 → 276,tests/test_audit_fixes_2608.py):重量派生的优先级/0.0 合法性/兜底链、吨计价 PI 列值、null price_unit/currency 存活性、CI↔PL 净重一致、schema 校验接受/拒绝矩阵、标签剥离、报价单公式模式、PL 守卫。