@whyshu/argent-engine 0.0.0-stage → 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.
- package/MANIFEST.json +341 -0
- package/engine/VERSION +1 -0
- package/engine/agents/catalog.json +454 -0
- package/engine/configs/_aliases.proposed.json +651 -0
- package/engine/configs/_migration-plan.proposed.json +899 -0
- package/engine/configs/account.json +272 -0
- package/engine/configs/advanced.json +205 -0
- package/engine/configs/host.json +149 -0
- package/engine/configs/hr.json +27 -0
- package/engine/configs/notify.json +264 -0
- package/engine/core/cli/argent.mjs +277 -0
- package/engine/core/contracts/argent.contracts.json +1065 -0
- package/engine/core/contracts/verbs.schema.json +2539 -0
- package/engine/core/lib/argent-datadir.mjs +223 -0
- package/engine/core/lib/audit.mjs +187 -0
- package/engine/core/lib/claim.mjs +258 -0
- package/engine/core/lib/config-catalog.mjs +1172 -0
- package/engine/core/lib/doctor-customer.mjs +577 -0
- package/engine/core/lib/flow-list.mjs +138 -0
- package/engine/core/lib/flow-registry.mjs +48 -0
- package/engine/core/lib/init.mjs +247 -0
- package/engine/core/lib/ledger.mjs +124 -0
- package/engine/core/lib/license-activate.mjs +813 -0
- package/engine/core/lib/license-device.mjs +314 -0
- package/engine/core/lib/license-file.mjs +349 -0
- package/engine/core/lib/license-guard.mjs +123 -0
- package/engine/core/lib/license-ops.mjs +1645 -0
- package/engine/core/lib/license-verdict.mjs +133 -0
- package/engine/core/lib/license.mjs +577 -0
- package/engine/core/lib/notify/dedup.mjs +101 -0
- package/engine/core/lib/notify/feishu.mjs +38 -0
- package/engine/core/lib/notify/index.mjs +95 -0
- package/engine/core/lib/notify/verb.mjs +327 -0
- package/engine/core/lib/notify/webhook.mjs +135 -0
- package/engine/core/lib/notify/wecom.mjs +36 -0
- package/engine/core/lib/python.mjs +145 -0
- package/engine/core/lib/sop.mjs +116 -0
- package/engine/core/lib/versions.mjs +205 -0
- package/engine/core/mcp/client.mjs +67 -0
- package/engine/core/mcp/http.mjs +138 -0
- package/engine/core/mcp/server.mjs +702 -0
- package/engine/scripts/_engine_rules_probe.py +15 -0
- package/engine/scripts/analyze-full-day.py +568 -0
- package/engine/scripts/archive/feishu-bridge.js +246 -0
- package/engine/scripts/argent-agents.mjs +527 -0
- package/engine/scripts/argent-capabilities.mjs +776 -0
- package/engine/scripts/argent-catalog.mjs +186 -0
- package/engine/scripts/argent-datadir.py +62 -0
- package/engine/scripts/argent-doctor.mjs +624 -0
- package/engine/scripts/argent-env-write.mjs +302 -0
- package/engine/scripts/argent-schedule.mjs +620 -0
- package/engine/scripts/argent-setup-check.mjs +369 -0
- package/engine/scripts/argent-upgrade.mjs +1046 -0
- package/engine/scripts/base-bootstrap.sh +576 -0
- package/engine/scripts/billing-mode-daily.sh +13 -0
- package/engine/scripts/build-engine-package.mjs +126 -0
- package/engine/scripts/capability-packs.lock.json +484 -0
- package/engine/scripts/capability_pack.py +1242 -0
- package/engine/scripts/check-1.0-criteria.mjs +355 -0
- package/engine/scripts/check-agent-catalog.mjs +141 -0
- package/engine/scripts/check-billing-mode.sh +378 -0
- package/engine/scripts/check-child-sessions.mjs +83 -0
- package/engine/scripts/check-config-catalog.mjs +677 -0
- package/engine/scripts/check-docs-drift.mjs +522 -0
- package/engine/scripts/check-dsh-web-expose.sh +98 -0
- package/engine/scripts/check-judgment-drift.mjs +403 -0
- package/engine/scripts/check-license.mjs +199 -0
- package/engine/scripts/check-model-auth.py +443 -0
- package/engine/scripts/check-pack-closure.mjs +661 -0
- package/engine/scripts/check-portable-symbols.mjs +116 -0
- package/engine/scripts/check-preset-landing.mjs +124 -0
- package/engine/scripts/check-production-home.mjs +602 -0
- package/engine/scripts/check-role-mapping.mjs +461 -0
- package/engine/scripts/check-sample-redaction.mjs +320 -0
- package/engine/scripts/check-skill-classification.mjs +188 -0
- package/engine/scripts/check-sop-consistency.mjs +431 -0
- package/engine/scripts/check-test-env-safety.mjs +627 -0
- package/engine/scripts/check-version-consistency.mjs +452 -0
- package/engine/scripts/ci-run.sh +558 -0
- package/engine/scripts/collect-evidence.mjs +42 -0
- package/engine/scripts/config-explain.mjs +278 -0
- package/engine/scripts/configure-provider.rb +29 -0
- package/engine/scripts/customer-config-migrate.mjs +179 -0
- package/engine/scripts/customer-preflight.py +892 -0
- package/engine/scripts/customer-readiness.sh +135 -0
- package/engine/scripts/daily-report-inbox.py +242 -0
- package/engine/scripts/daily-report-reconcile.mjs +312 -0
- package/engine/scripts/daily-report.mjs +1310 -0
- package/engine/scripts/daily-report.sh +47 -0
- package/engine/scripts/data/lip-product-dict.json +306 -0
- package/engine/scripts/deliver-customer.mjs +932 -0
- package/engine/scripts/delivery-faces.txt +31 -0
- package/engine/scripts/desktop-trial.sh +146 -0
- package/engine/scripts/dev-window.sh +261 -0
- package/engine/scripts/drill-clean-machine.sh +315 -0
- package/engine/scripts/dsh-api-client.mjs +215 -0
- package/engine/scripts/dsh-backup-scope.sh +83 -0
- package/engine/scripts/dsh-offline-pack.mjs +77 -0
- package/engine/scripts/dsh-overlay-pin.sh +87 -0
- package/engine/scripts/dsh-plane.mjs +153 -0
- package/engine/scripts/dsh-portforward.py +223 -0
- package/engine/scripts/dsh-preflight.mjs +179 -0
- package/engine/scripts/dsh-release-watch-daily.sh +19 -0
- package/engine/scripts/dsh-release-watch.mjs +458 -0
- package/engine/scripts/dsh-remote-check.sh +99 -0
- package/engine/scripts/dsh-restart.sh +179 -0
- package/engine/scripts/dsh-restore.sh +111 -0
- package/engine/scripts/dsh-scope-rewire.mjs +803 -0
- package/engine/scripts/dsh-snapshot.mjs +116 -0
- package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
- package/engine/scripts/dsh-upgrade.mjs +612 -0
- package/engine/scripts/dsh-url.sh +86 -0
- package/engine/scripts/dsh-web-launch.mjs +155 -0
- package/engine/scripts/ensure-store-windows.py +509 -0
- package/engine/scripts/entitlements.py +458 -0
- package/engine/scripts/env_probe.py +411 -0
- package/engine/scripts/feishu-card.mjs +252 -0
- package/engine/scripts/final-check.mjs +43 -0
- package/engine/scripts/fix-skill-frontmatter.py +209 -0
- package/engine/scripts/flow-auto-retry.py +454 -0
- package/engine/scripts/flow-requirements.json +735 -0
- package/engine/scripts/flow-scaffold.py +275 -0
- package/engine/scripts/flow-timers-plist.sh +87 -0
- package/engine/scripts/flow-timers.sh +184 -0
- package/engine/scripts/flow-trigger-wake.mjs +284 -0
- package/engine/scripts/flow-trigger.sh +571 -0
- package/engine/scripts/flow_billing.py +354 -0
- package/engine/scripts/flow_capability_stats.py +314 -0
- package/engine/scripts/flow_case.py +194 -0
- package/engine/scripts/flow_claim.py +497 -0
- package/engine/scripts/flow_config.py +1025 -0
- package/engine/scripts/flow_failure.py +588 -0
- package/engine/scripts/flow_learn.py +105 -0
- package/engine/scripts/flow_ledger.py +235 -0
- package/engine/scripts/flow_policy.py +238 -0
- package/engine/scripts/flow_preflight.py +339 -0
- package/engine/scripts/flow_registry.py +341 -0
- package/engine/scripts/flow_retry.py +304 -0
- package/engine/scripts/flow_return_to_ops.py +191 -0
- package/engine/scripts/flow_source.py +315 -0
- package/engine/scripts/flow_sources/__init__.py +53 -0
- package/engine/scripts/flow_sources/cases_json.py +106 -0
- package/engine/scripts/flow_sources/common.py +428 -0
- package/engine/scripts/flow_sources/feishu_base.py +273 -0
- package/engine/scripts/flow_stale.py +185 -0
- package/engine/scripts/flow_status_reconcile.py +344 -0
- package/engine/scripts/flow_triage.py +849 -0
- package/engine/scripts/flow_write_verify.py +496 -0
- package/engine/scripts/fulfillment.js +586 -0
- package/engine/scripts/gen-agent-bundles.mjs +276 -0
- package/engine/scripts/gen-preset-rows.mjs +259 -0
- package/engine/scripts/init-ml.sh +2278 -0
- package/engine/scripts/install-internal-role.sh +190 -0
- package/engine/scripts/install-license.mjs +641 -0
- package/engine/scripts/install-one-shot.mjs +665 -0
- package/engine/scripts/integration_contract.py +220 -0
- package/engine/scripts/integrations/wenyao/adapter.py +98 -0
- package/engine/scripts/integrations/xzh/adapter.py +75 -0
- package/engine/scripts/invoice-mapping.example.json +40 -0
- package/engine/scripts/invoice-table.sh +195 -0
- package/engine/scripts/invoice_carrier_notice.py +279 -0
- package/engine/scripts/invoice_fail_closeout.py +297 -0
- package/engine/scripts/invoice_lip_runner.py +1508 -0
- package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
- package/engine/scripts/invoice_ready_audit.py +191 -0
- package/engine/scripts/invoice_runner.py +675 -0
- package/engine/scripts/invoice_stageb.py +516 -0
- package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
- package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
- package/engine/scripts/lease.py +377 -0
- package/engine/scripts/lib/agent-install.mjs +649 -0
- package/engine/scripts/lib/argent-datadir.mjs +16 -0
- package/engine/scripts/lib/argent-datadir.sh +18 -0
- package/engine/scripts/lib/argent_datadir.py +285 -0
- package/engine/scripts/lib/argent_filelock.py +507 -0
- package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
- package/engine/scripts/lib/cdp_ports.py +197 -0
- package/engine/scripts/lib/customer-env.mjs +102 -0
- package/engine/scripts/lib/daily-report-core.mjs +517 -0
- package/engine/scripts/lib/dep-declaration.mjs +81 -0
- package/engine/scripts/lib/deps-migrate.mjs +310 -0
- package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
- package/engine/scripts/lib/dsh-scope.mjs +500 -0
- package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
- package/engine/scripts/lib/dsh-versions.mjs +96 -0
- package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
- package/engine/scripts/lib/is-direct-run.mjs +61 -0
- package/engine/scripts/lib/patch-compose.mjs +241 -0
- package/engine/scripts/lib/patch-target.mjs +35 -0
- package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
- package/engine/scripts/lib/plugin-packages.mjs +332 -0
- package/engine/scripts/lib/preset-compat.mjs +127 -0
- package/engine/scripts/lib/preset-install.mjs +575 -0
- package/engine/scripts/lib/preset-mechanism.mjs +56 -0
- package/engine/scripts/lib/preset-sync.mjs +59 -0
- package/engine/scripts/lib/profile-retire.mjs +97 -0
- package/engine/scripts/lib/python-runtime.mjs +190 -0
- package/engine/scripts/lib/registry.mjs +72 -0
- package/engine/scripts/lib/repo-root.sh +56 -0
- package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
- package/engine/scripts/lib/schedule-probe.mjs +835 -0
- package/engine/scripts/lib/scope-pointer.mjs +164 -0
- package/engine/scripts/lib/settings-panels.mjs +816 -0
- package/engine/scripts/lib/stdio_utf8.py +86 -0
- package/engine/scripts/lib/wake-auth.mjs +392 -0
- package/engine/scripts/lib/webdriver_ipc.py +264 -0
- package/engine/scripts/lib/zstd-frames.mjs +104 -0
- package/engine/scripts/license-e2e-drill.mjs +426 -0
- package/engine/scripts/license-gate.mjs +245 -0
- package/engine/scripts/lip_backlog.py +171 -0
- package/engine/scripts/lip_table.py +185 -0
- package/engine/scripts/local-usage.mjs +214 -0
- package/engine/scripts/migrate-providers.mjs +127 -0
- package/engine/scripts/migrate-skills.sh +65 -0
- package/engine/scripts/ml-oauth-setup.mjs +550 -0
- package/engine/scripts/ml-store-ids.json +26 -0
- package/engine/scripts/ml_artifact_attribution.py +333 -0
- package/engine/scripts/ml_capacity_probe.py +1158 -0
- package/engine/scripts/ml_hub_status.py +1556 -0
- package/engine/scripts/ml_schema_check.py +115 -0
- package/engine/scripts/ml_store_ids.py +139 -0
- package/engine/scripts/npm-publish-argent.mjs +499 -0
- package/engine/scripts/npm-publish-argent.sh +20 -0
- package/engine/scripts/orchestrator.py +13811 -0
- package/engine/scripts/pack-plugins.mjs +140 -0
- package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
- package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
- package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
- package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
- package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
- package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
- package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
- package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
- package/engine/scripts/patches/verify-hardening.mjs +167 -0
- package/engine/scripts/patches/verify-session-migration.mjs +149 -0
- package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
- package/engine/scripts/plan-role-rename.mjs +125 -0
- package/engine/scripts/plugin-bootcheck.mjs +451 -0
- package/engine/scripts/plugin-composition.json +15 -0
- package/engine/scripts/poc-mcp-host.mjs +455 -0
- package/engine/scripts/poll-fulfillment.sh +295 -0
- package/engine/scripts/poll-invoice.sh +76 -0
- package/engine/scripts/profile-doctor.mjs +686 -0
- package/engine/scripts/provider-select.sh +71 -0
- package/engine/scripts/provider-switch.sh +146 -0
- package/engine/scripts/provider-sync-target.check.mjs +64 -0
- package/engine/scripts/python-runtime.sh +78 -0
- package/engine/scripts/query_runner.py +310 -0
- package/engine/scripts/release-argent.sh +574 -0
- package/engine/scripts/repair-unresolved-tools.mjs +178 -0
- package/engine/scripts/retire-inventory.mjs +40 -0
- package/engine/scripts/run-tests.sh +257 -0
- package/engine/scripts/s1-audit.mjs +85 -0
- package/engine/scripts/self-sign-license.mjs +179 -0
- package/engine/scripts/session-health-scan.mjs +63 -0
- package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
- package/engine/scripts/setup.sh +22 -0
- package/engine/scripts/skill-install.mjs +629 -0
- package/engine/scripts/sku_translate.py +148 -0
- package/engine/scripts/sync-plugins.sh +90 -0
- package/engine/scripts/sync-presets.sh +27 -0
- package/engine/scripts/sync-scripts.sh +445 -0
- package/engine/scripts/telemetry.py +873 -0
- package/engine/scripts/ui-copy-count.mjs +220 -0
- package/engine/scripts/usage-anchor.sh +137 -0
- package/engine/scripts/usage_report.py +531 -0
- package/engine/scripts/validate-preset-schema.mjs +208 -0
- package/engine/scripts/vertical-init.mjs +323 -0
- package/engine/scripts/vertical-lint.mjs +482 -0
- package/engine/scripts/wenyao-browser.sh +74 -0
- package/engine/scripts/wenyao-fixture-export.py +116 -0
- package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
- package/engine/scripts/wenyao-login.sh +35 -0
- package/engine/scripts/wenyao-template-map.json +292 -0
- package/engine/scripts/wenyao_attach.py +131 -0
- package/engine/scripts/wenyao_download_template.py +144 -0
- package/engine/scripts/wenyao_fill_template.py +568 -0
- package/engine/scripts/wenyao_login.py +145 -0
- package/engine/scripts/wenyao_portal.py +633 -0
- package/engine/scripts/wenyao_recon.py +230 -0
- package/engine/scripts/wenyao_run.py +503 -0
- package/engine/scripts/wenyao_shadow_diff.py +160 -0
- package/engine/scripts/wenyao_skill_contract.py +156 -0
- package/engine/scripts/wenyao_template_fetch.py +175 -0
- package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
- package/engine/scripts/ziniao-auth-probe.sh +268 -0
- package/engine/scripts/ziniao-keepalive.sh +217 -0
- package/engine/scripts/ziniao-post-upgrade-check.sh +121 -0
- package/engine/scripts/ziniao-windows.sh +154 -0
- package/engine/skills/README.md +86 -0
- package/engine/skills/argent-dev/SKILL.md +41 -0
- package/engine/skills/internal.json +37 -0
- package/engine/skills/lktop-pm/SKILL.md +35 -0
- package/engine/skills/lktopapp-dev/SKILL.md +34 -0
- package/engine/skills/ml-config-guide/SKILL.md +50 -0
- package/engine/skills/ml-fulfillment-sop/SKILL.md +178 -0
- package/engine/skills/ml-fulfillment-sop/references/andes-components.md +69 -0
- package/engine/skills/ml-fulfillment-sop/references/cdp-port-mapping.md +35 -0
- package/engine/skills/ml-fulfillment-sop/references/feishu-base-fulfillment-schema.md +136 -0
- package/engine/skills/ml-fulfillment-sop/references/multi-window-concurrency.md +69 -0
- package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +77 -0
- package/engine/skills/ml-fulfillment-sop/references/selectors.md +243 -0
- package/engine/skills/ml-fulfillment-sop/scripts/fulfillment.js +586 -0
- package/engine/skills/ml-fulfillment-sop/scripts/poll-and-execute.sh +271 -0
- package/engine/skills/ml-fulfillment-sop/templates/report.md +55 -0
- package/engine/skills/ml-invoice/SKILL.md +506 -0
- package/engine/skills/ml-invoice/references/pitfalls.md +268 -0
- package/engine/skills/ml-invoice/references/portal-flow.md +360 -0
- package/engine/skills/ml-invoice/references/template-fill-contract.md +133 -0
- package/engine/skills/ml-invoice-xzh/SKILL.md +133 -0
- package/engine/skills/ml-invoice-xzh/references/pitfalls.md +230 -0
- package/engine/skills/ml-store-daily-report/SKILL.md +224 -0
- package/engine/skills/ml-store-daily-report/scripts/README.md +25 -0
- package/engine/skills/ml-store-daily-report/scripts/extract.js +691 -0
- package/engine/skills/ml-store-daily-report/scripts/generate.js +299 -0
- package/engine/skills/pet-dev/SKILL.md +42 -0
- package/engine/skills/whyshu-pm/SKILL.md +35 -0
- package/engine/skills/whyshu-web-dev/SKILL.md +50 -0
- package/engine/version.mjs +1 -0
- package/engine/verticals/README.md +103 -0
- package/engine/verticals/_engine-words.baseline.txt +237 -0
- package/engine/verticals/_engine-words.json +19 -0
- package/engine/verticals/mercadolibre/README.md +43 -0
- package/engine/verticals/mercadolibre/config.json +709 -0
- package/engine/verticals/mercadolibre/flows.json +738 -0
- package/engine/verticals/mercadolibre/manifest.json +538 -0
- package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
- package/engine/verticals/mercadolibre/preflight.py +143 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
- package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
- package/engine/verticals/mercadolibre/prompts/full.md +91 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
- package/package.json +22 -4
- package/requirements.txt +5 -0
- package/README.md +0 -3
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ml-oauth-setup.mjs — 美客多官方 API OAuth 授权向导(Argent on DSH · 日报数据源)
|
|
4
|
+
*
|
|
5
|
+
* 一次性配置:把卖家的每个店铺账号授权给卖家自己的开发者应用,
|
|
6
|
+
* 生成 ~/.dsh/argent/ml-api-stores.json(每店 user_id + refresh_token + access_token)。
|
|
7
|
+
*
|
|
8
|
+
* 前置:
|
|
9
|
+
* 1. 卖家在 https://developers.mercadolibre.com 创建应用,拿到 App ID / Secret;
|
|
10
|
+
* 2. 把 ML_APP_ID / ML_APP_SECRET 写入 ~/.dsh/argent/daily-report.env
|
|
11
|
+
* (模板由 init-ml.sh 生成,见 docs/daily-report.md)。
|
|
12
|
+
*
|
|
13
|
+
* 用法:
|
|
14
|
+
* node ml-oauth-setup.mjs # 交互引导:逐店授权(推荐)
|
|
15
|
+
* node ml-oauth-setup.mjs --redirect-uri <url> # 覆盖回调地址(默认 https://example.com/callback)
|
|
16
|
+
* node ml-oauth-setup.mjs --https # 本地回环 https 回调(老场景/特殊站点;自签证书)
|
|
17
|
+
* node ml-oauth-setup.mjs --list # 列出已授权店铺
|
|
18
|
+
* # 非交互(**异步授权**推荐:客户在飞书把回调 URL 发回来,一条命令落库)
|
|
19
|
+
* node ml-oauth-setup.mjs --store 1店主账号 --site MLM --code '<回调URL或纯code>'
|
|
20
|
+
* · 自动用 ml-store-ids.json 交叉校验授权账号是不是这家店(不符则**拒绝落库**)
|
|
21
|
+
* · --expect-uid <id> 显式指定期望 uid;--force 跳过校验(危险)
|
|
22
|
+
* node ml-oauth-setup.mjs --remove <名称> # 移除店铺授权
|
|
23
|
+
* node ml-oauth-setup.mjs --refresh-all # 校验并刷新全部 access token
|
|
24
|
+
* node ml-oauth-setup.mjs --env <路径> # 指定 daily-report.env 路径
|
|
25
|
+
*
|
|
26
|
+
* 数据: ~/.dsh/argent/ml-api-stores.json(chmod 600,update 不覆盖)
|
|
27
|
+
* 说明: access_token 6 小时有效;daily-report.mjs 每次运行自动用 refresh_token
|
|
28
|
+
* 换新(refresh_token 轮换,回写本文件)。refresh_token 有效期约 6 个月,
|
|
29
|
+
* 过期后需重新授权(重跑本向导 --remove + 重新添加,或直接重跑交互流程)。
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { createServer } from 'node:http'
|
|
33
|
+
import { createServer as createHttpsServer } from 'node:https'
|
|
34
|
+
import { execFileSync } from 'node:child_process'
|
|
35
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from 'node:fs'
|
|
36
|
+
import { homedir } from 'node:os'
|
|
37
|
+
import { fileURLToPath } from 'node:url'
|
|
38
|
+
import { join } from 'node:path'
|
|
39
|
+
import readline from 'node:readline'
|
|
40
|
+
import { dirname } from 'node:path'
|
|
41
|
+
import { isDirectRun } from './lib/is-direct-run.mjs'
|
|
42
|
+
import { resolveDataDir } from './lib/argent-datadir.mjs'
|
|
43
|
+
|
|
44
|
+
const API_BASE = process.env.ML_API_BASE || 'https://api.mercadolibre.com'
|
|
45
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
|
|
46
|
+
// 授权域名按站点取国家域名:CloudFront WAF(2026)对通用域名 auth.mercadolibre.com 拦截
|
|
47
|
+
// redirect_uri 指向 127.0.0.1/localhost(SSRF 防护),而站点国家域名(如 .com.mx)放行 localhost。
|
|
48
|
+
// 已实测:MLM 用 auth.mercadolibre.com.mx + https://localhost:8080/callback 通过(302)。
|
|
49
|
+
// 其他站点域名实测被拦(403),后续逐个站点验证后更新此表。
|
|
50
|
+
const AUTH_HOSTS = {
|
|
51
|
+
MLM: 'auth.mercadolibre.com.mx',
|
|
52
|
+
MLB: 'auth.mercadolibre.com.br',
|
|
53
|
+
MLA: 'auth.mercadolibre.com.ar',
|
|
54
|
+
MLC: 'auth.mercadolibre.cl',
|
|
55
|
+
MCO: 'auth.mercadolibre.com.co',
|
|
56
|
+
MPE: 'auth.mercadolibre.com.pe',
|
|
57
|
+
MLU: 'auth.mercadolibre.com.uy',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const SITE_CHOICES = [
|
|
61
|
+
['MLM', '墨西哥'],
|
|
62
|
+
['MLB', '巴西'],
|
|
63
|
+
['MLA', '阿根廷'],
|
|
64
|
+
['MLC', '智利'],
|
|
65
|
+
['MCO', '哥伦比亚'],
|
|
66
|
+
['MPE', '秘鲁'],
|
|
67
|
+
['MLU', '乌拉圭'],
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
// ── 参数 ───────────────────────────────────────────────────
|
|
71
|
+
const args = process.argv.slice(2)
|
|
72
|
+
const argVal = (name) => {
|
|
73
|
+
const i = args.indexOf(name)
|
|
74
|
+
return i >= 0 && args[i + 1] ? args[i + 1] : null
|
|
75
|
+
}
|
|
76
|
+
// 数据目录:内核三级链(唯一判据;原为硬编码 ~/.dsh/argent)
|
|
77
|
+
const DATA_DIR = resolveDataDir({ env: process.env }).dir
|
|
78
|
+
const ENV_PATH = argVal('--env') || join(DATA_DIR, 'daily-report.env')
|
|
79
|
+
const STORES_PATH = process.env.ML_API_STORES_JSON || join(DATA_DIR, 'ml-api-stores.json')
|
|
80
|
+
// 回调端口:美客多对 redirect_uri 精确匹配 → 必须固定端口(默认 8080),
|
|
81
|
+
// 卖家在开发者应用里注册 http(s)://127.0.0.1:8080/callback。--redirect-port 覆盖;--port 0 随机(仅测试)
|
|
82
|
+
// TLS:美客多新门户强制 Redirect URI 以 https:// 开头(2026 起)→ --https 开启自签证书回调
|
|
83
|
+
const REDIRECT_PORT = args.includes('--port') ? Number(argVal('--port')) : Number(argVal('--redirect-port') || 8080)
|
|
84
|
+
const USE_TLS = args.includes('--https')
|
|
85
|
+
const FLAG_LIST = args.includes('--list')
|
|
86
|
+
const FLAG_REMOVE = args.includes('--remove')
|
|
87
|
+
const FLAG_REFRESH = args.includes('--refresh-all')
|
|
88
|
+
const FLAG_CODE = args.includes('--code') // 非交互一键授权(值可为空 ⇒ 报错而不是掉进交互)
|
|
89
|
+
|
|
90
|
+
// ── 小工具 ─────────────────────────────────────────────────
|
|
91
|
+
function loadEnvFile(path) {
|
|
92
|
+
const out = {}
|
|
93
|
+
if (!existsSync(path)) return out
|
|
94
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
95
|
+
const t = line.trim()
|
|
96
|
+
if (!t || t.startsWith('#')) continue
|
|
97
|
+
const eq = t.indexOf('=')
|
|
98
|
+
if (eq > 0) out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim()
|
|
99
|
+
}
|
|
100
|
+
return out
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function loadStores() {
|
|
104
|
+
if (!existsSync(STORES_PATH)) return { stores: [] }
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(readFileSync(STORES_PATH, 'utf8'))
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error(`⚠️ ml-api-stores.json 解析失败(${err.message}),按空表处理`)
|
|
109
|
+
return { stores: [] }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function saveStores(data) {
|
|
114
|
+
mkdirSync(DATA_DIR, { recursive: true })
|
|
115
|
+
writeFileSync(STORES_PATH, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 })
|
|
116
|
+
try { chmodSync(STORES_PATH, 0o600) } catch { /* 平台差异忽略 */ }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 非交互 stdin(管道 /dev/null)下的健壮处理:
|
|
120
|
+
// - readableEnded 快速路径:EOF 已发生后新建 readline 接口收不到已过去的 close 事件,直接返回默认值
|
|
121
|
+
// - close 监听兜底:本次读取恰好遇到 EOF 时用默认值继续(EOF 时 question 回调可能永不触发)
|
|
122
|
+
// ASK_EOF 供调用方在非交互下退出交互循环(如「继续授权?」默认 Y 会死循环)
|
|
123
|
+
let ASK_EOF = false
|
|
124
|
+
// 非 TTY(管道/重定向)下 readline 的语义坑:**没有待答 question 时到达的行直接丢弃**
|
|
125
|
+
// ⇒ `printf '店名\n站点\n回调URL\n' | node …` 只会答上前一两问,粘贴那步拿到空、静默 `成功授权 0 家`。
|
|
126
|
+
// 所以非 TTY 时先把 stdin 读成行队列,再按顺序分发(并把消费到的行回显到日志里,便于复盘)。
|
|
127
|
+
let _piped = null
|
|
128
|
+
async function loadPipedLines() {
|
|
129
|
+
if (_piped) return _piped
|
|
130
|
+
if (process.stdin.isTTY) { _piped = null; return null }
|
|
131
|
+
const chunks = []
|
|
132
|
+
for await (const c of process.stdin) chunks.push(c)
|
|
133
|
+
_piped = Buffer.concat(chunks).toString('utf8').split('\n').map((l) => l.replace(/\r$/, ''))
|
|
134
|
+
while (_piped.length && _piped[_piped.length - 1] === '') _piped.pop()
|
|
135
|
+
return _piped
|
|
136
|
+
}
|
|
137
|
+
// ⚠️ 必须**全程共用一个** readline 接口(2026-09-24 事故):
|
|
138
|
+
// 每次 ask() 新建接口时,Node 已经把 stdin 的一整块读进第一个接口的缓冲,
|
|
139
|
+
// 接口一 close,缓冲里**后面几行就丢了** ⇒ 表现为「店铺名/站点答上了,粘贴回调 URL 那一步却是空」
|
|
140
|
+
// ⇒ 静默 `成功授权 0 家店铺`(授权码还被白烧掉)。共享一个接口后,排队输入按顺序发给各个 prompt。
|
|
141
|
+
let _rl = null
|
|
142
|
+
function getRl() {
|
|
143
|
+
if (!_rl) {
|
|
144
|
+
_rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
145
|
+
_rl.on('close', () => { ASK_EOF = true })
|
|
146
|
+
}
|
|
147
|
+
return _rl
|
|
148
|
+
}
|
|
149
|
+
function ask(question, def = '') {
|
|
150
|
+
if (_piped) { // 非 TTY:从行队列里取(确定性)
|
|
151
|
+
const prompt = def ? `${question} [${def}]: ` : `${question}: `
|
|
152
|
+
process.stdout.write(prompt)
|
|
153
|
+
if (!_piped.length) { ASK_EOF = true; process.stdout.write('\n'); return Promise.resolve(def) }
|
|
154
|
+
const line = _piped.shift()
|
|
155
|
+
process.stdout.write(`${line}\n`)
|
|
156
|
+
return Promise.resolve(line.trim() || def)
|
|
157
|
+
}
|
|
158
|
+
if (ASK_EOF || process.stdin.readableEnded) { ASK_EOF = true; return Promise.resolve(def) }
|
|
159
|
+
const rl = getRl()
|
|
160
|
+
return new Promise((resolve) => {
|
|
161
|
+
let answered = false
|
|
162
|
+
const onClose = () => done('', true)
|
|
163
|
+
const done = (ans, eof = false) => {
|
|
164
|
+
if (answered) return
|
|
165
|
+
answered = true
|
|
166
|
+
if (eof) ASK_EOF = true
|
|
167
|
+
rl.removeListener('close', onClose)
|
|
168
|
+
resolve(String(ans == null ? '' : ans).trim() || def)
|
|
169
|
+
}
|
|
170
|
+
rl.once('close', onClose)
|
|
171
|
+
rl.question(def ? `${question} [${def}]: ` : `${question}: `, (ans) => done(ans))
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function apiToken(form) {
|
|
176
|
+
const res = await fetch(`${API_BASE}/oauth/token`, {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
179
|
+
body: new URLSearchParams(form).toString(),
|
|
180
|
+
})
|
|
181
|
+
const body = await res.json().catch(() => ({}))
|
|
182
|
+
if (!res.ok || !body.access_token) {
|
|
183
|
+
throw new Error(`token 交换失败 HTTP ${res.status}: ${JSON.stringify(body).slice(0, 300)}`)
|
|
184
|
+
}
|
|
185
|
+
return body
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function apiGet(path, token) {
|
|
189
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
190
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
191
|
+
})
|
|
192
|
+
const body = await res.json().catch(() => ({}))
|
|
193
|
+
if (!res.ok) throw new Error(`API ${path} 失败 HTTP ${res.status}: ${JSON.stringify(body).slice(0, 300)}`)
|
|
194
|
+
return body
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function exchangeCode(appId, appSecret, redirectUri, code) {
|
|
198
|
+
return apiToken({
|
|
199
|
+
grant_type: 'authorization_code',
|
|
200
|
+
client_id: appId,
|
|
201
|
+
client_secret: appSecret,
|
|
202
|
+
code,
|
|
203
|
+
redirect_uri: redirectUri,
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function refreshAccess(appId, appSecret, refreshToken) {
|
|
208
|
+
return apiToken({
|
|
209
|
+
grant_type: 'refresh_token',
|
|
210
|
+
client_id: appId,
|
|
211
|
+
client_secret: appSecret,
|
|
212
|
+
refresh_token: refreshToken,
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── TLS 自签证书(--https 模式)────────────────────────────
|
|
217
|
+
// 证书缓存在 ~/.dsh/argent/ml-oauth-cert/,不存在时用 openssl 生成(macOS/Linux 自带)
|
|
218
|
+
function ensureTlsCert() {
|
|
219
|
+
const dir = join(DATA_DIR, 'ml-oauth-cert')
|
|
220
|
+
const keyPath = join(dir, 'key.pem')
|
|
221
|
+
const certPath = join(dir, 'cert.pem')
|
|
222
|
+
if (!existsSync(keyPath) || !existsSync(certPath)) {
|
|
223
|
+
mkdirSync(dir, { recursive: true })
|
|
224
|
+
execFileSync('openssl', [
|
|
225
|
+
'req', '-x509', '-newkey', 'rsa:2048',
|
|
226
|
+
'-keyout', keyPath, '-out', certPath,
|
|
227
|
+
'-days', '365', '-nodes',
|
|
228
|
+
'-subj', '/CN=localhost',
|
|
229
|
+
'-addext', 'subjectAltName=DNS:localhost,IP:127.0.0.1',
|
|
230
|
+
], { stdio: 'ignore' })
|
|
231
|
+
}
|
|
232
|
+
return { key: readFileSync(keyPath), cert: readFileSync(certPath) }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── 授权码解析(模块级:交互与非交互共用)──────────────────
|
|
236
|
+
function extractCode(input) {
|
|
237
|
+
const t = String(input || '').trim()
|
|
238
|
+
if (!t) return ''
|
|
239
|
+
try {
|
|
240
|
+
const u = new URL(t)
|
|
241
|
+
// 是 URL:必须有 code,否则无效(避免把 example.com 这种地址当 code 发出去)
|
|
242
|
+
return u.searchParams.get('code') || ''
|
|
243
|
+
} catch { /* 不是 URL,当纯 code 值处理 */ }
|
|
244
|
+
return t
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── 授权账号 ↔ 店铺 交叉校验(防串店)──────────────────────
|
|
248
|
+
// ml-store-ids.json 是「店铺 → ML 卖家 user_id」的唯一真源(键 = 国家-店号)。
|
|
249
|
+
// 从任意店铺名里取 `<N>店`,再按「墨西哥-<N>店」查表 —— 与发票链路同一口径。
|
|
250
|
+
function expectedUidForStore(name) {
|
|
251
|
+
const m = String(name || '').match(/(\d+)店/)
|
|
252
|
+
if (!m) return null
|
|
253
|
+
const country = /巴西/.test(name) ? '巴西' : '墨西哥'
|
|
254
|
+
try {
|
|
255
|
+
const table = JSON.parse(readFileSync(join(SCRIPT_DIR, 'ml-store-ids.json'), 'utf8'))
|
|
256
|
+
const uid = (table.stores || {})[`${country}-${m[1]}店`]
|
|
257
|
+
return uid ? String(uid) : null
|
|
258
|
+
} catch { return null }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function verifyUid(name, uid, expectUidArg) {
|
|
262
|
+
const expected = expectUidArg ? String(expectUidArg) : expectedUidForStore(name)
|
|
263
|
+
if (!expected) return { ok: true, expected: null, why: '(表里没有这家店的期望 uid ⇒ 跳过校验)' }
|
|
264
|
+
if (String(uid) === expected) return { ok: true, expected, why: '与 ml-store-ids.json 一致' }
|
|
265
|
+
return { ok: false, expected, why: `授权账号 uid=${uid} ≠ 期望 ${expected}(**可能是子账号/别的店窗口**)` }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── 落库(交互与非交互共用;同 user_id 视为重复授权 ⇒ 覆盖)──
|
|
269
|
+
function persistStore(name, site, tok, me) {
|
|
270
|
+
const stores = loadStores()
|
|
271
|
+
const idx = stores.stores.findIndex((s) => String(s.user_id) === String(tok.user_id))
|
|
272
|
+
const entry = {
|
|
273
|
+
name,
|
|
274
|
+
site,
|
|
275
|
+
user_id: tok.user_id,
|
|
276
|
+
nickname: (me && me.nickname) || '',
|
|
277
|
+
refresh_token: tok.refresh_token,
|
|
278
|
+
access_token: tok.access_token,
|
|
279
|
+
access_expires_at: new Date(Date.now() + (tok.expires_in || 21600) * 1000).toISOString(),
|
|
280
|
+
added_at: new Date().toISOString(),
|
|
281
|
+
}
|
|
282
|
+
if (idx >= 0) { stores.stores[idx] = entry; saveStores(stores); return { updated: true, entry } }
|
|
283
|
+
stores.stores.push(entry); saveStores(stores); return { updated: false, entry }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── 非交互一键授权(异步授权:客户把回调 URL 发回来 → 一条命令落库)──
|
|
287
|
+
async function authorizeOnce() {
|
|
288
|
+
const env = loadEnvFile(ENV_PATH)
|
|
289
|
+
const appId = env.ML_APP_ID || process.env.ML_APP_ID
|
|
290
|
+
const appSecret = env.ML_APP_SECRET || process.env.ML_APP_SECRET
|
|
291
|
+
const name = argVal('--store')
|
|
292
|
+
const site = (argVal('--site') || 'MLM').toUpperCase()
|
|
293
|
+
const rawCode = argVal('--code')
|
|
294
|
+
const code = extractCode(rawCode)
|
|
295
|
+
if (!name) { console.error('✗ 非交互模式必须给 --store <店铺名>(如 --store 1店主账号)'); return 1 }
|
|
296
|
+
if (!appId || !appSecret) { console.error(`✗ 缺少应用凭据(${ENV_PATH} 里的 ML_APP_ID / ML_APP_SECRET)`); return 1 }
|
|
297
|
+
if (!code) {
|
|
298
|
+
console.error('✗ --code 为空或里面没有 code= 参数。请粘贴**整段回调 URL**(如 https://example.com/callback?code=TG-…)')
|
|
299
|
+
return 1
|
|
300
|
+
}
|
|
301
|
+
const redirectUri = argVal('--redirect-uri') || 'https://example.com/callback'
|
|
302
|
+
try {
|
|
303
|
+
const tok = await exchangeCode(appId, appSecret, redirectUri, code)
|
|
304
|
+
const me = await apiGet('/users/me', tok.access_token)
|
|
305
|
+
const chk = verifyUid(name, tok.user_id, argVal('--expect-uid'))
|
|
306
|
+
console.log(` 授权账号:${me.nickname || '(无昵称)'} uid=${tok.user_id} site=${me.site_id || site}`)
|
|
307
|
+
console.log(` 店铺校验:${chk.ok ? '✅' : '❌'} ${chk.why}`)
|
|
308
|
+
if (!chk.ok && !args.includes('--force')) {
|
|
309
|
+
console.error('✗ **拒绝落库**:授权账号与店铺不匹配(授权码已消耗,请在正确的店铺窗口重新授权)')
|
|
310
|
+
return 1
|
|
311
|
+
}
|
|
312
|
+
const r = persistStore(name, site, tok, me)
|
|
313
|
+
console.log(`✅ ${r.updated ? '已更新' : '已新增'}授权:${name}(${r.entry.nickname || r.entry.user_id})→ ${STORES_PATH}`)
|
|
314
|
+
console.log(` 下一步:node scripts/daily-report.mjs --force(或等约次日推送)`)
|
|
315
|
+
return 0
|
|
316
|
+
} catch (err) {
|
|
317
|
+
console.error(`✗ 授权失败:${err.message}`)
|
|
318
|
+
console.error(' 常见原因:授权码已过期/已被使用(约 10 分钟有效,且只能用一次)⇒ 让客户重新点一次链接,把新的回调 URL 发来')
|
|
319
|
+
return 1
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── 交互授权(本地回调)────────────────────────────────────
|
|
324
|
+
// 返回 { server, port, waitCode }:waitCode 是 Promise<code>(失败 reject)
|
|
325
|
+
// port 固定(美客多 redirect_uri 精确匹配);端口被占用时抛错提示换端口
|
|
326
|
+
function startCallbackServer(port) {
|
|
327
|
+
return new Promise((resolve, reject) => {
|
|
328
|
+
const handler = (req, res) => {
|
|
329
|
+
const url = new URL(req.url, 'http://127.0.0.1')
|
|
330
|
+
if (url.pathname !== '/callback') {
|
|
331
|
+
res.writeHead(404); res.end(); return
|
|
332
|
+
}
|
|
333
|
+
const code = url.searchParams.get('code')
|
|
334
|
+
const error = url.searchParams.get('error')
|
|
335
|
+
const desc = url.searchParams.get('error_description') || ''
|
|
336
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
337
|
+
if (code) {
|
|
338
|
+
res.end('<html><body><h3>✅ 授权成功,可以关闭此页面了</h3></body></html>')
|
|
339
|
+
server.close()
|
|
340
|
+
waitResolve(code)
|
|
341
|
+
} else {
|
|
342
|
+
res.end(`<html><body><h3>❌ 授权失败:${error} ${desc}</h3></body></html>`)
|
|
343
|
+
server.close()
|
|
344
|
+
waitReject(new Error(`授权失败:${error} ${desc}`))
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const server = USE_TLS ? createHttpsServer(ensureTlsCert(), handler) : createServer(handler)
|
|
348
|
+
server.on('error', (err) => {
|
|
349
|
+
reject(new Error(err.code === 'EADDRINUSE'
|
|
350
|
+
? `回调端口 ${port} 被占用——换一个端口(--redirect-port <n>),并同步修改开发者应用里注册的 Redirect URI`
|
|
351
|
+
: `回调服务启动失败:${err.message}`))
|
|
352
|
+
})
|
|
353
|
+
let waitResolve, waitReject
|
|
354
|
+
const waitCode = new Promise((res, rej) => { waitResolve = res; waitReject = rej })
|
|
355
|
+
server.listen(port, '127.0.0.1', () => {
|
|
356
|
+
resolve({ server, port: server.address().port, waitCode })
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function addStoreInteractive(env) {
|
|
362
|
+
const appId = env.ML_APP_ID || process.env.ML_APP_ID
|
|
363
|
+
const appSecret = env.ML_APP_SECRET || process.env.ML_APP_SECRET
|
|
364
|
+
if (!appId || !appSecret) {
|
|
365
|
+
console.error('✗ 缺少 ML_APP_ID / ML_APP_SECRET(写入 daily-report.env 或环境变量)')
|
|
366
|
+
return false
|
|
367
|
+
}
|
|
368
|
+
const name = await ask('店铺名称(用于日报展示)', '墨西哥站')
|
|
369
|
+
console.log('可选站点:' + SITE_CHOICES.map(([c, n]) => `${c}=${n}`).join(' '))
|
|
370
|
+
const site = (await ask('站点代码', 'MLM')).toUpperCase()
|
|
371
|
+
if (!SITE_CHOICES.some(([c]) => c === site)) {
|
|
372
|
+
console.log(`⚠️ 未知站点 ${site},仍可继续(时区按站点代码匹配,未知则用 America/Mexico_City)`)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Redirect URI:默认 https://example.com/callback(2026 新门户表单只认真实域名,
|
|
376
|
+
// localhost/127.0.0.1 均被拒;example.com 为 IANA 保留域名,可作占位回调地址)。
|
|
377
|
+
// 授权后浏览器跳到该地址(内容无关),从地址栏复制整段 URL 粘贴回来即可。
|
|
378
|
+
// 正式环境可换问述自有域名:--redirect-uri https://whyshu.com/ml-oauth-callback(需应用注册一致)
|
|
379
|
+
const redirectUri = argVal('--redirect-uri') || 'https://example.com/callback'
|
|
380
|
+
// 本地回环回调(老场景/特殊站点):起本地回调服务器自动收 code
|
|
381
|
+
const isLocalCallback = /^(127\.0\.0\.1|localhost|\[::1\])/.test(new URL(redirectUri).hostname)
|
|
382
|
+
// 授权域名:站点国家域名(CloudFront WAF 对通用域名 + 本地回环 redirect_uri 拦 403)
|
|
383
|
+
const authBase = `https://${AUTH_HOSTS[site] || 'auth.mercadolibre.com'}/authorization`
|
|
384
|
+
console.log('\n步骤:')
|
|
385
|
+
console.log(` 0. 确认卖家开发者应用里注册的 Redirect URI 为:${redirectUri}`)
|
|
386
|
+
console.log(' (developers.mercadolibre.com → 应用 → Redirect URI;美客多要求与授权请求精确匹配)')
|
|
387
|
+
console.log(` 1. 打开下面链接(用【${name}】店铺的紫鸟浏览器打开,确保已登录该店账号)`)
|
|
388
|
+
console.log(` 2. 在授权页点击「Autorizar / 允许」`)
|
|
389
|
+
if (isLocalCallback) {
|
|
390
|
+
console.log(' 3. 本机回调自动完成授权(约 5 分钟内)\n')
|
|
391
|
+
if (USE_TLS) {
|
|
392
|
+
console.log(` ⚠️ 自签证书:浏览器首次访问 ${redirectUri} 会提示证书不受信任,`)
|
|
393
|
+
console.log(' 点「高级/Avanzado → 继续前往/Continuar」即可(仅本地回环,一次性操作)')
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
console.log(' 3. 授权后浏览器会跳到注册的回调地址(如 example.com,内容无关)')
|
|
397
|
+
console.log(' 4. 复制地址栏整段 URL(含 code=…),粘贴回本终端,回车完成授权\n')
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
let server = null
|
|
401
|
+
let waitCode = null
|
|
402
|
+
if (isLocalCallback) {
|
|
403
|
+
;({ server, waitCode } = await startCallbackServer(REDIRECT_PORT))
|
|
404
|
+
}
|
|
405
|
+
const authUrl = `${authBase}?response_type=code&client_id=${encodeURIComponent(appId)}&redirect_uri=${encodeURIComponent(redirectUri)}`
|
|
406
|
+
console.log(` 授权链接:\n ${authUrl}\n`)
|
|
407
|
+
|
|
408
|
+
let code = null
|
|
409
|
+
if (isLocalCallback) {
|
|
410
|
+
try {
|
|
411
|
+
code = await Promise.race([
|
|
412
|
+
waitCode,
|
|
413
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('等待授权超时(5 分钟)')), 300000)),
|
|
414
|
+
])
|
|
415
|
+
} catch (err) {
|
|
416
|
+
console.log(`\n⚠️ ${err.message}`)
|
|
417
|
+
code = extractCode(await ask('请把浏览器地址栏里 code= 后面的值粘贴到这里(或留空取消)', ''))
|
|
418
|
+
if (!code) { server.close(); return false }
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
code = extractCode(await ask('授权完成后,请把浏览器地址栏整段 URL(含 code=…)粘贴到这里(或留空取消)', ''))
|
|
422
|
+
if (!code) return false
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const tok = await exchangeCode(appId, appSecret, redirectUri, code)
|
|
427
|
+
const me = await apiGet('/users/me', tok.access_token)
|
|
428
|
+
const chk = verifyUid(name, tok.user_id, argVal('--expect-uid'))
|
|
429
|
+
console.log(` 店铺校验:${chk.ok ? '✅' : '❌'} ${chk.why}`)
|
|
430
|
+
if (!chk.ok && !args.includes('--force')) {
|
|
431
|
+
console.error('✗ 拒绝落库:授权账号与店铺不匹配(请在正确的店铺窗口重新授权)')
|
|
432
|
+
return false
|
|
433
|
+
}
|
|
434
|
+
const r = persistStore(name, site, tok, me)
|
|
435
|
+
console.log(`✅ ${r.updated ? '已更新' : '已新增'}授权:${name}(${me.nickname || tok.user_id})`)
|
|
436
|
+
return true
|
|
437
|
+
} catch (err) {
|
|
438
|
+
console.error(`✗ 授权失败:${err.message}`)
|
|
439
|
+
return false
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ── --list ─────────────────────────────────────────────────
|
|
444
|
+
function listStores() {
|
|
445
|
+
const { stores } = loadStores()
|
|
446
|
+
if (!stores.length) {
|
|
447
|
+
console.log('(暂无已授权店铺 —— 运行 node ml-oauth-setup.mjs 开始授权)')
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
console.log(`已授权店铺(${stores.length}):`)
|
|
451
|
+
for (const s of stores) {
|
|
452
|
+
const exp = s.access_expires_at ? new Date(s.access_expires_at).toISOString().slice(0, 16).replace('T', ' ') : '?'
|
|
453
|
+
console.log(` · ${s.name}(${s.site})user_id=${s.user_id} ${s.nickname ? '『' + s.nickname + '』' : ''} access 有效期至 ${exp}`)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ── --remove ───────────────────────────────────────────────
|
|
458
|
+
function removeStore(target) {
|
|
459
|
+
const stores = loadStores()
|
|
460
|
+
const before = stores.stores.length
|
|
461
|
+
stores.stores = stores.stores.filter((s) => s.name !== target && String(s.user_id) !== target)
|
|
462
|
+
if (stores.stores.length === before) {
|
|
463
|
+
console.log(`✗ 未找到店铺:${target}(可用 --list 查看)`)
|
|
464
|
+
return 1
|
|
465
|
+
}
|
|
466
|
+
saveStores(stores)
|
|
467
|
+
console.log(`✅ 已移除授权:${target}`)
|
|
468
|
+
return 0
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ── --refresh-all ──────────────────────────────────────────
|
|
472
|
+
async function refreshAll(env) {
|
|
473
|
+
const appId = env.ML_APP_ID || process.env.ML_APP_ID
|
|
474
|
+
const appSecret = env.ML_APP_SECRET || process.env.ML_APP_SECRET
|
|
475
|
+
if (!appId || !appSecret) {
|
|
476
|
+
console.error('✗ 缺少 ML_APP_ID / ML_APP_SECRET')
|
|
477
|
+
return 1
|
|
478
|
+
}
|
|
479
|
+
const stores = loadStores()
|
|
480
|
+
if (!stores.stores.length) { console.log('(暂无已授权店铺)'); return 0 }
|
|
481
|
+
let fail = 0
|
|
482
|
+
for (const s of stores.stores) {
|
|
483
|
+
try {
|
|
484
|
+
const tok = await refreshAccess(appId, appSecret, s.refresh_token)
|
|
485
|
+
s.access_token = tok.access_token
|
|
486
|
+
s.access_expires_at = new Date(Date.now() + (tok.expires_in || 21600) * 1000).toISOString()
|
|
487
|
+
if (tok.refresh_token) s.refresh_token = tok.refresh_token
|
|
488
|
+
const me = await apiGet('/users/me', tok.access_token)
|
|
489
|
+
s.nickname = me.nickname || s.nickname
|
|
490
|
+
console.log(`✅ ${s.name}(${s.site})token 已刷新(${me.nickname || s.user_id})`)
|
|
491
|
+
} catch (err) {
|
|
492
|
+
fail++
|
|
493
|
+
console.error(`✗ ${s.name} 刷新失败:${err.message}`)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
saveStores(stores)
|
|
497
|
+
console.log(fail ? `完成,${fail} 家店铺失败(refresh_token 可能已过期,请重新授权)` : '全部刷新成功')
|
|
498
|
+
return fail ? 1 : 0
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 导出给回归测试(纯函数:不联网、不写文件)
|
|
502
|
+
export { extractCode, expectedUidForStore, verifyUid }
|
|
503
|
+
|
|
504
|
+
// ── main ───────────────────────────────────────────────────
|
|
505
|
+
async function main() {
|
|
506
|
+
const env = loadEnvFile(ENV_PATH)
|
|
507
|
+
await loadPipedLines()
|
|
508
|
+
if (FLAG_CODE) return authorizeOnce()
|
|
509
|
+
if (FLAG_LIST) { listStores(); return 0 }
|
|
510
|
+
if (FLAG_REMOVE) return removeStore(argVal('--remove'))
|
|
511
|
+
if (FLAG_REFRESH) return refreshAll(env)
|
|
512
|
+
|
|
513
|
+
console.log('╔══════════════════════════════════════════════╗')
|
|
514
|
+
console.log('║ 美客多官方 API 授权向导(日报数据源) ║')
|
|
515
|
+
console.log('╚══════════════════════════════════════════════╝')
|
|
516
|
+
console.log(` 应用凭据来源:${ENV_PATH}${existsSync(ENV_PATH) ? '' : '(文件不存在,请先运行 init-ml.sh 生成模板)'}`)
|
|
517
|
+
console.log(` 授权数据:${STORES_PATH}\n`)
|
|
518
|
+
|
|
519
|
+
if (!env.ML_APP_ID && !process.env.ML_APP_ID) {
|
|
520
|
+
console.log('✗ 未找到 ML_APP_ID —— 请先运行 init-ml.sh(日报设置段)生成 daily-report.env 并填入应用凭据')
|
|
521
|
+
return 1
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// 展示已有授权
|
|
525
|
+
const existing = loadStores()
|
|
526
|
+
if (existing.stores.length) {
|
|
527
|
+
console.log('当前已授权店铺:')
|
|
528
|
+
existing.stores.forEach((s, i) => console.log(` ${i + 1}. ${s.name}(${s.site})user_id=${s.user_id}`))
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
console.log('\n开始逐店授权(每店:输入名称 → 用该店紫鸟浏览器打开链接 → 授权)\n')
|
|
532
|
+
let ok = 0
|
|
533
|
+
while (true) {
|
|
534
|
+
const done = await addStoreInteractive(env)
|
|
535
|
+
if (done) ok++
|
|
536
|
+
const more = await ask('继续授权下一家店铺? [Y/n] ')
|
|
537
|
+
if (ASK_EOF || (more.toLowerCase() !== 'y' && more !== '')) break
|
|
538
|
+
}
|
|
539
|
+
console.log(`\n完成:成功授权 ${ok} 家店铺。现在可以运行 node daily-report.mjs --force 测试推送。`)
|
|
540
|
+
return 0
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// 仅直接执行时跑主流程(import 单测时不触发)
|
|
544
|
+
const isMain = isDirectRun(import.meta.url)
|
|
545
|
+
if (isMain) {
|
|
546
|
+
main().then((code) => process.exit(code)).catch((err) => {
|
|
547
|
+
console.error(`✗ 异常:${err.message}`)
|
|
548
|
+
process.exit(1)
|
|
549
|
+
})
|
|
550
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_doc": "ML(美客多)店铺 → 卖家 user_id 的**唯一真源**(2026-09-19 用户亲口提供,运营口径)。",
|
|
3
|
+
"_why": [
|
|
4
|
+
"此前同一个概念散落三处、键格式还各不相同,结果鑫中航发票的 PO Number 静默退化成**紫鸟 store_id**(浏览器店铺 ID,不是 ML 卖家 ID),6 张发票写错都没人发现:",
|
|
5
|
+
" ① scripts/wenyao-template-map.json#storeIds 键='1店'(文鳐用,值本来就对)",
|
|
6
|
+
" ② ~/.dsh/argent/invoice-mapping.json#storeId 键='1店-子账号'(旧管线用,lip 改造后完整店铺名匹配不上 ⇒ 失效)",
|
|
7
|
+
" ③ ~/.dsh/argent/lip-po-mapping.json 键=lip 完整店铺名(**该文件从未创建** ⇒ 回退到紫鸟 ID)",
|
|
8
|
+
"现统一到本文件:键一律用 **'<N>店'**(与 wenyao-template-map 同口径),解析时从任意店铺名('墨西哥-2店-子账号' / '2店-主账号' / '墨西哥-6店')里取 '<N>店'。"
|
|
9
|
+
],
|
|
10
|
+
"_scope": [
|
|
11
|
+
"**主账号/子账号共用同一个 ML user_id**(2026-09-19 用户确认)⇒ 按『店』给一条即可,不要再按账号拆。",
|
|
12
|
+
"『4店』运营标注为**跨境店**(同一 ML ID)。"
|
|
13
|
+
],
|
|
14
|
+
"_verifiedBy": "~/.dsh/argent/ml-api-stores.json 里 3店主账号 user_id=2216458475,与本表 3店 完全一致(官方 ML API 交叉验证)",
|
|
15
|
+
"_unknown": [
|
|
16
|
+
"巴西-1店:lip 表 5 行,运营未提供 ML ID ⇒ 解析必须返回空串(响亮失败),绝不许回退成紫鸟 ID 或串到墨西哥的 ID。"
|
|
17
|
+
],
|
|
18
|
+
"stores": {
|
|
19
|
+
"墨西哥-1店": "2443747608",
|
|
20
|
+
"墨西哥-2店": "2475636269",
|
|
21
|
+
"墨西哥-3店": "2216458475",
|
|
22
|
+
"墨西哥-4店": "3126660693",
|
|
23
|
+
"墨西哥-6店": "3354492092"
|
|
24
|
+
},
|
|
25
|
+
"_keyNote": "键 = **国家-店号**('墨西哥-2店')而不是裸 '2店':2026-09-19 自测发现裸键会让 '**巴西**-1店' 命中 '1店' 并返回**墨西哥**-1店的 ID —— 与本次要修的 bug 同类(跨国家静默串号)。lip 表里确有 5 行『巴西-1店』,且运营尚未提供其 ML ID ⇒ 必须解析失败、不得猜。"
|
|
26
|
+
}
|