@whyshu/argent-engine 0.0.0-stage → 0.6.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.
- package/MANIFEST.json +307 -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 +575 -0
- package/engine/scripts/billing-mode-daily.sh +12 -0
- package/engine/scripts/build-engine-package.mjs +123 -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 +377 -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/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 +134 -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 +46 -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 +260 -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/fix-skill-frontmatter.py +209 -0
- package/engine/scripts/flow-auto-retry.py +454 -0
- package/engine/scripts/flow-requirements.json +720 -0
- package/engine/scripts/flow-scaffold.py +275 -0
- package/engine/scripts/flow-timers-plist.sh +86 -0
- package/engine/scripts/flow-timers.sh +182 -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 +1024 -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 +2275 -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 +194 -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.py +262 -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 +293 -0
- package/engine/scripts/poll-invoice.sh +74 -0
- package/engine/scripts/profile-doctor.mjs +686 -0
- package/engine/scripts/provider-select.sh +70 -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/run-tests.sh +256 -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 +136 -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 +73 -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 +34 -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 +216 -0
- package/engine/scripts/ziniao-post-upgrade-check.sh +120 -0
- package/engine/scripts/ziniao-windows.sh +153 -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 +706 -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 +21 -4
- package/README.md +0 -3
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* daily-report-core.mjs —— 店铺日报的**共用取数 + 聚合 + 口径**实现(L1 执行内核)。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么有它(2026-10-12,「日报口径透明化 + 只读对账」批)
|
|
5
|
+
* 客户在平台概览看到的「销售量」与卡片上的「订单数」对不上(现场:10-11 一店 595 vs 602)。
|
|
6
|
+
* 要回答"为什么有出入",就必须能**逐候选口径**重算同一窗口 —— 而那必须是**同一份**实现,
|
|
7
|
+
* 不是第二份("复制一份口径逻辑"必然在下一次口径调整时漂移,届时两边各自"正确")。
|
|
8
|
+
* 因此把 **窗口计算 + fetchOrders + aggregateOrders + 候选口径/命中判定** 抽到本文件:
|
|
9
|
+
* · `scripts/daily-report.mjs`(日报 CLI)—— 生产取数 + 卡片渲染;
|
|
10
|
+
* · `scripts/daily-report-reconcile.mjs`(只读对账 CLI)—— 逐候选重算 + 漂移对照;
|
|
11
|
+
* 两个 CLI 都 import 本文件,**没有任何第二份口径**。`daily-report.mjs` 的对外导出
|
|
12
|
+
* (`aggregateOrders` / `apiGet` / `localDateStr` / `startOfLocalDayUTC` / `tzOffsetMs` /
|
|
13
|
+
* `localHour`)**逐字转出**本文件,既有单测与调用方一字不改。
|
|
14
|
+
*
|
|
15
|
+
* ## 本文件为什么**不含平台域名/业务词**
|
|
16
|
+
* `node scripts/vertical-lint.mjs` 的 engine-words 档(`verticals/_engine-words.json`)
|
|
17
|
+
* 只拦**新增**业务词、且明令 ⛔ 不抬基线 ⇒ 引擎面新文件里出现平台域名即判红(实测过)。
|
|
18
|
+
* 所以 API 基址**不硬编码**:由调用方注入(日报 CLI 用自己那份历史常量 / `ML_API_BASE`),
|
|
19
|
+
* 或由 `ML_API_BASE` 环境变量给出(`apiGet`/`apiToken` 的兜底)。对账 CLI 另从**快照里记录的
|
|
20
|
+
* `apiBase`**(抓取时刻的事实)取,见 `daily-report-reconcile.mjs`。
|
|
21
|
+
*
|
|
22
|
+
* ## 纪律
|
|
23
|
+
* · **纯 L1**:只 import `node:` 内建,零宿主模块(`scripts/tests/kernel-purity.test.mjs` 扫内容);
|
|
24
|
+
* · 本文件里**零写盘**(写盘属调用方:日报 CLI 落快照、对账 CLI 默认完全不写);
|
|
25
|
+
* · 所有函数纯函数或纯读(`fetchOrders`/`apiGet` 是只读 GET)。
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// ── 依赖:只有 node 内建(L1 ⇒ ⛔ 不得 import 宿主模块)──────────
|
|
29
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
30
|
+
|
|
31
|
+
// ── 纯工具:env 文件解析(两个 CLI 共用同一份;⛔ 不要各写一个)────
|
|
32
|
+
/** 读 `K=V` 形态的 env 文件(缺文件 ⇒ 空对象;`#` 注释与空行跳过;值两侧空白去掉)。 */
|
|
33
|
+
export function loadEnvFile(path) {
|
|
34
|
+
const out = {}
|
|
35
|
+
if (!existsSync(path)) return out
|
|
36
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
37
|
+
const t = line.trim()
|
|
38
|
+
if (!t || t.startsWith('#')) continue
|
|
39
|
+
const eq = t.indexOf('=')
|
|
40
|
+
if (eq > 0) out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim()
|
|
41
|
+
}
|
|
42
|
+
return out
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── 站点 → 本地时区(IANA,自动处理夏令时)与货币 ─────────────
|
|
46
|
+
export const SITE_TZ = {
|
|
47
|
+
MLM: 'America/Mexico_City',
|
|
48
|
+
MLB: 'America/Sao_Paulo',
|
|
49
|
+
MLA: 'America/Argentina/Buenos_Aires',
|
|
50
|
+
MLC: 'America/Santiago',
|
|
51
|
+
MCO: 'America/Bogota',
|
|
52
|
+
MPE: 'America/Lima',
|
|
53
|
+
MLU: 'America/Montevideo',
|
|
54
|
+
MLV: 'America/Caracas',
|
|
55
|
+
}
|
|
56
|
+
export const SITE_CURRENCY = {
|
|
57
|
+
MLM: 'MXN', MLB: 'BRL', MLA: 'ARS', MLC: 'CLP',
|
|
58
|
+
MCO: 'COP', MPE: 'PEN', MLU: 'UYU', MLV: 'USD',
|
|
59
|
+
}
|
|
60
|
+
export const fallbackSite = (site) => (SITE_TZ[site] ? site : 'MLM')
|
|
61
|
+
|
|
62
|
+
/** API 基址:显式传入优先,其次 `ML_API_BASE`(排障旋钮/离线夹具)。两者都没有 ⇒ 抛(绝不猜域名)。 */
|
|
63
|
+
export function resolveApiBase(explicit) {
|
|
64
|
+
const base = explicit || process.env.ML_API_BASE
|
|
65
|
+
if (!base) throw new Error('缺少 API 基址:请传 base 或设 ML_API_BASE(引擎侧不硬编码平台域名)')
|
|
66
|
+
return base
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
70
|
+
|
|
71
|
+
// ── 时区工具(逐字搬自 daily-report.mjs;行为不得变化)──────────
|
|
72
|
+
// 某时区的「本地日期 YYYY-MM-DD」字符串
|
|
73
|
+
export function localDateStr(tz, date) {
|
|
74
|
+
return new Intl.DateTimeFormat('en-CA', {
|
|
75
|
+
timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit',
|
|
76
|
+
}).format(date)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 某时区在指定时刻的 UTC 偏移(毫秒),如 墨西哥 -6h → -21600000
|
|
80
|
+
export function tzOffsetMs(tz, date) {
|
|
81
|
+
try {
|
|
82
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
83
|
+
timeZone: tz, timeZoneName: 'longOffset',
|
|
84
|
+
}).formatToParts(date)
|
|
85
|
+
const name = (parts.find((p) => p.type === 'timeZoneName') || {}).value || ''
|
|
86
|
+
const m = /GMT([+-])(\d{2}):(\d{2})/.exec(name)
|
|
87
|
+
if (!m) return 0
|
|
88
|
+
const sign = m[1] === '-' ? -1 : 1
|
|
89
|
+
return sign * (Number(m[2]) * 60 + Number(m[3])) * 60000
|
|
90
|
+
} catch {
|
|
91
|
+
return 0
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 某时区「本地某日 00:00」对应的 UTC 毫秒
|
|
96
|
+
export function startOfLocalDayUTC(tz, date) {
|
|
97
|
+
const [y, m, d] = localDateStr(tz, date).split('-').map(Number)
|
|
98
|
+
return Date.UTC(y, m - 1, d) - tzOffsetMs(tz, date)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 某时区的本地小时(0-23)
|
|
102
|
+
export function localHour(tz, date) {
|
|
103
|
+
const parts = new Intl.DateTimeFormat('en-GB', {
|
|
104
|
+
timeZone: tz, hour: '2-digit', hour12: false,
|
|
105
|
+
}).formatToParts(date)
|
|
106
|
+
return Number((parts.find((p) => p.type === 'hour') || {}).value || 0)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── 抓取时刻 / 窗口的「人读 + 机读」两种表示(快照新增字段用)──────
|
|
110
|
+
const pad2 = (n) => String(n).padStart(2, '0')
|
|
111
|
+
|
|
112
|
+
/** 站点本地 `MM-DD HH:mm`(如 `10-12 14:05`)。绝不返回 `undefined`:取不到就返回 `null`。 */
|
|
113
|
+
export function localStamp(tz, date) {
|
|
114
|
+
const d = date instanceof Date ? date : new Date(date)
|
|
115
|
+
if (!Number.isFinite(d.getTime())) return null
|
|
116
|
+
try {
|
|
117
|
+
const parts = new Intl.DateTimeFormat('en-GB', {
|
|
118
|
+
timeZone: tz, month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
|
|
119
|
+
}).formatToParts(d)
|
|
120
|
+
const get = (t) => (parts.find((p) => p.type === t) || {}).value
|
|
121
|
+
const mo = get('month'), da = get('day'), ho = get('hour'), mi = get('minute')
|
|
122
|
+
if (!mo || !da || !ho || !mi) return null
|
|
123
|
+
return `${mo}-${da} ${ho}:${mi}`
|
|
124
|
+
} catch {
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 站点本地 ISO8601 **带偏移**(如 `2026-10-12T14:05:00.000-06:00`)。取不到 ⇒ `null`。 */
|
|
130
|
+
export function isoWithOffset(tz, date) {
|
|
131
|
+
const d = date instanceof Date ? date : new Date(date)
|
|
132
|
+
if (!Number.isFinite(d.getTime())) return null
|
|
133
|
+
const off = tzOffsetMs(tz, d)
|
|
134
|
+
const shifted = new Date(d.getTime() + off)
|
|
135
|
+
const s = `${shifted.getUTCFullYear()}-${pad2(shifted.getUTCMonth() + 1)}-${pad2(shifted.getUTCDate())}`
|
|
136
|
+
+ `T${pad2(shifted.getUTCHours())}:${pad2(shifted.getUTCMinutes())}:${pad2(shifted.getUTCSeconds())}`
|
|
137
|
+
+ `.${String(shifted.getUTCMilliseconds()).padStart(3, '0')}`
|
|
138
|
+
const sign = off < 0 ? '-' : '+'
|
|
139
|
+
const abs = Math.abs(off)
|
|
140
|
+
return `${s}${sign}${pad2(Math.floor(abs / 3600000))}:${pad2(Math.floor((abs % 3600000) / 60000))}`
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 一次日报的**窗口 + 抓取时刻**(快照新增字段的唯一来源;两个 CLI 共用)。
|
|
145
|
+
*
|
|
146
|
+
* 口径(与改动前的 `collectStore` **逐字等价**):
|
|
147
|
+
* · `reportStart` = 报告日(站点本地)00:00;`todayStart` = 今天(站点本地)00:00;
|
|
148
|
+
* · 报告窗口 = `[reportStart, todayStart)` —— **站点本地自然日**,天然含夏令时切换;
|
|
149
|
+
* · `weekStart` = `reportStart - winDays`(`winDays = max(1, SALES_WINDOW_DAYS - 1)`,缺省 6);
|
|
150
|
+
* · `prevStart` = `reportStart - (winDays + 7)`。
|
|
151
|
+
*/
|
|
152
|
+
export function reportWindows(site, reportDate, now, opts = {}) {
|
|
153
|
+
const siteCode = fallbackSite(site)
|
|
154
|
+
const tz = SITE_TZ[siteCode]
|
|
155
|
+
const salesWindowDays = opts.salesWindowDays != null && opts.salesWindowDays !== ''
|
|
156
|
+
? Number(opts.salesWindowDays) : 7
|
|
157
|
+
const winDays = Math.max(1, (Number.isFinite(salesWindowDays) ? salesWindowDays : 7) - 1)
|
|
158
|
+
const todayStart = startOfLocalDayUTC(tz, now)
|
|
159
|
+
const reportStart = startOfLocalDayUTC(tz, reportDate)
|
|
160
|
+
const weekStart = startOfLocalDayUTC(tz, new Date(reportDate.getTime() - winDays * 86400000))
|
|
161
|
+
const prevStart = startOfLocalDayUTC(tz, new Date(reportDate.getTime() - (winDays + 7) * 86400000))
|
|
162
|
+
return {
|
|
163
|
+
site: siteCode,
|
|
164
|
+
tz,
|
|
165
|
+
winDays,
|
|
166
|
+
weekStart,
|
|
167
|
+
reportStart,
|
|
168
|
+
todayStart,
|
|
169
|
+
prevStart,
|
|
170
|
+
windows: { weekStartMs: weekStart, reportStartMs: reportStart, todayStartMs: todayStart },
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 报告窗口的**自描述**(存进快照:换台机器/换个人也能一眼看懂这段数字覆盖哪段时间)。 */
|
|
175
|
+
export function windowInfo(site, reportStart, todayStart) {
|
|
176
|
+
const tz = SITE_TZ[fallbackSite(site)]
|
|
177
|
+
return {
|
|
178
|
+
tz,
|
|
179
|
+
from: new Date(reportStart).toISOString(),
|
|
180
|
+
to: new Date(todayStart).toISOString(),
|
|
181
|
+
fromLocal: `${localDateStr(tz, new Date(reportStart))} 00:00`,
|
|
182
|
+
toLocal: `${localDateStr(tz, new Date(todayStart))} 00:00`,
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** 快照新增的「抓取时刻」字段(站点本地时区语义)。 */
|
|
187
|
+
export function capturedAtInfo(site, now = new Date()) {
|
|
188
|
+
const tz = SITE_TZ[fallbackSite(site)]
|
|
189
|
+
return { capturedAt: isoWithOffset(tz, now), capturedAtLocal: localStamp(tz, now), capturedTz: tz }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── 店铺授权表:稳定 key + 匹配(两个 CLI 共用同一份,别写第二份)──
|
|
193
|
+
/** 快照/幂等标记的文件名 key:以 user_id 稳定命名(不按下标 —— 多时区店铺触发时刻不同,下标会漂移)。 */
|
|
194
|
+
export const storeKey = (store) => `store-${String(store.user_id ?? store.name)}`
|
|
195
|
+
|
|
196
|
+
/** `--store` 匹配:名称**子串**或 user_id **全等**(与日报 CLI 的历史行为逐字一致)。 */
|
|
197
|
+
export function matchStores(stores, queries) {
|
|
198
|
+
const q = (queries || []).map((x) => String(x).trim()).filter(Boolean)
|
|
199
|
+
if (!q.length) return stores || []
|
|
200
|
+
return (stores || []).filter((x) => q.some((t) => String(x.name).includes(t) || String(x.user_id) === t))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── 订单聚合:近7日 + 昨日,按 SKU ─────────────────────────
|
|
204
|
+
// orders: MELI /orders/search results;itemById: 商品信息 map
|
|
205
|
+
// windows: {weekStartMs, reportStartMs, todayStartMs}
|
|
206
|
+
export function aggregateOrders(orders, itemById, windows, site) {
|
|
207
|
+
const skus = new Map()
|
|
208
|
+
// 2026-09-27(用户口径):日报显示 **SKU**(替换西语标题——中国同事看不懂西语),且**不读 lip 表**。
|
|
209
|
+
// SKU 来源:`order_items[].item.seller_sku`(实测 3 店 × 200 单 = 600 行 100% 有值,含变体),
|
|
210
|
+
// 因此**不需要任何额外接口调用**;一个 listing 有多个变体 SKU 时,取**卖得最多**的那个。
|
|
211
|
+
const ensure = (id) => {
|
|
212
|
+
if (!skus.has(id)) {
|
|
213
|
+
skus.set(id, {
|
|
214
|
+
id, title: (itemById[id] && itemById[id].title) || `商品 ${id}`,
|
|
215
|
+
price: (itemById[id] && itemById[id].price) ?? null,
|
|
216
|
+
avail: (itemById[id] && itemById[id].available_quantity) ?? null,
|
|
217
|
+
sold: (itemById[id] && itemById[id].sold_quantity) ?? null,
|
|
218
|
+
units7: 0, gmv7: 0, unitsY: 0, gmvY: 0,
|
|
219
|
+
skuUnits: new Map(), // seller_sku → 近7日件数(用于挑主 SKU)
|
|
220
|
+
sku: '',
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
return skus.get(id)
|
|
224
|
+
}
|
|
225
|
+
const summary = { orders: 0, gmv: 0, cancelled: 0, units: 0, avg: null, fees: 0, coupons: 0, orders7: 0 }
|
|
226
|
+
const daily = new Map() // 站点本地日期 → {orders, gmv}(第 2 批:每日趋势)
|
|
227
|
+
const buyers = new Set() // 近 7 日买家(第 2 批:新客占比)
|
|
228
|
+
for (const o of orders || []) {
|
|
229
|
+
const created = Date.parse(o.date_created || '')
|
|
230
|
+
if (!created) continue
|
|
231
|
+
if (o.status === 'cancelled') {
|
|
232
|
+
if (created >= windows.reportStartMs && created < windows.todayStartMs) summary.cancelled++
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
// 2026 实测:有效订单状态为 paid(旧文档的 confirmed 已不再出现);
|
|
236
|
+
// 排除未付款的 payment_required,其余状态(paid/confirmed/shipped/delivered)都算有效订单
|
|
237
|
+
if (o.status === 'payment_required') continue
|
|
238
|
+
const inY = created >= windows.reportStartMs && created < windows.todayStartMs
|
|
239
|
+
const in7 = created >= windows.weekStartMs && created < windows.todayStartMs
|
|
240
|
+
const bid = (o.buyer && o.buyer.id) || null
|
|
241
|
+
if (bid) buyers.add(String(bid))
|
|
242
|
+
if (inY) {
|
|
243
|
+
summary.orders++
|
|
244
|
+
summary.gmv += Number(o.total_amount || 0)
|
|
245
|
+
summary.units += (o.order_items || []).reduce((a, i) => a + Number(i.quantity || 0), 0)
|
|
246
|
+
// 平台费用与优惠券(都在订单响应里 ⇒ 零额外请求)
|
|
247
|
+
for (const oi of o.order_items || []) summary.fees += Number(oi.sale_fee || 0)
|
|
248
|
+
for (const pay of o.payments || []) summary.coupons += Number(pay.coupon_amount || 0)
|
|
249
|
+
}
|
|
250
|
+
if (in7) { // 趋势要 7 个点 ⇒ 按近 7 日分桶(不是只桶昨日)
|
|
251
|
+
summary.orders7++
|
|
252
|
+
const dk = localDateStr(SITE_TZ[fallbackSite(site)] || 'America/Mexico_City', new Date(created))
|
|
253
|
+
const bucket = daily.get(dk) || { date: dk, orders: 0, gmv: 0 }
|
|
254
|
+
bucket.orders++
|
|
255
|
+
bucket.gmv += Number(o.total_amount || 0)
|
|
256
|
+
daily.set(dk, bucket)
|
|
257
|
+
}
|
|
258
|
+
if (!in7) continue
|
|
259
|
+
for (const oi of o.order_items || []) {
|
|
260
|
+
const id = String((oi.item && oi.item.id) || '')
|
|
261
|
+
if (!id) continue
|
|
262
|
+
const sku = ensure(id)
|
|
263
|
+
const qty = Number(oi.quantity || 0)
|
|
264
|
+
const amt = qty * Number(oi.unit_price || 0)
|
|
265
|
+
const code = String((oi.item && oi.item.seller_sku) || '').trim()
|
|
266
|
+
if (code) sku.skuUnits.set(code, (sku.skuUnits.get(code) || 0) + qty)
|
|
267
|
+
if (inY) { sku.unitsY += qty; sku.gmvY += amt }
|
|
268
|
+
sku.units7 += qty
|
|
269
|
+
sku.gmv7 += amt
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
summary.avg = summary.orders ? summary.gmv / summary.orders : null
|
|
273
|
+
for (const s of skus.values()) {
|
|
274
|
+
let best = '', bestN = -1
|
|
275
|
+
for (const [code, n] of s.skuUnits) if (n > bestN) { best = code; bestN = n } // 并列时保留先出现的(Map 保序)
|
|
276
|
+
s.sku = best
|
|
277
|
+
delete s.skuUnits
|
|
278
|
+
}
|
|
279
|
+
return { summary, skus: [...skus.values()], daily: [...daily.values()].sort((a, b) => (a.date < b.date ? -1 : 1)), buyers }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── 「为什么对不上」:候选口径逐项重算(纯函数,只读)──────────────
|
|
283
|
+
// 现场结论(2026-10-12 实测):**没有一个候选等于平台的 602** ⇒ 差异不是口径而是时点/未知。
|
|
284
|
+
// 所以这里把**所有能算的候选**都算出来并排显示,而不是猜一个。
|
|
285
|
+
export const CANDIDATE_DESC = {
|
|
286
|
+
orders: '我方口径:窗口内、date_created 命中、排除 cancelled 与 payment_required 的订单笔数',
|
|
287
|
+
paidApproved: '候选:窗口内有效订单中,payments[] 里至少一笔 status=approved',
|
|
288
|
+
withShipping: '候选:窗口内有效订单中带发货信息(shipping.id/mode/status 任一存在)',
|
|
289
|
+
allCreated: '候选:窗口内全部创建的订单**含取消**(= 有效订单 + 取消单;仍排除 payment_required)',
|
|
290
|
+
paymentRequired: '候选:窗口内 status=payment_required(未付款)的订单数',
|
|
291
|
+
uniqueBuyers: '候选:窗口内有效订单的**去重买家**数(buyer.id)',
|
|
292
|
+
units: '候选:窗口内有效订单 Σ order_items[].quantity(= 卡片「已售件数」)',
|
|
293
|
+
cancelled: '候选:窗口内 status=cancelled 的订单数(卡片「取消」)',
|
|
294
|
+
gmv: '候选:窗口内有效订单 Σ total_amount(= 卡片「销售额」,可能带小数)',
|
|
295
|
+
avg: '候选:我方客单价(gmv / orders,四舍五入到整数)',
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** 窗口内命中(按 `date_created`;与 `aggregateOrders` 用同一判据)。 */
|
|
299
|
+
export const inWindow = (o, fromMs, toMs) => {
|
|
300
|
+
const t = Date.parse((o && o.date_created) || '')
|
|
301
|
+
return !!t && t >= fromMs && t < toMs
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const hasShippingInfo = (o) => !!(o && o.shipping && (o.shipping.id || o.shipping.mode || o.shipping.status))
|
|
305
|
+
const hasApprovedPayment = (o) => ((o && o.payments) || []).some((p) => p && p.status === 'approved')
|
|
306
|
+
const unitsOf = (list) => list.reduce((a, o) => a + (o.order_items || []).reduce((x, i) => x + Number(i.quantity || 0), 0), 0)
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* 逐候选口径(**纯函数**):给定订单集与窗口 ⇒ 我方口径 + 各候选值。
|
|
310
|
+
* `orders` 需要覆盖**整个窗口**(含取消/未付款),调用方取数时已含。
|
|
311
|
+
*/
|
|
312
|
+
export function candidateMetrics(orders, windows, { tz = 'America/Mexico_City' } = {}) {
|
|
313
|
+
const from = windows.reportStartMs
|
|
314
|
+
const to = windows.todayStartMs
|
|
315
|
+
const win = (orders || []).filter((o) => inWindow(o, from, to))
|
|
316
|
+
const cancelled = win.filter((o) => o.status === 'cancelled')
|
|
317
|
+
const paymentRequired = win.filter((o) => o.status === 'payment_required')
|
|
318
|
+
const valid = win.filter((o) => o.status !== 'cancelled' && o.status !== 'payment_required')
|
|
319
|
+
const gmv = Math.round(valid.reduce((a, o) => a + Number(o.total_amount || 0), 0) * 100) / 100 // 金额到分(浮点累加会带噪声)
|
|
320
|
+
const units = unitsOf(valid)
|
|
321
|
+
const buyers = new Set()
|
|
322
|
+
for (const o of valid) if (o.buyer && o.buyer.id != null) buyers.add(String(o.buyer.id))
|
|
323
|
+
const values = {
|
|
324
|
+
orders: valid.length,
|
|
325
|
+
paidApproved: valid.filter(hasApprovedPayment).length,
|
|
326
|
+
withShipping: valid.filter(hasShippingInfo).length,
|
|
327
|
+
allCreated: valid.length + cancelled.length,
|
|
328
|
+
paymentRequired: paymentRequired.length,
|
|
329
|
+
uniqueBuyers: buyers.size,
|
|
330
|
+
units,
|
|
331
|
+
cancelled: cancelled.length,
|
|
332
|
+
gmv,
|
|
333
|
+
avg: valid.length ? Math.round(gmv / valid.length) : null,
|
|
334
|
+
}
|
|
335
|
+
const candidates = Object.keys(CANDIDATE_DESC).map((key) => ({
|
|
336
|
+
key, label: key, value: values[key], desc: CANDIDATE_DESC[key],
|
|
337
|
+
}))
|
|
338
|
+
return {
|
|
339
|
+
values,
|
|
340
|
+
candidates,
|
|
341
|
+
summary: { orders: values.orders, units: values.units, gmv, cancelled: cancelled.length, avg: values.avg },
|
|
342
|
+
window: { from, to, tz, fromLocal: localDateStr(tz, new Date(from)), toLocal: localDateStr(tz, new Date(to)) },
|
|
343
|
+
counts: { window: win.length, valid: valid.length, cancelled: cancelled.length, paymentRequired: paymentRequired.length },
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** 窗口平移敏感度:把窗口整体平移 N 小时,重算「我方口径」的订单数。 */
|
|
348
|
+
export function windowShiftSensitivity(orders, windows, shiftsHours = [-3, -2, -1, 1, 2, 3]) {
|
|
349
|
+
const base = { from: windows.reportStartMs, to: windows.todayStartMs }
|
|
350
|
+
const at = (h) => {
|
|
351
|
+
const from = base.from + h * 3600000
|
|
352
|
+
const to = base.to + h * 3600000
|
|
353
|
+
return (orders || []).filter((o) => inWindow(o, from, to)
|
|
354
|
+
&& o.status !== 'cancelled' && o.status !== 'payment_required').length
|
|
355
|
+
}
|
|
356
|
+
return { base: at(0), shifts: shiftsHours.map((h) => ({ hours: h, orders: at(h) })) }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** `--platform-kpi "订单数量=602,已售件数=640"`(也接受 JSON `{"订单数量":602}`)⇒ `[{label,value}]`。 */
|
|
360
|
+
export function parsePlatformKpi(spec) {
|
|
361
|
+
if (spec == null || spec === '') return []
|
|
362
|
+
const text = String(spec).trim()
|
|
363
|
+
if (!text) return []
|
|
364
|
+
const out = []
|
|
365
|
+
if (text.startsWith('{')) {
|
|
366
|
+
const obj = JSON.parse(text)
|
|
367
|
+
for (const [label, value] of Object.entries(obj || {})) {
|
|
368
|
+
const n = Number(value)
|
|
369
|
+
if (Number.isFinite(n)) out.push({ label: String(label), value: n })
|
|
370
|
+
}
|
|
371
|
+
return out
|
|
372
|
+
}
|
|
373
|
+
for (const part of text.split(',')) {
|
|
374
|
+
const seg = part.trim()
|
|
375
|
+
if (!seg) continue
|
|
376
|
+
const i = seg.lastIndexOf('=') >= 0 ? seg.lastIndexOf('=') : seg.indexOf(':')
|
|
377
|
+
if (i < 0) throw new Error(`--platform-kpi 需要「名称=数值」形式(实得 ${JSON.stringify(seg)})`)
|
|
378
|
+
const label = seg.slice(0, i).trim()
|
|
379
|
+
const raw = seg.slice(i + 1).trim().replace(/[,_\s]/g, '')
|
|
380
|
+
const n = Number(raw)
|
|
381
|
+
if (!label || !Number.isFinite(n)) throw new Error(`--platform-kpi 的「${seg}」解析不出数值`)
|
|
382
|
+
out.push({ label, value: n })
|
|
383
|
+
}
|
|
384
|
+
return out
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* 命中判据:把每个平台 KPI 的数值与**全部候选值**比对。
|
|
389
|
+
* 返回 `{ hits: [{label, value, matched:[candidateKey…]}], misses: [{label,value}] }`。
|
|
390
|
+
* ⛔ 不"就近匹配"、不四舍五入 —— 对不上就是对不上(正是本次事故的结论)。
|
|
391
|
+
*/
|
|
392
|
+
export function matchCandidates(metrics, kpis) {
|
|
393
|
+
const cands = (metrics && metrics.candidates) || []
|
|
394
|
+
const hits = []
|
|
395
|
+
const misses = []
|
|
396
|
+
for (const k of kpis || []) {
|
|
397
|
+
const matched = cands.filter((c) => Number(c.value) === Number(k.value)).map((c) => c.key)
|
|
398
|
+
if (matched.length) hits.push({ ...k, matched })
|
|
399
|
+
else misses.push({ ...k })
|
|
400
|
+
}
|
|
401
|
+
return { hits, misses, hitAll: misses.length === 0 && hits.length > 0 }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ── 只读 HTTP(429 退避重试;base 由调用方注入)──────────────
|
|
405
|
+
/**
|
|
406
|
+
* 平台 GET。**429(local_rate_limited)自动退避重试**:
|
|
407
|
+
* 2026-09-27 实测——短时间密集跑几轮后会连续 429,而日报此前一次失败就整店放弃
|
|
408
|
+
* ("全部店铺采集失败,不推送")⇒ 客户当天收不到日报。限流是**瞬时**的,退避即恢复。
|
|
409
|
+
*/
|
|
410
|
+
export async function apiGet(path, token, opts = {}) {
|
|
411
|
+
const base = resolveApiBase(opts.base)
|
|
412
|
+
const retries = opts.retries != null ? opts.retries : Number(process.env.ML_HTTP_RETRIES || 3)
|
|
413
|
+
// 退避序列可覆盖(测试用 ML_HTTP_BACKOFF_MS=20,30 跑得快;默认 5s/15s/30s)
|
|
414
|
+
const wait = String(process.env.ML_HTTP_BACKOFF_MS || '5000,15000,30000').split(',').map((x) => Number(x.trim())).filter((x) => x >= 0)
|
|
415
|
+
for (let attempt = 0; ; attempt++) {
|
|
416
|
+
const res = await fetch(`${base}${path}`, { headers: { Authorization: `Bearer ${token}` } })
|
|
417
|
+
const body = await res.json().catch(() => ({}))
|
|
418
|
+
if (res.ok) return body
|
|
419
|
+
const short = String(path.split('?')[0])
|
|
420
|
+
const msg = `API ${short} 失败 HTTP ${res.status}: ${JSON.stringify(body).slice(0, 200)}`
|
|
421
|
+
const limited = res.status === 429 || /local_rate_limited/i.test(msg)
|
|
422
|
+
if (!limited || attempt >= retries) throw new Error(msg)
|
|
423
|
+
const ms = wait[Math.min(attempt, wait.length - 1)]
|
|
424
|
+
console.error(`[daily-report] ⏳ 触发限流(429)⇒ ${Math.round(ms / 1000)}s 后重试(第 ${attempt + 1}/${retries} 次):${short}`)
|
|
425
|
+
await sleep(ms)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** 拉窗口内订单(分页 + 节流)。只读。 */
|
|
430
|
+
export async function fetchOrders(token, sellerId, fromISO, toISO, opts = {}) {
|
|
431
|
+
const all = []
|
|
432
|
+
let offset = 0
|
|
433
|
+
for (;;) {
|
|
434
|
+
const p = new URLSearchParams({
|
|
435
|
+
seller: String(sellerId),
|
|
436
|
+
'order.date_created.from': fromISO,
|
|
437
|
+
'order.date_created.to': toISO,
|
|
438
|
+
limit: '50',
|
|
439
|
+
offset: String(offset),
|
|
440
|
+
})
|
|
441
|
+
const d = await apiGet(`/orders/search?${p}`, token, { base: opts.base })
|
|
442
|
+
all.push(...(d.results || []))
|
|
443
|
+
const total = (d.paging && d.paging.total) || all.length
|
|
444
|
+
if (all.length >= total || all.length >= 5000 || !(d.results || []).length) break
|
|
445
|
+
offset += 50
|
|
446
|
+
await sleep(Number(process.env.ML_PAGE_GAP_MS || 60)) // 翻页节流:降低被限流概率
|
|
447
|
+
}
|
|
448
|
+
return all
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ── OAuth token(刷新 + 轮换回写交给调用方)──────────────────
|
|
452
|
+
async function apiToken(form, opts = {}) {
|
|
453
|
+
const base = resolveApiBase(opts.base)
|
|
454
|
+
const res = await fetch(`${base}/oauth/token`, {
|
|
455
|
+
method: 'POST',
|
|
456
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
457
|
+
body: new URLSearchParams(form).toString(),
|
|
458
|
+
})
|
|
459
|
+
const body = await res.json().catch(() => ({}))
|
|
460
|
+
if (!res.ok || !body.access_token) {
|
|
461
|
+
throw new Error(`token 刷新失败 HTTP ${res.status}: ${JSON.stringify(body).slice(0, 200)}`)
|
|
462
|
+
}
|
|
463
|
+
return body
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** access_token 剩余有效期(毫秒);缺字段 ⇒ `null`(调用方按"需要刷新"处理)。 */
|
|
467
|
+
export function tokenLeftMs(store, now = Date.now()) {
|
|
468
|
+
if (!store || !store.access_token || !store.access_expires_at) return null
|
|
469
|
+
const t = new Date(store.access_expires_at).getTime()
|
|
470
|
+
return Number.isFinite(t) ? t - now : null
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* token 复用 + 轮换刷新。
|
|
475
|
+
*
|
|
476
|
+
* ⚠️ **refresh_token 是轮换制**:刷新成功后**必须**把新的 refresh_token 原子写回凭据文件,
|
|
477
|
+
* 否则下次仍用已作废的旧 token 刷新 ⇒ 撞 429(2026-09-04/05 连续两日断供的根因)。
|
|
478
|
+
* 写回**不在本函数里**(本文件零写盘):由调用方通过 `opts.onRotate(store)` 承担,且调用方
|
|
479
|
+
* 必须显式开启(日报 CLI 恒写回;对账 CLI 只在 `--allow-token-refresh` 分支才写回)。
|
|
480
|
+
*/
|
|
481
|
+
export async function refreshStoreToken(env, store, opts = {}) {
|
|
482
|
+
const appId = env.ML_APP_ID || process.env.ML_APP_ID
|
|
483
|
+
const appSecret = env.ML_APP_SECRET || process.env.ML_APP_SECRET
|
|
484
|
+
if (!appId || !appSecret) throw new Error('缺少 ML_APP_ID / ML_APP_SECRET(daily-report.env)')
|
|
485
|
+
// token 复用:access_token 剩余有效期 > 15min 直接用,不无谓刷新(grant 限流保护)
|
|
486
|
+
const left = tokenLeftMs(store)
|
|
487
|
+
if (left != null && left > 15 * 60 * 1000) return store.access_token
|
|
488
|
+
if (opts.allowRefresh === false) {
|
|
489
|
+
throw new Error(`access_token 不可用(剩余 ${left == null ? '未知' : Math.round(left / 1000) + 's'})⇒ 未开启刷新`)
|
|
490
|
+
}
|
|
491
|
+
const tok = await apiToken({
|
|
492
|
+
grant_type: 'refresh_token',
|
|
493
|
+
client_id: appId,
|
|
494
|
+
client_secret: appSecret,
|
|
495
|
+
refresh_token: store.refresh_token,
|
|
496
|
+
}, { base: opts.base })
|
|
497
|
+
store.access_token = tok.access_token
|
|
498
|
+
store.access_expires_at = new Date(Date.now() + (tok.expires_in || 21600) * 1000).toISOString()
|
|
499
|
+
if (tok.refresh_token) store.refresh_token = tok.refresh_token
|
|
500
|
+
if (opts.onRotate) opts.onRotate(store) // 轮换后的新 token 立即落盘(原子写),防旧 token 复用触发 429
|
|
501
|
+
return tok.access_token
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ── 卡片上的「数据截至 + 口径」两行(老快照优雅降级 ⇒ 返回空数组)──
|
|
505
|
+
export const CALIBER_LINE = '**口径**:订单数=订单笔数(不含取消/未付款)|已售件数=件数|平台可事后变更历史订单状态 ⇒ 与平台概览可能相差个位数'
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* 概览段追加的两行。**只有拿得到抓取时刻时才追加**(老快照/无字段 ⇒ 返回 `[]`,
|
|
509
|
+
* ⛔ 绝不打印 `undefined`、也绝不猜一个时刻)。
|
|
510
|
+
*/
|
|
511
|
+
export function captureNoteLines(r, tz) {
|
|
512
|
+
if (!r) return []
|
|
513
|
+
let local = r.capturedAtLocal || null
|
|
514
|
+
if (!local && r.capturedAt) local = localStamp(tz, new Date(r.capturedAt))
|
|
515
|
+
if (!local) return []
|
|
516
|
+
return [`**数据截至**:${local}(${tz})`, CALIBER_LINE]
|
|
517
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 子包"漏声明"分类(**唯一真源**,纯函数,可离线测试)。
|
|
3
|
+
*
|
|
4
|
+
* 由来(2026-09-28 升级前审查):`dsh-preflight.mjs` 的漏声明检测用**目录名**
|
|
5
|
+
* (`dsh-client-ui-sidebar`)去比**包名**(`@deepseek-ai/dsh-client-ui-sidebar`)⇒ 一个都匹配不上
|
|
6
|
+
* ⇒ 把 0.1.7-rc.2 的 **52 个 UI 子包全报成"未被主包声明"**。全是噪音的告警等于没有告警:
|
|
7
|
+
* 真正的孤儿(`dsh-client-ui-primitives` / `dsh-client-ui-slots`,前者正是 rc.8 断链事故的包名)
|
|
8
|
+
* 会被淹在 52 行里没人看。
|
|
9
|
+
*
|
|
10
|
+
* 正确分类(三级):
|
|
11
|
+
* declared —— 被声明方(dsh / dsh-web-app / …)在 dependencies/peer/optional 里声明 ⇒ 正常;
|
|
12
|
+
* transitive —— 没被声明方声明,但**别的包依赖它** ⇒ 传递依赖,也正常(npm 会装、解析得到);
|
|
13
|
+
* orphan —— 既没被声明、也没有任何包依赖 ⇒ **真可疑**(磁盘上有、没人引用),才值得告警。
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
16
|
+
import { join } from 'node:path'
|
|
17
|
+
|
|
18
|
+
/** 读取一个 scope 目录(`.../node_modules/@deepseek-ai`)下所有包的 package.json。 */
|
|
19
|
+
export function readScope(scopeDir) {
|
|
20
|
+
const out = new Map() // 包名 → package.json
|
|
21
|
+
if (!existsSync(scopeDir)) return out
|
|
22
|
+
for (const dir of readdirSync(scopeDir)) {
|
|
23
|
+
const pj = join(scopeDir, dir, 'package.json')
|
|
24
|
+
if (!existsSync(pj)) continue
|
|
25
|
+
try {
|
|
26
|
+
const j = JSON.parse(readFileSync(pj, 'utf8'))
|
|
27
|
+
if (j.name) out.set(j.name, j)
|
|
28
|
+
} catch { /* 坏 package.json 跳过 */ }
|
|
29
|
+
}
|
|
30
|
+
return out
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 分类子包。
|
|
35
|
+
* @param {object} o
|
|
36
|
+
* @param {Map<string, object>} o.packages readScope 的结果
|
|
37
|
+
* @param {string[]} [o.declarers] 声明方包名
|
|
38
|
+
* @param {(name: string) => boolean} [o.interest] 只关心哪些子包(默认全部)
|
|
39
|
+
* @returns {{ declared: string[], transitive: string[], orphan: string[], declaredBy: Map<string,string[]> }}
|
|
40
|
+
*/
|
|
41
|
+
export function classifySubpackages({ packages, declarers = ['@deepseek-ai/dsh', '@deepseek-ai/dsh-web-app'], interest = () => true }) {
|
|
42
|
+
const DEP_KEYS = ['dependencies', 'peerDependencies', 'optionalDependencies']
|
|
43
|
+
const declaredBy = new Map()
|
|
44
|
+
for (const d of declarers) {
|
|
45
|
+
const pkg = packages.get(d)
|
|
46
|
+
if (!pkg) continue
|
|
47
|
+
for (const key of DEP_KEYS) {
|
|
48
|
+
for (const dep of Object.keys(pkg[key] || {})) {
|
|
49
|
+
if (!declaredBy.has(dep)) declaredBy.set(dep, [])
|
|
50
|
+
declaredBy.get(dep).push(d)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// 谁依赖了谁(用于判"传递依赖")
|
|
55
|
+
const dependedBy = new Map()
|
|
56
|
+
for (const [name, pkg] of packages) {
|
|
57
|
+
for (const key of DEP_KEYS) {
|
|
58
|
+
for (const dep of Object.keys(pkg[key] || {})) {
|
|
59
|
+
if (!dependedBy.has(dep)) dependedBy.set(dep, [])
|
|
60
|
+
dependedBy.get(dep).push(name)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const declared = []
|
|
66
|
+
const transitive = []
|
|
67
|
+
const orphan = []
|
|
68
|
+
for (const name of packages.keys()) {
|
|
69
|
+
if (!interest(name)) continue
|
|
70
|
+
if (declaredBy.has(name)) { declared.push(name); continue }
|
|
71
|
+
const users = (dependedBy.get(name) || []).filter((u) => u !== name)
|
|
72
|
+
if (users.length) transitive.push(name)
|
|
73
|
+
else orphan.push(name)
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
declared: declared.sort(),
|
|
77
|
+
transitive: transitive.sort(),
|
|
78
|
+
orphan: orphan.sort(),
|
|
79
|
+
declaredBy,
|
|
80
|
+
}
|
|
81
|
+
}
|