@whyshu/argent-engine 0.7.5 → 0.7.7

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 (43) hide show
  1. package/MANIFEST.json +39 -35
  2. package/engine/VERSION +1 -1
  3. package/engine/core/contracts/argent.contracts.json +12 -4
  4. package/engine/scripts/appointment-create.mjs +37 -2
  5. package/engine/scripts/appointment-worker.mjs +72 -15
  6. package/engine/scripts/appointment_grab.py +141 -20
  7. package/engine/scripts/argent-machine-setup.mjs +118 -0
  8. package/engine/scripts/argent-runner.mjs +63 -10
  9. package/engine/scripts/capability-packs.lock.json +54 -51
  10. package/engine/scripts/ci-run.sh +14 -2
  11. package/engine/scripts/collect-evidence.mjs +1 -1
  12. package/engine/scripts/customer-preflight.py +33 -0
  13. package/engine/scripts/flow-auto-retry.py +3 -0
  14. package/engine/scripts/flow-requirements.json +23 -13
  15. package/engine/scripts/flow-session-retention.mjs +88 -0
  16. package/engine/scripts/flow-trigger-wake.mjs +13 -1
  17. package/engine/scripts/flow-trigger.sh +2 -1
  18. package/engine/scripts/flow_config.py +4 -0
  19. package/engine/scripts/flow_failure.py +9 -4
  20. package/engine/scripts/flow_preflight.py +30 -0
  21. package/engine/scripts/flow_retry.py +22 -7
  22. package/engine/scripts/gen-agent-bundles.mjs +1 -1
  23. package/engine/scripts/gen-preset-rows.mjs +2 -2
  24. package/engine/scripts/invoice_lip_runner.py +30 -2
  25. package/engine/scripts/invoice_stageb.py +101 -0
  26. package/engine/scripts/login-readiness.py +220 -0
  27. package/engine/scripts/ml-oauth-setup.mjs +1 -1
  28. package/engine/scripts/orchestrator.py +416 -30
  29. package/engine/scripts/probe_ml_site.py +103 -0
  30. package/engine/scripts/release-argent.sh +26 -2
  31. package/engine/skills/argent-machine-onboarding/SKILL.md +143 -0
  32. package/engine/skills/internal.json +4 -0
  33. package/engine/skills/ml-full-appointment/SKILL.md +32 -13
  34. package/engine/skills/ml-invoice/SKILL.md +1 -1
  35. package/engine/skills/ml-invoice-xzh/SKILL.md +2 -2
  36. package/engine/version.mjs +1 -1
  37. package/engine/verticals/_engine-words.baseline.txt +42 -23
  38. package/engine/verticals/mercadolibre/flows.json +9 -5
  39. package/engine/verticals/mercadolibre/manifest.json +5 -4
  40. package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +1 -1
  41. package/engine/verticals/mercadolibre/prompts/daily-report.md +7 -10
  42. package/package.json +1 -1
  43. package/engine/scripts/ml-ads.mjs +0 -181
@@ -61,6 +61,7 @@ def login_xinhang(page, m, c):
61
61
  pwd = c.get("xhInvoicePass", "")
62
62
  page.goto(url, timeout=TIMEOUT, wait_until="domcontentloaded")
63
63
  page.wait_for_timeout(2000)
64
+ close_overlays(page)
64
65
  # 会话复用探测(2026-09-08):登录 URL 固定含 /Login/(goto 即登录页),
65
66
  # 直接导航 home 验证 profile 会话:能进 home = 已登录复用;被弹回 Login = 需填表。
66
67
  # 避免每票/每轮重复登录(频繁登录有账号风控风险)。
@@ -69,6 +70,7 @@ def login_xinhang(page, m, c):
69
70
  page.goto(base + "/home/EleIndex?lang=cn", timeout=TIMEOUT,
70
71
  wait_until="domcontentloaded")
71
72
  page.wait_for_timeout(2500)
73
+ close_overlays(page)
72
74
  except Exception:
73
75
  pass
74
76
  cur = page.url or ""
@@ -223,6 +225,71 @@ def _hover_menu(page, text):
223
225
  }''', text)
224
226
 
225
227
 
228
+ def close_overlays(page) -> int:
229
+ """关闭鑫中航门户的公告/遮罩弹窗(系统升级公告等),避免挡住菜单/搜索/上传。
230
+
231
+ 2026-10-21 生产:门户系统公告弹窗遮挡正常流程,脚本没有统一关闭入口。
232
+ 这里只点“可见且带关闭语义”的按钮/×,不做强制 DOM 删除,避免误删业务弹窗。
233
+ """
234
+ import time
235
+ n = 0
236
+ try:
237
+ page.keyboard.press("Escape")
238
+ except Exception:
239
+ pass
240
+ # 优先 × 关闭图标(Element UI / layui / 通用)
241
+ selectors = [
242
+ ".layui-layer-close", ".el-dialog__close", ".ant-modal-close",
243
+ ".ivu-modal-close", ".modal-close", "[aria-label='关闭']",
244
+ "button.close", "a.close", "i.close", ".close",
245
+ ]
246
+ for sel in selectors:
247
+ try:
248
+ loc = page.locator(sel)
249
+ for i in range(min(loc.count(), 6)):
250
+ el = loc.nth(i)
251
+ try:
252
+ if el.is_visible():
253
+ el.click(timeout=1500, force=True)
254
+ n += 1
255
+ except Exception:
256
+ continue
257
+ except Exception:
258
+ continue
259
+ # 文本按钮兜底(系统公告常见“我知道了/知道了/关闭”)
260
+ for text in ("我知道了", "知道了", "关闭公告", "关闭", "确定", "继续"):
261
+ try:
262
+ loc = page.get_by_text(text, exact=True)
263
+ for i in range(min(loc.count(), 4)):
264
+ el = loc.nth(i)
265
+ try:
266
+ if el.is_visible():
267
+ el.click(timeout=1500, force=True)
268
+ n += 1
269
+ except Exception:
270
+ continue
271
+ except Exception:
272
+ continue
273
+ # 弹窗可能在 iframe 内,遍历 frame 再试一次按钮
274
+ try:
275
+ for f in page.frames:
276
+ for text in ("我知道了", "知道了", "关闭公告", "关闭"):
277
+ try:
278
+ loc = f.get_by_text(text, exact=True)
279
+ for i in range(min(loc.count(), 3)):
280
+ el = loc.nth(i)
281
+ if el.is_visible():
282
+ el.click(timeout=1200, force=True); n += 1
283
+ except Exception:
284
+ continue
285
+ except Exception:
286
+ pass
287
+ if n:
288
+ print(f" [overlay] 已关闭门户弹窗次数={n}", flush=True)
289
+ time.sleep(0.5)
290
+ return n
291
+
292
+
226
293
  def _find_order_frame(page, timeout=150):
227
294
  """找大货订单页 frame(含「单票导入」或「订单号码」文案)。"""
228
295
  import time
@@ -305,6 +372,29 @@ def _check_target_row(frame, order_no):
305
372
  }''', order_no)
306
373
 
307
374
 
375
+ def close_temp_tabs(ctx, keep=None) -> int:
376
+ """关闭发票脚本/门户自己打开的临时标签页(下单 PDF、下载预览等),保留主门户页。"""
377
+ n = 0
378
+ try:
379
+ pages = list(ctx.pages)
380
+ except Exception:
381
+ return 0
382
+ for pg in pages:
383
+ if keep is not None and pg is keep:
384
+ continue
385
+ try:
386
+ url = pg.url or ""
387
+ except Exception:
388
+ continue
389
+ if any(k in url for k in ("/orderhebing/", "_PHD.pdf", "SelectLableTypes", "about:blank")):
390
+ try:
391
+ pg.close()
392
+ n += 1
393
+ except Exception:
394
+ pass
395
+ return n
396
+
397
+
308
398
  def download_label(order_no, save_name=None, wait_keep=15):
309
399
  """按订单号搜索 → 单行校验 → 勾选 → 选大货标签模板 → 捕获 popup PDF → 保存。
310
400
 
@@ -322,6 +412,7 @@ def download_label(order_no, save_name=None, wait_keep=15):
322
412
  if not ok:
323
413
  return None
324
414
  page.wait_for_timeout(3000)
415
+ close_overlays(page)
325
416
  menu_ok = False
326
417
  for attempt in range(3):
327
418
  r1 = _click_menu(page, "大货订单管理")
@@ -418,6 +509,10 @@ def download_label(order_no, save_name=None, wait_keep=15):
418
509
  traceback.print_exc()
419
510
  return None
420
511
  finally:
512
+ try:
513
+ close_temp_tabs(ctx, keep=locals().get('page'))
514
+ except Exception:
515
+ pass
421
516
  try:
422
517
  p.stop()
423
518
  except Exception:
@@ -462,6 +557,7 @@ def upload_invoice(invoice_path, order_no=None, wait_keep=20):
462
557
  print("✗ 订单页未就绪", flush=True)
463
558
  return False
464
559
  print("订单页就绪,开始上传", flush=True)
560
+ close_overlays(page)
465
561
  r = frame.evaluate('''() => {
466
562
  const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
467
563
  let node;
@@ -477,6 +573,7 @@ def upload_invoice(invoice_path, order_no=None, wait_keep=20):
477
573
  }''')
478
574
  print(" 点击单票导入:", r, flush=True)
479
575
  page.wait_for_timeout(6000)
576
+ close_overlays(page)
480
577
  fi = frame.locator("input[type=file]")
481
578
  if fi.count():
482
579
  fi.first.set_input_files(invoice_path)
@@ -504,6 +601,10 @@ def upload_invoice(invoice_path, order_no=None, wait_keep=20):
504
601
  traceback.print_exc()
505
602
  return False
506
603
  finally:
604
+ try:
605
+ close_temp_tabs(ctx, keep=locals().get('page'))
606
+ except Exception:
607
+ pass
507
608
  try:
508
609
  p.stop()
509
610
  except Exception:
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env python3
2
+ """登录态就绪检查(安装/生产前置;只读,除显式 --open 不启动浏览器)。
3
+
4
+ 目标(2026-10-21 用户口径):
5
+ · 登录态是**安装期**前置条件,不是生产第一票才暴露的问题;
6
+ · 文鳐:需要先打开长驻 Chrome 窗口/标签页。若门户出现滑块,由用户人工拖动;
7
+ 登录成功后保持该窗口/标签页不关,等于保持登录态。
8
+ · 鑫中航:同样保留一个标签页/浏览器,供发票流程 CDP 复用;
9
+ · 紫鸟:常驻 WebDriver 客户端 + 每个店铺窗口保留标签页,供 FULL/抢预约复用。
10
+
11
+ 本脚本只做**检测**:
12
+ - 检查配置/凭据文件是否存在;
13
+ - 检查 CDP 端口是否有页面(文鳐 9333 / 鑫中航 9223);
14
+ - 检查紫鸟 WebDriver IPC 是否正常、能否列店铺;
15
+ - 给出可照做的修复命令。
16
+ 显式 `--open` 才会启动文鳐浏览器并提示人工过滑块(不会自动提交滑块,也不写业务表)。
17
+ 退出码:0 = 无 fail(warn 不阻断);1 = 有 fail;2 = 参数/脚本错误。
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import os
24
+ import pathlib
25
+ import subprocess
26
+ import sys
27
+
28
+ HERE = pathlib.Path(__file__).resolve().parent
29
+ sys.path.insert(0, str(HERE / 'lib'))
30
+ from argent_datadir import resolve_data_dir # noqa: E402
31
+ DATA = pathlib.Path(resolve_data_dir()['dir'])
32
+
33
+
34
+ def _read_env(name: str) -> dict:
35
+ out = {}
36
+ try:
37
+ for line in (DATA / name).read_text(encoding='utf-8').splitlines():
38
+ t = line.strip()
39
+ if t and not t.startswith('#') and '=' in t:
40
+ k, _, v = t.partition('=')
41
+ out[k.strip()] = v.strip().strip('"').strip("'")
42
+ except Exception:
43
+ pass
44
+ return out
45
+
46
+
47
+ def _run(cmd: list[str], timeout: int = 30) -> tuple[int, str, str]:
48
+ try:
49
+ p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
50
+ return p.returncode, (p.stdout or ''), (p.stderr or '')
51
+ except FileNotFoundError:
52
+ return 127, '', f'命令不存在: {cmd[0]}'
53
+ except subprocess.TimeoutExpired:
54
+ return 124, '', f'超时: {cmd[:2]}'
55
+ except Exception as exc: # noqa: BLE001
56
+ return 125, '', str(exc)
57
+
58
+
59
+ def _pages(port: int) -> list:
60
+ """CDP /json/list:返回可复用页面;不可用返回空列表。"""
61
+ try:
62
+ import urllib.request
63
+ with urllib.request.urlopen(f'http://127.0.0.1:{port}/json/list', timeout=3) as r:
64
+ doc = json.loads(r.read().decode('utf-8', 'replace'))
65
+ return [x for x in (doc or []) if isinstance(x, dict) and x.get('type') == 'page']
66
+ except Exception: # noqa: BLE001
67
+ return []
68
+
69
+
70
+ def _item(i: str, level: str, title: str, detail: str = '', fix: str = '') -> dict:
71
+ return {'id': i, 'level': level, 'title': title, 'detail': detail, 'fix': fix}
72
+
73
+
74
+ def check_wenyao(open_it: bool = False) -> list[dict]:
75
+ env = _read_env('wenyao.env')
76
+ miss = [k for k in ('WENYAO_URL', 'WENYAO_PHONE', 'WENYAO_PASSWORD') if not env.get(k)]
77
+ if miss:
78
+ return [_item('login.wenyao.config', 'fail', '文鳐门户凭据缺失',
79
+ f'缺 {",".join(miss)}', '在客户机 ~/.dsh/argent/wenyao.env 补齐(600;不要贴到对话)')]
80
+ items = [_item('login.wenyao.config', 'ok', '文鳐门户凭据存在', 'wenyao.env(600)')]
81
+ script = HERE / 'wenyao-browser.sh'
82
+ pages = _pages(9333)
83
+ if not pages:
84
+ if open_it and script.exists():
85
+ code, out, err = _run(['bash', str(script), 'start'], timeout=120)
86
+ pages = _pages(9333)
87
+ items.append(_item('login.wenyao.browser', 'warn' if pages else 'fail',
88
+ '文鳐长驻窗口未打开' if not pages else '文鳐窗口已启动,等待人工登录',
89
+ (out or err).strip()[-300:] if not pages else '请在弹出的 Chrome 里拖滑块并点登录,成功后不要关窗口',
90
+ f'bash {script} start'))
91
+ else:
92
+ items.append(_item('login.wenyao.browser', 'warn', '文鳐长驻窗口未打开',
93
+ '安装/生产前应打开并保留一个标签页;没有窗口不等于凭据失效,但第一票会重新登录',
94
+ f'bash {script} start'))
95
+ return items
96
+ # 有页面:调用 attach 状态判断是否登录(只读)
97
+ code, out, err = _run([sys.executable, str(HERE / 'wenyao_attach.py'), '--status'], timeout=30)
98
+ text = ((out or '') + (err or '')).strip()
99
+ logged = code == 0 and not any(x in text for x in ('未登录', '登录失败', '需要登录'))
100
+ items.append(_item('login.wenyao.browser', 'ok' if logged else 'warn',
101
+ '文鳐窗口已有标签页' if logged else '文鳐窗口在,但登录态未确认',
102
+ (text.splitlines()[-1] if text else '已连接 CDP 9333'),
103
+ '若未登录:在窗口里完成滑块+登录;完成后保持标签页不关'))
104
+ return items
105
+
106
+
107
+ def check_xzh(open_it: bool = False) -> list[dict]:
108
+ items = []
109
+ mapping = DATA / 'invoice-mapping.json'
110
+ if not mapping.exists():
111
+ items.append(_item('login.xzh.config', 'warn', '鑫中航映射文件缺失',
112
+ str(mapping), '补齐 invoice-mapping.json(含 xinzhonghang.url)'))
113
+ else:
114
+ items.append(_item('login.xzh.config', 'ok', '鑫中航映射文件存在', str(mapping)))
115
+ pages = _pages(9223)
116
+ if not pages and open_it:
117
+ # 不直接起浏览器:由 launchd 常驻;只提示并尝试 kickstart
118
+ code, out, err = _run(['launchctl', 'kickstart', f'gui/{os.getuid()}/com.whyshu.xzh-browser'], timeout=30)
119
+ pages = _pages(9223)
120
+ items.append(_item('login.xzh.browser', 'warn' if not pages else 'ok',
121
+ '鑫中航常驻浏览器未打开' if not pages else '鑫中航浏览器已启动',
122
+ (out or err).strip()[-200:] if not pages else '保留标签页/登录态',
123
+ 'launchctl kickstart gui/$(id -u)/com.whyshu.xzh-browser'))
124
+ elif not pages:
125
+ items.append(_item('login.xzh.browser', 'warn', '鑫中航常驻浏览器未打开',
126
+ 'CDP 9223 无页面;安装/生产前应保留标签页/登录态',
127
+ 'launchctl kickstart gui/$(id -u)/com.whyshu.xzh-browser(或安装 com.whyshu.xzh-browser)'))
128
+ else:
129
+ items.append(_item('login.xzh.browser', 'ok', '鑫中航浏览器已有标签页',
130
+ f'CDP 9223:{len(pages)} 个页面;保持标签页/登录态'))
131
+ return items
132
+
133
+
134
+ def check_ziniao(open_it: bool = False, open_stores: bool = False) -> list[dict]:
135
+ items = []
136
+ cli = os.environ.get('ZINIAO_CLI') or 'ziniao-cli'
137
+ code, out, err = _run([cli, 'store', 'list', '--all', '--format', 'json'], timeout=30)
138
+ stores = []
139
+ if code == 0:
140
+ try:
141
+ doc = json.loads(out)
142
+ if isinstance(doc, list):
143
+ stores = doc
144
+ else:
145
+ data = doc.get('data') or doc
146
+ stores = data.get('items') if isinstance(data, dict) else (data or [])
147
+ except Exception: # noqa: BLE001
148
+ stores = []
149
+ if code != 0 or not stores:
150
+ items.append(_item('login.ziniao.stores', 'warn', '紫鸟店铺清单读取失败',
151
+ (err or out).strip()[-200:], '确认 ziniao-cli 已登录/有权限;再跑 bash scripts/ziniao-keepalive.sh status'))
152
+ else:
153
+ items.append(_item('login.ziniao.stores', 'ok', f'紫鸟店铺清单可读:{len(stores)} 个',
154
+ '每个已打开店铺窗口应保留一个标签页,供 FULL/预约复用'))
155
+ if open_stores:
156
+ opened, failed = [], []
157
+ for st in stores:
158
+ sid = str((st or {}).get('storeId') or (st or {}).get('id') or '').strip()
159
+ if not sid:
160
+ continue
161
+ c, o, e = _run([cli, 'store', 'open', '--id', sid], timeout=60)
162
+ (opened if c == 0 else failed).append(sid)
163
+ items.append(_item('login.ziniao.open', 'ok' if not failed else 'warn',
164
+ f'紫鸟店铺窗口已打开 {len(opened)} 个',
165
+ ('失败:' + '、'.join(failed[:6])) if failed else '每个窗口保留一个标签页,不要关闭',
166
+ f'ziniao-cli store open --id <storeId>(逐个补开)'))
167
+ keep = HERE / 'ziniao-keepalive.sh'
168
+ if keep.exists():
169
+ code, out, err = _run(['bash', str(keep), 'status'], timeout=30)
170
+ txt = ((out or '') + (err or '')).strip()
171
+ ipc_ok = 'getRunningInfo 正常' in txt or 'IPC 正常' in txt
172
+ agent_ok = '未安装' not in txt and 'com.whyshu.ziniao-keepalive' in txt
173
+ level = 'ok' if ipc_ok else 'warn'
174
+ fix = '' if ipc_ok else f'bash {keep} install(并确认客户端以 WebDriver 模式运行)'
175
+ items.append(_item('login.ziniao.keepalive', level,
176
+ '紫鸟 WebDriver IPC 正常' if ipc_ok else '紫鸟 WebDriver IPC 异常/守护未安装',
177
+ ('守护已加载' if agent_ok else '守护未安装(install)') + '|' + txt.splitlines()[-1][:160],
178
+ fix))
179
+ else:
180
+ items.append(_item('login.ziniao.keepalive', 'warn', '缺 ziniao-keepalive.sh',
181
+ '', 'git pull 后重跑'))
182
+ return items
183
+
184
+
185
+ def main(argv=None) -> int:
186
+ ap = argparse.ArgumentParser(description='登录态就绪检查(安装/生产前置)')
187
+ ap.add_argument('--flow', action='append', choices=['wenyao-invoice', 'xzh-invoice', 'full'],
188
+ help='只检查指定流程;默认三个都检查')
189
+ ap.add_argument('--open', action='store_true', help='文鳐未打开时启动长驻窗口;鑫中航尝试 kickstart(人工完成滑块/登录)')
190
+ ap.add_argument('--open-ziniao', action='store_true', help='逐个打开紫鸟店铺窗口(安装时保留标签页;会打开多个窗口)')
191
+ ap.add_argument('--json', action='store_true')
192
+ a = ap.parse_args(argv)
193
+ flows = a.flow or ['wenyao-invoice', 'xzh-invoice', 'full']
194
+ items = []
195
+ if 'wenyao-invoice' in flows:
196
+ items += check_wenyao(a.open)
197
+ if 'xzh-invoice' in flows:
198
+ items += check_xzh(a.open)
199
+ if 'full' in flows:
200
+ items += check_ziniao(a.open, a.open_ziniao)
201
+ counts = {k: sum(1 for it in items if it['level'] == k) for k in ('ok', 'warn', 'fail')}
202
+ if a.json:
203
+ print(json.dumps({'dataDir': str(DATA), 'flows': flows, 'checks': items, 'summary': counts},
204
+ ensure_ascii=False, indent=2))
205
+ else:
206
+ icon = {'ok': '✅', 'warn': '⚠️', 'fail': '❌'}
207
+ for it in items:
208
+ print(f"{icon.get(it['level'], '·')} [{it['id']}] {it['title']}")
209
+ if it.get('detail'):
210
+ print(f" {it['detail']}")
211
+ if it.get('fix'):
212
+ print(f" 修复:{it['fix']}")
213
+ print(f"\n登录态:ok={counts['ok']} warn={counts['warn']} fail={counts['fail']}")
214
+ if counts['warn']:
215
+ print('提示:warn 不阻断流程,但生产前应完成窗口/标签页登录;文鳐滑块需人工完成。')
216
+ return 1 if counts['fail'] else 0
217
+
218
+
219
+ if __name__ == '__main__':
220
+ sys.exit(main())
@@ -49,7 +49,7 @@ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
49
49
  // 其他站点域名实测被拦(403),后续逐个站点验证后更新此表。
50
50
  const AUTH_HOSTS = {
51
51
  MLM: 'auth.mercadolibre.com.mx',
52
- MLB: 'auth.mercadolivre.com.br', // 2026-10-17 实测修正:巴西站点域名是 mercadolivre(auth.mercadolibre.com.br 无 DNS)
52
+ MLB: 'auth.mercadolibre.com.br',
53
53
  MLA: 'auth.mercadolibre.com.ar',
54
54
  MLC: 'auth.mercadolibre.cl',
55
55
  MCO: 'auth.mercadolibre.com.co',