@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,2275 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ============================================================
|
|
3
|
+
# init-ml.sh — ML 卖家诊断/后备工具(KB-071:配置操作以 Argent 设置页 UI 为准,本脚本保留为
|
|
4
|
+
# 诊断(--verify 体检)与后备(批量/无 UI 场景)用途;--verify 由设置页「一键体检」调用)
|
|
5
|
+
# 一次配置全部客户侧参数:紫鸟环境 / ziniao apiKey / 飞书表格 /
|
|
6
|
+
# 店铺映射(stores.json)/ CDP 端口 / cron 自动化
|
|
7
|
+
#
|
|
8
|
+
# 用法:
|
|
9
|
+
# init-ml.sh 完整初始化(首次使用)
|
|
10
|
+
# init-ml.sh --refresh-stores 仅刷新店铺选项(重跑 3a-3g,
|
|
11
|
+
# 紫鸟新增/改名环境后同步)
|
|
12
|
+
# init-ml.sh --reconfig 强制重新配置(忽略已有 fulfillment.env)
|
|
13
|
+
# init-ml.sh --verify 全链一致性校验(只读,输出红绿清单,
|
|
14
|
+
# 每个红灯附中文修复指引)
|
|
15
|
+
# init-ml.sh --daily-report 仅配置美客多日报推送(OAuth 授权 + cron;日常配置请用设置页 ML 配置区)
|
|
16
|
+
# init-ml.sh --feishu-gateway 仅配置飞书 gateway 对话接入
|
|
17
|
+
# (App ID/Secret + 白名单 + gateway install)
|
|
18
|
+
#
|
|
19
|
+
# 飞书配置(App ID/Secret/Webhook)是 ML 角色的唯一配置点:
|
|
20
|
+
# argent setup ③ 对 ML 角色已跳过(打印「由 ML 环境向导统一处理」),
|
|
21
|
+
# 本向导统一处理飞书凭据/推送/登录/表格;已有配置自动检测复用(幂等)。
|
|
22
|
+
# 明确顺序:先 argent setup,后 init-ml。
|
|
23
|
+
#
|
|
24
|
+
# 产物(argent-dsh 自持路径——脚本与数据均属 argent-dsh,不触碰 hermes 目录):
|
|
25
|
+
# ~/.dsh/argent/stores.json 店铺映射(环境名 → store_id/platform/cdp_port)
|
|
26
|
+
# ~/.dsh/argent/fulfillment.env 飞书/店铺凭据
|
|
27
|
+
# ~/.dsh/argent/cdp-port-map.json CDP 端口缓存(storeId → {store_name, env_name, cdp_port},
|
|
28
|
+
# 三方一致映射,由 ziniao-cli store list 提供名称)
|
|
29
|
+
# ~/.dsh/argent/daily-report.env 日报配置模板(ML 应用凭据 / 飞书 webhook / 补货参数)
|
|
30
|
+
# ~/.dsh/argent/ml-api-stores.json 美客多官方 API OAuth 授权(ml-oauth-setup.mjs 生成)
|
|
31
|
+
# ~/.argent-dsh/scripts/ 固化脚本(poll/orchestrator/init/daily-report*,安装器部署)
|
|
32
|
+
# cron 系统 crontab(每 5 分钟轮询 + 每小时日报门控)
|
|
33
|
+
#
|
|
34
|
+
# 飞书应用凭据(App ID/Secret)走 Web 配置中心(~/.dsh/argent/config.json);
|
|
35
|
+
# 本向导内 feishu 配置 / config bind / gateway 段为 hermes 遗留流程,新装请勿使用。
|
|
36
|
+
# ============================================================
|
|
37
|
+
set -euo pipefail
|
|
38
|
+
|
|
39
|
+
# argent-dsh 自持路径:数据默认 ~/.dsh/argent/(update 不覆盖);
|
|
40
|
+
# 脚本默认 ~/.argent-dsh/scripts/(安装器部署)。ARGENT_DATA_HOME /
|
|
41
|
+
# ARGENT_PROFILE_NAME / ARGENT_PROFILE_HOME 可覆盖(测试/多机)。
|
|
42
|
+
PROFILE_NAME="${ARGENT_PROFILE_NAME:-argent-ml-seller}"
|
|
43
|
+
PROFILE_HOME="${ARGENT_PROFILE_HOME:-$HOME/.argent-dsh}"
|
|
44
|
+
PROFILE_SCRIPTS="$PROFILE_HOME/scripts"
|
|
45
|
+
DATA_SCRIPTS="${ARGENT_DATA_HOME:-$HOME/.dsh/argent}"
|
|
46
|
+
ENV_FILE="$DATA_SCRIPTS/fulfillment.env"
|
|
47
|
+
STORES_JSON="$DATA_SCRIPTS/stores.json"
|
|
48
|
+
CDP_CACHE="$DATA_SCRIPTS/cdp-port-map.json"
|
|
49
|
+
DAILY_ENV="$DATA_SCRIPTS/daily-report.env"
|
|
50
|
+
ML_API_STORES="$DATA_SCRIPTS/ml-api-stores.json"
|
|
51
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
52
|
+
POLL_SRC="$SCRIPT_DIR/poll-fulfillment.sh"
|
|
53
|
+
|
|
54
|
+
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
|
55
|
+
log_info() { echo -e "${CYAN}→${NC} $*"; }
|
|
56
|
+
log_ok() { echo -e "${GREEN}✓${NC} $*"; }
|
|
57
|
+
log_warn() { echo -e "${YELLOW}⚠${NC} $*"; }
|
|
58
|
+
log_err() { echo -e "${RED}✗${NC} $*"; }
|
|
59
|
+
|
|
60
|
+
# 交互确认(默认 Yes):返回 0=确认 / 1=拒绝;非交互(stdin EOF)视为拒绝
|
|
61
|
+
_prompt() {
|
|
62
|
+
local ans
|
|
63
|
+
read -r -p "$1" ans
|
|
64
|
+
case "$(echo "$ans" | tr '[:upper:]' '[:lower:]')" in
|
|
65
|
+
""|y|yes) return 0 ;;
|
|
66
|
+
*) return 1 ;;
|
|
67
|
+
esac
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
REFRESH_STORES=0
|
|
71
|
+
FORCE_RECONFIG=0
|
|
72
|
+
FEISHU_GATEWAY_ONLY=0
|
|
73
|
+
VERIFY_ONLY=0
|
|
74
|
+
DAILY_REPORT_ONLY=0
|
|
75
|
+
for arg in "$@"; do
|
|
76
|
+
case "$arg" in
|
|
77
|
+
--refresh-stores) REFRESH_STORES=1 ;;
|
|
78
|
+
--reconfig) FORCE_RECONFIG=1 ;;
|
|
79
|
+
--verify) VERIFY_ONLY=1 ;;
|
|
80
|
+
--daily-report) DAILY_REPORT_ONLY=1 ;;
|
|
81
|
+
--feishu-gateway) FEISHU_GATEWAY_ONLY=1 ;;
|
|
82
|
+
*) echo "未知参数: ${arg}(支持 --refresh-stores / --reconfig / --verify / --daily-report / --feishu-gateway)" >&2; exit 1 ;;
|
|
83
|
+
esac
|
|
84
|
+
done
|
|
85
|
+
|
|
86
|
+
# ── 工具函数 ──────────────────────────────────────────────
|
|
87
|
+
# 跨平台紫鸟客户端进程名:macOS 是 /Applications/ziniao.app(进程名 ziniao/Ziniao),
|
|
88
|
+
# Windows 是 ziniaob(旧名 ziniaobro 兼容)——不再写死单一进程名
|
|
89
|
+
ZINIAO_PROCS="ziniao|Ziniao|ziniaob|ziniaobro"
|
|
90
|
+
|
|
91
|
+
# 探测已知 CDP 端口段(紫鸟店铺窗口 CDP 端口 52420+ 递增,curl /json/version 验证)
|
|
92
|
+
probe_known_cdp_ports() {
|
|
93
|
+
local p
|
|
94
|
+
for p in $(seq 52420 52460); do
|
|
95
|
+
if curl -s -m 1 "http://127.0.0.1:${p}/json/version" >/dev/null 2>&1; then
|
|
96
|
+
echo "$p"
|
|
97
|
+
fi
|
|
98
|
+
done
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# 跨平台紫鸟客户端检测:进程名(Mac: ziniao/Ziniao;Win: ziniaob)OR CDP 端口探测。
|
|
102
|
+
# 返回 0=客户端运行中;1=未检测到。
|
|
103
|
+
ziniao_client_running() {
|
|
104
|
+
local p
|
|
105
|
+
for p in ziniao Ziniao ziniaob ziniaobro; do
|
|
106
|
+
if pgrep -x "$p" >/dev/null 2>&1; then
|
|
107
|
+
return 0
|
|
108
|
+
fi
|
|
109
|
+
done
|
|
110
|
+
# CDP 端口探测兜底(进程名匹配不到时,端口通即认为客户端在跑)
|
|
111
|
+
[ -n "$(probe_known_cdp_ports)" ]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# 跨平台 CDP 端口发现:紫鸟进程(Mac: ziniao/Ziniao;Win: ziniaob)监听的
|
|
115
|
+
# 127.0.0.1 端口逐端口 curl /json/version 验证(排除 9480/9481 等本地 API 端口),
|
|
116
|
+
# 无进程匹配时回退已知端口段探测(与 orchestrator._discover_cdp_ports 语义一致)
|
|
117
|
+
discover_cdp_ports() {
|
|
118
|
+
local ports p out=""
|
|
119
|
+
ports=$(lsof -i -P -n 2>/dev/null | awk "/${ZINIAO_PROCS}/ && /LISTEN/ { for (i=1;i<=NF;i++) if (\$i ~ /^127\\.0\\.0\\.1:[0-9]+\$/) { split(\$i, a, \":\"); print a[2] } }" | sort -n | uniq)
|
|
120
|
+
for p in $ports; do
|
|
121
|
+
# 必须响应含 "Browser" 才是 CDP 调试端口(curl 对 404/Forbidden 也返回 0)
|
|
122
|
+
if curl -s -m 1 "http://127.0.0.1:${p}/json/version" 2>/dev/null | grep -q '"Browser"'; then
|
|
123
|
+
out="$out $p"
|
|
124
|
+
fi
|
|
125
|
+
done
|
|
126
|
+
if [ -z "$out" ]; then
|
|
127
|
+
out=$(probe_known_cdp_ports)
|
|
128
|
+
fi
|
|
129
|
+
echo "$out"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
has_api_key() {
|
|
133
|
+
# 检测紫鸟 apiKey 是否已配置(~/.ziniao/config.json 或 ~/.ziniao-cli/config.json 或 CLI 状态)
|
|
134
|
+
if [ -f "$HOME/.ziniao/config.json" ] && grep -q '"apiKey"' "$HOME/.ziniao/config.json" 2>/dev/null; then
|
|
135
|
+
return 0
|
|
136
|
+
fi
|
|
137
|
+
if [ -f "$HOME/.ziniao-cli/config.json" ] && grep -q '"apiKey"' "$HOME/.ziniao-cli/config.json" 2>/dev/null; then
|
|
138
|
+
return 0
|
|
139
|
+
fi
|
|
140
|
+
if command -v ziniao-cli >/dev/null 2>&1; then
|
|
141
|
+
if ziniao-cli config show 2>/dev/null | grep -q "apiKey"; then
|
|
142
|
+
return 0
|
|
143
|
+
fi
|
|
144
|
+
fi
|
|
145
|
+
return 1
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# 旧布局迁移:早期数据可能落在脚本目录(~/.argent-dsh/scripts/)——
|
|
149
|
+
# 检测旧路径文件存在且新路径(~/.dsh/argent/)无同名文件时复制过去(一次性)。
|
|
150
|
+
migrate_legacy_data() {
|
|
151
|
+
local f
|
|
152
|
+
for f in stores.json fulfillment.env cdp-port-map.json; do
|
|
153
|
+
if [ -f "$PROFILE_SCRIPTS/$f" ] && [ ! -e "$DATA_SCRIPTS/$f" ]; then
|
|
154
|
+
mkdir -p "$DATA_SCRIPTS"
|
|
155
|
+
cp "$PROFILE_SCRIPTS/$f" "$DATA_SCRIPTS/$f"
|
|
156
|
+
log_ok "已迁移旧配置 ${PROFILE_SCRIPTS}/${f} → ${DATA_SCRIPTS}/${f}(v0.6.1 客户数据新位置)"
|
|
157
|
+
fi
|
|
158
|
+
done
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# ── 1. 环境检测 ───────────────────────────────────────────
|
|
162
|
+
check_env() {
|
|
163
|
+
echo ""
|
|
164
|
+
echo "════════════════════════════════════════════════"
|
|
165
|
+
echo " 🔧 环境检测"
|
|
166
|
+
echo "════════════════════════════════════════════════"
|
|
167
|
+
|
|
168
|
+
if command -v ziniao-cli >/dev/null 2>&1; then
|
|
169
|
+
log_ok "ziniao-cli 可用: $(command -v ziniao-cli)"
|
|
170
|
+
else
|
|
171
|
+
log_err "未找到 ziniao-cli"
|
|
172
|
+
echo " 请先安装紫鸟 CLI:"
|
|
173
|
+
echo " npm install -g ziniao-cli"
|
|
174
|
+
echo " 或参考 https://open.ziniao.com 安装说明"
|
|
175
|
+
return 1
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
local ports
|
|
179
|
+
if ziniao_client_running; then
|
|
180
|
+
log_ok "紫鸟客户端运行中(跨平台检测:进程名 OR CDP 端口)"
|
|
181
|
+
ports=$(discover_cdp_ports)
|
|
182
|
+
if [ -n "$ports" ]; then
|
|
183
|
+
log_ok "发现 CDP 端口: $(echo "$ports" | tr '\n' ' ')"
|
|
184
|
+
else
|
|
185
|
+
log_warn "客户端运行中但暂无店铺窗口(CDP 端口未探测到)"
|
|
186
|
+
echo " 稍后向导将自动打开所选店铺窗口(ziniao-cli store open --id <storeId>)"
|
|
187
|
+
echo " 如自动打开失败,请手动双击紫鸟中的店铺环境"
|
|
188
|
+
fi
|
|
189
|
+
else
|
|
190
|
+
log_warn "未检测到紫鸟客户端(Mac: ziniao/Ziniao;Win: ziniaob)"
|
|
191
|
+
echo " 请先启动紫鸟客户端(登录后向导可自动打开店铺窗口),再继续"
|
|
192
|
+
read -r -p " 按回车继续(或 Ctrl-C 退出)... " _dummy || true
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
if command -v lark-cli >/dev/null 2>&1; then
|
|
196
|
+
log_ok "lark-cli 可用: $(command -v lark-cli)"
|
|
197
|
+
else
|
|
198
|
+
echo " ℹ 检测到 lark-cli(飞书 CLI)未安装"
|
|
199
|
+
if ! command -v npm >/dev/null 2>&1; then
|
|
200
|
+
log_err "未找到 npm,请先安装 Node.js(LTS 版自带 npm):"
|
|
201
|
+
echo " https://nodejs.org/zh-cn/download"
|
|
202
|
+
echo " 或 macOS: brew install node"
|
|
203
|
+
return 1
|
|
204
|
+
fi
|
|
205
|
+
if _prompt " 是否自动安装?[Y/n]: "; then
|
|
206
|
+
# 国内客户机网络:npmmirror 可达(npmjs.org 不通)——优先国内源,失败 fallback 官方源
|
|
207
|
+
npm install -g @larksuite/cli --registry https://registry.npmmirror.com 2>/dev/null \
|
|
208
|
+
|| npm install -g @larksuite/cli 2>/dev/null || true
|
|
209
|
+
if command -v lark-cli >/dev/null 2>&1; then
|
|
210
|
+
log_ok "lark-cli 就绪: $(command -v lark-cli)"
|
|
211
|
+
else
|
|
212
|
+
log_err "lark-cli 安装失败,请手动安装:"
|
|
213
|
+
echo " npm install -g @larksuite/cli --registry https://registry.npmmirror.com"
|
|
214
|
+
return 1
|
|
215
|
+
fi
|
|
216
|
+
else
|
|
217
|
+
log_err "请先安装 lark-cli: npm install -g @larksuite/cli --registry https://registry.npmmirror.com"
|
|
218
|
+
return 1
|
|
219
|
+
fi
|
|
220
|
+
fi
|
|
221
|
+
return 0
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# ── 2. ziniao apiKey 配置 ─────────────────────────────────
|
|
225
|
+
ensure_api_key() {
|
|
226
|
+
echo ""
|
|
227
|
+
echo "════════════════════════════════════════════════"
|
|
228
|
+
echo " 🔑 紫鸟开放平台 apiKey"
|
|
229
|
+
echo "════════════════════════════════════════════════"
|
|
230
|
+
|
|
231
|
+
if has_api_key; then
|
|
232
|
+
log_ok "apiKey 已配置($(ziniao-cli config show 2>/dev/null | grep -o '"configPath": *"[^"]*"' | head -1 | sed 's/.*: *"//;s/"$//' || echo '~/.ziniao-cli/config.json'))"
|
|
233
|
+
return 0
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
log_warn "未检测到紫鸟 apiKey 配置"
|
|
237
|
+
echo " 请登录紫鸟开放平台(https://open.ziniao.com)获取你的 apiKey"
|
|
238
|
+
echo " ·「用户应用管理」vs「应用管理」区分:用户应用管理里创建的 Key 才能调用 ZClaw Bridge(store list 等本地自动化);应用管理里的 Key 只能调服务端 ERP,store list 会报「API Key 认证失败」"
|
|
239
|
+
echo " (拿错菜单是最常见坑,2026-08-15 实测:1504=应用管理不可用,6032=用户应用管理正确)"
|
|
240
|
+
echo " · 新机器白名单:本机终端识别码须先加入「用户应用管理 → 终端管理」白名单,否则调用报 40004 拒绝"
|
|
241
|
+
echo " · 成员/子账号:本脚本 config init 已用 --member 模式(跳过企业信息获取,绕过企业级 IP 白名单),用成员身份申请的 Key 即可"
|
|
242
|
+
read -r -p " 请输入 apiKey: " api_key
|
|
243
|
+
api_key="$(echo "$api_key" | tr -d '[:space:]')"
|
|
244
|
+
if [ -z "$api_key" ]; then
|
|
245
|
+
log_err "apiKey 不能为空"
|
|
246
|
+
return 1
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
if command -v ziniao-cli >/dev/null 2>&1; then
|
|
250
|
+
# 参照本机现有方式:config init 写入 ~/.ziniao-cli/config.json(明文,成员账号)
|
|
251
|
+
if ! echo "$api_key" | ziniao-cli config init --api-key-stdin --member --no-keychain --profile ml-seller >/dev/null 2>&1; then
|
|
252
|
+
log_warn "ziniao-cli config init 失败,直接写入 ~/.ziniao/config.json"
|
|
253
|
+
mkdir -p "$HOME/.ziniao"
|
|
254
|
+
cat > "$HOME/.ziniao/config.json" << EOF
|
|
255
|
+
{
|
|
256
|
+
"apiKey": "$api_key",
|
|
257
|
+
"serverUrl": "https://sbappstoreapi.ziniao.com"
|
|
258
|
+
}
|
|
259
|
+
EOF
|
|
260
|
+
fi
|
|
261
|
+
ziniao-cli config use ml-seller >/dev/null 2>&1 || true
|
|
262
|
+
else
|
|
263
|
+
mkdir -p "$HOME/.ziniao"
|
|
264
|
+
cat > "$HOME/.ziniao/config.json" << EOF
|
|
265
|
+
{
|
|
266
|
+
"apiKey": "$api_key",
|
|
267
|
+
"serverUrl": "https://sbappstoreapi.ziniao.com"
|
|
268
|
+
}
|
|
269
|
+
EOF
|
|
270
|
+
fi
|
|
271
|
+
log_ok "apiKey 已保存"
|
|
272
|
+
return 0
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
# ── 3. 店铺初始化(核心)─────────────────────────────────
|
|
276
|
+
# 3a. 获取环境列表
|
|
277
|
+
fetch_store_list() {
|
|
278
|
+
local out
|
|
279
|
+
out=$(ziniao-cli store list 2>/dev/null) || {
|
|
280
|
+
# KB-071:区分「未配置 apiKey」与「已配置但认证失败」
|
|
281
|
+
if has_api_key; then
|
|
282
|
+
log_err "ziniao-cli store list 失败(apiKey 已配置但认证失败)"
|
|
283
|
+
echo " 💡 修复:Argent 设置页 → ML 配置区 → 紫鸟 API Key 重新配置"
|
|
284
|
+
echo " · 注意 key 来源:必须在紫鸟开放平台「用户应用管理」创建(才能调 ZClaw Bridge/store list);"
|
|
285
|
+
echo " 「应用管理」里的 Key 只能调服务端 ERP,store list 会报「API Key 认证失败」"
|
|
286
|
+
else
|
|
287
|
+
log_err "ziniao-cli store list 失败(未检测到 apiKey 配置)"
|
|
288
|
+
echo " 💡 修复:Argent 设置页 → ML 配置区 → 填写紫鸟 API Key(开放平台「用户应用管理」获取)"
|
|
289
|
+
fi
|
|
290
|
+
echo " · 同时确认紫鸟客户端已启动并登录(store list 依赖本地客户端)"
|
|
291
|
+
return 1
|
|
292
|
+
}
|
|
293
|
+
python3 - "$out" << 'PYEOF'
|
|
294
|
+
import json, sys
|
|
295
|
+
try:
|
|
296
|
+
d = json.loads(sys.argv[1])
|
|
297
|
+
items = d.get("data", {}).get("items", [])
|
|
298
|
+
except Exception:
|
|
299
|
+
sys.exit(1)
|
|
300
|
+
# 过滤 MercadoLibre 平台
|
|
301
|
+
ml = [i for i in items if "MercadoLibre" in (i.get("platformName") or "")]
|
|
302
|
+
for i in ml:
|
|
303
|
+
print(f"{i.get('storeName')}\t{i.get('storeId')}\t{i.get('platformName')}")
|
|
304
|
+
PYEOF
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
# 3c/3d. 交互选择 + 生成 stores.json
|
|
308
|
+
init_stores() {
|
|
309
|
+
echo ""
|
|
310
|
+
echo "════════════════════════════════════════════════"
|
|
311
|
+
echo " 🏬 店铺初始化(紫鸟环境 → FULL 货件)"
|
|
312
|
+
echo "════════════════════════════════════════════════"
|
|
313
|
+
|
|
314
|
+
local list
|
|
315
|
+
list=$(fetch_store_list) || return 1
|
|
316
|
+
if [ -z "$list" ]; then
|
|
317
|
+
log_err "未找到 MercadoLibre 环境(ziniao-cli store list 无结果)"
|
|
318
|
+
return 1
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
echo " 紫鸟中的 MercadoLibre 环境:"
|
|
322
|
+
echo ""
|
|
323
|
+
local i=0 name id platform
|
|
324
|
+
while IFS=$'\t' read -r name id platform; do
|
|
325
|
+
[ -z "$name" ] && continue
|
|
326
|
+
i=$((i + 1))
|
|
327
|
+
printf " [%d] %s (storeId=%s, %s)\n" "$i" "$name" "$id" "$platform"
|
|
328
|
+
done <<< "$list"
|
|
329
|
+
echo ""
|
|
330
|
+
|
|
331
|
+
read -r -p " 选择用于 FULL 货件的环境编号(逗号分隔,如 1,2;回车=全部): " choice
|
|
332
|
+
choice="$(echo "$choice" | tr -d '[:space:]')"
|
|
333
|
+
if [ -z "$choice" ]; then
|
|
334
|
+
choice=$(seq -s, 1 "$i")
|
|
335
|
+
fi
|
|
336
|
+
|
|
337
|
+
# 校验选择
|
|
338
|
+
local selected=""
|
|
339
|
+
local c
|
|
340
|
+
IFS=',' read -ra picks <<< "$choice"
|
|
341
|
+
for c in "${picks[@]}"; do
|
|
342
|
+
if [ "$c" -ge 1 ] 2>/dev/null && [ "$c" -le "$i" ] 2>/dev/null; then
|
|
343
|
+
selected="$selected $c"
|
|
344
|
+
else
|
|
345
|
+
log_warn "忽略无效编号: $c"
|
|
346
|
+
fi
|
|
347
|
+
done
|
|
348
|
+
if [ -z "$selected" ]; then
|
|
349
|
+
log_err "未选择任何环境"
|
|
350
|
+
return 1
|
|
351
|
+
fi
|
|
352
|
+
|
|
353
|
+
# 3d. 生成 stores.json(cdp_port 待探测)
|
|
354
|
+
echo ""
|
|
355
|
+
log_info "生成店铺映射 stores.json ..."
|
|
356
|
+
mkdir -p "$DATA_SCRIPTS"
|
|
357
|
+
local sel_list="$selected"
|
|
358
|
+
python3 - "$STORES_JSON" "$sel_list" << 'PYEOF'
|
|
359
|
+
import json, os, subprocess, sys
|
|
360
|
+
|
|
361
|
+
stores_path, sel_str = sys.argv[1], sys.argv[2]
|
|
362
|
+
selected = [int(x) for x in sel_str.split()]
|
|
363
|
+
|
|
364
|
+
# 重新取 store list 保证名称/id 一致
|
|
365
|
+
out = subprocess.run(["ziniao-cli", "store", "list"], capture_output=True, text=True)
|
|
366
|
+
items = json.loads(out.stdout)["data"]["items"]
|
|
367
|
+
ml = [i for i in items if "MercadoLibre" in (i.get("platformName") or "")]
|
|
368
|
+
|
|
369
|
+
# 重建而非合并:客户取消选择的环境应从 stores.json 移除(--refresh-stores 同步语义)
|
|
370
|
+
old = {}
|
|
371
|
+
try:
|
|
372
|
+
with open(stores_path) as f:
|
|
373
|
+
old = json.load(f)
|
|
374
|
+
except Exception:
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
stores = {}
|
|
378
|
+
for idx in selected:
|
|
379
|
+
if idx - 1 >= len(ml):
|
|
380
|
+
continue
|
|
381
|
+
it = ml[idx - 1]
|
|
382
|
+
name = it["storeName"]
|
|
383
|
+
prev = old.get(name, {})
|
|
384
|
+
stores[name] = {
|
|
385
|
+
"store_id": it["storeId"],
|
|
386
|
+
"platform": it.get("platformName", ""),
|
|
387
|
+
"cdp_port": prev.get("cdp_port"),
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if unresolved:
|
|
391
|
+
print()
|
|
392
|
+
print(f" ⚠ {len(unresolved)} 个店铺端口无法自动关联({', '.join(unresolved)})——"
|
|
393
|
+
f"若这些窗口均已打开且无法区分,请关闭多余窗口后重跑自动探测,"
|
|
394
|
+
f"或在 stores.json 手动填写 cdp_port 字段")
|
|
395
|
+
|
|
396
|
+
with open(stores_path, "w") as f:
|
|
397
|
+
json.dump(stores, f, ensure_ascii=False, indent=2)
|
|
398
|
+
print("stores.json 已更新:", list(stores.keys()))
|
|
399
|
+
PYEOF
|
|
400
|
+
return 0
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
# 3e. 打开窗口 + 探测 CDP 端口
|
|
404
|
+
probe_store_ports() {
|
|
405
|
+
echo ""
|
|
406
|
+
log_info "打开所选环境窗口并探测 CDP 端口 ..."
|
|
407
|
+
echo " (每个环境会打开独立可见窗口,请勿关闭;已打开的窗口将复用)"
|
|
408
|
+
|
|
409
|
+
python3 - "$STORES_JSON" "$CDP_CACHE" << 'PYEOF'
|
|
410
|
+
import json, os, subprocess, sys, time, urllib.request
|
|
411
|
+
|
|
412
|
+
stores_path, cache_path = sys.argv[1], sys.argv[2]
|
|
413
|
+
with open(stores_path) as f:
|
|
414
|
+
stores = json.load(f)
|
|
415
|
+
|
|
416
|
+
unresolved = []
|
|
417
|
+
|
|
418
|
+
# CDP 端口 → store_id 精确映射:监听端口进程的 user-data-dir 含 chrome_<storeId>
|
|
419
|
+
# (紫鸟每个店铺独立用户数据目录,实测可精确关联已打开窗口的端口;
|
|
420
|
+
# 只在 user-data-dir 参数内匹配,且仅认 stores.json 中真实存在的 store_id)
|
|
421
|
+
def ports_by_store_id(store_ids):
|
|
422
|
+
import re
|
|
423
|
+
mapping = {}
|
|
424
|
+
try:
|
|
425
|
+
out = subprocess.run(["lsof", "-i", "-P", "-n", "-sTCP:LISTEN"],
|
|
426
|
+
capture_output=True, text=True, timeout=10)
|
|
427
|
+
pid_by_port = {}
|
|
428
|
+
for line in out.stdout.splitlines():
|
|
429
|
+
m = re.search(r":(\d+)\s+\(LISTEN\)", line)
|
|
430
|
+
if m:
|
|
431
|
+
pid_by_port.setdefault(int(m.group(1)), set()).add(line.split()[1])
|
|
432
|
+
for port, pids in pid_by_port.items():
|
|
433
|
+
for pid in pids:
|
|
434
|
+
try:
|
|
435
|
+
cmd = subprocess.run(["ps", "-p", str(pid), "-o", "command="],
|
|
436
|
+
capture_output=True, text=True, timeout=5).stdout
|
|
437
|
+
m2 = re.search(r"user-data-dir[=\s\"']+[^\"']*?chrome_(\d+)", cmd)
|
|
438
|
+
if m2 and m2.group(1) in store_ids:
|
|
439
|
+
mapping[port] = m2.group(1)
|
|
440
|
+
break
|
|
441
|
+
except Exception:
|
|
442
|
+
continue
|
|
443
|
+
except Exception:
|
|
444
|
+
pass
|
|
445
|
+
return mapping
|
|
446
|
+
|
|
447
|
+
port2store = ports_by_store_id({str(e.get("store_id") or "") for e in stores.values()})
|
|
448
|
+
|
|
449
|
+
# 跨平台紫鸟进程名:Mac: ziniao/Ziniao(/Applications/ziniao.app);Win: ziniaob(旧名 ziniaobro 兼容)
|
|
450
|
+
ZINIAO_MARKS = ("ziniao", "ziniaob")
|
|
451
|
+
|
|
452
|
+
def is_cdp_port(port, timeout=1.0):
|
|
453
|
+
"""curl /json/version 验证端口是 CDP 调试端口(排除 9480/9481 等本地 API 端口)。"""
|
|
454
|
+
try:
|
|
455
|
+
with urllib.request.urlopen(f"http://127.0.0.1:{port}/json/version", timeout=timeout) as r:
|
|
456
|
+
return b"Browser" in r.read(200)
|
|
457
|
+
except Exception:
|
|
458
|
+
return False
|
|
459
|
+
|
|
460
|
+
def discover_ports():
|
|
461
|
+
"""跨平台 CDP 端口发现:紫鸟进程(Mac ziniao/Ziniao;Win ziniaob)监听端口
|
|
462
|
+
+ /json/version 验证;无进程匹配时回退已知端口段(52420+ 递增)探测。"""
|
|
463
|
+
ports = []
|
|
464
|
+
try:
|
|
465
|
+
out = subprocess.run(["lsof", "-i", "-P", "-n"], capture_output=True, text=True, timeout=10)
|
|
466
|
+
for line in out.stdout.splitlines():
|
|
467
|
+
low = line.lower()
|
|
468
|
+
if any(m in low for m in ZINIAO_MARKS) and "LISTEN" in line:
|
|
469
|
+
for p in line.split():
|
|
470
|
+
if p.startswith("127.0.0.1:"):
|
|
471
|
+
port = p.split(":")[-1]
|
|
472
|
+
if port.isdigit():
|
|
473
|
+
ports.append(int(port))
|
|
474
|
+
except Exception:
|
|
475
|
+
pass
|
|
476
|
+
ports = [p for p in sorted(set(ports)) if is_cdp_port(p)]
|
|
477
|
+
if not ports:
|
|
478
|
+
# 回退:已知 CDP 端口段(52420+ 递增)
|
|
479
|
+
for p in range(52420, 52461):
|
|
480
|
+
if is_cdp_port(p, timeout=0.5):
|
|
481
|
+
ports.append(p)
|
|
482
|
+
return sorted(set(ports))
|
|
483
|
+
|
|
484
|
+
# 读缓存(storeId → {store_name, env_name, cdp_port};兼容旧格式 店铺名 → 端口)
|
|
485
|
+
cached = {}
|
|
486
|
+
try:
|
|
487
|
+
with open(cache_path) as f:
|
|
488
|
+
raw_cache = json.load(f)
|
|
489
|
+
for k, v in raw_cache.items():
|
|
490
|
+
if isinstance(v, dict) and v.get("cdp_port"):
|
|
491
|
+
cached[str(k)] = {"store_name": v.get("store_name", k),
|
|
492
|
+
"env_name": v.get("env_name", ""),
|
|
493
|
+
"cdp_port": int(v["cdp_port"])}
|
|
494
|
+
elif str(v).isdigit():
|
|
495
|
+
cached[str(k)] = {"store_name": k, "env_name": "", "cdp_port": int(v)}
|
|
496
|
+
except Exception:
|
|
497
|
+
pass
|
|
498
|
+
|
|
499
|
+
for name, entry in stores.items():
|
|
500
|
+
before = set(discover_ports())
|
|
501
|
+
store_id = str(entry.get("store_id") or "")
|
|
502
|
+
# 打开窗口(非 headless 显式声明);优先 --id(name 可能重复/变更),旧数据无 store_id 时回退 --name
|
|
503
|
+
cmd = ["ziniao-cli", "store", "open", "--headless=false"]
|
|
504
|
+
if store_id:
|
|
505
|
+
cmd += ["--id", store_id]
|
|
506
|
+
else:
|
|
507
|
+
cmd += ["--name", name]
|
|
508
|
+
try:
|
|
509
|
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
510
|
+
raw = r.stdout
|
|
511
|
+
start = raw.find("{")
|
|
512
|
+
data = json.loads(raw[start:]) if start >= 0 else {}
|
|
513
|
+
reused = data.get("data", {}).get("reused", False)
|
|
514
|
+
except Exception as e:
|
|
515
|
+
print(f" ⚠ {name}: store open 失败: {e}")
|
|
516
|
+
print(f" 请手动双击紫鸟中的「{name}」环境打开窗口,再重新运行")
|
|
517
|
+
continue
|
|
518
|
+
time.sleep(4)
|
|
519
|
+
after = set(discover_ports())
|
|
520
|
+
new_ports = sorted(after - before)
|
|
521
|
+
port = None
|
|
522
|
+
if len(new_ports) == 1:
|
|
523
|
+
port = new_ports[0]
|
|
524
|
+
elif reused and store_id in cached and cached[store_id]["cdp_port"] in after:
|
|
525
|
+
port = cached[store_id]["cdp_port"]
|
|
526
|
+
elif len(after) == 1:
|
|
527
|
+
port = list(after)[0]
|
|
528
|
+
elif not new_ports and store_id in cached and cached[store_id]["cdp_port"] in after:
|
|
529
|
+
port = cached[store_id]["cdp_port"]
|
|
530
|
+
|
|
531
|
+
if port is None and port2store:
|
|
532
|
+
# user-data-dir 匹配:chrome_<storeId> 精确关联已打开窗口(仅 CDP 端口集合)
|
|
533
|
+
sid = str(entry.get("store_id") or "")
|
|
534
|
+
for pnum, psid in port2store.items():
|
|
535
|
+
if psid == sid and pnum in after:
|
|
536
|
+
port = pnum
|
|
537
|
+
print(f" ✓ {name}: CDP 端口 = {port}(窗口 user-data-dir 匹配 chrome_{sid})")
|
|
538
|
+
break
|
|
539
|
+
if port is not None:
|
|
540
|
+
entry["cdp_port"] = int(port)
|
|
541
|
+
print(f" ✓ {name}: CDP 端口 = {port}{'(复用缓存)' if port in [v['cdp_port'] for v in cached.values()] and port not in new_ports else ''}")
|
|
542
|
+
else:
|
|
543
|
+
entry["cdp_port"] = None
|
|
544
|
+
print(f" ⚠ {name}: 无法确定 CDP 端口(当前端口: {sorted(after)}),请手动补充 stores.json cdp_port")
|
|
545
|
+
unresolved.append(name)
|
|
546
|
+
|
|
547
|
+
if unresolved:
|
|
548
|
+
print()
|
|
549
|
+
print(f" ⚠ {len(unresolved)} 个店铺端口无法自动关联({', '.join(unresolved)})——"
|
|
550
|
+
f"若这些窗口均已打开且无法区分,请关闭多余窗口后重跑自动探测,"
|
|
551
|
+
f"或在 stores.json 手动填写 cdp_port 字段")
|
|
552
|
+
|
|
553
|
+
with open(stores_path, "w") as f:
|
|
554
|
+
json.dump(stores, f, ensure_ascii=False, indent=2)
|
|
555
|
+
|
|
556
|
+
# 更新缓存:storeId → {store_name, env_name, cdp_port}(三方一致映射——
|
|
557
|
+
# storeId/店铺名/环境名 均来自 ziniao-cli store list,端口来自本次探测)
|
|
558
|
+
meta = {}
|
|
559
|
+
try:
|
|
560
|
+
out = subprocess.run(["ziniao-cli", "store", "list"], capture_output=True, text=True, timeout=30)
|
|
561
|
+
items = json.loads(out.stdout)["data"]["items"]
|
|
562
|
+
meta = {str(i["storeId"]): {"store_name": i.get("storeName", ""),
|
|
563
|
+
"env_name": i.get("platformName", "")} for i in items}
|
|
564
|
+
except Exception:
|
|
565
|
+
pass
|
|
566
|
+
|
|
567
|
+
port_map = {}
|
|
568
|
+
for name, entry in stores.items():
|
|
569
|
+
sid = str(entry.get("store_id") or "")
|
|
570
|
+
if entry.get("cdp_port") and sid:
|
|
571
|
+
m = meta.get(sid, {})
|
|
572
|
+
port_map[sid] = {
|
|
573
|
+
"store_name": m.get("store_name") or name,
|
|
574
|
+
"env_name": m.get("env_name", ""),
|
|
575
|
+
"cdp_port": int(entry["cdp_port"]),
|
|
576
|
+
}
|
|
577
|
+
with open(cache_path, "w") as f:
|
|
578
|
+
json.dump(port_map, f, ensure_ascii=False, indent=2)
|
|
579
|
+
PYEOF
|
|
580
|
+
return 0
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
# 3f. 飞书「店铺名称」字段同步为单选
|
|
584
|
+
# 表格为 BOT 所建(方案 B)——默认 bot 身份即可(bot 是 Base 创建者/管理员,
|
|
585
|
+
# 转移所有者后仍保留可管理 full_access)。
|
|
586
|
+
sync_feishu_field() {
|
|
587
|
+
echo ""
|
|
588
|
+
log_info "同步飞书「店铺名称」字段为单选选项 ..."
|
|
589
|
+
local base_token="$1" table_id="$2"
|
|
590
|
+
|
|
591
|
+
python3 - "$STORES_JSON" "$base_token" "$table_id" << 'PYEOF'
|
|
592
|
+
import json, os, subprocess, sys
|
|
593
|
+
|
|
594
|
+
stores_path, base_token, table_id = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
595
|
+
with open(stores_path) as f:
|
|
596
|
+
stores = json.load(f)
|
|
597
|
+
names = list(stores.keys())
|
|
598
|
+
if not names:
|
|
599
|
+
print(" ⚠ stores.json 为空,跳过字段同步")
|
|
600
|
+
sys.exit(0)
|
|
601
|
+
|
|
602
|
+
options = [{"name": n} for n in names]
|
|
603
|
+
# lark-cli 1.0.53 API 要求字符串判别值 type="select",options 在顶层
|
|
604
|
+
# ("type":3 数字形式会被 API 拒绝: Invalid discriminator value)
|
|
605
|
+
field_json = json.dumps({
|
|
606
|
+
"name": "店铺名称",
|
|
607
|
+
"type": "select",
|
|
608
|
+
"multiple": False,
|
|
609
|
+
"options": options,
|
|
610
|
+
}, ensure_ascii=False)
|
|
611
|
+
|
|
612
|
+
cmd = ["lark-cli", "base", "+field-update",
|
|
613
|
+
"--base-token", base_token, "--table-id", table_id,
|
|
614
|
+
"--field-id", "店铺名称", "--json", field_json, "--yes"]
|
|
615
|
+
|
|
616
|
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
617
|
+
if r.returncode != 0:
|
|
618
|
+
print(f" ✗ 字段更新失败: {r.stderr or r.stdout}")
|
|
619
|
+
sys.exit(1)
|
|
620
|
+
print(f" ✓ 「店铺名称」已改为单选,选项: {names}")
|
|
621
|
+
PYEOF
|
|
622
|
+
return $?
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
# 3g. 旧记录迁移
|
|
626
|
+
migrate_old_records() {
|
|
627
|
+
echo ""
|
|
628
|
+
log_info "检查旧记录「店铺名称」值是否需要迁移 ..."
|
|
629
|
+
local base_token="$1" table_id="$2"
|
|
630
|
+
|
|
631
|
+
# 第一步:查询并输出迁移计划(old_value \t target \t record_id,...)
|
|
632
|
+
# 注意:python 通过 heredoc 运行,stdin 被占用,交互确认必须在 bash 层做
|
|
633
|
+
local plan
|
|
634
|
+
plan=$(python3 - "$STORES_JSON" "$base_token" "$table_id" << 'PYEOF'
|
|
635
|
+
import json, os, subprocess, sys
|
|
636
|
+
|
|
637
|
+
stores_path, base_token, table_id = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
638
|
+
with open(stores_path) as f:
|
|
639
|
+
stores = json.load(f)
|
|
640
|
+
valid = set(stores.keys())
|
|
641
|
+
|
|
642
|
+
cmd = ["lark-cli", "base", "+record-list",
|
|
643
|
+
"--base-token", base_token, "--table-id", table_id,
|
|
644
|
+
"--format", "json"]
|
|
645
|
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
646
|
+
if r.returncode != 0:
|
|
647
|
+
print(f" ⚠ 查询记录失败: {r.stderr or r.stdout}", file=sys.stderr)
|
|
648
|
+
sys.exit(0)
|
|
649
|
+
d = json.loads(r.stdout)
|
|
650
|
+
fields = d["data"]["fields"]
|
|
651
|
+
rows = d["data"]["data"]
|
|
652
|
+
rids = d["data"]["record_id_list"]
|
|
653
|
+
try:
|
|
654
|
+
col = fields.index("店铺名称")
|
|
655
|
+
except ValueError:
|
|
656
|
+
print(" ⚠ 表格无「店铺名称」字段", file=sys.stderr)
|
|
657
|
+
sys.exit(0)
|
|
658
|
+
|
|
659
|
+
# 旧值 → [record_ids]
|
|
660
|
+
old_map = {}
|
|
661
|
+
for i, row in enumerate(rows):
|
|
662
|
+
val = row[col] if col < len(row) else None
|
|
663
|
+
if val is None:
|
|
664
|
+
continue
|
|
665
|
+
if isinstance(val, list):
|
|
666
|
+
val = val[0] if val else None
|
|
667
|
+
if val is None or str(val) in valid:
|
|
668
|
+
continue
|
|
669
|
+
old_map.setdefault(str(val), []).append(rids[i])
|
|
670
|
+
|
|
671
|
+
if not old_map:
|
|
672
|
+
print(" ✓ 无需要迁移的旧记录", file=sys.stderr)
|
|
673
|
+
sys.exit(0)
|
|
674
|
+
|
|
675
|
+
# 迁移映射(bash 层交互确认)
|
|
676
|
+
LEGACY_MAP = {"1店": "1店-子账号", "2店": "2店-子账号", "3店": "3店-主账号"}
|
|
677
|
+
for val, ids in old_map.items():
|
|
678
|
+
target = LEGACY_MAP.get(val)
|
|
679
|
+
if target and target in valid:
|
|
680
|
+
print(f"PLAN\t{val}\t{target}\t{','.join(ids)}")
|
|
681
|
+
else:
|
|
682
|
+
print(f"MANUAL\t{val}\t\t{','.join(ids)}")
|
|
683
|
+
PYEOF
|
|
684
|
+
)
|
|
685
|
+
[ $? -eq 0 ] || return 1
|
|
686
|
+
|
|
687
|
+
if [ -z "$plan" ]; then
|
|
688
|
+
return 0
|
|
689
|
+
fi
|
|
690
|
+
|
|
691
|
+
# 第二步:bash 交互确认 + 逐条迁移
|
|
692
|
+
# 用 fd 3 读计划,避免 here-string 被内层 read(用户输入)抢走
|
|
693
|
+
local plan_file
|
|
694
|
+
plan_file=$(mktemp)
|
|
695
|
+
printf '%s\n' "$plan" > "$plan_file"
|
|
696
|
+
local failed=0
|
|
697
|
+
while IFS=$'\t' read -r kind val target rid_csv <&3; do
|
|
698
|
+
[ -z "$kind" ] && continue
|
|
699
|
+
if [ "$kind" = "MANUAL" ]; then
|
|
700
|
+
local n_manual
|
|
701
|
+
n_manual=$(echo "$rid_csv" | tr ',' '\n' | grep -c . || true)
|
|
702
|
+
log_warn "「${val}」无法自动映射(${n_manual} 条记录),请人工在飞书表格中处理"
|
|
703
|
+
continue
|
|
704
|
+
fi
|
|
705
|
+
local n
|
|
706
|
+
n=$(echo "$rid_csv" | tr ',' '\n' | grep -c . || true)
|
|
707
|
+
echo " 建议迁移: 「${val}」→「${target}」($n 条)"
|
|
708
|
+
read -r -p " 确认迁移? [Y/n] " ans
|
|
709
|
+
case "$(echo "$ans" | tr '[:upper:]' '[:lower:]')" in
|
|
710
|
+
""|y|yes)
|
|
711
|
+
local ok=1 rid
|
|
712
|
+
IFS=',' read -ra rid_arr <<< "$rid_csv"
|
|
713
|
+
for rid in "${rid_arr[@]}"; do
|
|
714
|
+
upd=$(lark-cli base +record-upsert \
|
|
715
|
+
--base-token "$base_token" --table-id "$table_id" \
|
|
716
|
+
--record-id "$rid" --json "{\"店铺名称\":\"$target\"}" 2>&1) || { ok=0; echo " ✗ 更新 $rid 失败: $upd"; }
|
|
717
|
+
done
|
|
718
|
+
if [ "$ok" = "1" ]; then
|
|
719
|
+
echo " ✓ 已迁移 $n 条 →「${target}」"
|
|
720
|
+
else
|
|
721
|
+
failed=1
|
|
722
|
+
fi
|
|
723
|
+
;;
|
|
724
|
+
*)
|
|
725
|
+
echo " - 跳过「${val}」"
|
|
726
|
+
;;
|
|
727
|
+
esac
|
|
728
|
+
done 3< "$plan_file"
|
|
729
|
+
rm -f "$plan_file"
|
|
730
|
+
return "$failed"
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
# 3h. 一致性验证
|
|
734
|
+
# 表格为 BOT 所建——默认 bot 身份即可
|
|
735
|
+
verify_consistency() {
|
|
736
|
+
echo ""
|
|
737
|
+
log_info "验证 stores.json 与表格选项一致性 ..."
|
|
738
|
+
local base_token="$1" table_id="$2"
|
|
739
|
+
|
|
740
|
+
python3 - "$STORES_JSON" "$base_token" "$table_id" << 'PYEOF'
|
|
741
|
+
import json, os, subprocess, sys
|
|
742
|
+
|
|
743
|
+
stores_path, base_token, table_id = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
744
|
+
with open(stores_path) as f:
|
|
745
|
+
stores = json.load(f)
|
|
746
|
+
store_keys = set(stores.keys())
|
|
747
|
+
|
|
748
|
+
cmd = ["lark-cli", "base", "+field-list",
|
|
749
|
+
"--base-token", base_token, "--table-id", table_id]
|
|
750
|
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
751
|
+
if r.returncode != 0:
|
|
752
|
+
print(f" ✗ 查询字段失败: {r.stderr or r.stdout}")
|
|
753
|
+
sys.exit(1)
|
|
754
|
+
d = json.loads(r.stdout)
|
|
755
|
+
target = None
|
|
756
|
+
for fld in d["data"]["fields"]:
|
|
757
|
+
if fld.get("name") == "店铺名称":
|
|
758
|
+
target = fld
|
|
759
|
+
break
|
|
760
|
+
if target is None:
|
|
761
|
+
print(" ✗ 未找到「店铺名称」字段")
|
|
762
|
+
sys.exit(1)
|
|
763
|
+
|
|
764
|
+
if target.get("type") != "select":
|
|
765
|
+
print(f" ✗ 「店铺名称」字段类型不是单选(当前: {target.get('type')})")
|
|
766
|
+
sys.exit(1)
|
|
767
|
+
|
|
768
|
+
opts = {(o.get("name") if isinstance(o, dict) else o) for o in (target.get("options") or [])}
|
|
769
|
+
if opts == store_keys:
|
|
770
|
+
print(f" ✓ 一致性通过: 表格选项 == stores.json keys({sorted(store_keys)})")
|
|
771
|
+
else:
|
|
772
|
+
print(f" ✗ 不一致!")
|
|
773
|
+
print(f" 表格选项: {sorted(opts)}")
|
|
774
|
+
print(f" stores.json: {sorted(store_keys)}")
|
|
775
|
+
print(" 请重新运行 --refresh-stores 同步")
|
|
776
|
+
sys.exit(1)
|
|
777
|
+
PYEOF
|
|
778
|
+
return $?
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
# ── 4. 飞书配置 ──────────────────────────────────────────
|
|
782
|
+
# 4a0. 飞书凭据/推送配置(ML 角色唯一配置点)
|
|
783
|
+
# argent setup ③ 对 ML 角色已跳过(打印「飞书配置将由 ML 环境向导统一处理」),
|
|
784
|
+
# 本函数是 App ID/Secret/Webhook 的唯一入口;已有配置自动检测复用(幂等)。
|
|
785
|
+
# 返回 0(跳过/失败都不中止向导——后续 check_lark_login 决定是否继续表格配置)。
|
|
786
|
+
_write_feishu_config_webhook() {
|
|
787
|
+
# 写 config.yaml argent.feishu(mode=webhook);缺 python3-yaml 时返回 1
|
|
788
|
+
local url="$1"
|
|
789
|
+
python3 - "$PROFILE_HOME" "$url" <<'PYEOF'
|
|
790
|
+
import sys, os
|
|
791
|
+
home, url = sys.argv[1], sys.argv[2]
|
|
792
|
+
cfg_path = os.path.join(home, "config.yaml")
|
|
793
|
+
try:
|
|
794
|
+
import yaml
|
|
795
|
+
except Exception:
|
|
796
|
+
print("NO_YAML", file=sys.stderr)
|
|
797
|
+
sys.exit(1)
|
|
798
|
+
cfg = {}
|
|
799
|
+
if os.path.exists(cfg_path):
|
|
800
|
+
with open(cfg_path) as f:
|
|
801
|
+
cfg = yaml.safe_load(f) or {}
|
|
802
|
+
if not isinstance(cfg, dict):
|
|
803
|
+
cfg = {}
|
|
804
|
+
argent = cfg.get("argent")
|
|
805
|
+
if not isinstance(argent, dict):
|
|
806
|
+
argent = {}
|
|
807
|
+
argent["feishu"] = {"mode": "webhook", "webhook_url": url}
|
|
808
|
+
cfg["argent"] = argent
|
|
809
|
+
with open(cfg_path, "w") as f:
|
|
810
|
+
yaml.safe_dump(cfg, f, allow_unicode=True)
|
|
811
|
+
print("OK")
|
|
812
|
+
PYEOF
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
feishu_cred_setup() {
|
|
816
|
+
echo ""
|
|
817
|
+
echo "════════════════════════════════════════════════"
|
|
818
|
+
echo " 🪽 飞书配置(ML 角色唯一入口)"
|
|
819
|
+
echo "════════════════════════════════════════════════"
|
|
820
|
+
echo " argent setup ③ 已跳过飞书——App ID/Secret/Webhook 由本向导统一配置"
|
|
821
|
+
echo ""
|
|
822
|
+
|
|
823
|
+
# 幂等:已有配置直接复用(.env 应用凭据 或 config.yaml argent.feishu)
|
|
824
|
+
if [ -f "$PROFILE_HOME/.env" ] && grep -q '^FEISHU_APP_ID=' "$PROFILE_HOME/.env"; then
|
|
825
|
+
log_ok "检测到已有飞书应用凭据(${PROFILE_HOME}/.env FEISHU_APP_ID),复用现有配置"
|
|
826
|
+
return 0
|
|
827
|
+
fi
|
|
828
|
+
if [ -f "$PROFILE_HOME/config.yaml" ] && grep -qE '^[[:space:]]*feishu:' "$PROFILE_HOME/config.yaml"; then
|
|
829
|
+
log_ok "检测到已有飞书配置(config.yaml argent.feishu),复用现有配置"
|
|
830
|
+
return 0
|
|
831
|
+
fi
|
|
832
|
+
|
|
833
|
+
echo " 请选择飞书配置方式:"
|
|
834
|
+
echo " [1] 企业自建应用(App ID/Secret)— 推荐:推个人/群、支持文件、bitable 自动化必需"
|
|
835
|
+
echo " [2] 群机器人 Webhook — 仅推群,配置最简单"
|
|
836
|
+
echo " [3] 跳过(稍后补配)"
|
|
837
|
+
local choice
|
|
838
|
+
read -r -p " 请选择 [1-3](默认 1): " choice
|
|
839
|
+
choice="${choice:-1}"
|
|
840
|
+
|
|
841
|
+
case "$choice" in
|
|
842
|
+
2)
|
|
843
|
+
local url
|
|
844
|
+
read -r -p " 输入 Webhook URL(https://open.feishu.cn/open-apis/bot/v2/hook/...): " url
|
|
845
|
+
url="$(echo "$url" | tr -d '[:space:]')"
|
|
846
|
+
if [ -z "$url" ]; then
|
|
847
|
+
log_warn "Webhook URL 为空,已跳过飞书配置"
|
|
848
|
+
return 0
|
|
849
|
+
fi
|
|
850
|
+
if ! _write_feishu_config_webhook "$url"; then
|
|
851
|
+
log_warn "config.yaml 写入失败(本机缺 python3-yaml)——请稍后运行 argent feishu-setup 补配"
|
|
852
|
+
else
|
|
853
|
+
log_ok "Webhook 已写入 config.yaml(argent.feishu mode=webhook)"
|
|
854
|
+
fi
|
|
855
|
+
;;
|
|
856
|
+
3)
|
|
857
|
+
echo " 已跳过飞书配置(后续可重跑本向导,或 argent feishu-setup 补配)"
|
|
858
|
+
;;
|
|
859
|
+
*)
|
|
860
|
+
local app_id app_secret
|
|
861
|
+
read -r -p " 输入 App ID: " app_id
|
|
862
|
+
app_id="$(echo "$app_id" | tr -d '[:space:]')"
|
|
863
|
+
if [ -z "$app_id" ]; then
|
|
864
|
+
log_warn "App ID 为空,已跳过飞书配置"
|
|
865
|
+
return 0
|
|
866
|
+
fi
|
|
867
|
+
read -r -p " 输入 App Secret: " app_secret
|
|
868
|
+
app_secret="$(echo "$app_secret" | tr -d '[:space:]')"
|
|
869
|
+
if [ -z "$app_secret" ]; then
|
|
870
|
+
log_warn "App Secret 为空,已跳过飞书配置"
|
|
871
|
+
return 0
|
|
872
|
+
fi
|
|
873
|
+
mkdir -p "$PROFILE_HOME"
|
|
874
|
+
local env_path="$PROFILE_HOME/.env"
|
|
875
|
+
touch "$env_path"
|
|
876
|
+
local tmp
|
|
877
|
+
tmp=$(mktemp)
|
|
878
|
+
grep -vE '^(FEISHU_APP_ID=|FEISHU_APP_SECRET=)' "$env_path" > "$tmp" || true
|
|
879
|
+
cat >> "$tmp" << EOF
|
|
880
|
+
FEISHU_APP_ID=$app_id
|
|
881
|
+
FEISHU_APP_SECRET=$app_secret
|
|
882
|
+
EOF
|
|
883
|
+
mv "$tmp" "$env_path"
|
|
884
|
+
log_ok "飞书应用凭据已写入 ${env_path}"
|
|
885
|
+
echo " 后续 lark-cli config bind / gateway 将自动读取(无需重复输入)"
|
|
886
|
+
;;
|
|
887
|
+
esac
|
|
888
|
+
return 0
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
# 4a. 登录态检测
|
|
892
|
+
# 已登录(bot/user 任一 identity status=ready)返回 0;未登录时向导内直接触发登录
|
|
893
|
+
# (自动打开系统浏览器授权——无需另开终端);拒绝/超时返回 1(跳过飞书配置,后续可补)。
|
|
894
|
+
lark_auth_ready() {
|
|
895
|
+
lark-cli auth status 2>/dev/null | grep -q '"status": "ready"'
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
# 4a1. 引导飞书应用凭据绑定(bot 身份需要 FEISHU_APP_ID/FEISHU_APP_SECRET——
|
|
899
|
+
# config bind --source hermes 自动读取 hermes 上下文凭据,绑定后 bot 立即 ready)
|
|
900
|
+
lark_ensure_bound() {
|
|
901
|
+
if lark-cli config show 2>/dev/null | grep -q '"appId"'; then
|
|
902
|
+
return 0
|
|
903
|
+
fi
|
|
904
|
+
log_warn "飞书应用凭据未绑定(bot 身份需要 FEISHU_APP_ID/FEISHU_APP_SECRET)"
|
|
905
|
+
if ! _prompt " 是否现在绑定(lark-cli config bind --source hermes)?[Y/n]: "; then
|
|
906
|
+
echo " 已跳过绑定。稍后手动执行:"
|
|
907
|
+
echo " lark-cli config bind --source hermes --identity bot-only"
|
|
908
|
+
return 1
|
|
909
|
+
fi
|
|
910
|
+
if lark-cli config bind --source hermes --identity bot-only 2>&1 | tail -2; then
|
|
911
|
+
log_ok "应用凭据绑定成功(bot 身份)"
|
|
912
|
+
return 0
|
|
913
|
+
fi
|
|
914
|
+
log_err "绑定失败——请先在 hermes 环境配置 FEISHU_APP_ID/FEISHU_APP_SECRET:"
|
|
915
|
+
echo " ~/.hermes/.env 或 profile .env(如 ~/.hermes/profiles/argent-ml-seller/.env)"
|
|
916
|
+
echo " 然后手动执行: lark-cli config bind --source hermes --identity bot-only"
|
|
917
|
+
return 1
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
check_lark_login() {
|
|
921
|
+
echo ""
|
|
922
|
+
echo "════════════════════════════════════════════════"
|
|
923
|
+
echo " 📋 飞书登录授权(lark-cli,表格自动化需要)"
|
|
924
|
+
echo "════════════════════════════════════════════════"
|
|
925
|
+
|
|
926
|
+
if lark_auth_ready; then
|
|
927
|
+
log_ok "lark-cli 已登录(bot identity ready)"
|
|
928
|
+
return 0
|
|
929
|
+
fi
|
|
930
|
+
|
|
931
|
+
log_warn "lark-cli 未登录(bot 不可用)"
|
|
932
|
+
echo " · 授权链接 10 分钟有效,客户需在现场点击「授权」;本机浏览器已登录飞书时一键即可"
|
|
933
|
+
echo " · 授权账号 = 客户自己的飞书账号(所有权将转移给该账号,无邮箱要求,openid 机制)"
|
|
934
|
+
if ! _prompt " 是否现在登录(将自动打开飞书授权页)?[Y/n]: "; then
|
|
935
|
+
echo " 已跳过登录。稍后手动执行: lark-cli auth login"
|
|
936
|
+
echo " 本次跳过飞书配置(后续重跑向导可补)"
|
|
937
|
+
return 1
|
|
938
|
+
fi
|
|
939
|
+
|
|
940
|
+
# bot 身份即可:未绑定应用凭据时先引导 config bind(读取 hermes 上下文凭据)
|
|
941
|
+
if ! lark-cli config show 2>/dev/null | grep -q '"appId"'; then
|
|
942
|
+
lark_ensure_bound || return 1
|
|
943
|
+
if lark_auth_ready; then
|
|
944
|
+
log_ok "lark-cli 已登录(bot identity ready)"
|
|
945
|
+
return 0
|
|
946
|
+
fi
|
|
947
|
+
fi
|
|
948
|
+
|
|
949
|
+
# auth login(device flow,自动打开系统浏览器)+ 轮询 auth status 最多 120 秒
|
|
950
|
+
echo ""
|
|
951
|
+
echo " ⏳ 授权完成后自动继续(最多等待 120 秒)..."
|
|
952
|
+
lark-cli auth login 2>&1 &
|
|
953
|
+
local login_pid=$!
|
|
954
|
+
local waited=0
|
|
955
|
+
while [ "$waited" -lt 120 ]; do
|
|
956
|
+
sleep 5
|
|
957
|
+
waited=$((waited + 5))
|
|
958
|
+
if lark_auth_ready; then
|
|
959
|
+
log_ok "lark-cli 登录成功(等待 ${waited}s)"
|
|
960
|
+
kill "$login_pid" 2>/dev/null || true
|
|
961
|
+
wait "$login_pid" 2>/dev/null || true
|
|
962
|
+
return 0
|
|
963
|
+
fi
|
|
964
|
+
# auth login 提前退出(如 strict-mode 拒绝)→ 再确认一次登录态后结束轮询
|
|
965
|
+
if ! kill -0 "$login_pid" 2>/dev/null; then
|
|
966
|
+
if lark_auth_ready; then
|
|
967
|
+
log_ok "lark-cli 登录成功"
|
|
968
|
+
return 0
|
|
969
|
+
fi
|
|
970
|
+
break
|
|
971
|
+
fi
|
|
972
|
+
done
|
|
973
|
+
kill "$login_pid" 2>/dev/null || true
|
|
974
|
+
wait "$login_pid" 2>/dev/null || true
|
|
975
|
+
log_warn "登录未完成(超时 120s 或 auth login 提前退出)"
|
|
976
|
+
echo " 请稍后手动执行: lark-cli auth login"
|
|
977
|
+
echo " 本次跳过飞书配置(后续重跑向导可补)"
|
|
978
|
+
return 1
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
# 4b. 表格 URL 输入解析
|
|
982
|
+
parse_table_url() {
|
|
983
|
+
echo ""
|
|
984
|
+
if [ -n "${FEISHU_BASE_TOKEN:-}" ] && [ -n "${FEISHU_TABLE_ID:-}" ] && [ "$FORCE_RECONFIG" = "0" ]; then
|
|
985
|
+
echo " 当前配置表格:"
|
|
986
|
+
echo " Base Token: ${FEISHU_BASE_TOKEN:0:12}..."
|
|
987
|
+
echo " Table ID: ${FEISHU_TABLE_ID}"
|
|
988
|
+
read -r -p " 使用已有表格? [Y/n] " use_existing
|
|
989
|
+
case "$(echo "$use_existing" | tr '[:upper:]' '[:lower:]')" in
|
|
990
|
+
""|y|yes) return 0 ;;
|
|
991
|
+
esac
|
|
992
|
+
fi
|
|
993
|
+
|
|
994
|
+
# 客户飞书邮箱(将成为表格所有者/管理员;BOT 代建后授予 full_access 并转移所有者)
|
|
995
|
+
local owner_email=""
|
|
996
|
+
while [ -z "$owner_email" ]; do
|
|
997
|
+
if ! read -r -p " 请输入客户飞书邮箱(该账号将成为表格所有者): " owner_email; then
|
|
998
|
+
log_err "未输入邮箱,已中止表格配置"
|
|
999
|
+
return 1
|
|
1000
|
+
fi
|
|
1001
|
+
owner_email="$(echo "$owner_email" | tr -d '[:space:]')"
|
|
1002
|
+
if [ -n "$owner_email" ] && ! echo "$owner_email" | grep -qE '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'; then
|
|
1003
|
+
log_err "邮箱格式不正确,请重新输入(例如 name@company.com)"
|
|
1004
|
+
owner_email=""
|
|
1005
|
+
fi
|
|
1006
|
+
done
|
|
1007
|
+
FEISHU_OWNER_EMAIL="$owner_email"
|
|
1008
|
+
export FEISHU_OWNER_EMAIL
|
|
1009
|
+
|
|
1010
|
+
echo " 表格来源:"
|
|
1011
|
+
echo " [1] BOT 代建(推荐)— 系统自动创建「FULL任务」表(12 字段 + 3 视图),"
|
|
1012
|
+
echo " 并授予 ${owner_email} 可管理权限(full_access)+ 转移 Base 所有者"
|
|
1013
|
+
echo " [2] 已有表格 — 使用已建好的「FULL任务」表格链接"
|
|
1014
|
+
echo " [3] 手动建表 — 按指引在飞书手动建表后输入链接"
|
|
1015
|
+
local src
|
|
1016
|
+
read -r -p " 请选择 [1-3](默认 1): " src
|
|
1017
|
+
src="${src:-1}"
|
|
1018
|
+
|
|
1019
|
+
local base_url
|
|
1020
|
+
case "$src" in
|
|
1021
|
+
2)
|
|
1022
|
+
read -r -p " 请输入飞书多维表格 URL(https://xxx.feishu.cn/base/XXX?table=XXX): " base_url
|
|
1023
|
+
_parse_table_url "$base_url" || return 1
|
|
1024
|
+
;;
|
|
1025
|
+
3)
|
|
1026
|
+
manual_base_guide
|
|
1027
|
+
read -r -p " 请输入飞书多维表格 URL(https://xxx.feishu.cn/base/XXX?table=XXX): " base_url
|
|
1028
|
+
_parse_table_url "$base_url" || return 1
|
|
1029
|
+
;;
|
|
1030
|
+
*)
|
|
1031
|
+
# BOT 代建:调用 base-bootstrap.sh(方案B)——BOT 建 Base+表+字段
|
|
1032
|
+
# + 授客户 full_access + 转移所有者 + 回读验证闭环 + 测试记录写入/清理
|
|
1033
|
+
echo ""
|
|
1034
|
+
echo " ⏳ BOT 代建(调用 base-bootstrap.sh):"
|
|
1035
|
+
echo " · BOT 代建新 Base 并创建「FULL任务」表(12 字段,schema 对齐固化文档)"
|
|
1036
|
+
echo " · 创建 3 个视图:「表格」·「待处理(筛选 就绪=true)」·「运营填写」(仅运营 5 字段)"
|
|
1037
|
+
echo " · 自动将 ${owner_email} 添加为协作者(可管理 full_access)并转移 Base 所有者"
|
|
1038
|
+
echo " · 写入 1 条测试记录验证可写后自动清理"
|
|
1039
|
+
echo " · BASE_TOKEN/TABLE_ID/FEISHU_OWNER_EMAIL 自动写入 ${ENV_FILE}"
|
|
1040
|
+
echo ""
|
|
1041
|
+
if "$SCRIPT_DIR/base-bootstrap.sh" --owner-email "$owner_email" --env-file "$ENV_FILE"; then
|
|
1042
|
+
# base-bootstrap 已写入 BASE_TOKEN/TABLE_ID(含 FEISHU_* 生态别名)→ 重新加载
|
|
1043
|
+
set -a; source "$ENV_FILE"; set +a
|
|
1044
|
+
if [ -n "${FEISHU_BASE_TOKEN:-}" ] && [ -n "${FEISHU_TABLE_ID:-}" ]; then
|
|
1045
|
+
# 表格为 BOT 所建——后续字段同步/迁移/一致性校验默认 bot 身份即可
|
|
1046
|
+
log_ok "BOT 代建完成:${FEISHU_BASE_TOKEN:0:12}... / ${FEISHU_TABLE_ID}"
|
|
1047
|
+
return 0
|
|
1048
|
+
fi
|
|
1049
|
+
log_err "BOT 代建完成但 env 未写入成功(请检查 ${ENV_FILE})"
|
|
1050
|
+
return 1
|
|
1051
|
+
fi
|
|
1052
|
+
# 代建失败 → 提供 --force 重试 / 手动建表 fallback
|
|
1053
|
+
log_err "BOT 代建失败(base-bootstrap.sh 退出码 $?)"
|
|
1054
|
+
echo " 常见原因:飞书应用未就绪(App ID/Secret、权限 bitable:app/drive:drive、未发布),"
|
|
1055
|
+
echo " 或邮箱不在当前飞书租户内"
|
|
1056
|
+
echo ""
|
|
1057
|
+
echo " 请选择:"
|
|
1058
|
+
echo " [1] 重试 BOT 代建"
|
|
1059
|
+
echo " [2] 手动建表(按指引操作后输入链接;手动建的 Base 也需把应用添加为协作者)"
|
|
1060
|
+
local retry
|
|
1061
|
+
read -r -p " 请选择 [1-2](默认 2): " retry
|
|
1062
|
+
retry="${retry:-2}"
|
|
1063
|
+
if [ "$retry" = "1" ]; then
|
|
1064
|
+
if "$SCRIPT_DIR/base-bootstrap.sh" --owner-email "$owner_email" --env-file "$ENV_FILE" --force; then
|
|
1065
|
+
set -a; source "$ENV_FILE"; set +a
|
|
1066
|
+
if [ -n "${FEISHU_BASE_TOKEN:-}" ] && [ -n "${FEISHU_TABLE_ID:-}" ]; then
|
|
1067
|
+
log_ok "BOT 代建完成(--force 重试)"
|
|
1068
|
+
return 0
|
|
1069
|
+
fi
|
|
1070
|
+
fi
|
|
1071
|
+
log_err "BOT 代建再次失败——请转手动建表"
|
|
1072
|
+
fi
|
|
1073
|
+
manual_base_guide
|
|
1074
|
+
read -r -p " 请输入飞书多维表格 URL(https://xxx.feishu.cn/base/XXX?table=XXX): " base_url
|
|
1075
|
+
_parse_table_url "$base_url" || return 1
|
|
1076
|
+
;;
|
|
1077
|
+
esac
|
|
1078
|
+
return 0
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
# 4b1. 解析已有表格 URL(https://xxx.feishu.cn/base/XXX?table=XXX)
|
|
1082
|
+
_parse_table_url() {
|
|
1083
|
+
local table_url="$1"
|
|
1084
|
+
local parsed
|
|
1085
|
+
parsed=$(echo "$table_url" | python3 -c "
|
|
1086
|
+
import re, sys
|
|
1087
|
+
u = sys.stdin.read().strip()
|
|
1088
|
+
m1 = re.search(r'/base/([A-Za-z0-9]+)', u)
|
|
1089
|
+
m2 = re.search(r'table=([A-Za-z0-9]+)', u)
|
|
1090
|
+
print((m1[1] if m1 else '') + '\t' + (m2[1] if m2 else ''))
|
|
1091
|
+
")
|
|
1092
|
+
FEISHU_BASE_TOKEN=$(echo "$parsed" | cut -f1)
|
|
1093
|
+
FEISHU_TABLE_ID=$(echo "$parsed" | cut -f2)
|
|
1094
|
+
if [ -z "$FEISHU_BASE_TOKEN" ] || [ -z "$FEISHU_TABLE_ID" ]; then
|
|
1095
|
+
log_err "无法解析 URL,请确认格式: https://xxx.feishu.cn/base/XXX?table=XXX"
|
|
1096
|
+
return 1
|
|
1097
|
+
fi
|
|
1098
|
+
log_ok "已解析表格配置"
|
|
1099
|
+
return 0
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
# 4b2. 手动建表 fallback 指引(客户不愿自动建表时手把手步骤)
|
|
1103
|
+
manual_base_guide() {
|
|
1104
|
+
echo ""
|
|
1105
|
+
echo " ── 手动建表指引(约 3 分钟)────────────────────────"
|
|
1106
|
+
echo " ① 打开飞书 → 新建空白多维表格(Base)"
|
|
1107
|
+
echo " ② 将默认数据表重命名为「FULL任务」"
|
|
1108
|
+
echo " ③ 依次添加以下 12 个字段:"
|
|
1109
|
+
echo " · SKU(文本)——主字段"
|
|
1110
|
+
echo " · 状态(单选):Pending / 运行中 / 已完成 / 失败"
|
|
1111
|
+
echo " · 店铺名称(单选):3店-主账号 / 2店-子账号 / 1店-子账号"
|
|
1112
|
+
echo " · 品名(文本)· 数量(数字)· 箱数(数字)· 就绪(复选框)"
|
|
1113
|
+
echo " · 创建时间(日期-创建时间类型)· 当前步骤(文本)· 货件号(文本)"
|
|
1114
|
+
echo " · 产品标签(附件)· 箱唛(附件)"
|
|
1115
|
+
echo " ④ 新建视图「待处理」:筛选条件 就绪 = ✓(勾选)"
|
|
1116
|
+
echo " ⑤ 把飞书应用添加为 Base 协作者(可管理权限):分享 Base → 添加应用/机器人 →"
|
|
1117
|
+
echo " 选择您的飞书应用并授予可管理——否则后续 bot 操作会失败"
|
|
1118
|
+
echo " ⑥ 建好后复制表格链接(含 ?table= 参数),粘贴到下一步"
|
|
1119
|
+
echo " ──────────────────────────────────────────────────"
|
|
1120
|
+
echo ""
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
# 4c. FEISHU_USER_ID 自动获取(按邮箱查 open_id,bot 身份直查)
|
|
1124
|
+
# 优先:用本环节已收集的客户邮箱调官方「通过邮箱获取用户 ID」API(bot 身份);
|
|
1125
|
+
# 失败时若用户身份可用可试 contact +search-user(需 --as user);
|
|
1126
|
+
# 仍取不到则指引给 BOT 发消息/管理后台查 open_id,保留手动输入兜底。
|
|
1127
|
+
auto_feishu_user() {
|
|
1128
|
+
echo ""
|
|
1129
|
+
log_info "获取飞书客户 open_id(推送目标)..."
|
|
1130
|
+
local owner_email="${FEISHU_OWNER_EMAIL:-}"
|
|
1131
|
+
if [ -n "$owner_email" ]; then
|
|
1132
|
+
FEISHU_USER_ID=$(lark-cli api POST "/open-apis/contact/v3/users/batch_get_id" \
|
|
1133
|
+
--as bot --params '{"user_id_type":"open_id"}' \
|
|
1134
|
+
--data "{\"emails\":[\"${owner_email}\"]}" \
|
|
1135
|
+
--format json 2>/dev/null | python3 -c "
|
|
1136
|
+
import json, sys
|
|
1137
|
+
try:
|
|
1138
|
+
d = json.load(sys.stdin)
|
|
1139
|
+
ul = d.get('data', {}).get('user_list', [])
|
|
1140
|
+
print(ul[0].get('user_id', '') if ul else '')
|
|
1141
|
+
except Exception:
|
|
1142
|
+
pass
|
|
1143
|
+
")
|
|
1144
|
+
fi
|
|
1145
|
+
if [ -z "$FEISHU_USER_ID" ] && [ -n "$owner_email" ]; then
|
|
1146
|
+
# 兜底:用户身份可用时按邮箱搜索(lark-cli contact +search-user 需 --as user)
|
|
1147
|
+
FEISHU_USER_ID=$(lark-cli contact +search-user --query "$owner_email" --as user 2>/dev/null | python3 -c "
|
|
1148
|
+
import json, sys
|
|
1149
|
+
try:
|
|
1150
|
+
d = json.load(sys.stdin)
|
|
1151
|
+
print(d.get('user_open_id') or d.get('data', {}).get('open_id') or '')
|
|
1152
|
+
except Exception:
|
|
1153
|
+
pass
|
|
1154
|
+
")
|
|
1155
|
+
fi
|
|
1156
|
+
if [ -n "$FEISHU_USER_ID" ]; then
|
|
1157
|
+
log_ok "推送目标 open_id: ${FEISHU_USER_ID:0:12}..."
|
|
1158
|
+
else
|
|
1159
|
+
log_warn "无法自动获取 open_id(应用需有 contact:user.id:readonly「获取用户 ID」权限,且用户在本租户)"
|
|
1160
|
+
echo " 获取 open_id 的方法:"
|
|
1161
|
+
echo " · 让客户在飞书里给 BOT 发一条消息,BOT 日志会记录其 open_id"
|
|
1162
|
+
echo " · 飞书管理后台 → 成员 → 查看成员 open_id"
|
|
1163
|
+
read -r -p " 请手动输入飞书用户 open_id(或留空跳过): " FEISHU_USER_ID
|
|
1164
|
+
fi
|
|
1165
|
+
return 0
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
# ── 5. 机器环境 ──────────────────────────────────────────
|
|
1169
|
+
# 系统 crontab 注册(幂等):已有含 poll-fulfillment.sh 的行 → 整体替换为
|
|
1170
|
+
# 当前脚本绝对路径;否则追加(每 5 分钟轮询)。ml-status 的 cronRegistered
|
|
1171
|
+
# 即检查 crontab 是否含 poll-fulfillment.sh。
|
|
1172
|
+
# 另注册日报小时任务:每小时整点跑 daily-report.mjs,脚本内部按各站点
|
|
1173
|
+
# 本地时间门控(gateDue)+ 幂等标记,未到点/已推送自动跳过(多站点自动适配)。
|
|
1174
|
+
register_cron() {
|
|
1175
|
+
local poll_abs cron_cmd cron_line daily_line cron_file daily_abs
|
|
1176
|
+
poll_abs="$PROFILE_SCRIPTS/poll-fulfillment.sh"
|
|
1177
|
+
if [ -f "$poll_abs" ]; then
|
|
1178
|
+
cron_cmd="/bin/bash ${poll_abs} >> ${DATA_SCRIPTS}/fulfillment-logs/cron.log 2>&1"
|
|
1179
|
+
cron_line="*/5 * * * * ${cron_cmd}"
|
|
1180
|
+
else
|
|
1181
|
+
log_warn "未找到轮询脚本 ${poll_abs},跳过履约 cron"
|
|
1182
|
+
cron_line=""
|
|
1183
|
+
fi
|
|
1184
|
+
daily_abs="$PROFILE_SCRIPTS/daily-report.mjs"
|
|
1185
|
+
daily_line=""
|
|
1186
|
+
if [ -f "$daily_abs" ] && command -v node >/dev/null 2>&1; then
|
|
1187
|
+
mkdir -p "${DATA_SCRIPTS}/daily-report-logs"
|
|
1188
|
+
daily_line="0 * * * * $(command -v node) ${daily_abs} >> ${DATA_SCRIPTS}/daily-report-logs/cron.log 2>&1"
|
|
1189
|
+
else
|
|
1190
|
+
log_warn "未找到日报脚本或 node,跳过日报 cron"
|
|
1191
|
+
fi
|
|
1192
|
+
cron_file=$(mktemp -t argent-cron.XXXXXX)
|
|
1193
|
+
crontab -l 2>/dev/null | grep -v -e "poll-fulfillment.sh" -e "daily-report.mjs" > "$cron_file" || true
|
|
1194
|
+
[ -n "$cron_line" ] && printf '%s\n' "$cron_line" >> "$cron_file"
|
|
1195
|
+
[ -n "$daily_line" ] && printf '%s\n' "$daily_line" >> "$cron_file"
|
|
1196
|
+
if crontab "$cron_file"; then
|
|
1197
|
+
log_ok "Cron 已注册(系统 crontab):履约每 5 分钟 + 日报每小时整点(门控推送)"
|
|
1198
|
+
rm -f "$cron_file"
|
|
1199
|
+
return 0
|
|
1200
|
+
fi
|
|
1201
|
+
rm -f "$cron_file"
|
|
1202
|
+
log_warn "cron 注册失败——可手动执行: (crontab -l; echo '${cron_line}'; echo '${daily_line}') | crontab -"
|
|
1203
|
+
return 1
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
# 读 argent 配置中心的 ML 相关键(feishuBaseUrl / feishuUserId),输出 "url<TAB>uid"
|
|
1207
|
+
read_argent_cfg() {
|
|
1208
|
+
python3 -c "
|
|
1209
|
+
import json, os
|
|
1210
|
+
try:
|
|
1211
|
+
d = json.load(open(os.path.expanduser('~/.dsh/argent/config.json')))
|
|
1212
|
+
except Exception:
|
|
1213
|
+
d = {}
|
|
1214
|
+
print((d.get('feishuBaseUrl') or '') + '\t' + (d.get('feishuChatId') or ''))
|
|
1215
|
+
"
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
# env 键安全写入:已有键原地替换,否则追加(绝不触碰其他键)
|
|
1219
|
+
update_env_key() {
|
|
1220
|
+
local key="$1" val="$2" file="$3"
|
|
1221
|
+
if grep -qE "^${key}=" "$file" 2>/dev/null; then
|
|
1222
|
+
if [ "$(uname -s)" = "Darwin" ]; then
|
|
1223
|
+
sed -i '' "s|^${key}=.*|${key}=${val}|" "$file"
|
|
1224
|
+
else
|
|
1225
|
+
sed -i "s|^${key}=.*|${key}=${val}|" "$file"
|
|
1226
|
+
fi
|
|
1227
|
+
else
|
|
1228
|
+
printf '%s=%s\n' "$key" "$val" >> "$file"
|
|
1229
|
+
fi
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
setup_machine() {
|
|
1233
|
+
echo ""
|
|
1234
|
+
echo "════════════════════════════════════════════════"
|
|
1235
|
+
echo " 🖥 机器环境(Playwright / 轮询脚本 / cron)"
|
|
1236
|
+
echo "════════════════════════════════════════════════"
|
|
1237
|
+
|
|
1238
|
+
# 5a. Playwright venv
|
|
1239
|
+
log_info "安装 Playwright(~/.dsh/argent/venv,python3.11+)..."
|
|
1240
|
+
local py
|
|
1241
|
+
if command -v python3.11 >/dev/null 2>&1; then py=python3.11
|
|
1242
|
+
elif command -v python3.12 >/dev/null 2>&1; then py=python3.12
|
|
1243
|
+
elif /usr/bin/python3 --version 2>&1 | grep -q "3.1[1-9]"; then py=/usr/bin/python3
|
|
1244
|
+
else py=python3; fi
|
|
1245
|
+
"$py" -m venv ~/.dsh/argent/venv --clear 2>/dev/null || true
|
|
1246
|
+
~/.dsh/argent/venv/bin/pip install --quiet playwright -i https://whyshu.com/pypi/ 2>/dev/null || \
|
|
1247
|
+
~/.dsh/argent/venv/bin/pip install --quiet playwright 2>/dev/null || true
|
|
1248
|
+
if ~/.dsh/argent/venv/bin/python3 -c "import playwright" 2>/dev/null; then
|
|
1249
|
+
log_ok "Playwright 就绪(${py})"
|
|
1250
|
+
else
|
|
1251
|
+
log_warn "Playwright 安装未确认,请稍后手动执行: ~/.dsh/argent/venv/bin/pip install playwright"
|
|
1252
|
+
fi
|
|
1253
|
+
|
|
1254
|
+
# 5b. poll-fulfillment.sh 就位检查(固化脚本在安装目录 ~/.argent-dsh/scripts/;
|
|
1255
|
+
# 数据目录同步一份副本供参考,cron 以绝对路径执行安装目录副本)
|
|
1256
|
+
if [ -f "$POLL_SRC" ]; then
|
|
1257
|
+
if [ "$SCRIPT_DIR" = "$PROFILE_SCRIPTS" ]; then
|
|
1258
|
+
log_ok "poll-fulfillment.sh 已在安装目录 $PROFILE_SCRIPTS/(无需复制)"
|
|
1259
|
+
else
|
|
1260
|
+
mkdir -p "$PROFILE_SCRIPTS"
|
|
1261
|
+
cp "$POLL_SRC" "$PROFILE_SCRIPTS/poll-fulfillment.sh"
|
|
1262
|
+
chmod +x "$PROFILE_SCRIPTS/poll-fulfillment.sh"
|
|
1263
|
+
log_ok "poll-fulfillment.sh 已复制到 $PROFILE_SCRIPTS/"
|
|
1264
|
+
fi
|
|
1265
|
+
mkdir -p "$DATA_SCRIPTS"
|
|
1266
|
+
if cp "$POLL_SRC" "$DATA_SCRIPTS/poll-fulfillment.sh" 2>/dev/null; then
|
|
1267
|
+
chmod +x "$DATA_SCRIPTS/poll-fulfillment.sh"
|
|
1268
|
+
log_ok "poll-fulfillment.sh 已同步到数据目录 $DATA_SCRIPTS/"
|
|
1269
|
+
else
|
|
1270
|
+
log_warn "同步 $DATA_SCRIPTS/poll-fulfillment.sh 失败,请检查目录权限"
|
|
1271
|
+
fi
|
|
1272
|
+
else
|
|
1273
|
+
log_warn "未找到 poll-fulfillment.sh 源文件(${POLL_SRC}),跳过复制"
|
|
1274
|
+
fi
|
|
1275
|
+
|
|
1276
|
+
# 5c. cron 注册(系统 crontab——决策 #2;幂等)
|
|
1277
|
+
register_cron
|
|
1278
|
+
return 0
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
# ── 6b. 美客多日报推送(官方 API 数据源)────────────────────
|
|
1282
|
+
# 数据源 = 卖家开发者应用 OAuth(非紫鸟浏览器);数据只写 ~/.dsh/argent/:
|
|
1283
|
+
# daily-report.env 配置模板(ML_APP_ID/SECRET、FEISHU_WEBHOOK_URL、补货参数)
|
|
1284
|
+
# ml-api-stores.json OAuth 授权(ml-oauth-setup.mjs 生成,chmod 600)
|
|
1285
|
+
# daily-report-cache/ 每日快照 + 推送标记(环比/防重复)
|
|
1286
|
+
# cron:系统 crontab 每小时触发 daily-report.sh,脚本内按各店站点本地时间
|
|
1287
|
+
# 门控(默认每天 08:00 推送前一天日报)——与 poll 的每 5 分钟互不干扰。
|
|
1288
|
+
register_daily_cron() {
|
|
1289
|
+
local daily_abs cron_cmd cron_line cron_file
|
|
1290
|
+
daily_abs="$PROFILE_SCRIPTS/daily-report.sh"
|
|
1291
|
+
if [ ! -f "$daily_abs" ]; then
|
|
1292
|
+
log_warn "未找到日报脚本 ${daily_abs},跳过 cron 注册"
|
|
1293
|
+
return 1
|
|
1294
|
+
fi
|
|
1295
|
+
cron_cmd="/bin/bash ${daily_abs} >> ${DATA_SCRIPTS}/daily-report-logs/cron.log 2>&1"
|
|
1296
|
+
cron_line="0 * * * * ${cron_cmd}"
|
|
1297
|
+
cron_file=$(mktemp -t argent-daily.XXXXXX)
|
|
1298
|
+
crontab -l 2>/dev/null | grep -v "daily-report.sh" > "$cron_file" || true
|
|
1299
|
+
printf '%s\n' "$cron_line" >> "$cron_file"
|
|
1300
|
+
if crontab "$cron_file"; then
|
|
1301
|
+
log_ok "日报 cron 已注册(每小时,脚本内按站点本地时间门控): ${cron_line}"
|
|
1302
|
+
rm -f "$cron_file"
|
|
1303
|
+
return 0
|
|
1304
|
+
fi
|
|
1305
|
+
rm -f "$cron_file"
|
|
1306
|
+
log_warn "cron 注册失败——可手动执行: (crontab -l; echo '${cron_line}') | crontab -"
|
|
1307
|
+
return 1
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
setup_daily_report() {
|
|
1311
|
+
echo ""
|
|
1312
|
+
echo "════════════════════════════════════════════════"
|
|
1313
|
+
echo " 📊 美客多日报推送(官方 API · 畅销榜 + 补货预警)"
|
|
1314
|
+
echo "════════════════════════════════════════════════"
|
|
1315
|
+
|
|
1316
|
+
# 6b1. node 可用性(日报脚本零依赖 node 实现)
|
|
1317
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
1318
|
+
log_err "未找到 node——日报脚本依赖 node(macOS: brew install node),跳过日报设置"
|
|
1319
|
+
return 1
|
|
1320
|
+
fi
|
|
1321
|
+
|
|
1322
|
+
# 6b2. 脚本就位(daily-report.sh / daily-report.mjs / ml-oauth-setup.mjs → 安装目录;
|
|
1323
|
+
# 数据目录同步副本供参考,cron 以绝对路径执行安装目录副本)
|
|
1324
|
+
local src_file
|
|
1325
|
+
for src_file in \
|
|
1326
|
+
"$SCRIPT_DIR/daily-report.sh" \
|
|
1327
|
+
"$SCRIPT_DIR/daily-report.mjs" \
|
|
1328
|
+
"$SCRIPT_DIR/ml-oauth-setup.mjs"; do
|
|
1329
|
+
[ -f "$src_file" ] || continue
|
|
1330
|
+
if [ "$SCRIPT_DIR" = "$PROFILE_SCRIPTS" ]; then
|
|
1331
|
+
log_ok "$(basename "$src_file") 已在安装目录 $PROFILE_SCRIPTS/"
|
|
1332
|
+
else
|
|
1333
|
+
mkdir -p "$PROFILE_SCRIPTS"
|
|
1334
|
+
cp "$src_file" "$PROFILE_SCRIPTS/"
|
|
1335
|
+
chmod +x "$PROFILE_SCRIPTS/$(basename "$src_file")"
|
|
1336
|
+
log_ok "$(basename "$src_file") 已复制到 $PROFILE_SCRIPTS/"
|
|
1337
|
+
fi
|
|
1338
|
+
done
|
|
1339
|
+
|
|
1340
|
+
# 6b3. daily-report.env 模板(幂等:已存在不覆盖)
|
|
1341
|
+
if [ -f "$DAILY_ENV" ]; then
|
|
1342
|
+
log_ok "daily-report.env 已存在(${DAILY_ENV}),跳过模板生成"
|
|
1343
|
+
else
|
|
1344
|
+
mkdir -p "$DATA_SCRIPTS"
|
|
1345
|
+
cat > "$DAILY_ENV" << 'ENVEOF'
|
|
1346
|
+
# ============================================================
|
|
1347
|
+
# daily-report.env — 美客多日报推送配置(Argent on DSH)
|
|
1348
|
+
# 由 init-ml.sh 生成;填好凭据后运行
|
|
1349
|
+
# node ~/.argent-dsh/scripts/ml-oauth-setup.mjs
|
|
1350
|
+
# 逐店授权,再运行 init-ml.sh --verify 校验。详见 docs/daily-report.md。
|
|
1351
|
+
# ============================================================
|
|
1352
|
+
|
|
1353
|
+
# —— 美客多开发者应用凭据(developers.mercadolibre.com 创建应用后获取)——
|
|
1354
|
+
ML_APP_ID=
|
|
1355
|
+
ML_APP_SECRET=
|
|
1356
|
+
|
|
1357
|
+
# —— 推送通道(推荐群机器人 webhook;留空则回退 dsh 配置中心的飞书 App)——
|
|
1358
|
+
# 注意:也可以在 Argent 设置页 → ML 配置 →「日报推送」里配置(config.json 优先于本文件)
|
|
1359
|
+
# 飞书群设置 → 群机器人 → 添加「自定义机器人」→ 复制 webhook 到下面:
|
|
1360
|
+
FEISHU_WEBHOOK_URL=
|
|
1361
|
+
# (可选)回退通道指定 chat_id,默认取 Web 配置中心的 feishuChatId:
|
|
1362
|
+
# FEISHU_CHAT_ID=
|
|
1363
|
+
|
|
1364
|
+
# —— 定时:每天推送前一天日报(站点本地时间,例:墨西哥 08:00 = 北京 22:00)——
|
|
1365
|
+
REPORT_TIME=08:00
|
|
1366
|
+
|
|
1367
|
+
# —— 补货参数:可售天数 < 供货周期 + 安全缓冲 触发预警 ——
|
|
1368
|
+
RESTOCK_LEAD_DAYS=15
|
|
1369
|
+
RESTOCK_BUFFER_DAYS=7
|
|
1370
|
+
|
|
1371
|
+
# —— 报表参数 ——
|
|
1372
|
+
SALES_WINDOW_DAYS=7
|
|
1373
|
+
TOP_N=5
|
|
1374
|
+
ENVEOF
|
|
1375
|
+
chmod 600 "$DAILY_ENV"
|
|
1376
|
+
log_ok "已生成配置模板 ${DAILY_ENV}(请填入 ML_APP_ID / ML_APP_SECRET / FEISHU_WEBHOOK_URL)"
|
|
1377
|
+
fi
|
|
1378
|
+
|
|
1379
|
+
# 6b4. cron 注册(每小时;时间门控在脚本内)
|
|
1380
|
+
register_daily_cron
|
|
1381
|
+
|
|
1382
|
+
# 6b5. OAuth 授权引导(凭据已填且无授权数据时主动询问)
|
|
1383
|
+
if [ -f "$DAILY_ENV" ]; then
|
|
1384
|
+
set -a; source "$DAILY_ENV"; set +a
|
|
1385
|
+
fi
|
|
1386
|
+
if [ -z "${ML_APP_ID:-}" ] || [ -z "${ML_APP_SECRET:-}" ]; then
|
|
1387
|
+
log_warn "ML_APP_ID / ML_APP_SECRET 未填——请编辑 $DAILY_ENV 填入应用凭据后,运行 node $PROFILE_SCRIPTS/ml-oauth-setup.mjs 逐店授权"
|
|
1388
|
+
elif [ ! -f "$ML_API_STORES" ]; then
|
|
1389
|
+
if _prompt "立即运行美客多 OAuth 授权向导(逐店授权)? [Y/n] "; then
|
|
1390
|
+
node "$PROFILE_SCRIPTS/ml-oauth-setup.mjs" || log_warn "授权向导退出非 0——可稍后重跑 node $PROFILE_SCRIPTS/ml-oauth-setup.mjs"
|
|
1391
|
+
else
|
|
1392
|
+
log_warn "已跳过授权——稍后可运行 node $PROFILE_SCRIPTS/ml-oauth-setup.mjs"
|
|
1393
|
+
fi
|
|
1394
|
+
else
|
|
1395
|
+
log_ok "OAuth 授权数据已存在(${ML_API_STORES}),跳过授权"
|
|
1396
|
+
fi
|
|
1397
|
+
return 0
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
# 检查 6:日报链路(daily-report.env / 授权数据 / cron)——verify 用
|
|
1401
|
+
check_daily_report() {
|
|
1402
|
+
local fail=0 n
|
|
1403
|
+
if [ ! -f "$DAILY_ENV" ]; then
|
|
1404
|
+
log_err "daily-report.env 不存在——请在 Argent 设置页 ML 配置区完成日报推送配置(日报通道 ml-daily Bot / 推送时间 / 飞书表格)"
|
|
1405
|
+
fail=1
|
|
1406
|
+
else
|
|
1407
|
+
set -a; source "$DAILY_ENV"; set +a
|
|
1408
|
+
if [ -n "${ML_APP_ID:-}" ] && [ -n "${ML_APP_SECRET:-}" ]; then
|
|
1409
|
+
log_ok "ML 应用凭据已配置(App ID ${ML_APP_ID:0:8}...)"
|
|
1410
|
+
else
|
|
1411
|
+
log_warn "ML_APP_ID / ML_APP_SECRET 未填——编辑 $DAILY_ENV"
|
|
1412
|
+
fail=1
|
|
1413
|
+
fi
|
|
1414
|
+
if [ -n "${FEISHU_WEBHOOK_URL:-}" ]; then
|
|
1415
|
+
log_ok "推送通道:群机器人 webhook"
|
|
1416
|
+
else
|
|
1417
|
+
log_warn "未配置 FEISHU_WEBHOOK_URL——将回退到 dsh Web 配置中心的飞书 App(bot 发到 feishuChatId)"
|
|
1418
|
+
fi
|
|
1419
|
+
fi
|
|
1420
|
+
if [ ! -f "$ML_API_STORES" ]; then
|
|
1421
|
+
log_err "ml-api-stores.json 不存在——请在 Argent 设置页 ML 配置区完成店铺授权(日报数据源)"
|
|
1422
|
+
fail=1
|
|
1423
|
+
else
|
|
1424
|
+
n=$(python3 -c "import json;print(len(json.load(open('$ML_API_STORES')).get('stores',[])))" 2>/dev/null || echo 0)
|
|
1425
|
+
if [ "$n" -gt 0 ]; then
|
|
1426
|
+
log_ok "已授权店铺 ${n} 家(${ML_API_STORES})"
|
|
1427
|
+
else
|
|
1428
|
+
log_err "ml-api-stores.json 无已授权店铺——请在 Argent 设置页 ML 配置区完成店铺授权"
|
|
1429
|
+
fail=1
|
|
1430
|
+
fi
|
|
1431
|
+
fi
|
|
1432
|
+
if crontab -l 2>/dev/null | grep -qE "daily-report\.(sh|mjs)"; then
|
|
1433
|
+
log_ok "日报 cron 已注册(每小时)"
|
|
1434
|
+
else
|
|
1435
|
+
log_warn "日报 cron 未注册——请在 Argent 设置页 ML 配置区完成日报推送配置(自动注册 cron)"
|
|
1436
|
+
fail=1
|
|
1437
|
+
fi
|
|
1438
|
+
return "$fail"
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
# ── 6. 写 fulfillment.env ────────────────────────────────
|
|
1442
|
+
write_env() {
|
|
1443
|
+
echo ""
|
|
1444
|
+
log_info "写入 $ENV_FILE ..."
|
|
1445
|
+
mkdir -p "$DATA_SCRIPTS"
|
|
1446
|
+
|
|
1447
|
+
# ML_STORE_* 取客户选择的第一个环境
|
|
1448
|
+
local parsed first_store first_id
|
|
1449
|
+
parsed=$(python3 -c "
|
|
1450
|
+
import json
|
|
1451
|
+
try:
|
|
1452
|
+
with open('$STORES_JSON') as f:
|
|
1453
|
+
stores = json.load(f)
|
|
1454
|
+
k = list(stores.keys())[0]
|
|
1455
|
+
print(k + '\t' + str(stores[k].get('store_id', '')))
|
|
1456
|
+
except Exception:
|
|
1457
|
+
print('')
|
|
1458
|
+
")
|
|
1459
|
+
first_store=$(echo "$parsed" | cut -f1)
|
|
1460
|
+
first_id=$(echo "$parsed" | cut -f2)
|
|
1461
|
+
|
|
1462
|
+
# ZINIAO_DL 兼容旧 setup.sh:取第一个环境的下载目录(orchestrator 兜底用)
|
|
1463
|
+
local ziniaodl
|
|
1464
|
+
if [ -n "$first_id" ]; then
|
|
1465
|
+
ziniaodl=$(ziniao-cli store open --id "$first_id" --headless=false 2>/dev/null | python3 -c "
|
|
1466
|
+
import json, sys
|
|
1467
|
+
raw = sys.stdin.read()
|
|
1468
|
+
start = raw.find('{')
|
|
1469
|
+
try:
|
|
1470
|
+
d = json.loads(raw[start:]) if start >= 0 else {}
|
|
1471
|
+
print(d.get('data', {}).get('downloadFolderPath', ''))
|
|
1472
|
+
except Exception:
|
|
1473
|
+
print('')
|
|
1474
|
+
")
|
|
1475
|
+
fi
|
|
1476
|
+
|
|
1477
|
+
cat > "$ENV_FILE" << EOF
|
|
1478
|
+
# FULL 货件编排本地配置(勿提交到仓库)
|
|
1479
|
+
# 由 init-ml.sh 生成;orchestrator 在环境变量缺失时读取
|
|
1480
|
+
# 位置: ~/.dsh/argent/(argent-dsh 客户数据,update 不覆盖)
|
|
1481
|
+
# 注意:含空格的值必须加引号(bash source 与 Python load_config 均兼容)
|
|
1482
|
+
FEISHU_BASE_TOKEN=${FEISHU_BASE_TOKEN:-}
|
|
1483
|
+
FEISHU_TABLE_ID=${FEISHU_TABLE_ID:-}
|
|
1484
|
+
# base-bootstrap 生态别名(与 FEISHU_* 同值;base-bootstrap/--verify 依赖)
|
|
1485
|
+
BASE_TOKEN=${FEISHU_BASE_TOKEN:-}
|
|
1486
|
+
TABLE_ID=${FEISHU_TABLE_ID:-}
|
|
1487
|
+
# 客户飞书邮箱(表格所有者/管理员;base-bootstrap 代建时写入,按邮箱查 open_id 用)
|
|
1488
|
+
FEISHU_OWNER_EMAIL=${FEISHU_OWNER_EMAIL:-}
|
|
1489
|
+
FEISHU_USER_ID=${FEISHU_USER_ID:-}
|
|
1490
|
+
ML_STORE_ID=${first_id:-}
|
|
1491
|
+
ML_STORE_NAME=${first_store:-}
|
|
1492
|
+
ZINIAO_DL="${ziniaodl:-}"
|
|
1493
|
+
STORES_JSON=$STORES_JSON
|
|
1494
|
+
FULFILLMENT_ORCH=${FULFILLMENT_ORCH:-}
|
|
1495
|
+
# ── 可选:紫鸟 WebDriver 混动开关(改动1,默认 bridge=现有行为零变化)──
|
|
1496
|
+
# ZINIAO_MODE=bridge # bridge=ziniao-cli(默认)| webdriver=WebDriver 生命周期层
|
|
1497
|
+
# ZINIAO_WD_PORT=19515 # WebDriver IPC 端口(默认 19515)
|
|
1498
|
+
# ZINIAO_WD_HEADLESS=0 # 无头模式(预留,默认 0=有头;1=无头,open 后需等 ≥8s 再连 CDP)
|
|
1499
|
+
# ZINIAO_WD_ENV= # WebDriver 凭据文件路径(默认 ~/.dsh/argent/ziniao-webdriver.env)
|
|
1500
|
+
EOF
|
|
1501
|
+
log_ok "配置已保存"
|
|
1502
|
+
return 0
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
# ── 7. 连通性校验(汇总)────────────────────────────────
|
|
1506
|
+
verify_all() {
|
|
1507
|
+
echo ""
|
|
1508
|
+
echo "════════════════════════════════════════════════"
|
|
1509
|
+
echo " 🔍 连通性校验(汇总)"
|
|
1510
|
+
echo "════════════════════════════════════════════════"
|
|
1511
|
+
local base_token="${FEISHU_BASE_TOKEN:-}" table_id="${FEISHU_TABLE_ID:-}"
|
|
1512
|
+
local fail=0
|
|
1513
|
+
|
|
1514
|
+
# 7a. lark-cli 查表格
|
|
1515
|
+
echo ""
|
|
1516
|
+
log_info "[1/4] 飞书表格连通性 ..."
|
|
1517
|
+
if [ -z "$base_token" ] || [ -z "$table_id" ]; then
|
|
1518
|
+
log_warn "未配置飞书表格(跳过登录或未输入 URL),跳过表格连通性检查"
|
|
1519
|
+
elif lark-cli base +record-list --base-token "$base_token" --table-id "$table_id" --format json >/dev/null 2>&1; then
|
|
1520
|
+
log_ok "表格可查询"
|
|
1521
|
+
else
|
|
1522
|
+
log_err "表格查询失败(检查 token/table_id 与网络)"
|
|
1523
|
+
fail=1
|
|
1524
|
+
fi
|
|
1525
|
+
|
|
1526
|
+
# 7b. CDP 可连接
|
|
1527
|
+
echo ""
|
|
1528
|
+
log_info "[2/4] 紫鸟窗口 CDP 可连接性 ..."
|
|
1529
|
+
if [ -f "$STORES_JSON" ]; then
|
|
1530
|
+
python3 - "$STORES_JSON" << 'PYEOF'
|
|
1531
|
+
import json, sys, urllib.request
|
|
1532
|
+
with open(sys.argv[1]) as f:
|
|
1533
|
+
stores = json.load(f)
|
|
1534
|
+
ok = True
|
|
1535
|
+
for name, entry in stores.items():
|
|
1536
|
+
port = entry.get("cdp_port")
|
|
1537
|
+
if not port:
|
|
1538
|
+
print(f" ⚠ {name}: 未配置 cdp_port")
|
|
1539
|
+
ok = False
|
|
1540
|
+
continue
|
|
1541
|
+
try:
|
|
1542
|
+
with urllib.request.urlopen(f"http://127.0.0.1:{port}/json/version", timeout=3) as r:
|
|
1543
|
+
v = json.loads(r.read())
|
|
1544
|
+
print(f" ✓ {name}: 端口 {port} 可连接({v.get('Browser', '')[:40]})")
|
|
1545
|
+
except Exception as e:
|
|
1546
|
+
print(f" ✗ {name}: 端口 {port} 连接失败: {e}")
|
|
1547
|
+
ok = False
|
|
1548
|
+
sys.exit(0 if ok else 1)
|
|
1549
|
+
PYEOF
|
|
1550
|
+
[ $? -eq 0 ] || fail=1
|
|
1551
|
+
else
|
|
1552
|
+
log_warn "stores.json 不存在,跳过 CDP 检查"
|
|
1553
|
+
fi
|
|
1554
|
+
|
|
1555
|
+
# 7c. stores.json 与表格选项一致
|
|
1556
|
+
echo ""
|
|
1557
|
+
log_info "[3/4] 店铺映射一致性 ..."
|
|
1558
|
+
if [ -n "$base_token" ] && [ -n "$table_id" ] && [ -f "$STORES_JSON" ]; then
|
|
1559
|
+
verify_consistency "$base_token" "$table_id" || fail=1
|
|
1560
|
+
else
|
|
1561
|
+
log_warn "跳过(缺少表格配置或 stores.json)"
|
|
1562
|
+
fi
|
|
1563
|
+
|
|
1564
|
+
# 7d. 配置摘要
|
|
1565
|
+
echo ""
|
|
1566
|
+
log_info "[4/4] 配置摘要"
|
|
1567
|
+
echo ""
|
|
1568
|
+
echo " ┌─ 紫鸟环境 ─────────────────────────────"
|
|
1569
|
+
if [ -f "$STORES_JSON" ]; then
|
|
1570
|
+
python3 - "$STORES_JSON" << 'PYEOF'
|
|
1571
|
+
import json, sys
|
|
1572
|
+
with open(sys.argv[1]) as f:
|
|
1573
|
+
stores = json.load(f)
|
|
1574
|
+
for name, e in stores.items():
|
|
1575
|
+
print(f" │ {name} (store_id={e.get('store_id')}, cdp_port={e.get('cdp_port')})")
|
|
1576
|
+
PYEOF
|
|
1577
|
+
fi
|
|
1578
|
+
echo " ├─ 飞书表格 ─────────────────────────────"
|
|
1579
|
+
echo " │ Base: ${base_token:0:12}... Table: ${table_id}"
|
|
1580
|
+
echo " │ 推送目标: ${FEISHU_USER_ID:0:12}..."
|
|
1581
|
+
echo " ├─ cron ────────────────────────────────"
|
|
1582
|
+
if crontab -l 2>/dev/null | grep -q "poll-fulfillment.sh"; then
|
|
1583
|
+
echo " │ 状态: 已注册(系统 crontab,每 5 分钟轮询)"
|
|
1584
|
+
else
|
|
1585
|
+
echo " │ 状态: 未注册"
|
|
1586
|
+
fi
|
|
1587
|
+
echo " └────────────────────────────────────────"
|
|
1588
|
+
echo ""
|
|
1589
|
+
|
|
1590
|
+
if [ "$fail" = "0" ]; then
|
|
1591
|
+
log_ok "全部校验通过 🎉"
|
|
1592
|
+
else
|
|
1593
|
+
log_warn "存在校验失败项,请按上方提示修复后重试"
|
|
1594
|
+
fi
|
|
1595
|
+
return "$fail"
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
# ── 8. --verify 全链一致性校验(体检 + 自动修复)────
|
|
1599
|
+
# 只读数据源(紫鸟环境 / 飞书);缺失项自动初始化(只写 argent 自己的目录):
|
|
1600
|
+
# [1/5] stores.json 缺失 → 从紫鸟实时读取并保存到 ~/.dsh/argent/
|
|
1601
|
+
# [2/5] fulfillment.env 缺业务键 → 自动生成(紫鸟/stores.json/设置页第 6 项)
|
|
1602
|
+
# [3/5] 表格未配置 → 复用已有(设置页第 5 项 URL)或模板自动代建(幂等)
|
|
1603
|
+
# [4/5] cron 未注册 → 自动注册(系统 crontab,每 5 分钟,幂等)
|
|
1604
|
+
# [5/5] CDP 端口未探测 → 自动打开紫鸟窗口探测并回填 stores.json
|
|
1605
|
+
# 不读取 hermes 相关;每个红灯附中文修复指引(一句人话 + 对应命令)。
|
|
1606
|
+
# 检查项:
|
|
1607
|
+
# 1. stores.json 存在且 storeName/storeId 与紫鸟环境三方一致(store list 比对)
|
|
1608
|
+
# 2. fulfillment.env 键完整(BASE_TOKEN/TABLE_ID/FEISHU_* 等)
|
|
1609
|
+
# 3. Base schema 回读与 references/feishu-base-fulfillment-schema.md 比对(**历史基线**;
|
|
1610
|
+
# 差异只告警、不再判红灯,更不引导删表重建 —— 真实缺口用 scripts/ml_schema_check.py 查,
|
|
1611
|
+
# 见 skills/ml-config-guide)
|
|
1612
|
+
# 4. cron 已注册且指向正确脚本(poll-fulfillment.sh)
|
|
1613
|
+
# 5. 紫鸟 CDP 端口可达
|
|
1614
|
+
verify_chain() {
|
|
1615
|
+
migrate_legacy_data
|
|
1616
|
+
if [ -f "$ENV_FILE" ]; then
|
|
1617
|
+
set -a; source "$ENV_FILE"; set +a
|
|
1618
|
+
fi
|
|
1619
|
+
|
|
1620
|
+
echo ""
|
|
1621
|
+
echo "════════════════════════════════════════════════"
|
|
1622
|
+
echo " ✅ 全链一致性校验(--verify,体检 + 自动修复缺失项)"
|
|
1623
|
+
echo "════════════════════════════════════════════════"
|
|
1624
|
+
local fail=0
|
|
1625
|
+
|
|
1626
|
+
echo ""
|
|
1627
|
+
echo "── [1/5] 店铺映射(stores.json ↔ 紫鸟环境)──"
|
|
1628
|
+
check_stores_consistency || fail=$((fail + 1))
|
|
1629
|
+
|
|
1630
|
+
echo ""
|
|
1631
|
+
echo "── [2/5] 凭据文件(fulfillment.env 键完整)──"
|
|
1632
|
+
check_env_keys || fail=$((fail + 1))
|
|
1633
|
+
|
|
1634
|
+
echo ""
|
|
1635
|
+
echo "── [3/5] 表格 schema(回读 ↔ 固化 schema)──"
|
|
1636
|
+
check_base_schema || fail=$((fail + 1))
|
|
1637
|
+
|
|
1638
|
+
echo ""
|
|
1639
|
+
echo "── [4/5] 自动化(cron 注册 + 脚本指向)──"
|
|
1640
|
+
check_cron_registered || fail=$((fail + 1))
|
|
1641
|
+
|
|
1642
|
+
echo ""
|
|
1643
|
+
echo "── [5/5] 紫鸟窗口(CDP 端口可达)──"
|
|
1644
|
+
check_cdp_ports || fail=$((fail + 1))
|
|
1645
|
+
|
|
1646
|
+
echo ""
|
|
1647
|
+
echo "── [6/6] 日报推送(OAuth 授权 + cron)──"
|
|
1648
|
+
check_daily_report || fail=$((fail + 1))
|
|
1649
|
+
|
|
1650
|
+
echo ""
|
|
1651
|
+
echo "════════════════════════════════════════════════"
|
|
1652
|
+
if [ "$fail" = "0" ]; then
|
|
1653
|
+
log_ok "全链一致性校验通过(6/6 绿灯)🎉"
|
|
1654
|
+
else
|
|
1655
|
+
log_err "存在 ${fail} 项红灯,请按上方修复指引处理后重跑 init-ml.sh --verify"
|
|
1656
|
+
fi
|
|
1657
|
+
echo "════════════════════════════════════════════════"
|
|
1658
|
+
return "$fail"
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
# 检查 1:stores.json 与紫鸟环境一致;本地缓存缺失时从紫鸟实时读取并保存
|
|
1662
|
+
# (数据源 = 紫鸟环境本体;不读取 hermes 相关)
|
|
1663
|
+
check_stores_consistency() {
|
|
1664
|
+
local list need_save=0
|
|
1665
|
+
list=$(ziniao-cli store list 2>/dev/null) || {
|
|
1666
|
+
log_err "ziniao-cli store list 失败——检查 apiKey 与紫鸟客户端状态"
|
|
1667
|
+
echo " 💡 修复:Argent 设置页 → ML 配置区 → 紫鸟 API Key 重新配置(或先启动紫鸟客户端)"
|
|
1668
|
+
return 1
|
|
1669
|
+
}
|
|
1670
|
+
if [ ! -f "$STORES_JSON" ]; then
|
|
1671
|
+
log_warn "本地 stores.json 不存在(${STORES_JSON})——将从紫鸟实时读取并保存"
|
|
1672
|
+
need_save=1
|
|
1673
|
+
fi
|
|
1674
|
+
python3 - "$STORES_JSON" "$list" "$need_save" << 'PYEOF'
|
|
1675
|
+
import json, sys, os
|
|
1676
|
+
stores_path, raw, need_save = sys.argv[1], sys.argv[2], int(sys.argv[3])
|
|
1677
|
+
try:
|
|
1678
|
+
items = json.loads(raw).get("data", {}).get("items", [])
|
|
1679
|
+
except Exception as e:
|
|
1680
|
+
print(f" ✗ ziniao-cli store list 输出解析失败: {e}")
|
|
1681
|
+
sys.exit(1)
|
|
1682
|
+
ml = [i for i in items if "MercadoLibre" in (i.get("platformName") or "")]
|
|
1683
|
+
env_map = {i.get("storeName"): str(i.get("storeId")) for i in ml}
|
|
1684
|
+
if not ml:
|
|
1685
|
+
print(" ✗ 紫鸟中未找到 MercadoLibre 环境(已打开/已创建的环境列表为空)")
|
|
1686
|
+
print(" 💡 修复:在紫鸟客户端打开/创建 MercadoLibre 店铺环境后重跑")
|
|
1687
|
+
sys.exit(1)
|
|
1688
|
+
if need_save:
|
|
1689
|
+
# 从紫鸟初始化本地映射缓存(store_id 来自紫鸟;cdp_port 运行时探测后回填)
|
|
1690
|
+
stores = {name: {"store_id": eid, "platform": "MercadoLibre", "cdp_port": 0}
|
|
1691
|
+
for name, eid in env_map.items()}
|
|
1692
|
+
try:
|
|
1693
|
+
os.makedirs(os.path.dirname(stores_path), exist_ok=True)
|
|
1694
|
+
with open(stores_path, "w", encoding="utf-8") as f:
|
|
1695
|
+
json.dump(stores, f, ensure_ascii=False, indent=2)
|
|
1696
|
+
print(f" ✓ 已从紫鸟实时读取并保存 stores.json({len(stores)} 店 → {stores_path})")
|
|
1697
|
+
except Exception as e:
|
|
1698
|
+
print(f" ✗ 保存 stores.json 失败: {e}")
|
|
1699
|
+
sys.exit(1)
|
|
1700
|
+
else:
|
|
1701
|
+
try:
|
|
1702
|
+
stores = json.load(open(stores_path))
|
|
1703
|
+
except Exception as e:
|
|
1704
|
+
print(f" ✗ stores.json 解析失败: {e}")
|
|
1705
|
+
print(" 💡 修复:删除损坏文件后在设置页 ML 配置区重新同步店铺映射")
|
|
1706
|
+
sys.exit(1)
|
|
1707
|
+
problems = []
|
|
1708
|
+
if not stores:
|
|
1709
|
+
problems.append("stores.json 为空(未配置任何店铺)")
|
|
1710
|
+
for name, entry in stores.items():
|
|
1711
|
+
eid = str(entry.get("store_id") or "")
|
|
1712
|
+
if name not in env_map:
|
|
1713
|
+
problems.append(f"「{name}」不在紫鸟环境列表中(环境已删除/改名)")
|
|
1714
|
+
continue
|
|
1715
|
+
if eid and eid != env_map[name]:
|
|
1716
|
+
problems.append(f"「{name}」store_id 不一致: stores.json={eid},紫鸟={env_map[name]}")
|
|
1717
|
+
if problems:
|
|
1718
|
+
for p in problems:
|
|
1719
|
+
print(f" ✗ {p}")
|
|
1720
|
+
print(" 💡 修复:紫鸟环境有变动——设置页 ML 配置区重新同步店铺映射:")
|
|
1721
|
+
print(" bash init-ml.sh --refresh-stores")
|
|
1722
|
+
sys.exit(1)
|
|
1723
|
+
print(f" ✓ stores.json 与紫鸟环境一致({len(stores)} 店,storeName/store_id 全部匹配)")
|
|
1724
|
+
PYEOF
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
# 检查 2:fulfillment.env 键完整
|
|
1728
|
+
check_env_keys() {
|
|
1729
|
+
# token 键(FEISHU_BASE_TOKEN 等)由 [3/5] 表格环节负责,本检查只覆盖业务键
|
|
1730
|
+
local required="ML_STORE_ID ML_STORE_NAME STORES_JSON ZINIAO_DL"
|
|
1731
|
+
local missing="" k
|
|
1732
|
+
if [ -f "$ENV_FILE" ]; then
|
|
1733
|
+
# 推送目标二选一:键值非空才算已配置(FEISHU_USER_ID 私信 / FEISHU_CHAT_ID 群)
|
|
1734
|
+
if ! grep -qE '^FEISHU_USER_ID=.+' "$ENV_FILE" 2>/dev/null && ! grep -qE '^FEISHU_CHAT_ID=.+' "$ENV_FILE" 2>/dev/null; then
|
|
1735
|
+
missing="$missing 推送目标(FEISHU_USER_ID/FEISHU_CHAT_ID)"
|
|
1736
|
+
fi
|
|
1737
|
+
for k in $required; do
|
|
1738
|
+
if ! grep -qE "^${k}=.+" "$ENV_FILE"; then missing="$missing $k"; fi
|
|
1739
|
+
done
|
|
1740
|
+
else
|
|
1741
|
+
missing="(文件不存在)"
|
|
1742
|
+
fi
|
|
1743
|
+
if [ -z "$missing" ]; then
|
|
1744
|
+
log_ok "fulfillment.env 键完整(业务键 + 推送目标)"
|
|
1745
|
+
return 0
|
|
1746
|
+
fi
|
|
1747
|
+
log_warn "fulfillment.env 缺失键: ${missing}——自动生成(数据源=紫鸟/stores.json/设置页)..."
|
|
1748
|
+
mkdir -p "$DATA_SCRIPTS"
|
|
1749
|
+
local cfg_out
|
|
1750
|
+
cfg_out=$(read_argent_cfg)
|
|
1751
|
+
python3 - "$ENV_FILE" "$STORES_JSON" "$cfg_out" << 'PYEOF'
|
|
1752
|
+
import json, os, subprocess, sys
|
|
1753
|
+
env_path, stores_path, cfg = sys.argv[1], sys.argv[2], sys.argv[3].split("\t")
|
|
1754
|
+
chat_id = (cfg + ["", ""])[1]
|
|
1755
|
+
push_user = chat_id if chat_id.startswith("ou_") else ""
|
|
1756
|
+
push_chat = chat_id if chat_id.startswith("oc_") else ""
|
|
1757
|
+
stores = {}
|
|
1758
|
+
try:
|
|
1759
|
+
with open(stores_path) as f:
|
|
1760
|
+
stores = json.load(f)
|
|
1761
|
+
except Exception:
|
|
1762
|
+
pass
|
|
1763
|
+
first = next(iter(stores), None)
|
|
1764
|
+
first_id = str(stores[first].get("store_id") or "") if first else ""
|
|
1765
|
+
ziniaodl = ""
|
|
1766
|
+
# 下载目录为固定路径模式(.../ziniao browser/<店铺名>),目录存在则直接用,不额外开窗
|
|
1767
|
+
if first:
|
|
1768
|
+
try:
|
|
1769
|
+
cand = os.path.expanduser("~/Library/Application Support/ziniaobrowserdatas/ziniao browser/" + first)
|
|
1770
|
+
if os.path.isdir(cand):
|
|
1771
|
+
ziniaodl = cand
|
|
1772
|
+
except Exception:
|
|
1773
|
+
pass
|
|
1774
|
+
if not ziniaodl and first_id:
|
|
1775
|
+
try:
|
|
1776
|
+
r = subprocess.run(["ziniao-cli", "store", "open", "--id", first_id, "--headless=false"],
|
|
1777
|
+
capture_output=True, text=True, timeout=60)
|
|
1778
|
+
raw = r.stdout
|
|
1779
|
+
start = raw.find("{")
|
|
1780
|
+
d = json.loads(raw[start:]) if start >= 0 else {}
|
|
1781
|
+
ziniaodl = d.get("data", {}).get("downloadFolderPath", "")
|
|
1782
|
+
except Exception:
|
|
1783
|
+
pass
|
|
1784
|
+
pairs = [
|
|
1785
|
+
("FEISHU_USER_ID", push_user),
|
|
1786
|
+
("FEISHU_CHAT_ID", push_chat),
|
|
1787
|
+
("ML_STORE_ID", first_id),
|
|
1788
|
+
("ML_STORE_NAME", first or ""),
|
|
1789
|
+
("STORES_JSON", stores_path),
|
|
1790
|
+
# ZINIAO_DL 含空格路径,必须带引号(bash source 兼容)
|
|
1791
|
+
("ZINIAO_DL", '"' + ziniaodl + '"'),
|
|
1792
|
+
]
|
|
1793
|
+
lines = []
|
|
1794
|
+
if os.path.exists(env_path):
|
|
1795
|
+
lines = open(env_path, encoding="utf-8").read().splitlines()
|
|
1796
|
+
keys = {l.split("=", 1)[0] for l in lines if "=" in l and not l.startswith("#")}
|
|
1797
|
+
for k, v in pairs:
|
|
1798
|
+
if k not in keys:
|
|
1799
|
+
lines.append(f"{k}={v}")
|
|
1800
|
+
os.makedirs(os.path.dirname(env_path), exist_ok=True)
|
|
1801
|
+
with open(env_path, "w", encoding="utf-8") as f:
|
|
1802
|
+
f.write("\n".join(lines) + "\n")
|
|
1803
|
+
print(" ✓ 已自动生成 fulfillment.env(ML_STORE=" + str(first) + ",推送目标="
|
|
1804
|
+
+ (chat_id if (push_user or push_chat) else "未填(可在设置页第 4 项填写,ou_ 私信 / oc_ 群)") + ")")
|
|
1805
|
+
PYEOF
|
|
1806
|
+
local still="" k
|
|
1807
|
+
for k in $required; do
|
|
1808
|
+
if ! grep -qE "^${k}=" "$ENV_FILE" 2>/dev/null; then still="$still $k"; fi
|
|
1809
|
+
done
|
|
1810
|
+
if [ -n "$still" ]; then
|
|
1811
|
+
log_err "自动生成仍缺键:${still}(请检查 stores.json 与紫鸟 API Key)"
|
|
1812
|
+
return 1
|
|
1813
|
+
fi
|
|
1814
|
+
return 0
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
# 检查 3:Base schema 回读与固化 schema 比对(期望与 base-bootstrap.sh 一致)
|
|
1818
|
+
check_base_schema() {
|
|
1819
|
+
local base_token table_id
|
|
1820
|
+
base_token=$(grep -E '^FEISHU_BASE_TOKEN=' "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2-)
|
|
1821
|
+
table_id=$(grep -E '^FEISHU_TABLE_ID=' "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2-)
|
|
1822
|
+
if [ -z "$base_token" ] || [ -z "$table_id" ]; then
|
|
1823
|
+
log_warn "表格未配置(缺少 FEISHU_BASE_TOKEN/FEISHU_TABLE_ID)——自动处理:已有表格复用,否则代建..."
|
|
1824
|
+
local cfg_out base_url
|
|
1825
|
+
cfg_out=$(read_argent_cfg)
|
|
1826
|
+
base_url=$(echo "$cfg_out" | cut -f1)
|
|
1827
|
+
local bb_log
|
|
1828
|
+
bb_log=$(mktemp -t argent-bb.XXXXXX.log)
|
|
1829
|
+
if [ -n "$base_url" ]; then
|
|
1830
|
+
# 已有表格 URL(设置页第 5 项):解析 token → 已有 FULL任务 表直接复用;否则在其内建表
|
|
1831
|
+
log_info "检测到已配置表格 URL: ${base_url}"
|
|
1832
|
+
local token2 tbl2
|
|
1833
|
+
token2=$(echo "$base_url" | python3 -c "import re,sys; m=re.search(r'/base/([A-Za-z0-9]+)', sys.stdin.read()); print(m[1] if m else '')")
|
|
1834
|
+
if [ -z "$token2" ]; then
|
|
1835
|
+
log_err "无法从表格 URL 解析 Base Token: ${base_url}"
|
|
1836
|
+
else
|
|
1837
|
+
tbl2=$(lark-cli base +table-list --base-token "$token2" --as bot --format json 2>/dev/null | python3 -c "
|
|
1838
|
+
import json,sys
|
|
1839
|
+
try:
|
|
1840
|
+
d = json.load(sys.stdin)
|
|
1841
|
+
print(next((t['id'] for t in d.get('data', {}).get('tables', []) if t.get('name') == 'FULL任务'), ''))
|
|
1842
|
+
except Exception:
|
|
1843
|
+
print('')
|
|
1844
|
+
" 2>/dev/null || true)
|
|
1845
|
+
if [ -n "$tbl2" ]; then
|
|
1846
|
+
log_ok "已有表格含「FULL任务」表(${tbl2})——直接复用,不重建"
|
|
1847
|
+
update_env_key "BASE_TOKEN" "$token2" "$ENV_FILE"
|
|
1848
|
+
update_env_key "TABLE_ID" "$tbl2" "$ENV_FILE"
|
|
1849
|
+
update_env_key "FEISHU_BASE_TOKEN" "$token2" "$ENV_FILE"
|
|
1850
|
+
update_env_key "FEISHU_TABLE_ID" "$tbl2" "$ENV_FILE"
|
|
1851
|
+
else
|
|
1852
|
+
log_info "该 Base 无「FULL任务」表——在其中自动创建..."
|
|
1853
|
+
if ! "$SCRIPT_DIR/base-bootstrap.sh" --url "$base_url" --env-file "$ENV_FILE" --non-interactive --skip-test > "$bb_log" 2>&1; then
|
|
1854
|
+
log_warn "在该 Base 内建表失败——输出:"
|
|
1855
|
+
tail -5 "$bb_log" | sed 's/^/ /'
|
|
1856
|
+
fi
|
|
1857
|
+
fi
|
|
1858
|
+
fi
|
|
1859
|
+
else
|
|
1860
|
+
# 未配置表格 URL → 模板复制自动代建(设置页第 5 项留空时)
|
|
1861
|
+
log_info "未配置表格 URL——从生产模板自动代建..."
|
|
1862
|
+
if ! "$SCRIPT_DIR/base-bootstrap.sh" --env-file "$ENV_FILE" --non-interactive > "$bb_log" 2>&1; then
|
|
1863
|
+
log_warn "自动代建失败——输出:"
|
|
1864
|
+
tail -5 "$bb_log" | sed 's/^/ /'
|
|
1865
|
+
fi
|
|
1866
|
+
fi
|
|
1867
|
+
rm -f "$bb_log"
|
|
1868
|
+
base_token=$(grep -E '^FEISHU_BASE_TOKEN=' "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2-)
|
|
1869
|
+
table_id=$(grep -E '^FEISHU_TABLE_ID=' "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2-)
|
|
1870
|
+
fi
|
|
1871
|
+
if [ -z "$base_token" ] || [ -z "$table_id" ]; then
|
|
1872
|
+
log_err "表格仍未配置(自动处理未成功)——请检查设置页第 5 项表格 URL、飞书应用权限与模板访问权限"
|
|
1873
|
+
return 1
|
|
1874
|
+
fi
|
|
1875
|
+
local fields_json
|
|
1876
|
+
fields_json=$(lark-cli base +field-list --base-token "$base_token" --table-id "$table_id" --format json 2>/dev/null) || {
|
|
1877
|
+
log_err "回读表格字段失败——检查 lark-cli 登录态与表格访问权限"
|
|
1878
|
+
echo " 💡 修复:确认 lark-cli 身份与表格分享——表格回读使用 lark-cli 默认 profile 身份(lark-cli auth status 可查,当前为 cli_aa0ea42f87389cd3),表格需分享给该 Bot;分享方法:飞书表格 → 分享 → 添加协作者 → 搜索对应 Bot 应用名;如需用角色 Bot 身份需在脚本调用加 --profile <role>"
|
|
1879
|
+
return 1
|
|
1880
|
+
}
|
|
1881
|
+
# 期望 schema:名 → (类型, select 选项列表或 None);「店铺名称」选项以
|
|
1882
|
+
# stores.json 为准(sync_feishu_field 后为店铺名),无 stores.json 时用固化默认值
|
|
1883
|
+
python3 - "$STORES_JSON" "$fields_json" << 'PYEOF'
|
|
1884
|
+
import json, os, sys
|
|
1885
|
+
stores_path, raw = sys.argv[1], sys.argv[2]
|
|
1886
|
+
try:
|
|
1887
|
+
d = json.loads(raw)
|
|
1888
|
+
fields = {f["name"]: f for f in d.get("data", {}).get("fields", [])}
|
|
1889
|
+
except Exception as e:
|
|
1890
|
+
print(f" ✗ 字段回读输出解析失败: {e}")
|
|
1891
|
+
sys.exit(1)
|
|
1892
|
+
|
|
1893
|
+
default_store_opts = ["3店-主账号", "2店-子账号", "1店-子账号"]
|
|
1894
|
+
store_opts = default_store_opts
|
|
1895
|
+
try:
|
|
1896
|
+
with open(stores_path) as f:
|
|
1897
|
+
store_opts = list(json.load(f).keys()) or default_store_opts
|
|
1898
|
+
except Exception:
|
|
1899
|
+
pass
|
|
1900
|
+
|
|
1901
|
+
expect = {
|
|
1902
|
+
"SKU": ("text", None),
|
|
1903
|
+
"状态": ("select", ["Pending", "运行中", "已完成", "失败"]),
|
|
1904
|
+
"店铺名称": ("select", store_opts),
|
|
1905
|
+
"品名": ("text", None),
|
|
1906
|
+
"数量": ("number", None),
|
|
1907
|
+
"箱数": ("number", None),
|
|
1908
|
+
"就绪": ("checkbox", None),
|
|
1909
|
+
# 创建时间:飞书 API 类型 5(DateTime);建表 API 建出 created_at,
|
|
1910
|
+
# 人工创建/回读可能为 datetime——两者均接受
|
|
1911
|
+
"创建时间": (("created_at", "datetime"), None),
|
|
1912
|
+
"当前步骤": ("text", None),
|
|
1913
|
+
"货件号": ("text", None),
|
|
1914
|
+
"产品标签": ("attachment", None),
|
|
1915
|
+
"箱唛": ("attachment", None),
|
|
1916
|
+
}
|
|
1917
|
+
diffs = []
|
|
1918
|
+
for n in expect:
|
|
1919
|
+
if n not in fields:
|
|
1920
|
+
diffs.append(f"缺字段「{n}」")
|
|
1921
|
+
continue
|
|
1922
|
+
f = fields[n]
|
|
1923
|
+
etypes = expect[n][0]
|
|
1924
|
+
if isinstance(etypes, tuple):
|
|
1925
|
+
if f.get("type") not in etypes:
|
|
1926
|
+
diffs.append(f"「{n}」类型不符: 期望 {'/'.join(etypes)},实际 {f.get('type')}")
|
|
1927
|
+
elif f.get("type") != etypes:
|
|
1928
|
+
diffs.append(f"「{n}」类型不符: 期望 {etypes},实际 {f.get('type')}")
|
|
1929
|
+
opts = expect[n][1]
|
|
1930
|
+
if opts is not None:
|
|
1931
|
+
actual = [o.get("name") for o in f.get("options", []) if o.get("name")]
|
|
1932
|
+
for o in opts:
|
|
1933
|
+
if o not in actual:
|
|
1934
|
+
diffs.append(f"「{n}」缺选项: {o}")
|
|
1935
|
+
for o in actual:
|
|
1936
|
+
if o not in opts:
|
|
1937
|
+
diffs.append(f"「{n}」多出选项: {o}")
|
|
1938
|
+
if diffs:
|
|
1939
|
+
# ⚠️ 2026-09-15 改:这里原先指引 `base-bootstrap.sh --force`(**删表重建**)。
|
|
1940
|
+
# 由于 references/feishu-base-fulfillment-schema.md 是 2026-08-13 的**历史快照**(12 字段),
|
|
1941
|
+
# 而生产表结构随后已演进(如 lip 表 +6 系统字段),该指引会**误导运维删除生产表**。
|
|
1942
|
+
# 新口径:快照只作历史基线;**是否缺字段以 live schema 为准**,用下面的脚本查、按 SKILL 引导补。
|
|
1943
|
+
print(f" ⚠ schema 与历史快照(references/feishu-base-fulfillment-schema.md)存在差异:")
|
|
1944
|
+
for x in diffs:
|
|
1945
|
+
print(f" · {x}")
|
|
1946
|
+
print(" 💡 这是**提示而非故障**:该快照是 2026-08-13 的历史基线,表结构演进属正常。")
|
|
1947
|
+
print(" 查真实缺口(读 live schema,不会改任何东西):")
|
|
1948
|
+
print(" python3 scripts/ml_schema_check.py --flow <flow-id> --table \"$TABLE_ID\"")
|
|
1949
|
+
print(" 按 skills/ml-config-guide 的引导补字段(HS 等报关敏感字段由运营人工维护,系统不预填)。")
|
|
1950
|
+
print(" ⛔ 不要用 base-bootstrap.sh --force 重建表——那会删除生产数据。")
|
|
1951
|
+
schema_warn=1
|
|
1952
|
+
else:
|
|
1953
|
+
schema_warn=0
|
|
1954
|
+
print(f" ✓ schema 与历史快照一致(references/feishu-base-fulfillment-schema.md)")
|
|
1955
|
+
PYEOF
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
# 检查 4:cron 已注册且指向正确脚本
|
|
1959
|
+
check_cron_registered() {
|
|
1960
|
+
if [ ! -d "$PROFILE_HOME" ]; then
|
|
1961
|
+
log_err "角色 profile ${PROFILE_NAME} 未安装(${PROFILE_HOME})"
|
|
1962
|
+
echo " 💡 修复:先安装角色 profile(argent update 或分发安装包)"
|
|
1963
|
+
return 1
|
|
1964
|
+
fi
|
|
1965
|
+
local ok=0
|
|
1966
|
+
if [ ! -f "$PROFILE_SCRIPTS/poll-fulfillment.sh" ]; then
|
|
1967
|
+
log_err "轮询脚本未部署(${PROFILE_SCRIPTS}/poll-fulfillment.sh 不存在)"
|
|
1968
|
+
echo " 💡 修复:运行初始化向导的机器环境环节自动复制脚本:"
|
|
1969
|
+
echo " bash init-ml.sh"
|
|
1970
|
+
ok=1
|
|
1971
|
+
fi
|
|
1972
|
+
local cron_line
|
|
1973
|
+
cron_line=$(crontab -l 2>/dev/null | grep "poll-fulfillment.sh" | head -1 || true)
|
|
1974
|
+
if [ -n "$cron_line" ]; then
|
|
1975
|
+
log_ok "cron 已注册(系统 crontab): ${cron_line}"
|
|
1976
|
+
log_ok "(本机 hermes agent 承担 FULL 轮询时,可用 ~/.dsh/argent/PAUSE_POLL 停用本脚本轮询)"
|
|
1977
|
+
else
|
|
1978
|
+
log_warn "cron 未注册 poll-fulfillment.sh(argent-dsh 默认不承担 FULL 轮询)"
|
|
1979
|
+
log_warn "如由 argent-dsh 承担 FULL 轮询,手动注册:"
|
|
1980
|
+
echo " (crontab -l; echo '*/5 * * * * /bin/bash ${PROFILE_SCRIPTS}/poll-fulfillment.sh >> ${DATA_SCRIPTS}/fulfillment-logs/cron.log 2>&1') | crontab -"
|
|
1981
|
+
fi
|
|
1982
|
+
return "$ok"
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
# 检查 5:紫鸟 CDP 端口可达
|
|
1986
|
+
check_cdp_ports() {
|
|
1987
|
+
if [ ! -f "$STORES_JSON" ]; then
|
|
1988
|
+
log_warn "stores.json 不存在(已由 [1/5] 红灯),跳过 CDP 端口检查"
|
|
1989
|
+
return 0
|
|
1990
|
+
fi
|
|
1991
|
+
# 缺 cdp_port → 自动打开紫鸟窗口并探测端口回填(数据源=紫鸟;已打开窗口复用)
|
|
1992
|
+
local missing_ports
|
|
1993
|
+
missing_ports=$(python3 -c "
|
|
1994
|
+
import json
|
|
1995
|
+
try:
|
|
1996
|
+
d = json.load(open('$STORES_JSON'))
|
|
1997
|
+
print('yes' if any(not e.get('cdp_port') for e in d.values()) else 'no')
|
|
1998
|
+
except Exception:
|
|
1999
|
+
print('no')
|
|
2000
|
+
")
|
|
2001
|
+
if [ "$missing_ports" = "yes" ]; then
|
|
2002
|
+
log_warn "检测到未探测的 CDP 端口——自动打开紫鸟窗口并探测..."
|
|
2003
|
+
probe_store_ports
|
|
2004
|
+
fi
|
|
2005
|
+
# 当前已打开的紫鸟窗口端口(供未关联端口降级为黄灯判断;运行期 orchestrator
|
|
2006
|
+
# 会实时扫描 CDP 端口,stores.json 的 cdp_port 仅为缓存,缺失不阻断)
|
|
2007
|
+
local current_ports
|
|
2008
|
+
current_ports=$(discover_cdp_ports || true)
|
|
2009
|
+
python3 - "$STORES_JSON" "$current_ports" << 'PYEOF'
|
|
2010
|
+
import json, sys, urllib.request
|
|
2011
|
+
stores_path, cur = sys.argv[1], sys.argv[2]
|
|
2012
|
+
cur_ports = [int(x) for x in cur.split() if x.isdigit()]
|
|
2013
|
+
with open(stores_path) as f:
|
|
2014
|
+
stores = json.load(f)
|
|
2015
|
+
if not stores:
|
|
2016
|
+
print(" ⚠ stores.json 为空,无端口可检查")
|
|
2017
|
+
sys.exit(0)
|
|
2018
|
+
bad = 0
|
|
2019
|
+
warned = 0
|
|
2020
|
+
for name, entry in stores.items():
|
|
2021
|
+
port = entry.get("cdp_port")
|
|
2022
|
+
if not port:
|
|
2023
|
+
if cur_ports:
|
|
2024
|
+
print(f" ⚠ {name}: 端口未关联(紫鸟窗口已打开,当前端口 {sorted(cur_ports)})——"
|
|
2025
|
+
f"首次出现唯一窗口时将自动关联,或手动填写 stores.json cdp_port")
|
|
2026
|
+
warned += 1
|
|
2027
|
+
else:
|
|
2028
|
+
print(f" ✗ {name}: 未配置 cdp_port 且未检测到紫鸟窗口(请打开紫鸟客户端窗口后重跑)")
|
|
2029
|
+
bad += 1
|
|
2030
|
+
continue
|
|
2031
|
+
try:
|
|
2032
|
+
with urllib.request.urlopen(f"http://127.0.0.1:{port}/json/version", timeout=3) as r:
|
|
2033
|
+
v = json.loads(r.read())
|
|
2034
|
+
print(f" ✓ {name}: 端口 {port} 可达({v.get('Browser', '')[:30]})")
|
|
2035
|
+
except Exception:
|
|
2036
|
+
print(f" ✗ {name}: 端口 {port} 不可达——紫鸟窗口未打开或已关闭")
|
|
2037
|
+
bad += 1
|
|
2038
|
+
if bad:
|
|
2039
|
+
print(" 💡 修复:打开紫鸟客户端店铺窗口后重跑一键体检(自动探测端口)")
|
|
2040
|
+
sys.exit(1)
|
|
2041
|
+
if warned:
|
|
2042
|
+
print(" (运行期会实时扫描端口,端口关联可稍后自动完成,不阻断使用)")
|
|
2043
|
+
PYEOF
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
# ── 8. 飞书 gateway 对话接入配置 ─────────────────────────
|
|
2047
|
+
# 8a. 探测角色 profile 路径
|
|
2048
|
+
detect_profile_home() {
|
|
2049
|
+
# v0.6.0:角色即 profile——优先 argent-ml-seller;旧版 argent-ml 兜底探测(存量机器)
|
|
2050
|
+
# 与 argent_tui 一致:ARGENT_PROFILE_NAME > 已安装探测 > 默认 argent-ml-seller
|
|
2051
|
+
local name="${ARGENT_PROFILE_NAME:-}"
|
|
2052
|
+
if [ -z "$name" ]; then
|
|
2053
|
+
for cand in argent-ml-seller argent-ml; do
|
|
2054
|
+
if [ -d "$HOME/.hermes/profiles/$cand" ]; then
|
|
2055
|
+
name="$cand"
|
|
2056
|
+
break
|
|
2057
|
+
fi
|
|
2058
|
+
done
|
|
2059
|
+
fi
|
|
2060
|
+
name="${name:-argent-ml-seller}"
|
|
2061
|
+
PROFILE_NAME="$name"
|
|
2062
|
+
PROFILE_HOME="${ARGENT_PROFILE_HOME:-$HOME/.hermes/profiles/$name}"
|
|
2063
|
+
PROFILE_SCRIPTS="$PROFILE_HOME/scripts"
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
# 8b. 交互配置 feishu gateway(App ID/Secret + 白名单 + gateway install)
|
|
2067
|
+
feishu_gateway_setup() {
|
|
2068
|
+
echo ""
|
|
2069
|
+
echo "════════════════════════════════════════════════"
|
|
2070
|
+
echo " 🪽 飞书对话接入配置(gateway + 角色 profile)"
|
|
2071
|
+
echo "════════════════════════════════════════════════"
|
|
2072
|
+
log_warn "【hermes 遗留流程】DSH 版飞书对话走 feishu-bridge.js(Web 配置中心配置"
|
|
2073
|
+
log_warn "App ID/Secret 即可),此 gateway 段仅兼容旧流程,新装请勿使用。"
|
|
2074
|
+
echo ""
|
|
2075
|
+
echo ""
|
|
2076
|
+
echo " 前置准备(约 5 分钟):"
|
|
2077
|
+
echo " ① 打开 https://open.feishu.cn → 创建企业自建应用"
|
|
2078
|
+
echo " ② 添加「机器人」能力"
|
|
2079
|
+
echo " ③ 权限管理 → 开启: im:message, im:message:send_as_bot, im:resource"
|
|
2080
|
+
echo " + 多维表格(bitable:app) + 云文档(drive:drive)"
|
|
2081
|
+
echo " + base:app:create(BOT 代建新 Base 必需,缺它报 99991672 建 Base 被拒)"
|
|
2082
|
+
echo " + space:document:delete(可选:bot 清理测试残留用,无它测试 Base 只能人工删)"
|
|
2083
|
+
echo " (建表/授权需要 bitable:app 与 drive:drive;按邮箱查用户 ID 需要"
|
|
2084
|
+
echo " contact:user.id:readonly「获取用户 ID」)"
|
|
2085
|
+
echo " ④ 创建版本并发布应用(可用性全员)——开完 scope 必须发布才生效,否则所有 API 报 app_scope_not_applied (99991672)"
|
|
2086
|
+
echo " ⑤ 复制 App ID 和 App Secret"
|
|
2087
|
+
echo ""
|
|
2088
|
+
|
|
2089
|
+
detect_profile_home
|
|
2090
|
+
log_ok "目标 profile: ${PROFILE_HOME}"
|
|
2091
|
+
|
|
2092
|
+
local app_id app_secret
|
|
2093
|
+
read -r -p " 请输入 App ID: " app_id
|
|
2094
|
+
app_id="$(echo "$app_id" | tr -d '[:space:]')"
|
|
2095
|
+
if [ -z "$app_id" ]; then
|
|
2096
|
+
log_err "App ID 不能为空"
|
|
2097
|
+
return 1
|
|
2098
|
+
fi
|
|
2099
|
+
read -r -p " 请输入 App Secret: " app_secret
|
|
2100
|
+
app_secret="$(echo "$app_secret" | tr -d '[:space:]')"
|
|
2101
|
+
if [ -z "$app_secret" ]; then
|
|
2102
|
+
log_err "App Secret 不能为空"
|
|
2103
|
+
return 1
|
|
2104
|
+
fi
|
|
2105
|
+
|
|
2106
|
+
# 7c. 白名单模式
|
|
2107
|
+
echo ""
|
|
2108
|
+
echo " 对话白名单模式:"
|
|
2109
|
+
echo " [1] allowlist(推荐)— 仅允许指定飞书用户(open_id)对话"
|
|
2110
|
+
echo " [2] 放行所有用户(单客户机器信任环境)"
|
|
2111
|
+
local mode
|
|
2112
|
+
read -r -p " 请选择 [1-2](默认 1): " mode
|
|
2113
|
+
mode="${mode:-1}"
|
|
2114
|
+
local allowed=""
|
|
2115
|
+
if [ "$mode" = "2" ]; then
|
|
2116
|
+
ALLOW_ALL="true"
|
|
2117
|
+
log_warn "已选择放行所有飞书用户(仅限单客户机器信任环境)"
|
|
2118
|
+
else
|
|
2119
|
+
ALLOW_ALL="false"
|
|
2120
|
+
echo ""
|
|
2121
|
+
echo " 输入允许对话的飞书用户 open_id(逗号分隔,如 ou_xxx,ou_yyy)"
|
|
2122
|
+
echo " 获取方式:飞书管理后台 → 成员 → 头像 → open_id;"
|
|
2123
|
+
echo " 或本机执行 lark-cli auth status 查看当前登录用户"
|
|
2124
|
+
read -r -p " open_id 列表: " allowed
|
|
2125
|
+
allowed="$(echo "$allowed" | tr -d '[:space:]')"
|
|
2126
|
+
if [ -z "$allowed" ]; then
|
|
2127
|
+
log_err "allowlist 不能为空(或选择模式 2 放行全部)"
|
|
2128
|
+
return 1
|
|
2129
|
+
fi
|
|
2130
|
+
fi
|
|
2131
|
+
|
|
2132
|
+
# 7d. 写入 profile .env(保留已有 FEISHU_* 之外的行)
|
|
2133
|
+
mkdir -p "$PROFILE_HOME"
|
|
2134
|
+
local env_path="$PROFILE_HOME/.env"
|
|
2135
|
+
touch "$env_path"
|
|
2136
|
+
local tmp
|
|
2137
|
+
tmp=$(mktemp)
|
|
2138
|
+
grep -vE '^(FEISHU_APP_ID=|FEISHU_APP_SECRET=|FEISHU_DOMAIN=|FEISHU_CONNECTION_MODE=|FEISHU_ALLOWED_USERS=|FEISHU_ALLOW_ALL_USERS=)' "$env_path" > "$tmp" || true
|
|
2139
|
+
cat >> "$tmp" << EOF
|
|
2140
|
+
FEISHU_APP_ID=$app_id
|
|
2141
|
+
FEISHU_APP_SECRET=$app_secret
|
|
2142
|
+
FEISHU_DOMAIN=feishu
|
|
2143
|
+
FEISHU_CONNECTION_MODE=websocket
|
|
2144
|
+
FEISHU_ALLOWED_USERS=$allowed
|
|
2145
|
+
FEISHU_ALLOW_ALL_USERS=$ALLOW_ALL
|
|
2146
|
+
EOF
|
|
2147
|
+
mv "$tmp" "$env_path"
|
|
2148
|
+
log_ok "飞书凭据已写入 ${env_path}"
|
|
2149
|
+
|
|
2150
|
+
# 7e. gateway 服务安装
|
|
2151
|
+
echo ""
|
|
2152
|
+
log_info "安装 gateway 后台服务(hermes -p ${PROFILE_HOME##*/} gateway install)..."
|
|
2153
|
+
if hermes -p "${PROFILE_HOME##*/}" gateway install 2>&1 | tail -5; then
|
|
2154
|
+
log_ok "gateway 服务已安装"
|
|
2155
|
+
else
|
|
2156
|
+
log_warn "gateway install 失败,请手动执行: hermes -p ${PROFILE_HOME##*/} gateway install"
|
|
2157
|
+
fi
|
|
2158
|
+
|
|
2159
|
+
# 8f. 状态确认
|
|
2160
|
+
echo ""
|
|
2161
|
+
log_info "检查 gateway 状态 ..."
|
|
2162
|
+
hermes -p "${PROFILE_HOME##*/}" gateway status 2>&1 | head -10 || true
|
|
2163
|
+
echo ""
|
|
2164
|
+
echo " ──────────────────────────────────────────────"
|
|
2165
|
+
echo " ✅ 飞书对话接入配置完成!"
|
|
2166
|
+
echo " 验证:客户飞书私聊 bot 发送「货物状态查询」"
|
|
2167
|
+
echo " 应收到 Agent 回复(含技能加载)"
|
|
2168
|
+
echo " 日志:tail -f ${PROFILE_HOME}/logs/gateway.log"
|
|
2169
|
+
echo " ──────────────────────────────────────────────"
|
|
2170
|
+
return 0
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
# ── 主流程 ──────────────────────────────────────────────
|
|
2174
|
+
main() {
|
|
2175
|
+
echo ""
|
|
2176
|
+
echo "╔══════════════════════════════════════════════╗"
|
|
2177
|
+
echo "║ ML 卖家 — FULL 货件自动化初始化向导 ║"
|
|
2178
|
+
echo "║ Argent · 一次配置全部客户参数 ║"
|
|
2179
|
+
echo "╚══════════════════════════════════════════════╝"
|
|
2180
|
+
echo ""
|
|
2181
|
+
|
|
2182
|
+
# v0.6.1:旧版 profile scripts/ 客户数据迁移到 ~/.hermes/scripts/(update 不覆盖)
|
|
2183
|
+
migrate_legacy_data
|
|
2184
|
+
|
|
2185
|
+
# 老用户兼容:已有 fulfillment.env → 跳过重配直接校验
|
|
2186
|
+
if [ -f "$ENV_FILE" ] && [ "$FORCE_RECONFIG" = "0" ] && [ "$REFRESH_STORES" = "0" ]; then
|
|
2187
|
+
set -a; source "$ENV_FILE"; set +a
|
|
2188
|
+
if [ -n "${FEISHU_BASE_TOKEN:-}" ] && [ -n "${FEISHU_TABLE_ID:-}" ]; then
|
|
2189
|
+
echo " 检测到已有配置(${ENV_FILE})"
|
|
2190
|
+
read -r -p " 跳过重新配置,直接进入连通性校验? [Y/n] " skip
|
|
2191
|
+
case "$(echo "$skip" | tr '[:upper:]' '[:lower:]')" in
|
|
2192
|
+
""|y|yes)
|
|
2193
|
+
verify_all
|
|
2194
|
+
return $?
|
|
2195
|
+
;;
|
|
2196
|
+
esac
|
|
2197
|
+
FORCE_RECONFIG=1
|
|
2198
|
+
fi
|
|
2199
|
+
fi
|
|
2200
|
+
|
|
2201
|
+
check_env || return 1
|
|
2202
|
+
ensure_api_key || return 1
|
|
2203
|
+
|
|
2204
|
+
# 飞书凭据/推送配置(唯一配置点,幂等;跳过也不中止)
|
|
2205
|
+
feishu_cred_setup || return 1
|
|
2206
|
+
|
|
2207
|
+
# 飞书配置前置(3e/3f 需要表格 token);拒绝/超时登录 → 跳过飞书表格段(后续可补)
|
|
2208
|
+
if check_lark_login; then
|
|
2209
|
+
parse_table_url || return 1
|
|
2210
|
+
auto_feishu_user
|
|
2211
|
+
else
|
|
2212
|
+
log_warn "已跳过飞书表格配置(稍后手动 lark-cli auth login 后重跑向导可补全)"
|
|
2213
|
+
fi
|
|
2214
|
+
|
|
2215
|
+
init_stores || return 1
|
|
2216
|
+
probe_store_ports
|
|
2217
|
+
if [ -n "${FEISHU_BASE_TOKEN:-}" ] && [ -n "${FEISHU_TABLE_ID:-}" ]; then
|
|
2218
|
+
sync_feishu_field "$FEISHU_BASE_TOKEN" "$FEISHU_TABLE_ID" || return 1
|
|
2219
|
+
migrate_old_records "$FEISHU_BASE_TOKEN" "$FEISHU_TABLE_ID"
|
|
2220
|
+
verify_consistency "$FEISHU_BASE_TOKEN" "$FEISHU_TABLE_ID" || return 1
|
|
2221
|
+
else
|
|
2222
|
+
log_warn "未配置飞书表格,跳过店铺选项同步/一致性校验(登录后可重跑向导补全)"
|
|
2223
|
+
fi
|
|
2224
|
+
|
|
2225
|
+
setup_machine
|
|
2226
|
+
write_env
|
|
2227
|
+
setup_daily_report
|
|
2228
|
+
verify_all
|
|
2229
|
+
return $?
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
# ── --refresh-stores:只重跑 3a-3g ───────────────────────
|
|
2233
|
+
if [ "$REFRESH_STORES" = "1" ]; then
|
|
2234
|
+
echo ""
|
|
2235
|
+
echo "🔄 刷新店铺选项模式(--refresh-stores)"
|
|
2236
|
+
echo " 仅重新同步: 环境列表 → stores.json → CDP 端口 → 表格单选选项 → 旧记录迁移 → 一致性验证"
|
|
2237
|
+
echo ""
|
|
2238
|
+
migrate_legacy_data
|
|
2239
|
+
if [ -f "$ENV_FILE" ]; then
|
|
2240
|
+
set -a; source "$ENV_FILE"; set +a
|
|
2241
|
+
fi
|
|
2242
|
+
if [ -z "${FEISHU_BASE_TOKEN:-}" ] || [ -z "${FEISHU_TABLE_ID:-}" ]; then
|
|
2243
|
+
log_err "缺少表格配置($ENV_FILE 不存在或不完整),请先完整运行 init-ml.sh"
|
|
2244
|
+
exit 1
|
|
2245
|
+
fi
|
|
2246
|
+
check_env || exit 1
|
|
2247
|
+
init_stores || exit 1
|
|
2248
|
+
probe_store_ports
|
|
2249
|
+
sync_feishu_field "$FEISHU_BASE_TOKEN" "$FEISHU_TABLE_ID" || exit 1
|
|
2250
|
+
migrate_old_records "$FEISHU_BASE_TOKEN" "$FEISHU_TABLE_ID"
|
|
2251
|
+
verify_consistency "$FEISHU_BASE_TOKEN" "$FEISHU_TABLE_ID" || exit 1
|
|
2252
|
+
log_ok "店铺选项刷新完成"
|
|
2253
|
+
exit 0
|
|
2254
|
+
fi
|
|
2255
|
+
|
|
2256
|
+
# ── --verify:全链一致性校验(只读,独立子命令)──────────
|
|
2257
|
+
if [ "$VERIFY_ONLY" = "1" ]; then
|
|
2258
|
+
verify_chain
|
|
2259
|
+
exit $?
|
|
2260
|
+
fi
|
|
2261
|
+
|
|
2262
|
+
# ── --daily-report:仅配置日报推送(OAuth 授权 + cron)───
|
|
2263
|
+
if [ "$DAILY_REPORT_ONLY" = "1" ]; then
|
|
2264
|
+
migrate_legacy_data
|
|
2265
|
+
setup_daily_report
|
|
2266
|
+
exit $?
|
|
2267
|
+
fi
|
|
2268
|
+
|
|
2269
|
+
# ── --feishu-gateway:仅配置飞书对话接入 ─────────────────
|
|
2270
|
+
if [ "$FEISHU_GATEWAY_ONLY" = "1" ]; then
|
|
2271
|
+
feishu_gateway_setup
|
|
2272
|
+
exit $?
|
|
2273
|
+
fi
|
|
2274
|
+
|
|
2275
|
+
main "$@"
|