@whyshu/argent-engine 0.7.7 → 0.8.0-rc.2

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.
@@ -65,6 +65,8 @@ ENV_DATA_DIR = "ARGENT_DATA_DIR"
65
65
  ENV_DSH_HOME = "DSH_HOME"
66
66
  #: 隔离目录下**显式**放开失败回路的开关(必须精确等于 `1`)。
67
67
  ENV_ALLOW_ISOLATED_PUMP = "ARGENT_ALLOW_ISOLATED_PUMP"
68
+ #: 部署方声明的本机规范数据目录(生产客户机用)。
69
+ ENV_CANONICAL_DATA_DIR = "ARGENT_CANONICAL_DATA_DIR"
68
70
 
69
71
  #: **老机器**的数据目录(`~/.dsh/argent`):存在即沿用 —— 这一级同时是"客户机现状"。
70
72
  DATA_DIR_SEGMENTS = (".dsh", "argent")
@@ -148,6 +150,9 @@ def default_data_dir(env=None, home=None) -> str:
148
150
  与 JS 内核 `core/lib/argent-datadir.mjs#defaultDataDir` **同批、同夹具**对拍
149
151
  (`scripts/tests/argent-datadir-parity.test.mjs`);优先级链**不变**。
150
152
  """
153
+ canonical = _get(_env(env), ENV_CANONICAL_DATA_DIR)
154
+ if canonical:
155
+ return _p.abspath(canonical) # 部署方显式规范目录
151
156
  if legacy_data_dir_exists(env, home):
152
157
  return legacy_data_dir(env, home) # ① 存在即沿用(老机器)
153
158
  return _join(home or os_home(_env(env)), *NO_HOST_DATA_DIR_SEGMENTS) # ② 新装(无宿主)⇒ ~/.argent
@@ -88,7 +88,7 @@ units 页上都会出现 ⇒ 单凭它会把正常票判成尺寸阻塞。空间
88
88
  本样本**不依赖它**(仅凭 hub 即可判 `true`)。
89
89
 
90
90
  ⚠️ `operationally_complete=True` ⇒ 货件**被视为已完成**(无论表里写什么),但
91
- **⛔ 绝不是让引擎去改写 `FULL状态`** —— 改写权在运营/用户;引擎只写「识别为运营手动完成(依据 hub-v2 实测)」的备注。
91
+ **⛔ 绝不是让引擎去改写 `FULL状态`** —— 改写权在运营/用户;引擎只写「识别为已完成(依据 hub-v2 实测;来源未声明)」的备注。
92
92
 
93
93
  ## 容错(平台改版是常态)
94
94
 
@@ -1305,14 +1305,14 @@ def summary_zh(state: dict) -> str:
1305
1305
  def completion_verdict(state: dict) -> dict:
1306
1306
  """完成度裁定(**确定性规则**,供收口/前置校验/失败现场三处共用)。
1307
1307
 
1308
- `operationally_complete=True` ⇒ 「识别为运营手动完成(依据 hub-v2 实测)」。
1308
+ `operationally_complete=True` ⇒ 「识别为已完成(依据 hub-v2 实测;来源未声明)」。
1309
1309
  ⛔ 本函数**不改任何数据**:它只给结论,写表与否由调用方按各自白名单决定。
1310
1310
  """
1311
1311
  ok = bool(state.get("operationally_complete"))
1312
1312
  return {
1313
1313
  "operationally_complete": ok,
1314
1314
  "identified_as": "manual-completion-by-hub" if ok else "",
1315
- "note_zh": ("识别为运营手动完成(依据 hub-v2 实测):" + summary_zh(state)) if ok else "",
1315
+ "note_zh": ("识别为已完成(依据 hub-v2 实测;来源未声明):" + summary_zh(state)) if ok else "",
1316
1316
  "reason": state.get("complete_reason") or "",
1317
1317
  "items": item_lines(state),
1318
1318
  "shipment_id": state.get("shipment_id") or "",
@@ -2320,6 +2320,11 @@ BOX_LABEL_DL_ROUNDS = int(os.environ.get("BOX_LABEL_DL_ROUNDS", "") or 3)
2320
2320
  # 现在:等待按"业务信号"轮询 + 分层预算,超时才落终态;预算可调(客户机也能按现场调)。
2321
2321
  NAV_TIMEOUT_SEC = float(os.environ.get("NAV_TIMEOUT_SEC", "") or 90) # page.goto 单次
2322
2322
  URL_TRANSITION_SEC = float(os.environ.get("URL_TRANSITION_SEC", "") or 120) # 等跳转(如 hub-v2)
2323
+ # 2026-10-24:平台把 hub 落地页从 `/hub-v2` 改为 `/hub`;历史判据只认 `hub-v2`
2324
+ # ⇒ 写操作明明生效也判失败(FULL 步骤4/5 假失败、误取消预约)。统一用正则接受两种形态。
2325
+ HUB_URL_RE = re.compile(r"/inbounds/\d+/hub(?:-v2)?(?:[/?#]|$)")
2326
+ def is_hub_url(url: str) -> bool:
2327
+ return bool(HUB_URL_RE.search(str(url or "")))
2323
2328
  SLOW_GRACE_SEC = float(os.environ.get("SLOW_GRACE_SEC", "") or 60) # 慢渲染宽限(再等一轮)
2324
2329
  # 错误弹窗(Algo salió mal)自愈的**退避**(2026-09-23):
2325
2330
  # 旧行为:失败→重载→固定等 8s→再来一次,两次不成即判失败 ⇒ 平台慢时等于"连打同一页面"
@@ -7455,7 +7460,7 @@ class Orchestrator:
7455
7460
  """**唯一入口**:把 `FULL状态` 置为「已完成」。
7456
7461
 
7457
7462
  2026-10-16:原先只有 `_finalize_ticket_complete`(引擎自己跑完)能置位;现在
7458
- 「运营手动完成(hub-v2 实测)」也要置位(用户口径「都要」)⇒ 抽出本入口,两条路径
7463
+ 「已完成(hub-v2 实测;来源未声明)」也要置位(用户口径「都要」)⇒ 抽出本入口,两条路径
7459
7464
  **都走这里**,保持"表状态只有一个写点"的不变量(回归见 ticket-completion-criteria /
7460
7465
  step-progress-realtime 的 AST 断言)。
7461
7466
  """
@@ -7881,7 +7886,7 @@ class Orchestrator:
7881
7886
  "shipment_id": "", "url_inbound": "", "probe_ok": False}
7882
7887
 
7883
7888
  # 需求 1a:hub 判定的**运营手动完成**在表里固定用这句话(运维与角色都按它对齐口径)
7884
- HUB_MANUAL_NOTE = "识别为运营手动完成(依据 hub-v2 实测)"
7889
+ HUB_MANUAL_NOTE = "识别为已完成(依据 hub-v2 实测;来源未声明)"
7885
7890
 
7886
7891
  def _hub_manual_complete(self, verdict: dict[str, Any], *, status: str = "") -> dict[str, Any]:
7887
7892
  """hub-v2 判定「预约已取消 ∧ 产品标签已下载 ∧ 箱唛已下载」⇒ **认定为已完成**。
@@ -7890,7 +7895,7 @@ class Orchestrator:
7890
7895
  三个派生结论**都必须是 `True`**(`None` 一律不算);只要有一个判不了 ⇒ 不是完成。
7891
7896
 
7892
7897
  ## 写什么 / ⛔ 不写什么(用户口径)
7893
- - **写**:`FULL备注` ← 「识别为运营手动完成(依据 hub-v2 实测):<逐项>」(同源同文,
7898
+ - **写**:`FULL备注` ← 「识别为已完成(依据 hub-v2 实测;来源未声明):<逐项>」(同源同文,
7894
7899
  与 `--mode hub-status` 打印、与 `failures.jsonl#hub_status` 完全一致);
7895
7900
  `当前步骤` ← **「全部完成」**(2026-10-16 用户口径:表口径与"引擎自己跑完"一致);`就绪=false`(消费掉拾取位)。
7896
7901
  **`FULL状态` ← 已完成**(2026-10-16 用户口径「都要」:认定即置位,否则票一直挂在"失败"里,
@@ -9299,7 +9304,7 @@ class Orchestrator:
9299
9304
  while time.time() < deadline:
9300
9305
  rounds += 1
9301
9306
  try:
9302
- if "hub-v2" in (self.browser.page.url or ""):
9307
+ if is_hub_url(self.browser.page.url or ""):
9303
9308
  break
9304
9309
  except Exception:
9305
9310
  pass
@@ -10094,7 +10099,7 @@ class Orchestrator:
10094
10099
  for _ in range(38): # 0.4s × 38 ≈ 15s
10095
10100
  try:
10096
10101
  url = self.browser.page.url or ""
10097
- if "hub-v2" in url:
10102
+ if is_hub_url(url):
10098
10103
  submit_ok = True
10099
10104
  break
10100
10105
  hint = await self.browser.page.evaluate(
@@ -10754,7 +10759,7 @@ class Orchestrator:
10754
10759
  self._log(" ✅ 已确认重新贴标弹窗")
10755
10760
  return True
10756
10761
  # 无弹窗时快速返回:正常提交路径已跳转 hub-v2 → 零额外延迟
10757
- if "hub-v2" in (self.browser.page.url or ""):
10762
+ if is_hub_url(self.browser.page.url or ""):
10758
10763
  return False
10759
10764
  except Exception:
10760
10765
  return False
@@ -10877,12 +10882,24 @@ class Orchestrator:
10877
10882
  recovery_attempted=["checkbox_mouseevent"])
10878
10883
  await self.browser.click_with_fallback(5, "confirm_btn", "Confirmar")
10879
10884
  await asyncio.sleep(3)
10880
- # 提交后应跳转 hub-v2;未跳转说明包装确认未提交
10885
+ # 提交后应跳转 hub(平台 2026-10 已把 /hub-v2 改为 /hub);URL 判据只作快速路径。
10886
+ # 失败前必须用只读 hub 探针核验「包装确认」是否真的完成,平台慢渲染不得再判假失败。
10881
10887
  try:
10882
- await self.browser.page.wait_for_url("**/hub-v2**", timeout=15000)
10888
+ await self.browser.page.wait_for_url(
10889
+ re.compile(r".*/inbounds/\d+/hub(?:-v2)?(?:[?#].*)?$"),
10890
+ timeout=int(URL_TRANSITION_SEC * 1000),
10891
+ )
10883
10892
  except Exception:
10884
- raise StepError(5, "business", "包装确认后未跳转 hub-v2,确认可能未提交",
10885
- recovery_attempted=["check_procedures_state"])
10893
+ hub = await self._hub_truth(navigate=False, step=5)
10894
+ items = (hub or {}).get("items") or []
10895
+ packaging_done = any(
10896
+ str((it or {}).get("key") or "") == "packaging" and (it or {}).get("done") is True
10897
+ for it in items
10898
+ )
10899
+ if not packaging_done:
10900
+ raise StepError(5, "business", "包装确认后未到 hub / 未确认包装完成",
10901
+ recovery_attempted=["check_procedures_state", "hub_probe"])
10902
+ self._log(" ✅ 包装确认已由 hub 只读探针核验为完成(URL 判据未命中,按 effective 继续)")
10886
10903
  self._mark_done(5)
10887
10904
  if self.state.record_id:
10888
10905
  self.feishu.send_message(f"✅ 步骤5完成: 包装确认 - {self.state.sku}")
@@ -11837,7 +11854,7 @@ class Orchestrator:
11837
11854
  self._log(" ✅ 取消生效(appointmentCancellationSuccess 成功标志)")
11838
11855
  return True
11839
11856
  # b. hub-v2 跳转(取消成功后的正常去向)
11840
- if "/hub-v2" in url:
11857
+ if is_hub_url(url):
11841
11858
  self._log(" ✅ 取消生效(hub-v2 跳转)")
11842
11859
  return True
11843
11860
  # c. 按钮消失 且 无错误弹窗(排除侧栏)
@@ -12689,18 +12706,13 @@ class Orchestrator:
12689
12706
  f"失败:{exc.message}"
12690
12707
  f"({_capacity_defer_tag}·预检恢复即**当日**重试,最多 "
12691
12708
  f"{_capacity_max_lift} 次)")
12692
- # 🧹 预约已成功提交(步骤4完成)→ 失败分支自动取消预约,防超时罚款。
12693
- # 放在 auto_retry 网络重置返回之前:即使走自动重置分支先 return,
12694
- # 取消也已完成(防重置后预约残留到预约时间被罚)。
12695
- # dry_run 跳过;步骤4本身失败(completed_steps 不含4)说明预约未成功
12696
- # 提交、无残留,不触发。
12709
+ # ⛔ 2026-10-24 用户口径:失败分支**不再自动取消预约**。
12710
+ # 假失败(如 /hub-v2 → /hub 改版)下自动取消会毁掉本来成功的预约;
12711
+ # 预约是稀缺资源,必须先由评估/人工确认「确实需要取消」再走步骤8。
12712
+ # dry_run 同样不取消。
12697
12713
  cancel_note = ""
12698
12714
  if not dry_run and 4 in self.state.completed_steps:
12699
- self._log(" 🧹 预约已成功,失败分支自动取消预约…")
12700
- if await self._safe_cancel_appointment():
12701
- cancel_note = "\n✅ 预约已自动取消,无超时罚款风险"
12702
- else:
12703
- cancel_note = "\n⚠️ 预约取消失败!请立即人工取消预约,避免超时罚款"
12715
+ self._log(" ⏸ 失败分支不自动取消预约:先核实 hub/门户真实状态;确认需要取消时再走步骤8")
12704
12716
  if self.state.record_id:
12705
12717
  if auto_retry is None and auto_reset_fail is None:
12706
12718
  # 人工类 / 未触发自动恢复:客户群失败卡片(红底 ❌),失败降级文字
@@ -13801,7 +13813,7 @@ def build_parser() -> argparse.ArgumentParser:
13801
13813
  "三态;⛔ 不点按钮、不写表、不改 defer 状态;需 --store-name)")
13802
13814
  p.add_argument("--hub-write-note", action="store_true",
13803
13815
  help="hub-status 模式:当 hub 判定为「运营手动完成」时,**只**写 FULL备注/当前步骤"
13804
- "(识别为运营手动完成(依据 hub-v2 实测));⛔ 仍不改写 FULL状态")
13816
+ "(识别为已完成(依据 hub-v2 实测;来源未声明));⛔ 仍不改写 FULL状态")
13805
13817
  p.add_argument("--no-heal", action="store_true",
13806
13818
  help="cdp-preflight 模式:只检查不自愈(不重连/不重开店铺窗口,纯只读)")
13807
13819
  p.add_argument("--step", type=int, choices=list(range(1, 9)),