@whyshu/argent-engine 0.6.0 → 0.7.0

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 (68) hide show
  1. package/MANIFEST.json +64 -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 +31 -31
  8. package/engine/scripts/check-billing-mode.sh +2 -1
  9. package/engine/scripts/collect-evidence.mjs +50 -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/drill-clean-room.sh +32 -0
  14. package/engine/scripts/final-check.mjs +44 -0
  15. package/engine/scripts/flow-requirements.json +30 -10
  16. package/engine/scripts/flow-timers-plist.sh +2 -1
  17. package/engine/scripts/flow-timers.sh +3 -1
  18. package/engine/scripts/flow_config.py +2 -1
  19. package/engine/scripts/init-ml.sh +5 -2
  20. package/engine/scripts/invoice-table.sh +2 -1
  21. package/engine/scripts/lib/argent-datadir.sh +18 -0
  22. package/engine/scripts/lib/argent_datadir.py +23 -0
  23. package/engine/scripts/poll-fulfillment.sh +4 -2
  24. package/engine/scripts/poll-invoice.sh +4 -2
  25. package/engine/scripts/provider-select.sh +2 -1
  26. package/engine/scripts/release-argent.sh +1 -1
  27. package/engine/scripts/retire-inventory.mjs +40 -0
  28. package/engine/scripts/run-tests.sh +2 -1
  29. package/engine/scripts/s1-audit.mjs +85 -0
  30. package/engine/scripts/usage-anchor.sh +3 -2
  31. package/engine/scripts/wenyao-browser.sh +3 -2
  32. package/engine/scripts/wenyao-login.sh +3 -2
  33. package/engine/scripts/ziniao-keepalive.sh +2 -1
  34. package/engine/scripts/ziniao-post-upgrade-check.sh +2 -1
  35. package/engine/scripts/ziniao-windows.sh +2 -1
  36. package/engine/skills/README.md +86 -0
  37. package/engine/skills/argent-dev/SKILL.md +41 -0
  38. package/engine/skills/internal.json +37 -0
  39. package/engine/skills/lktop-pm/SKILL.md +35 -0
  40. package/engine/skills/lktopapp-dev/SKILL.md +34 -0
  41. package/engine/skills/ml-config-guide/SKILL.md +50 -0
  42. package/engine/skills/ml-fulfillment-sop/SKILL.md +178 -0
  43. package/engine/skills/ml-fulfillment-sop/references/andes-components.md +69 -0
  44. package/engine/skills/ml-fulfillment-sop/references/cdp-port-mapping.md +35 -0
  45. package/engine/skills/ml-fulfillment-sop/references/feishu-base-fulfillment-schema.md +136 -0
  46. package/engine/skills/ml-fulfillment-sop/references/multi-window-concurrency.md +69 -0
  47. package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +77 -0
  48. package/engine/skills/ml-fulfillment-sop/references/selectors.md +243 -0
  49. package/engine/skills/ml-fulfillment-sop/scripts/fulfillment.js +586 -0
  50. package/engine/skills/ml-fulfillment-sop/scripts/poll-and-execute.sh +271 -0
  51. package/engine/skills/ml-fulfillment-sop/templates/report.md +55 -0
  52. package/engine/skills/ml-invoice/SKILL.md +506 -0
  53. package/engine/skills/ml-invoice/references/pitfalls.md +268 -0
  54. package/engine/skills/ml-invoice/references/portal-flow.md +360 -0
  55. package/engine/skills/ml-invoice/references/template-fill-contract.md +133 -0
  56. package/engine/skills/ml-invoice-xzh/SKILL.md +133 -0
  57. package/engine/skills/ml-invoice-xzh/references/pitfalls.md +230 -0
  58. package/engine/skills/ml-store-daily-report/SKILL.md +224 -0
  59. package/engine/skills/ml-store-daily-report/scripts/README.md +25 -0
  60. package/engine/skills/ml-store-daily-report/scripts/extract.js +691 -0
  61. package/engine/skills/ml-store-daily-report/scripts/generate.js +299 -0
  62. package/engine/skills/pet-dev/SKILL.md +42 -0
  63. package/engine/skills/whyshu-pm/SKILL.md +35 -0
  64. package/engine/skills/whyshu-web-dev/SKILL.md +50 -0
  65. package/engine/version.mjs +1 -1
  66. package/engine/verticals/mercadolibre/flows.json +37 -5
  67. package/package.json +3 -2
  68. package/requirements.txt +5 -0
@@ -0,0 +1,586 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * mercadolibre-fulfillment-sop 执行引擎
4
+ * 管理 8 步货件创建流程的状态、命令生成和异常检测。
5
+ *
6
+ * 用法:
7
+ * node fulfillment.js status — 查看当前进度
8
+ * node fulfillment.js step <N> — 输出步骤 N 的命令
9
+ * node fulfillment.js step <N> --skus '[...]' — 带 SKU 参数执行
10
+ * node fulfillment.js step <N> --format markdown — Markdown 格式输出
11
+ * node fulfillment.js step <N> --push — 推送飞书卡片
12
+ * node fulfillment.js check --shipment-id <ID> — 检查货件状态
13
+ * node fulfillment.js exceptions — 查看异常处理矩阵
14
+ * node fulfillment.js forbidden — 查看禁止事项
15
+ * node fulfillment.js help — 查看帮助
16
+ *
17
+ * 前置检查:飞书 CLI 配置
18
+ * 使用 --push 或飞书多维表格功能前,请先运行 argent feishu-setup
19
+ */
20
+
21
+ const { execSync } = require("child_process");
22
+
23
+ /** 检查飞书 CLI 是否已配置 */
24
+ function checkFeishu() {
25
+ try {
26
+ execSync("which lark-cli", { stdio: "ignore" });
27
+ } catch {
28
+ console.error("❌ 未检测到 lark-cli。飞书功能不可用。");
29
+ console.error(" 请先安装 lark-cli 并运行 argent feishu-setup");
30
+ process.exit(1);
31
+ }
32
+ try {
33
+ const out = execSync("lark-cli auth status 2>&1", { encoding: "utf8", timeout: 5000 });
34
+ if (!out.includes("OK") && !out.includes("ok") && !out.includes("授权")) {
35
+ console.error("❌ 飞书未授权。请运行 argent feishu-setup 配置飞书 Bot");
36
+ process.exit(1);
37
+ }
38
+ } catch {
39
+ console.error("⚠️ 飞书状态检查失败——如果未使用 --push 或飞书表格功能,可忽略。");
40
+ }
41
+ }
42
+
43
+ // 仅在使用 --push 或飞书相关参数时检查
44
+ const args = process.argv.slice(2);
45
+ const needsFeishu = args.includes("--push") || args.includes("--table");
46
+ if (needsFeishu) checkFeishu();
47
+
48
+ // ---- 页面选择器(基于 Andes 设计系统,2026-07-27 紫鸟真实页面验证)----
49
+ // 重要:ML 使用 Andes 组件库。querySelectorAll 不支持 :has-text()/:contains()——内容匹配必须用 textContent。
50
+ // 已验证项标注 ✅,待创建货件时校准项标注 ⚠️。
51
+ const SELECTORS = {
52
+ 1: {
53
+ // ✅ 已验证
54
+ table: 'table.andes-table',
55
+ rows: 'table.andes-table tbody tr',
56
+ statusCellIndex: 4, // "Estado" 列(0-indexed)
57
+ actionsCellIndex: 5, // "Acciones" 列
58
+ // 状态文本值(用于 textContent 匹配)
59
+ statusValues: ['Vencido', 'En preparación', 'Pendiente de recepción', 'Procesamiento finalizado'],
60
+ // 库容警告:.andes-card 中搜索 "liberar espacio"
61
+ capacityWarning: '.andes-card',
62
+ capacityWarningText: 'liberar espacio',
63
+ // 数据提取脚本(page exec 用)
64
+ extract: `(function(){
65
+ var rows = document.querySelectorAll('table.andes-table tbody tr');
66
+ var shipments = [];
67
+ rows.forEach(function(row){
68
+ var cells = row.querySelectorAll('td');
69
+ if (cells.length < 6) return;
70
+ var statusText = (cells[4].textContent || '').trim();
71
+ var actionsText = (cells[5].textContent || '').trim();
72
+ shipments.push({
73
+ id: (cells[0].textContent || '').match(/#?(\\d{8})/)?.[1] || '',
74
+ declared: (cells[1].textContent || '').trim(),
75
+ appointment: (cells[2].textContent || '').trim(),
76
+ status: statusText,
77
+ action: actionsText
78
+ });
79
+ });
80
+ return JSON.stringify(shipments);
81
+ })();`,
82
+ },
83
+ 2: {
84
+ // ✅ 已确认组件类型(但库容满时按钮不显示)
85
+ // Andes 按钮:a.andes-button 或 button.andes-button
86
+ enviarBtnSelector: 'a.andes-button, button.andes-button',
87
+ enviarBtnText: 'Enviar productos',
88
+ note: '库容满时此按钮不显示,页面显示 "liberar espacio" 警告',
89
+ },
90
+ 3: {
91
+ // ⚠️ 待创建货件时验证
92
+ skuInput: 'input[placeholder*="SKU"], input.andes-form-control[placeholder*="SKU"]',
93
+ qtyInput: 'input[type="number"]',
94
+ continuarBtnText: 'Continuar',
95
+ confirmBtnText: 'Continuar con mi plan',
96
+ },
97
+ 4: {
98
+ // ✅ 已验证(2026-07-27 紫鸟真实预约页)
99
+ // 页面:/shipping/inbounds/{id}/appointment-v2
100
+ // 配送方式下拉
101
+ shipmentDropdown: '[id*=shipment-type-selection], [class*=dropdown__trigger]',
102
+ vehicleOptionText: 'Vehículo particular',
103
+ // 日期选择(focus-ui-calendar 组件)
104
+ dateInput: 'input[readonly][id^="_R_"]', // 只读日期输入框(动态 ID)
105
+ monthYear: '.focus-ui-datepicker__content__month-year',
106
+ nextMonthBtn: '.focus-ui-datepicker__content__next_month_selector',
107
+ prevMonthBtn: '.focus-ui-datepicker__content__back_month_selector',
108
+ availableDay: 'div.day:not(.day--disabled)', // 可选日期格子
109
+ selectedDay: 'div.day--selected',
110
+ // 时间槽(动态渲染在 .date-time-card-content 内)
111
+ timeSlotContainer: '.date-time-card-content',
112
+ // 确认按钮
113
+ confirmBtn: 'button:not([disabled])', // textContent === "Confirmar"
114
+ // 自动化步骤
115
+ automation: {
116
+ // === 灰圈算法(2026-07-27 验证) ===
117
+ // 目标:选择 30 天后的任意可用日期(不关心月份名)
118
+ algorithm: `
119
+ 1. 找灰圈: div.day--current(今日标记)
120
+ 2. 从灰圈的下一格开始数,每格计 1(不论是否 disabled),数 31 格
121
+ 3. 若当前视图不够 31 格(灰圈位置 + 31 > 42),PointerEvent 翻到下月
122
+ 4. 翻月后灰圈会移到前月尾部位置,重新执行步骤 2
123
+ 5. 选中后 fiber onClick(div.hour) 选时间 2:00
124
+ 6. 主 Confirm(top>700 的按钮)提交——日历 Confirm 始终 disabled,但主按钮可绕过`,
125
+ // === 交互模式 ===
126
+ pointerEvent: 'dispatchEvent(PointerEvent("pointerover+enter+down+up",{bubbles:true,clientX,clientY,pointerId:1,pointerType:"mouse"})) + MouseEvent("click")',
127
+ twoCall: '关键:PointerEvent 触发后需分两次 page exec——第一次触发事件,浏览器在调用间渲染 DOM,第二次与新元素交互',
128
+ // === 操作步骤 ===
129
+ step1_vehiculo: 'PointerEvent combobox → 下一调用找 [role=option] 含 "Vehículo" → click',
130
+ step2_openCal: 'PointerEvent dateInput(_r_j_) → 下一调用日历已开',
131
+ step3_nav: '灰圈 index+31 >= 42 → fiber onClick([aria-label="next month"]) → 下一调用验证月已变',
132
+ step4_select: '灰圈 index+31 < 42 → fiber onClick(day) 选中 → fiber onClick(div.hour) 选时',
133
+ step5_confirm: 'click 主按钮 button:not([disabled]):text("Confirmar")[top>700]',
134
+ knownIssue: '日历格子 fiber onClick 选 DOM 但 React 不更新 → 日历 Confirm disabled。主 Confirm 可绕过提交',
135
+ },
136
+ },
137
+ 5: {
138
+ // ✅ 已验证(2026-07-27)
139
+ // Hub 页面展开包装确认区域 → 勾选两个复选框 → 点击 Confirmar
140
+ confirmCheckbox: 'input[type="checkbox"]', // top > 350 的两个复选框
141
+ confirmBtn: 'button:not([disabled])', // textContent === "Confirmar"
142
+ note: '复选框为标准 input,直接 .click() 可用;但需先点击区域标题展开',
143
+ },
144
+ 6: {
145
+ // ✅ 已验证(2026-07-27 紫鸟真实标签页)
146
+ // 页面:/shipping/inbounds/{id}/labeling
147
+ // 从 Hub 页进入:a[href*="labeling"] + textContent === "Revisar"
148
+ hubEntryCard: '[data-testid="link-identifiers"]', // 初始:卡片本身 role="button",点击进入
149
+ hubEntryLink: 'a[href*="labeling"]', // 二次:textContent === "Revisar"
150
+ productExpandBtn: 'button', // textContent === "Producto"
151
+ productCheckboxes: 'input[type=checkbox]', // 父容器 .andes-checkbox__checkbox
152
+ descargarBtn: 'button', // textContent === "Descargar etiquetas", 需 !disabled
153
+ // 弹窗(modal)内元素
154
+ modalPdfOption: 'button[inModal]', // textContent === "PDF"
155
+ modalDownloadBtn: 'button[inModal]', // textContent === "Descargar"
156
+ confirmBtn: 'button', // textContent === "Confirmar"
157
+ // 🔴 关键 Pitfall:Andes 复选框是 React 受控组件
158
+ // cb.click() 不会触发 React onChange,必须用 MouseEvent:
159
+ // cb.dispatchEvent(new MouseEvent("click", {bubbles: true, cancelable: true, view: window}))
160
+ // 复选框 DOM checked 不会更新,但 React 内部状态正确更新
161
+ checkboxClickMethod: 'MouseEvent',
162
+ checkboxCode: 'cb.dispatchEvent(new MouseEvent("click", {bubbles:true, cancelable:true, view:window}))',
163
+ },
164
+ 7: {
165
+ // ⚠️ 待创建货件时验证
166
+ bultosOptionText: 'Bultos sin agrupar',
167
+ boxQtyInput: 'input[type="number"]',
168
+ downloadBtnText: 'Descargar',
169
+ },
170
+ 8: {
171
+ // ⚠️ 待创建货件时验证
172
+ editarBtnText: 'Editar',
173
+ cancelarReservaText: 'Cancelar reserva',
174
+ confirmCancelText: 'Cancelar cita',
175
+ },
176
+ };
177
+
178
+ // ---- textContent 辅助函数 ----
179
+ // Andes 组件不含业务 class,内容匹配必须用 textContent
180
+ function findByText(selector, text, exact = false) {
181
+ const els = document.querySelectorAll(selector);
182
+ for (const el of els) {
183
+ const t = (el.textContent || '').trim();
184
+ if (exact ? t === text : t.includes(text)) return el;
185
+ }
186
+ return null;
187
+ }
188
+
189
+ // ---- 步骤定义 ----
190
+ const STEPS = {
191
+ 1: {
192
+ name: "前期准备",
193
+ type: "read",
194
+ description: "确认账号、产品、库容状态正常",
195
+ commands: ({ storeId }) => [
196
+ `ziniao-cli page visit --store-id ${storeId} --url "https://myaccount.mercadolibre.com.mx/shipping/inbounds" --wait-until networkidle`,
197
+ `ziniao-cli page content --store-id ${storeId}`,
198
+ ],
199
+ checks: [
200
+ "检查货件列表中是否有「Vencido」过期项",
201
+ "检查「En preparación」货件数量",
202
+ "检查页面是否有「liberar espacio」库容警告",
203
+ ],
204
+ forbidden: [
205
+ "禁止超库容创建",
206
+ "禁止为下架/违规 Listing 创建货件",
207
+ ],
208
+ },
209
+ 2: {
210
+ name: "货件创建入口",
211
+ type: "read",
212
+ description: "导航到 FULL 管理页并点击 Enviar productos",
213
+ commands: ({ storeId }) => [
214
+ `ziniao-cli page visit --store-id ${storeId} --url "https://myaccount.mercadolibre.com.mx/shipping/inbounds" --wait-until networkidle`,
215
+ ],
216
+ checks: [
217
+ "确认页面为官方 MercadoLibre 后台",
218
+ "确认 Enviar productos 按钮可见",
219
+ ],
220
+ },
221
+ 3: {
222
+ name: "选择产品与数量",
223
+ type: "write",
224
+ description: "搜索 SKU、填写数量、确认提交",
225
+ commands: ({ storeId, skus }) => {
226
+ const cmds = [
227
+ `# ⚠️ 写操作 — 需要用户批准后才能执行`,
228
+ `# SKU 列表: ${JSON.stringify(skus)}`,
229
+ `ziniao-cli page visit --store-id ${storeId} --url "https://myaccount.mercadolibre.com.mx/shipping/inbounds" --wait-until networkidle`,
230
+ ];
231
+ if (skus && skus.length > 0) {
232
+ cmds.push(`# 对每个 SKU 执行:搜索 → 输入数量 → Continuar`);
233
+ for (const s of skus) {
234
+ cmds.push(`# SKU: ${s.sku} 数量: ${s.qty}`);
235
+ }
236
+ }
237
+ cmds.push(`# 弹窗出现后点击「Continuar con mi plan actual」`);
238
+ cmds.push(`# 等待系统响应(可能 10-30 秒)`);
239
+ return cmds;
240
+ },
241
+ onFailure: [
242
+ "超时/失败:停止重复提交",
243
+ "等待 2 分钟后核查后台状态",
244
+ "无有效货件:核对 SKU 信息、调整发货数量",
245
+ "产品异常:修正 Listing 信息后再操作",
246
+ ],
247
+ forbidden: [
248
+ "发货数量不得超出系统限制",
249
+ "禁止重复创建同款同数量货件",
250
+ ],
251
+ },
252
+ 4: {
253
+ name: "货件预约时间",
254
+ type: "write",
255
+ description: "选择自有车辆配送 + 30 天后的时间",
256
+ commands: ({ storeId }) => [
257
+ `# ⚠️ 写操作 — 需要用户批准后才能执行`,
258
+ `ziniao-cli page visit --store-id ${storeId} --url "https://myaccount.mercadolibre.com.mx/shipping/inbounds" --wait-until networkidle`,
259
+ `# 在「Elige cómo y cuándo vas a enviar」部分:`,
260
+ `# 1. 选择「Vehículo particular」`,
261
+ `# 2. 选择 30 天之后的任意时间`,
262
+ `# 3. 点击「Continuar」`,
263
+ ],
264
+ forbidden: [
265
+ "禁止手动修改仓库地址",
266
+ ],
267
+ },
268
+ 5: {
269
+ name: "包装确认",
270
+ type: "write",
271
+ description: "确认产品包装合规",
272
+ commands: ({ storeId }) => [
273
+ `# ⚠️ 写操作 — 需要用户批准后才能执行`,
274
+ `# 在「Revisa la preparación」部分:`,
275
+ `# 1. 勾选确认框`,
276
+ `# 2. 确认「Confirmo que el producto tiene un empaque correcto」`,
277
+ `# 3. 点击「Continuar」`,
278
+ ],
279
+ onFailure: [
280
+ "超时无结果:禁止重复提交",
281
+ "先进入 FULL 列表页核查后台状态",
282
+ "确认是否已生成货件记录",
283
+ ],
284
+ },
285
+ 6: {
286
+ name: "标签下载",
287
+ type: "read",
288
+ description: "下载产品标签并按要求命名",
289
+ commands: ({ storeId }) => [
290
+ `ziniao-cli page visit --store-id ${storeId} --url "https://myaccount.mercadolibre.com.mx/shipping/inbounds" --wait-until networkidle`,
291
+ `# 找到对应货件 → 点击「Descargar etiquetas」→ 「Descargar」`,
292
+ `# 命名规则: Código_ML + SKU (例: YNWE47995+HW-MX-026-01)`,
293
+ ],
294
+ namingRule: "格式: Código ML + SKU。示例: YNWE47995+HW-MX-026-01",
295
+ },
296
+ 7: {
297
+ name: "打印箱唛",
298
+ type: "write",
299
+ description: "填写箱数、生成箱唛",
300
+ commands: ({ storeId }) => [
301
+ `# ⚠️ 写操作 — 需要用户批准后才能执行`,
302
+ `# 在「Prepara bultos y/o pallets」部分:`,
303
+ `# 1. 选择「Bultos sin agrupar en pallets」(散箱)`,
304
+ `# 2. 填写箱数`,
305
+ `# 3. 下载箱唛`,
306
+ ],
307
+ namingRule: "格式: 货件号 + SKU。示例: 71667033+HW-MX-026-01",
308
+ },
309
+ 8: {
310
+ name: "取消预约时间",
311
+ type: "write",
312
+ description: "仅预约下载箱唛,下载后必须取消",
313
+ commands: ({ storeId }) => [
314
+ `# ⚠️ 写操作 — 需要用户批准后才能执行`,
315
+ `# 操作步骤:`,
316
+ `# 1. 点击「Editar」`,
317
+ `# 2. 选择「Cancelar reserva」`,
318
+ `# 3. 确认「Cancelar cita」`,
319
+ ],
320
+ reason: "国内发货预约时间仅为下载箱唛,未预约时间无法下载箱唛。下载完箱唛后必须取消预约。",
321
+ },
322
+ };
323
+
324
+ // ---- 异常处理矩阵(保持不变)----
325
+ const EXCEPTIONS = {
326
+ timeout: {
327
+ symptoms: ["超时", "timeout", "sin respuesta", "cargando"],
328
+ action: "停止重复提交,等待 2 分钟后核查后台状态。无有效货件则核对 SKU 信息、调整数量重新创建。",
329
+ },
330
+ split_shipment: {
331
+ symptoms: ["分仓", "分件", "múltiples envíos", "separado"],
332
+ action: "按系统生成的多个货件 ID 分开打包、单独贴标、分别入仓。禁止混装合并。",
333
+ },
334
+ label_failure: {
335
+ symptoms: ["标签", "etiqueta", "descargar", "código de barras"],
336
+ action: "刷新页面或重新进入货件页下载,重新打印标签,确保条码可正常扫描入库。",
337
+ },
338
+ };
339
+
340
+ // ---- 禁止事项(保持不变)----
341
+ const FORBIDDEN = [
342
+ "禁止超系统推荐库容创建货件",
343
+ "禁止多批次、多货件ID产品混装同一外箱",
344
+ "禁止涂改、遮挡、模糊标签",
345
+ "禁止重复创建同款同数量货件",
346
+ ];
347
+
348
+ // ---- 辅助函数 ----
349
+
350
+ /** 将步骤信息格式化为 Markdown(参考 templates/report.md) */
351
+ function formatStepMarkdown(stepNum, step, output) {
352
+ const typeLabel = step.type === "write" ? "写操作(需批准)" : "只读";
353
+ const lines = [
354
+ `# 步骤 ${stepNum}: ${step.name}`,
355
+ "",
356
+ `**类型**: ${typeLabel}`,
357
+ `**描述**: ${step.description}`,
358
+ "",
359
+ "## 执行命令",
360
+ "",
361
+ "```",
362
+ ...output.commands,
363
+ "```",
364
+ ];
365
+
366
+ if (output.checks && output.checks.length > 0) {
367
+ lines.push("", "## 检查项");
368
+ for (const c of output.checks) {
369
+ lines.push(`- ☐ ${c}`);
370
+ }
371
+ }
372
+
373
+ if (output.onFailure && output.onFailure.length > 0) {
374
+ lines.push("", "## 失败处理");
375
+ for (const f of output.onFailure) {
376
+ lines.push(`- ${f}`);
377
+ }
378
+ }
379
+
380
+ if (output.forbidden && output.forbidden.length > 0) {
381
+ lines.push("", "## 禁止事项");
382
+ for (const f of output.forbidden) {
383
+ lines.push(`- ⛔ ${f}`);
384
+ }
385
+ }
386
+
387
+ if (output.namingRule) {
388
+ lines.push("", `**命名规则**: ${output.namingRule}`);
389
+ }
390
+
391
+ if (output.reason) {
392
+ lines.push("", `**说明**: ${output.reason}`);
393
+ }
394
+
395
+ return lines.join("\n");
396
+ }
397
+
398
+ /** 构建飞书推送卡片 JSON(对接 argent_cli/notification.py) */
399
+ function buildPushCard(stepNum, step, output) {
400
+ const typeLabel = step.type === "write" ? "写操作(需批准)" : "只读";
401
+ const skuCount = output.commands.filter((c) => c.startsWith("# SKU:")).length;
402
+ const contentLines = [
403
+ `步骤 ${stepNum}: ${step.name}`,
404
+ `类型: ${typeLabel}`,
405
+ `描述: ${step.description}`,
406
+ `命令数: ${output.commands.length}`,
407
+ skuCount > 0 ? `SKU 数: ${skuCount}` : "",
408
+ ].filter(Boolean);
409
+
410
+ const elements = [
411
+ { tag: "div", text: { tag: "lark_md", content: contentLines.join("\n") } },
412
+ ];
413
+
414
+ if (output.checks && output.checks.length > 0) {
415
+ const checksText = output.checks.map((c) => `☐ ${c}`).join("\n");
416
+ elements.push({ tag: "hr" });
417
+ elements.push({ tag: "div", text: { tag: "lark_md", content: `**检查项**:\n${checksText}` } });
418
+ }
419
+
420
+ if (output.forbidden && output.forbidden.length > 0) {
421
+ const forbiddenText = output.forbidden.map((f) => `⛔ ${f}`).join("\n");
422
+ elements.push({ tag: "hr" });
423
+ elements.push({ tag: "div", text: { tag: "lark_md", content: `**禁止事项**:\n${forbiddenText}` } });
424
+ }
425
+
426
+ return {
427
+ msg_type: "interactive",
428
+ card: {
429
+ header: {
430
+ title: { tag: "plain_text", content: `FULL 货件操作 - 步骤 ${stepNum}` },
431
+ },
432
+ elements,
433
+ },
434
+ };
435
+ }
436
+
437
+ // ---- CLI 入口 ----
438
+ function main() {
439
+ const args = process.argv.slice(2);
440
+ const cmd = args[0];
441
+
442
+ if (!cmd || cmd === "help") {
443
+ console.log(`用法:
444
+ node fulfillment.js status 查看当前进度
445
+ node fulfillment.js step <N> 输出步骤 N 的命令
446
+ node fulfillment.js step <N> --skus '[...]' 带 SKU 参数执行
447
+ node fulfillment.js step <N> --format markdown 以 Markdown 格式输出
448
+ node fulfillment.js step <N> --push 推送飞书卡片通知
449
+ node fulfillment.js check --shipment-id <ID> 检查指定货件状态
450
+ node fulfillment.js exceptions 查看异常处理矩阵
451
+ node fulfillment.js forbidden 查看禁止事项
452
+ node fulfillment.js help 显示此帮助
453
+
454
+ 使用 --store-id <ID> 指定店铺,或用 ziniao-cli store list 查看可用店铺。
455
+ `);
456
+ return;
457
+ }
458
+
459
+ if (cmd === "status") {
460
+ console.log(JSON.stringify({
461
+ total_steps: 8,
462
+ read_steps: [1, 2, 6],
463
+ write_steps: [3, 4, 5, 7, 8],
464
+ ready: true,
465
+ hint: "请使用 --store-id 指定店铺,或运行 ziniao-cli store list 查看可用店铺",
466
+ }, null, 2));
467
+ return;
468
+ }
469
+
470
+ if (cmd === "exceptions") {
471
+ console.log(JSON.stringify(EXCEPTIONS, null, 2));
472
+ return;
473
+ }
474
+
475
+ if (cmd === "forbidden") {
476
+ console.log(JSON.stringify(FORBIDDEN, null, 2));
477
+ return;
478
+ }
479
+
480
+ if (cmd === "step") {
481
+ const stepNum = parseInt(args[1], 10);
482
+ if (!STEPS[stepNum]) {
483
+ console.error(`错误: 无效步骤 "${stepNum}"。有效范围: 1-8`);
484
+ process.exit(1);
485
+ }
486
+ const step = STEPS[stepNum];
487
+
488
+ // 解析额外参数:--skus, --store-id, --shipment-id, --format, --push
489
+ const params = { storeId: "<请传入 --store-id>" };
490
+ let format = null;
491
+ let pushMode = false;
492
+
493
+ for (let i = 2; i < args.length; i++) {
494
+ if (args[i] === "--store-id" && args[i + 1]) {
495
+ params.storeId = args[i + 1];
496
+ i++;
497
+ } else if (args[i] === "--skus" && args[i + 1]) {
498
+ try { params.skus = JSON.parse(args[i + 1]); } catch { console.error("错误: --skus 参数格式无效,请提供有效的 JSON 数组"); process.exit(1); }
499
+ i++;
500
+ } else if (args[i] === "--shipment-id" && args[i + 1]) {
501
+ params.shipmentId = args[i + 1];
502
+ i++;
503
+ } else if (args[i] === "--format" && args[i + 1]) {
504
+ format = args[i + 1];
505
+ i++;
506
+ } else if (args[i] === "--push") {
507
+ pushMode = true;
508
+ }
509
+ }
510
+
511
+ const output = {
512
+ step: stepNum,
513
+ name: step.name,
514
+ type: step.type,
515
+ description: step.description,
516
+ requires_approval: step.type === "write",
517
+ commands: step.commands(params),
518
+ checks: step.checks || [],
519
+ onFailure: step.onFailure || [],
520
+ forbidden: step.forbidden || [],
521
+ namingRule: step.namingRule || null,
522
+ reason: step.reason || null,
523
+ exceptionMatrix: EXCEPTIONS,
524
+ selectors: SELECTORS[stepNum] || null,
525
+ };
526
+
527
+ // --format markdown:以 Markdown 格式输出
528
+ if (format === "markdown") {
529
+ console.log(formatStepMarkdown(stepNum, step, output));
530
+ return;
531
+ }
532
+
533
+ // --push:输出飞书卡片 JSON
534
+ if (pushMode) {
535
+ console.log(JSON.stringify(buildPushCard(stepNum, step, output), null, 2));
536
+ return;
537
+ }
538
+
539
+ // 默认 JSON 输出
540
+ console.log(JSON.stringify(output, null, 2));
541
+ return;
542
+ }
543
+
544
+ if (cmd === "check") {
545
+ let shipmentId = null;
546
+ for (let i = 1; i < args.length; i++) {
547
+ if (args[i] === "--shipment-id" && args[i + 1]) {
548
+ shipmentId = args[i + 1];
549
+ break;
550
+ }
551
+ }
552
+ if (!shipmentId) {
553
+ console.error("错误: 请提供 --shipment-id 参数指定要检查的货件 ID");
554
+ process.exit(1);
555
+ }
556
+ console.log(JSON.stringify({
557
+ action: "check_shipment",
558
+ shipment_id: shipmentId,
559
+ commands: [
560
+ `ziniao-cli page visit --store-id <ID> --url "https://myaccount.mercadolibre.com.mx/shipping/inbounds" --wait-until networkidle`,
561
+ `ziniao-cli page content --store-id <ID>`,
562
+ `# 在页面中搜索货件号 ${shipmentId}`,
563
+ `# 确认状态: En preparación / Procesamiento finalizado / Vencido`,
564
+ ],
565
+ }, null, 2));
566
+ return;
567
+ }
568
+
569
+ console.error(`错误: 未知命令 "${cmd}"。请使用 "node fulfillment.js help" 查看可用命令`);
570
+ process.exit(1);
571
+ }
572
+
573
+ // ---- 模块导出(供 fulfillment_orchestrator.py 通过 require 导入 SELECTORS/STEPS)----
574
+ if (require.main === module) {
575
+ main();
576
+ }
577
+
578
+ module.exports = {
579
+ SELECTORS,
580
+ STEPS,
581
+ EXCEPTIONS,
582
+ FORBIDDEN,
583
+ findByText,
584
+ formatStepMarkdown,
585
+ buildPushCard,
586
+ };