chore(config): 工作流配置、清单与文档同步
workflows/schedules/commands/notification-routing 与 source-manifests 四模块 清单同步登录统一化与新工作流(product.hk_anpei_public_inventory.daily 等); docs 运维手册、控制台与耗时分布文档、CI 关键脚本清单、AGENTS 指南更新; 测试与模块注册对齐。
This commit is contained in:
1 parent
90282ae766
commit
dc8be88e60
27 files changed
+1094
-224
No files matched your search
+1
-1
@@ -19,7 +19,7 @@ lint:
|
|||||||
stage: quality
|
stage: quality
|
||||||
script:
|
script:
|
||||||
- uv run ruff check src tests
|
- uv run ruff check src tests
|
||||||
- uv run ruff check src/gyxx_flow/modules/content_marketing/daily_creator_exposure_scope.py src/gyxx_flow/modules/content_marketing/chanmama_export.py src/gyxx_flow/modules/content_marketing/self_operated_accounts.py src/gyxx_flow/modules/content_marketing/self_xhs_scraper.py src/gyxx_flow/modules/content_marketing/data/tools/sync_self_video_metrics.py src/gyxx_flow/modules/product_commerce/market_rank_limits.py src/gyxx_flow/modules/product_commerce/market_rank_product_import.py src/gyxx_flow/modules/shop_intelligence/collectors/dy_after_sales_quality.py src/gyxx_flow/modules/shop_intelligence/collectors/tmall_negative_reviews.py
|
- uv run ruff check src/gyxx_flow/modules/content_marketing/daily_creator_exposure_scope.py src/gyxx_flow/modules/content_marketing/chanmama_export.py src/gyxx_flow/modules/content_marketing/self_operated_accounts.py src/gyxx_flow/modules/content_marketing/self_xhs_scraper.py src/gyxx_flow/modules/content_marketing/data/tools/sync_self_video_metrics.py src/gyxx_flow/modules/product_commerce/market_rank_limits.py src/gyxx_flow/modules/product_commerce/market_rank_product_import.py src/gyxx_flow/modules/product_commerce/collect_douyin_qianchuan_ads.py src/gyxx_flow/modules/shop_intelligence/collectors/dy_after_sales_quality.py src/gyxx_flow/modules/shop_intelligence/collectors/tmall_negative_reviews.py
|
||||||
|
|
||||||
test:
|
test:
|
||||||
stage: test
|
stage: test
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ uv run gyxx accounts login|sync [account_id]
|
|||||||
- Manual/backfill/maintenance commands must be declared in `config/commands.json`; never reintroduce recursive executable-script discovery.
|
- Manual/backfill/maintenance commands must be declared in `config/commands.json`; never reintroduce recursive executable-script discovery.
|
||||||
- Each executable `workflow_id` compiles to a LangGraph `StateGraph`. Preserve the public contracts of the workflow model, `RunJournal`, `LockManager`, shadow comparison, and `EffectLedger` when changing graph execution.
|
- Each executable `workflow_id` compiles to a LangGraph `StateGraph`. Preserve the public contracts of the workflow model, `RunJournal`, `LockManager`, shadow comparison, and `EffectLedger` when changing graph execution.
|
||||||
- External systems: PostgreSQL via runtime-injected cloud DSN (`GYXX_POSTGRES_DSN`; no address/credential defaults in source, loopback rejected in cloud mode); Hermes loopback-only roles on `127.0.0.1:8642/8643` (key `GYXX_HERMES_API_KEY`); Feishu keeps its existing identity. Every browser binding keeps a unique CDP port plus isolated Profile/Cookie/storage-state; scripts sharing an account declare it under `accounts` in `runtime-bindings.json` — the vault `state/accounts/<id>/` is the single login authority (`gyxx accounts login/sync`) and its cookies merge read-only into member scripts while profiles stay isolated.
|
- External systems: PostgreSQL via runtime-injected cloud DSN (`GYXX_POSTGRES_DSN`; no address/credential defaults in source, loopback rejected in cloud mode); Hermes loopback-only roles on `127.0.0.1:8642/8643` (key `GYXX_HERMES_API_KEY`); Feishu keeps its existing identity. Every browser binding keeps a unique CDP port plus isolated Profile/Cookie/storage-state; scripts sharing an account declare it under `accounts` in `runtime-bindings.json` — the vault `state/accounts/<id>/` is the single login authority (`gyxx accounts login/sync`) and its cookies merge read-only into member scripts while profiles stay isolated.
|
||||||
|
- 登录统一管理:正式执行的工作流由 `src/gyxx_flow/cookie_gate.py` 在采集步骤前注入每账号 `cookie-gate.<id>` 步骤(blocking 账号失效→阻断并飞书通知人工重登;`login_command_id` 账号先自动账密恢复;`credential_env_names` 账号 defer 给脚本内账密登录)。阻断记录 `pending_reruns`,由调度器维护循环里的 `CookieGateRecoveryManager` 在登录态恢复后按原业务日期自动重跑并发飞书确认。采集脚本不得内嵌扫码/验证码等待——失效时抛 `CookieExpiredError`(`adapters/browser.py`)退出 75;编排器工作流在 `workflows.json` 用 `required_accounts` 声明账号依赖。不要重新引入 relogin_*/批量补采里的扫码重登子进程。
|
||||||
- 启动或重启工作流控制台(`gyxx console`)必须携带 `--env-file D:\product-collector-analyze-flow\.env`(与调度器进程一致),否则 `PG_HOST/PG_PORT/PG_DB/PG_USER/PG_PASSWORD` 等云端 PostgreSQL 凭据和账号配置不会注入,「商品经营日报采集」「聚水潭全店铺款式日报」等正式执行会报“缺少 PostgreSQL 运行时凭据”。参考启动脚本:`var/tmp/start-console.ps1`。
|
- 启动或重启工作流控制台(`gyxx console`)必须携带 `--env-file D:\product-collector-analyze-flow\.env`(与调度器进程一致),否则 `PG_HOST/PG_PORT/PG_DB/PG_USER/PG_PASSWORD` 等云端 PostgreSQL 凭据和账号配置不会注入,「商品经营日报采集」「聚水潭全店铺款式日报」等正式执行会报“缺少 PostgreSQL 运行时凭据”。参考启动脚本:`var/tmp/start-console.ps1`。
|
||||||
- Source sync (`gyxx sources status/apply`): manifest-driven three-way hash drift check; only untransformed, conflict-free one-sided changes may be auto-copied, and only with `--execute`. Credentials, cookies, logs, and collected data never enter sync manifests.
|
- Source sync (`gyxx sources status/apply`): manifest-driven three-way hash drift check; only untransformed, conflict-free one-sided changes may be auto-copied, and only with `--execute`. Credentials, cookies, logs, and collected data never enter sync manifests.
|
||||||
|
|
||||||
|
|||||||
@@ -50,14 +50,7 @@
|
|||||||
"kind": "python",
|
"kind": "python",
|
||||||
"notification_workflow_id": "content.marketing_report.daily"
|
"notification_workflow_id": "content.marketing_report.daily"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "content.login.refresh",
|
|
||||||
"module": "content_marketing",
|
|
||||||
"entry": "data/tools/friday_relogin_parallel.py",
|
|
||||||
"kind": "python",
|
|
||||||
"notification_workflow_id": "content.relogin.weekly"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "content.creator_report.generate",
|
"id": "content.creator_report.generate",
|
||||||
"module": "content_marketing",
|
"module": "content_marketing",
|
||||||
"entry": "data/tools/generate_creator_report.py",
|
"entry": "data/tools/generate_creator_report.py",
|
||||||
@@ -472,6 +465,12 @@
|
|||||||
"{business_date}"
|
"{business_date}"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "product.hk_anpei_public_inventory.refresh",
|
||||||
|
"module": "product_commerce",
|
||||||
|
"entry": "public_sku_inventory_sheet.py",
|
||||||
|
"kind": "python"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "product.backfill.run",
|
"id": "product.backfill.run",
|
||||||
"module": "product_commerce",
|
"module": "product_commerce",
|
||||||
|
|||||||
@@ -222,9 +222,6 @@
|
|||||||
"content.marketing_report.daily": {
|
"content.marketing_report.daily": {
|
||||||
"condition": "正式执行且启用发送时投递最终营销日报。"
|
"condition": "正式执行且启用发送时投递最终营销日报。"
|
||||||
},
|
},
|
||||||
"content.relogin.weekly": {
|
|
||||||
"condition": "刷新期间投递各平台扫码二维码,并在平台失败时投递失败汇总。"
|
|
||||||
},
|
|
||||||
"product.alert.daily": {
|
"product.alert.daily": {
|
||||||
"condition": "命中销量下滑规则且尚未重复通知时投递告警。"
|
"condition": "命中销量下滑规则且尚未重复通知时投递告警。"
|
||||||
},
|
},
|
||||||
|
|||||||
+9
-14
@@ -11,10 +11,7 @@
|
|||||||
"workflow_id": "content.metrics.daily"
|
"workflow_id": "content.metrics.daily"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"at": [
|
"at": "06:00",
|
||||||
"01:00",
|
|
||||||
"05:00"
|
|
||||||
],
|
|
||||||
"business_date_offset_days": 0,
|
"business_date_offset_days": 0,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"kind": "daily",
|
"kind": "daily",
|
||||||
@@ -42,16 +39,7 @@
|
|||||||
"kind": "daily",
|
"kind": "daily",
|
||||||
"workflow_id": "content.marketing_report.daily"
|
"workflow_id": "content.marketing_report.daily"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"at": "10:00",
|
|
||||||
"days": [
|
|
||||||
"Friday"
|
|
||||||
],
|
|
||||||
"enabled": false,
|
|
||||||
"kind": "weekly",
|
|
||||||
"workflow_id": "content.relogin.weekly"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"at": "12:00",
|
"at": "12:00",
|
||||||
"days": [
|
"days": [
|
||||||
"Sunday"
|
"Sunday"
|
||||||
@@ -221,6 +209,13 @@
|
|||||||
"kind": "daily",
|
"kind": "daily",
|
||||||
"workflow_id": "product.erp_all_shop_daily"
|
"workflow_id": "product.erp_all_shop_daily"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"at": "09:00",
|
||||||
|
"business_date_offset_days": 0,
|
||||||
|
"enabled": true,
|
||||||
|
"kind": "daily",
|
||||||
|
"workflow_id": "product.hk_anpei_public_inventory.daily"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"at": "12:00",
|
"at": "12:00",
|
||||||
"business_date_offset_days": 0,
|
"business_date_offset_days": 0,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"source_relative_path": "bilibili_comment_scraper.py",
|
"source_relative_path": "bilibili_comment_scraper.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/bilibili_comment_scraper.py",
|
"target_relative_path": "src/gyxx_flow/modules/content_marketing/bilibili_comment_scraper.py",
|
||||||
"source_sha256": "c7f116f30889125c668e7f4dd796e7249ba7ea8621d5ab5fb518028f56c7f8dc",
|
"source_sha256": "c7f116f30889125c668e7f4dd796e7249ba7ea8621d5ab5fb518028f56c7f8dc",
|
||||||
"target_sha256": "109a01a6dc2b79261cff68ff42818a7561f3319b6be3788d9300bb5ce0217a96",
|
"target_sha256": "d3ae63aab128a3e4078cf357aa4dfcf1f2c3b36c3116989d8f2252be62544e53",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"category": "source"
|
"category": "source"
|
||||||
},
|
},
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"source_relative_path": "chanmama_scraper.py",
|
"source_relative_path": "chanmama_scraper.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/chanmama_scraper.py",
|
"target_relative_path": "src/gyxx_flow/modules/content_marketing/chanmama_scraper.py",
|
||||||
"source_sha256": "2edc26879b57d73ec5992c80e5ef74d76a509f09bff7284b1ae17e90df543e07",
|
"source_sha256": "2edc26879b57d73ec5992c80e5ef74d76a509f09bff7284b1ae17e90df543e07",
|
||||||
"target_sha256": "8b87268e0f105cf4d349b192c7b2c24d5e318a9d88fcd14264824b083103b24e",
|
"target_sha256": "b808b412e9bb8d824c1a117cbecdd28234408a55fa60891f1ae748abeb7bc415",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"category": "source"
|
"category": "source"
|
||||||
},
|
},
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
"source_relative_path": "data/tools/batch_rescrape_bilibili.py",
|
"source_relative_path": "data/tools/batch_rescrape_bilibili.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/batch_rescrape_bilibili.py",
|
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/batch_rescrape_bilibili.py",
|
||||||
"source_sha256": "5b07fe27f1b76714ca3f3feae34e3d719277ed0cfe124d76cacd8e3c904b1fca",
|
"source_sha256": "5b07fe27f1b76714ca3f3feae34e3d719277ed0cfe124d76cacd8e3c904b1fca",
|
||||||
"target_sha256": "10e5dee90c591201daa7a7b0eb13a40ed1c97d7d51b1b93ef8f1d5d4b8e09580",
|
"target_sha256": "8fa0e5838615f36c9841deb08f6710693e63b011f1f91138c1121d99277dd391",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"category": "tool"
|
"category": "tool"
|
||||||
},
|
},
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
"source_relative_path": "data/tools/batch_rescrape_douyin.py",
|
"source_relative_path": "data/tools/batch_rescrape_douyin.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/batch_rescrape_douyin.py",
|
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/batch_rescrape_douyin.py",
|
||||||
"source_sha256": "15b1438513f8216a6c7a103744e8a1ca6bc63b32c111d7c9bce31c18be9e84a6",
|
"source_sha256": "15b1438513f8216a6c7a103744e8a1ca6bc63b32c111d7c9bce31c18be9e84a6",
|
||||||
"target_sha256": "3233a7840e07a87535a8df065562a7cb681dbc140e002d82970598de3d9768cf",
|
"target_sha256": "e0f1227a7b66d85ea65191b5f9c6c5bf588e85cc0cd66e20afae7a9dc7a2684e",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"category": "tool"
|
"category": "tool"
|
||||||
},
|
},
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
"source_relative_path": "data/tools/batch_rescrape_xiaohongshu.py",
|
"source_relative_path": "data/tools/batch_rescrape_xiaohongshu.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/batch_rescrape_xiaohongshu.py",
|
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/batch_rescrape_xiaohongshu.py",
|
||||||
"source_sha256": "6d85f7dfd52c7a92b951ac29e7ee3377aebdb743af2a362d1f38366188dcad08",
|
"source_sha256": "6d85f7dfd52c7a92b951ac29e7ee3377aebdb743af2a362d1f38366188dcad08",
|
||||||
"target_sha256": "01be5d1ba260ccd46e01b9e72b6f7dd3d08577615455fc9010de4efe8a710aea",
|
"target_sha256": "c481bd6af34040388abafed0466d5632b3892865bf192d245664627003a4c43f",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"category": "tool"
|
"category": "tool"
|
||||||
},
|
},
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
"source_relative_path": "data/tools/db.py",
|
"source_relative_path": "data/tools/db.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/db.py",
|
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/db.py",
|
||||||
"source_sha256": "83f158a78841244c1f02255d1500018b5a9dbe1f282cbdb937acc344b08abc25",
|
"source_sha256": "83f158a78841244c1f02255d1500018b5a9dbe1f282cbdb937acc344b08abc25",
|
||||||
"target_sha256": "62ffad854cc2bc3a3ae4f2a28b4ee345bc9eecf72101b647c3aa165c691b456a",
|
"target_sha256": "bfdb761761532263ae6c555ee455cad4efa300b7fa5639931813f4f95e79ff6a",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"category": "tool"
|
"category": "tool"
|
||||||
},
|
},
|
||||||
@@ -203,14 +203,6 @@
|
|||||||
"transformed": true,
|
"transformed": true,
|
||||||
"category": "tool"
|
"category": "tool"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"source_relative_path": "data/tools/friday_relogin_parallel.py",
|
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/friday_relogin_parallel.py",
|
|
||||||
"source_sha256": "affed4ab6e5e179c85dc92999502b98e007c71243ed26977b3c5eeba23b9e3dc",
|
|
||||||
"target_sha256": "438bedfb874c7eb566e1998815a22ca72b7d177cee221fd39625a96872bd450b",
|
|
||||||
"transformed": true,
|
|
||||||
"category": "tool"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"source_relative_path": "data/tools/generate_creator_report.py",
|
"source_relative_path": "data/tools/generate_creator_report.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/generate_creator_report.py",
|
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/generate_creator_report.py",
|
||||||
@@ -291,54 +283,6 @@
|
|||||||
"transformed": true,
|
"transformed": true,
|
||||||
"category": "tool"
|
"category": "tool"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"source_relative_path": "data/tools/relogin_bilibili.py",
|
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/relogin_bilibili.py",
|
|
||||||
"source_sha256": "b8dc12539bcd6f1a368d50ead1f5d33d6577a75c92f0b7bafa75a67edd7a73fa",
|
|
||||||
"target_sha256": "d47f26f62fbe3262128327b916151385e527125a18f27f7455b8382aa5a77ecb",
|
|
||||||
"transformed": true,
|
|
||||||
"category": "tool"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source_relative_path": "data/tools/relogin_douyin.py",
|
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/relogin_douyin.py",
|
|
||||||
"source_sha256": "3c2017f8a801528ff17e4c4bae24615256b8788c185f9a797fe87470e35b1645",
|
|
||||||
"target_sha256": "82a0ecf4101f2eaded94cc4cd9054cb08e1e27519a58cfc0d3126888cc1ce5d4",
|
|
||||||
"transformed": true,
|
|
||||||
"category": "tool"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source_relative_path": "data/tools/relogin_pgy.py",
|
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/relogin_pgy.py",
|
|
||||||
"source_sha256": "759c6a68a0851b4c87d8febfc5d94925831a3b2d7cdf1e6a6e74dcd52b83ae3f",
|
|
||||||
"target_sha256": "d75697ae2b6ac1f23bb79077a4df0af33113813a46525a6f3980b04391857cac",
|
|
||||||
"transformed": true,
|
|
||||||
"category": "tool"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source_relative_path": "data/tools/relogin_transaction.py",
|
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/relogin_transaction.py",
|
|
||||||
"source_sha256": "57ec20de8d99290268ed3d1fc2b66bf3faeaca807d7f3744dd51de6cc93628b2",
|
|
||||||
"target_sha256": "57ec20de8d99290268ed3d1fc2b66bf3faeaca807d7f3744dd51de6cc93628b2",
|
|
||||||
"transformed": false,
|
|
||||||
"category": "tool"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source_relative_path": "data/tools/relogin_xiaohongshu.py",
|
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/relogin_xiaohongshu.py",
|
|
||||||
"source_sha256": "fcdea30adecb066c4f3699ded457df0bc763d978248276ddbffa9f18bc9bdf2c",
|
|
||||||
"target_sha256": "e700dd57e5cf87d68529bcd6c062e73d6bd9f1e6ef06ef3122e2b16c288aaf1a",
|
|
||||||
"transformed": true,
|
|
||||||
"category": "tool"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source_relative_path": "data/tools/relogin_xingtu.py",
|
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/relogin_xingtu.py",
|
|
||||||
"source_sha256": "c9d434cb02a57ebd7b884598563077d733d56e45dad59ed30774eb7e1c668cb0",
|
|
||||||
"target_sha256": "06ec1088677d69511057008acb2b05cca2da015a8219aee10bf19bbae3c25d79",
|
|
||||||
"transformed": true,
|
|
||||||
"category": "tool"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"source_relative_path": "data/tools/retry_failed.py",
|
"source_relative_path": "data/tools/retry_failed.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/retry_failed.py",
|
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/retry_failed.py",
|
||||||
@@ -351,7 +295,7 @@
|
|||||||
"source_relative_path": "data/tools/schema_gyxx_super_data.sql",
|
"source_relative_path": "data/tools/schema_gyxx_super_data.sql",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/schema_gyxx_super_data.sql",
|
"target_relative_path": "src/gyxx_flow/modules/content_marketing/data/tools/schema_gyxx_super_data.sql",
|
||||||
"source_sha256": "43d202d7d0452b7d0671ee9c40149c21328db4b2863fa4d030af456f89defe3c",
|
"source_sha256": "43d202d7d0452b7d0671ee9c40149c21328db4b2863fa4d030af456f89defe3c",
|
||||||
"target_sha256": "001870851dea9c373a77b87335604c4363b06c3eb0ed7c97b4a8d5e891e56758",
|
"target_sha256": "4b2eca918bb81de0cac529b786d0d37ce04af2c1bc4be81cd20c8ba9456b1348",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"category": "sql"
|
"category": "sql"
|
||||||
},
|
},
|
||||||
@@ -415,7 +359,7 @@
|
|||||||
"source_relative_path": "douyin_comment_scraper.py",
|
"source_relative_path": "douyin_comment_scraper.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/douyin_comment_scraper.py",
|
"target_relative_path": "src/gyxx_flow/modules/content_marketing/douyin_comment_scraper.py",
|
||||||
"source_sha256": "ec785ead9c95c5de65c00b06212b1f03d0159fed475a95a2b593d3b24e486e43",
|
"source_sha256": "ec785ead9c95c5de65c00b06212b1f03d0159fed475a95a2b593d3b24e486e43",
|
||||||
"target_sha256": "6e7ac4357fe466656b93ca7bae6c49adc4d59fa75974a39c6885c7433bd4d402",
|
"target_sha256": "35d3caf8bc85a82ca15c3fc9ff317c21460173996106ffee8f1d3b5f1d13b801",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"category": "source"
|
"category": "source"
|
||||||
},
|
},
|
||||||
@@ -455,7 +399,7 @@
|
|||||||
"source_relative_path": "pgy_xhs_scraper_v2.py",
|
"source_relative_path": "pgy_xhs_scraper_v2.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/pgy_xhs_scraper_v2.py",
|
"target_relative_path": "src/gyxx_flow/modules/content_marketing/pgy_xhs_scraper_v2.py",
|
||||||
"source_sha256": "2be78ef76df38e80da2099a76e6e3fd9dcdc2e8875afb44279f54003ccb5f563",
|
"source_sha256": "2be78ef76df38e80da2099a76e6e3fd9dcdc2e8875afb44279f54003ccb5f563",
|
||||||
"target_sha256": "e5df4b34f67a565ec5d5ce4dd5857e80e363dd7559e7e29006dcd3e421276c8a",
|
"target_sha256": "472c13700adc98144b53f2921bbdde15ebb7dc138eaab76885b514dadd306887",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"category": "source"
|
"category": "source"
|
||||||
},
|
},
|
||||||
@@ -479,7 +423,7 @@
|
|||||||
"source_relative_path": "self_bilibili_scraper.py",
|
"source_relative_path": "self_bilibili_scraper.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/self_bilibili_scraper.py",
|
"target_relative_path": "src/gyxx_flow/modules/content_marketing/self_bilibili_scraper.py",
|
||||||
"source_sha256": "2ef650a16dde61697101bb219a8c3adcd682fec1529bb9c5312dc5b384d83a72",
|
"source_sha256": "2ef650a16dde61697101bb219a8c3adcd682fec1529bb9c5312dc5b384d83a72",
|
||||||
"target_sha256": "6ec5ed6991a93f806fb74d0870e953e5a05d52043a83f2fa261e2ec67bee6ad7",
|
"target_sha256": "1c09171d1269a3a38a5e6f9f99b98d0da4c7ff11d075613fc288ee209e82e9d2",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"category": "source"
|
"category": "source"
|
||||||
},
|
},
|
||||||
@@ -487,7 +431,7 @@
|
|||||||
"source_relative_path": "self_douyin_scraper.py",
|
"source_relative_path": "self_douyin_scraper.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/self_douyin_scraper.py",
|
"target_relative_path": "src/gyxx_flow/modules/content_marketing/self_douyin_scraper.py",
|
||||||
"source_sha256": "4aba1264dee052521e5712d7219916e534db5705e3cec75caaa1b025a4bc2506",
|
"source_sha256": "4aba1264dee052521e5712d7219916e534db5705e3cec75caaa1b025a4bc2506",
|
||||||
"target_sha256": "62307df115c8d7336b470baba074163ab3f3468bae8b0af266af370e2e42d9f1",
|
"target_sha256": "945659cd80435bab08bfe1dee13c451c8c4b5fe362313ea09591525c551d3c48",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"category": "source"
|
"category": "source"
|
||||||
},
|
},
|
||||||
@@ -511,7 +455,7 @@
|
|||||||
"source_relative_path": "xiaohongshu_comment_scraper.py",
|
"source_relative_path": "xiaohongshu_comment_scraper.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/xiaohongshu_comment_scraper.py",
|
"target_relative_path": "src/gyxx_flow/modules/content_marketing/xiaohongshu_comment_scraper.py",
|
||||||
"source_sha256": "58bc07bb2fd67897e18a9981f931df8fadf24a6cc5f09d55bd3aaa2e3c6756a4",
|
"source_sha256": "58bc07bb2fd67897e18a9981f931df8fadf24a6cc5f09d55bd3aaa2e3c6756a4",
|
||||||
"target_sha256": "afca1104324546da2315fdf7a427c3f34021eef31ddc2fea76877f818718ccb0",
|
"target_sha256": "fff780d9e16717e6a1ba9c5ee4db22cb1bb5a85f75807c811072c20a6bd390de",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"category": "source"
|
"category": "source"
|
||||||
},
|
},
|
||||||
@@ -527,7 +471,7 @@
|
|||||||
"source_relative_path": "xingtu_scraper_v2.py",
|
"source_relative_path": "xingtu_scraper_v2.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/content_marketing/xingtu_scraper_v2.py",
|
"target_relative_path": "src/gyxx_flow/modules/content_marketing/xingtu_scraper_v2.py",
|
||||||
"source_sha256": "0d4eb87779064e13b8d78daa6dba13698f1149a26b3a1de2e064c117a29cf49a",
|
"source_sha256": "0d4eb87779064e13b8d78daa6dba13698f1149a26b3a1de2e064c117a29cf49a",
|
||||||
"target_sha256": "b36339444e26048a90c8ac999a0a628bcddd4f64842924f6c60e600c7a3bec37",
|
"target_sha256": "dfdd07af20c8e2e59411fb6e32a82588db5601d6f3fba75d596e0b5ac19b2be8",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"category": "source"
|
"category": "source"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "cff768f79672cfc7db7f6866caf5ab59936b8c51eb2a7d729b81441940124512",
|
"source_sha256": "cff768f79672cfc7db7f6866caf5ab59936b8c51eb2a7d729b81441940124512",
|
||||||
"target_sha256": "d56fe8a934c1ae20a9249c7682823238a705f51f451241504512e83a8800a879",
|
"target_sha256": "c1c0167b96c818a4e7d44aa5c860b5510c9925cb096b7fb4510b8319b91592fc",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "analyze_style_with_hermes.py",
|
"source_relative_path": "analyze_style_with_hermes.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/analyze_style.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/analyze_style.py"
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "1493dbbfb3d864812df378d04d006e7de0f9ec4b6ab5812e5c6970c304c2dfe6",
|
"source_sha256": "1493dbbfb3d864812df378d04d006e7de0f9ec4b6ab5812e5c6970c304c2dfe6",
|
||||||
"target_sha256": "a1d526dfc27b110a8845a98cc66fe7e5cfb87ef70530728477af31a88734a576",
|
"target_sha256": "2b6e6adfea82b13dd8c819138a428a78b5802f204c199c58ed4e59f0158d1780",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "collect_dy_market_rank.py",
|
"source_relative_path": "collect_dy_market_rank.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/collect_dy_market_rank.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/collect_dy_market_rank.py"
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "abd4c82cbd6c41e111e213c174d2fdeba1214568496eaf1dede24bf374b3b052",
|
"source_sha256": "abd4c82cbd6c41e111e213c174d2fdeba1214568496eaf1dede24bf374b3b052",
|
||||||
"target_sha256": "9de3ab22d5a4dbf574b2d4fdbbed295bb4645bb4943f4c066fcbfd41b44b521c",
|
"target_sha256": "eedf83f1d85662dc2e6d968af29070be819c7caac25057c74d6cb66b75f52216",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "collect_dy_persona_to_bitable.py",
|
"source_relative_path": "collect_dy_persona_to_bitable.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/collect_dy_persona_to_bitable.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/collect_dy_persona_to_bitable.py"
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "c7b54d941ab7f056e2d0ea4355543ff4a5c34ad986c8ed32145e988ac9501ea2",
|
"source_sha256": "c7b54d941ab7f056e2d0ea4355543ff4a5c34ad986c8ed32145e988ac9501ea2",
|
||||||
"target_sha256": "aaba5dfe6ce5133b5c662232160bb9faef4d73b36902a484a56b89fa7df05311",
|
"target_sha256": "8576678688d7d90f6f7285110de005982bc1ba82bc7e9977224ba1bdb4101283",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "collect_erp_yesterday_metrics.py",
|
"source_relative_path": "collect_erp_yesterday_metrics.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/collect_erp_yesterday_metrics.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/collect_erp_yesterday_metrics.py"
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "8bb1f17f90a7b9c2db5dfb992cf746329892d19c00ae2ae988e2109c049598b2",
|
"source_sha256": "8bb1f17f90a7b9c2db5dfb992cf746329892d19c00ae2ae988e2109c049598b2",
|
||||||
"target_sha256": "c98abe563b223f49cc3c810054d997504d9465387da4805389b0f5ef78ceed6a",
|
"target_sha256": "9b24259854a440d5a13fa2bc397887ab86cf7d4a573b10d3bda6988ed37d0ed6",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "collect_jd_market_rank.py",
|
"source_relative_path": "collect_jd_market_rank.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/collect_jd_market_rank.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/collect_jd_market_rank.py"
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "f9a48c5f89b18472dc3993e340ad858bfecb2fcf59427c7e70db37512c17a6f7",
|
"source_sha256": "f9a48c5f89b18472dc3993e340ad858bfecb2fcf59427c7e70db37512c17a6f7",
|
||||||
"target_sha256": "a268df592f7136a68c2b82d031115055e43cdfaedcdd4a5f4afa9ef45879e055",
|
"target_sha256": "01798bd6110afc84e5a6aab50255deeb0c5980f77895fcaf605ccb93d24656a5",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "collect_jd_persona_to_bitable.py",
|
"source_relative_path": "collect_jd_persona_to_bitable.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/collect_jd_persona_to_bitable.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/collect_jd_persona_to_bitable.py"
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "13b6e68b4872b2e79c1de8ca1617cc4f1249f4ed32f234ad6efb7a04f5948612",
|
"source_sha256": "13b6e68b4872b2e79c1de8ca1617cc4f1249f4ed32f234ad6efb7a04f5948612",
|
||||||
"target_sha256": "a1b95cc4f090a25c8e7991b0ef015a7df612fc78249dafe75aeef9fa4dbefe08",
|
"target_sha256": "35b5838736306d58758dca870deb7a296a21a2cbc7bb0c4b84d7f233095bee22",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "collect_sycm_market_rank.py",
|
"source_relative_path": "collect_sycm_market_rank.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/collect_sycm_market_rank.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/collect_sycm_market_rank.py"
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "a8900b9f25db6cf2bef3117a1d2cd86283054fea6f5a8590fd91b5cfbbcaf87e",
|
"source_sha256": "a8900b9f25db6cf2bef3117a1d2cd86283054fea6f5a8590fd91b5cfbbcaf87e",
|
||||||
"target_sha256": "030d33c3f5c0c230bb67d603dd657f40cceff8581f8756a1f67340a89b5ae4b3",
|
"target_sha256": "47cdfd9ebf27e31be06497b2f74c80da4ffced259a873730eb024b0db5eba740",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "db/__init__.py",
|
"source_relative_path": "db/__init__.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/db/__init__.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/db/__init__.py"
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "78c74c0e52e3651b3b042a2095e596ab250e60309c7ae2de905850822aad4922",
|
"source_sha256": "78c74c0e52e3651b3b042a2095e596ab250e60309c7ae2de905850822aad4922",
|
||||||
"target_sha256": "09d7e08d2eec57055a3bc7949e49c32a8bf62713f2594ae5d9297c032bf212c8",
|
"target_sha256": "1e6ed0029c9d1f701ae1d80b670fdb465cdbfd13b9f63d0a3179c19585215246",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "dy_audience_profile_collect.py",
|
"source_relative_path": "dy_audience_profile_collect.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/dy_audience_profile_collect.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/dy_audience_profile_collect.py"
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "492916088fa5e8c170461cc9eba97a88ed1cc1e50c7b99e5f6a5e49167fea157",
|
"source_sha256": "492916088fa5e8c170461cc9eba97a88ed1cc1e50c7b99e5f6a5e49167fea157",
|
||||||
"target_sha256": "9dd0dc3e6f048683157fe8b8deb533ed88970bf34e9824e439bf3a5d52d80e0a",
|
"target_sha256": "7b0ae5d648245e89f4aacf262250009de812d3dbb8ee55386f3704cabca48799",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "dy_product_scraping.py",
|
"source_relative_path": "dy_product_scraping.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/dy_product_scraping.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/dy_product_scraping.py"
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "906278aa6fb3578d5dab231bdccc2f5a7c6ec8c85810a3e026fff3c7ba16dc90",
|
"source_sha256": "906278aa6fb3578d5dab231bdccc2f5a7c6ec8c85810a3e026fff3c7ba16dc90",
|
||||||
"target_sha256": "d9e4281c21a42841023c0a723aff4a00af1f9200c94e42add9f8965159d4cfb5",
|
"target_sha256": "c9ac5b78ce367eb0b87697255a88400d1e2e83416ee1fc105110d3abc3dd8245",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "import_product_daily.py",
|
"source_relative_path": "import_product_daily.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/import_product_daily.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/import_product_daily.py"
|
||||||
@@ -230,7 +230,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "e0f016bafb3f527f1cae46eaf3ef31510184128aaaa7f52024a7c3f978eec42b",
|
"source_sha256": "e0f016bafb3f527f1cae46eaf3ef31510184128aaaa7f52024a7c3f978eec42b",
|
||||||
"target_sha256": "f2692442a1e62a4b20dbb442eb3e27a3075ca6095ba2578c90193f0db3a1810e",
|
"target_sha256": "b157f24430170d5a991bb2283af0a747b26dd11c0f0dec37289774f115132fee",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "jd_main_image_collector.py",
|
"source_relative_path": "jd_main_image_collector.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/jd_main_image_collector.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/jd_main_image_collector.py"
|
||||||
@@ -238,7 +238,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "d99ce71a008128549ac83fc3da52d88540b74538e3fe5c85d47c3b985e414353",
|
"source_sha256": "d99ce71a008128549ac83fc3da52d88540b74538e3fe5c85d47c3b985e414353",
|
||||||
"target_sha256": "f821a0221b832bea0123bf4a8f85fd0785eba9206c8a632ad48065ea3c30c095",
|
"target_sha256": "cfe98b322e4ef7bd875325c2a9ffedeabb364e98b2983773dc98f9e0e75a6581",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "jd_product_data_collector.py",
|
"source_relative_path": "jd_product_data_collector.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/jd_product_data_collector.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/jd_product_data_collector.py"
|
||||||
@@ -294,7 +294,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "910e720fab4f47419ad56055f00a8c3fbe3a06c2577d81e35d3d38178a588a45",
|
"source_sha256": "910e720fab4f47419ad56055f00a8c3fbe3a06c2577d81e35d3d38178a588a45",
|
||||||
"target_sha256": "c9f8cfc8dff6a7e5617b9b357ed92872f94371f93360318a3ff78f7ec38d75df",
|
"target_sha256": "e515cdc287b39a315dec44b4fb1c17f98b2af13bf82715731f92919d45f87854",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "market_rank_product_import.py",
|
"source_relative_path": "market_rank_product_import.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/market_rank_product_import.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/market_rank_product_import.py"
|
||||||
@@ -358,7 +358,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "c9a51f1d1267b398cf260a86e65a8ee49de873d6c95bf9c90db2abb422005cf7",
|
"source_sha256": "c9a51f1d1267b398cf260a86e65a8ee49de873d6c95bf9c90db2abb422005cf7",
|
||||||
"target_sha256": "6cd70bb5406bc00257f9070760f980580e95c3c4e4326a67d33d30433dd74c32",
|
"target_sha256": "54c7b5802f66f9892b455aadd4a7396b7b693b2dda729aa7c22f8909821acd17",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "run_daily_persona.py",
|
"source_relative_path": "run_daily_persona.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/run_daily_persona.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/run_daily_persona.py"
|
||||||
@@ -390,7 +390,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "a1b097ead2a3f7e20ca63d7c15828a8d04030b0d6722934c3bf65d9e975fba76",
|
"source_sha256": "a1b097ead2a3f7e20ca63d7c15828a8d04030b0d6722934c3bf65d9e975fba76",
|
||||||
"target_sha256": "a231ccd2efacefca703a975e6c63e05a36c61a3db0975e46b1fab8d983fe84d2",
|
"target_sha256": "7d8587f77615040087d2d475a7b440ad16a39c5874e143418dd700ecbc5db0f8",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "scripts/insert_main_image_records.py",
|
"source_relative_path": "scripts/insert_main_image_records.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/scripts/insert_main_image_records.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/scripts/insert_main_image_records.py"
|
||||||
@@ -406,7 +406,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "4474dd7d68117247af87e20bbeb6b7c072368ef0f606de5d3e186f459d34a0f5",
|
"source_sha256": "4474dd7d68117247af87e20bbeb6b7c072368ef0f606de5d3e186f459d34a0f5",
|
||||||
"target_sha256": "cfbb46483113cda9e4cb301b092ac2ce6a35c5cc53b9a9cef01fdf93a00d3b94",
|
"target_sha256": "862ae9392227113ad4266c2170769595c36b2f98fdecc54e54a648ba834baebf",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "taobao_sycm_collect.py",
|
"source_relative_path": "taobao_sycm_collect.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/taobao_sycm_collect.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/taobao_sycm_collect.py"
|
||||||
@@ -438,7 +438,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "e4d99956d9649d108baeb3e3a2d1969c227622d1037062bd277af66375556d82",
|
"source_sha256": "e4d99956d9649d108baeb3e3a2d1969c227622d1037062bd277af66375556d82",
|
||||||
"target_sha256": "981ef2a5a59f73ae244240520320c73be9c9462b50546df1ff91d0c97e7c03f7",
|
"target_sha256": "2ccb359ab943c185b17854f0f00cfb76b8a1235d7b031209a1c07e9386f8d93f",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "upload_video_to_guanghe.py",
|
"source_relative_path": "upload_video_to_guanghe.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/upload_video_to_guanghe.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/upload_video_to_guanghe.py"
|
||||||
@@ -454,7 +454,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "cceb3861da0e68c686c0c58f64767c3ca2693986bb5006ca7405d3a5a39c4347",
|
"source_sha256": "cceb3861da0e68c686c0c58f64767c3ca2693986bb5006ca7405d3a5a39c4347",
|
||||||
"target_sha256": "8a947ce42d34ff26bb46c5173af9e37ee9d60eac1b595d2f54b855a881ac2d6c",
|
"target_sha256": "4a44a00ec0e13bac3c4369c0336c6b6410e012d59e39900f3b4d0c093c8def90",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "vendors/dy-data-flow/dy_store_competitor_store_scraping.py",
|
"source_relative_path": "vendors/dy-data-flow/dy_store_competitor_store_scraping.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/vendors/dy-data-flow/dy_store_competitor_store_scraping.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/vendors/dy-data-flow/dy_store_competitor_store_scraping.py"
|
||||||
@@ -488,7 +488,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "6daa16c03989fb2231e54545726f5aa488e4ba80b9b8aa6f937cb9a2fa13ff35",
|
"source_sha256": "6daa16c03989fb2231e54545726f5aa488e4ba80b9b8aa6f937cb9a2fa13ff35",
|
||||||
"target_sha256": "89ba008ef4388c6e1ed32e4a5c26ffa81d083164dd6d7231fc9e5886de8437cd",
|
"target_sha256": "5ea444181ca731d8bdd983fa8d0179422651fdd994ebe08a63ed2bfae155fca5",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "vendors/jd-data-flow/jd_data_collector.py",
|
"source_relative_path": "vendors/jd-data-flow/jd_data_collector.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/vendors/jd-data-flow/jd_data_collector.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/vendors/jd-data-flow/jd_data_collector.py"
|
||||||
@@ -496,7 +496,7 @@
|
|||||||
{
|
{
|
||||||
"category": "production_source",
|
"category": "production_source",
|
||||||
"source_sha256": "a53cc38b0ddeaa0065bc27a1d137ce16c2873ff9bec712483ca87bcedd6baa3a",
|
"source_sha256": "a53cc38b0ddeaa0065bc27a1d137ce16c2873ff9bec712483ca87bcedd6baa3a",
|
||||||
"target_sha256": "0549228539c2e1908c3181de13291457712eeaea8fea89903814844724a3b5ee",
|
"target_sha256": "a6ee61498a040be9d45c6b8f9ecdbc06213f5f1e5507783412188f35d2968640",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "vendors/jd-data-flow/jd_peer_product_data_collector.py",
|
"source_relative_path": "vendors/jd-data-flow/jd_peer_product_data_collector.py",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/vendors/jd-data-flow/jd_peer_product_data_collector.py"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/vendors/jd-data-flow/jd_peer_product_data_collector.py"
|
||||||
@@ -560,7 +560,7 @@
|
|||||||
{
|
{
|
||||||
"category": "runtime_resource",
|
"category": "runtime_resource",
|
||||||
"source_sha256": "9739407b61842c598491d3189a32f5f15aa944aed79a164a5cd5b5a34f20c746",
|
"source_sha256": "9739407b61842c598491d3189a32f5f15aa944aed79a164a5cd5b5a34f20c746",
|
||||||
"target_sha256": "8ff84f6c3c672af35e09f70d71aa73656ac101218d8a56126b8b0a75ab387c8b",
|
"target_sha256": "6bbd36048836ef7ed0807223fc028ac17349b20314b1ea353ccedb451957ba55",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "db/schema.sql",
|
"source_relative_path": "db/schema.sql",
|
||||||
"target_relative_path": "src/gyxx_flow/modules/product_commerce/db/schema.sql"
|
"target_relative_path": "src/gyxx_flow/modules/product_commerce/db/schema.sql"
|
||||||
@@ -672,7 +672,7 @@
|
|||||||
{
|
{
|
||||||
"category": "regression_test",
|
"category": "regression_test",
|
||||||
"source_sha256": "820e17b910b62a213f0bebafcff72998c2c978307c97f976c074276150ce7c6a",
|
"source_sha256": "820e17b910b62a213f0bebafcff72998c2c978307c97f976c074276150ce7c6a",
|
||||||
"target_sha256": "1159c9e3f0abdf17183e7d6fa2634c508fe8348ce2926e7d3c58e735b713c2e9",
|
"target_sha256": "cdedf8c9a4871ad763e0a9098fc18777f0c3206991e8d440a45ccdfbda089a07",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "tests/test_main_image_concurrency.py",
|
"source_relative_path": "tests/test_main_image_concurrency.py",
|
||||||
"target_relative_path": "tests/modules/product_commerce/test_main_image_concurrency.py"
|
"target_relative_path": "tests/modules/product_commerce/test_main_image_concurrency.py"
|
||||||
@@ -696,7 +696,7 @@
|
|||||||
{
|
{
|
||||||
"category": "regression_test",
|
"category": "regression_test",
|
||||||
"source_sha256": "f1e660d34e21d3cd7a26d3605c82d07982ba6c5002af7b56fb6a876cb706713d",
|
"source_sha256": "f1e660d34e21d3cd7a26d3605c82d07982ba6c5002af7b56fb6a876cb706713d",
|
||||||
"target_sha256": "85ca8603e2af8257ce2a940b99c69948c2c1362f688e6f6dac4cbf499f1e4d73",
|
"target_sha256": "4a21ff4424c92a0bed6bd2f70f6a7bc7b426b3a25ccc356612f27a7426ce1eca",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "tests/test_market_rank_product_import.py",
|
"source_relative_path": "tests/test_market_rank_product_import.py",
|
||||||
"target_relative_path": "tests/modules/product_commerce/test_market_rank_product_import.py"
|
"target_relative_path": "tests/modules/product_commerce/test_market_rank_product_import.py"
|
||||||
@@ -728,7 +728,7 @@
|
|||||||
{
|
{
|
||||||
"category": "regression_test",
|
"category": "regression_test",
|
||||||
"source_sha256": "f9cfbb49fd313444790ac194fbf980dae21912081b9f26ad8e3850912126f09e",
|
"source_sha256": "f9cfbb49fd313444790ac194fbf980dae21912081b9f26ad8e3850912126f09e",
|
||||||
"target_sha256": "0cc2b365144482ceb36667eedfb2c403e904437f3ea4c7e12f7099276c13a279",
|
"target_sha256": "043b2dd69172bf5ea1b324bf07f8bba4c43f4bca9817c30a2c805ffe8ce70525",
|
||||||
"transformed": true,
|
"transformed": true,
|
||||||
"source_relative_path": "tests/test_style_analysis_orchestration.py",
|
"source_relative_path": "tests/test_style_analysis_orchestration.py",
|
||||||
"target_relative_path": "tests/modules/product_commerce/test_style_analysis_orchestration.py"
|
"target_relative_path": "tests/modules/product_commerce/test_style_analysis_orchestration.py"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"target_relative_path": "src/gyxx_flow/modules/shop_intelligence/collectors/dy_store_competitor_store_scraping.py",
|
"target_relative_path": "src/gyxx_flow/modules/shop_intelligence/collectors/dy_store_competitor_store_scraping.py",
|
||||||
"category": "source",
|
"category": "source",
|
||||||
"source_sha256": "009879a7dd341ca6ecbc8eb66f5700114c73bcaba3a53f159014a76232d0aa6b",
|
"source_sha256": "009879a7dd341ca6ecbc8eb66f5700114c73bcaba3a53f159014a76232d0aa6b",
|
||||||
"target_sha256": "d5e85ded1f852ba418229c75c985d0ece967dfbd5fd31697fe407b64d1c3db05",
|
"target_sha256": "9ab1328c2f1743944e8f096e548255884bc9724bf1e900c60c069d443c886902",
|
||||||
"transformed": true
|
"transformed": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
"target_relative_path": "src/gyxx_flow/modules/shop_intelligence/collectors/jd_peer_store_data_collector.py",
|
"target_relative_path": "src/gyxx_flow/modules/shop_intelligence/collectors/jd_peer_store_data_collector.py",
|
||||||
"category": "source",
|
"category": "source",
|
||||||
"source_sha256": "4ebdb52df7b384d2bfdd2fc840d55d14e18864002c20a36685b63cee1966cf4c",
|
"source_sha256": "4ebdb52df7b384d2bfdd2fc840d55d14e18864002c20a36685b63cee1966cf4c",
|
||||||
"target_sha256": "39faf18e9cd5e441323a8e056e022f179018a44eac224d64383cd7ae9fa1d611",
|
"target_sha256": "f03b1a7226071f2225a2a71748ad2c27a102cde67ba02d37c63f15480dd0de32",
|
||||||
"transformed": true
|
"transformed": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
"target_relative_path": "src/gyxx_flow/modules/shop_intelligence/db/__init__.py",
|
"target_relative_path": "src/gyxx_flow/modules/shop_intelligence/db/__init__.py",
|
||||||
"category": "source",
|
"category": "source",
|
||||||
"source_sha256": "ffc89b78ff0081a129e04eb6de4985ab0a2a4613c9e8a1d8b2750d8c17e8bc05",
|
"source_sha256": "ffc89b78ff0081a129e04eb6de4985ab0a2a4613c9e8a1d8b2750d8c17e8bc05",
|
||||||
"target_sha256": "a4bae02d0c07f3e3986896b31e62f41bf15edbf74d3776b1e050e381c15a8413",
|
"target_sha256": "08b5b8920338100339e7c716423b0e9260d4e007d0a1cc3f7510d51970a66a44",
|
||||||
"transformed": true
|
"transformed": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
"target_relative_path": "src/gyxx_flow/modules/shop_intelligence/db/db.py",
|
"target_relative_path": "src/gyxx_flow/modules/shop_intelligence/db/db.py",
|
||||||
"category": "source",
|
"category": "source",
|
||||||
"source_sha256": "168b6f87c1827b267f763ddce7eb15d0c8d652c8c0c7af18dcd0c6c7da5cddd1",
|
"source_sha256": "168b6f87c1827b267f763ddce7eb15d0c8d652c8c0c7af18dcd0c6c7da5cddd1",
|
||||||
"target_sha256": "01469f9eb80aee5e80bc28e548dbe2cd5ce861ab35cd480b33921a2adc799320",
|
"target_sha256": "4ab56df6c8ff75b1a67256ad8cdef34c58bb97e1a126707aac7ccf82980a56ec",
|
||||||
"transformed": true
|
"transformed": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
"target_relative_path": "src/gyxx_flow/modules/shop_intelligence/db/schema.sql",
|
"target_relative_path": "src/gyxx_flow/modules/shop_intelligence/db/schema.sql",
|
||||||
"category": "sql",
|
"category": "sql",
|
||||||
"source_sha256": "73230590bde7ae6359c02c82c2db096c0d3faa55b374e30348c47d7b40ea498b",
|
"source_sha256": "73230590bde7ae6359c02c82c2db096c0d3faa55b374e30348c47d7b40ea498b",
|
||||||
"target_sha256": "a69fa151f989f106d00f344bab43ec9e6a09cf404bdc581624cc0901cdc13d20",
|
"target_sha256": "9558543cf313aadb785715fca8f5cb99219488e7cb6c37cb00310bfe84f7ee74",
|
||||||
"transformed": true
|
"transformed": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -120,7 +120,7 @@
|
|||||||
"target_relative_path": "src/gyxx_flow/modules/supply_chain/orchestrator/scripts/ProductReplenishment.py",
|
"target_relative_path": "src/gyxx_flow/modules/supply_chain/orchestrator/scripts/ProductReplenishment.py",
|
||||||
"category": "source",
|
"category": "source",
|
||||||
"source_sha256": "02f6345142ebd84e25dc2f3b0651bc139ef31dd3994ff739efe66344c77ee49e",
|
"source_sha256": "02f6345142ebd84e25dc2f3b0651bc139ef31dd3994ff739efe66344c77ee49e",
|
||||||
"target_sha256": "e74211f2fe97ac47e704a040f442f51c97fecc15ea30dd1f0dbeacaf5940fd80",
|
"target_sha256": "ae5154b1f2af1e03307c07e4188225cd9601d1e1b5316fc0f6a78d23303ba8de",
|
||||||
"transformed": true
|
"transformed": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+82
-51
@@ -3,6 +3,11 @@
|
|||||||
"workflows": [
|
"workflows": [
|
||||||
{
|
{
|
||||||
"id": "content.metrics.daily",
|
"id": "content.metrics.daily",
|
||||||
|
"required_accounts": [
|
||||||
|
"douyin-content",
|
||||||
|
"douyin-shop",
|
||||||
|
"xiaohongshu-content"
|
||||||
|
],
|
||||||
"module": "content_marketing",
|
"module": "content_marketing",
|
||||||
"trigger": "scheduled",
|
"trigger": "scheduled",
|
||||||
"execution": {
|
"execution": {
|
||||||
@@ -302,6 +307,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "content.metrics.backfill",
|
"id": "content.metrics.backfill",
|
||||||
|
"required_accounts": [
|
||||||
|
"douyin-content",
|
||||||
|
"douyin-shop",
|
||||||
|
"xiaohongshu-content"
|
||||||
|
],
|
||||||
"module": "content_marketing",
|
"module": "content_marketing",
|
||||||
"trigger": "scheduled",
|
"trigger": "scheduled",
|
||||||
"execution": {
|
"execution": {
|
||||||
@@ -430,56 +440,7 @@
|
|||||||
"task_name": "YingxiaoYunying_DailyMarketingReport"
|
"task_name": "YingxiaoYunying_DailyMarketingReport"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "content.relogin.weekly",
|
|
||||||
"module": "content_marketing",
|
|
||||||
"trigger": "scheduled",
|
|
||||||
"execution": {
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "relogin",
|
|
||||||
"name": "刷新内容平台登录态",
|
|
||||||
"description": "并行启动五个平台的二维码重新登录流程,按配置轮次重试,持久化登录状态并通知结果。",
|
|
||||||
"data_flow": {
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"system": "内容平台",
|
|
||||||
"label": "五个平台登录页与当前会话状态"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"processing": [
|
|
||||||
"并行发起二维码登录并按轮次重试",
|
|
||||||
"校验并持久化新的登录会话"
|
|
||||||
],
|
|
||||||
"destinations": [
|
|
||||||
{
|
|
||||||
"system": "浏览器状态",
|
|
||||||
"label": "各平台 Cookie 与 storage state"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"system": "飞书",
|
|
||||||
"label": "登录刷新结果通知"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"entry": "data/tools/friday_relogin_parallel.py",
|
|
||||||
"args": [
|
|
||||||
"--max-attempts",
|
|
||||||
"3",
|
|
||||||
"--round-timeout",
|
|
||||||
"300",
|
|
||||||
"--screenshot-delay",
|
|
||||||
"60"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provenance": {
|
|
||||||
"source_project": "content",
|
|
||||||
"task_name": "YingxiaoYunying_FridayRelogin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "content.creator_report.monthly",
|
"id": "content.creator_report.monthly",
|
||||||
"module": "content_marketing",
|
"module": "content_marketing",
|
||||||
"trigger": "scheduled",
|
"trigger": "scheduled",
|
||||||
@@ -781,6 +742,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "shop.metrics.weekly",
|
"id": "shop.metrics.weekly",
|
||||||
|
"required_accounts": [
|
||||||
|
"douyin-shop"
|
||||||
|
],
|
||||||
"module": "shop_intelligence",
|
"module": "shop_intelligence",
|
||||||
"trigger": "scheduled",
|
"trigger": "scheduled",
|
||||||
"execution": {
|
"execution": {
|
||||||
@@ -890,6 +854,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "shop.competitor.weekly",
|
"id": "shop.competitor.weekly",
|
||||||
|
"required_accounts": [
|
||||||
|
"douyin-shop"
|
||||||
|
],
|
||||||
"module": "shop_intelligence",
|
"module": "shop_intelligence",
|
||||||
"trigger": "scheduled",
|
"trigger": "scheduled",
|
||||||
"execution": {
|
"execution": {
|
||||||
@@ -1592,6 +1559,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "product.daily",
|
"id": "product.daily",
|
||||||
|
"required_accounts": [
|
||||||
|
"douyin-shop",
|
||||||
|
"erp-jushuitan"
|
||||||
|
],
|
||||||
"module": "product_commerce",
|
"module": "product_commerce",
|
||||||
"trigger": "scheduled",
|
"trigger": "scheduled",
|
||||||
"execution": {
|
"execution": {
|
||||||
@@ -1916,6 +1887,7 @@
|
|||||||
],
|
],
|
||||||
"processing": [
|
"processing": [
|
||||||
"校验广告主 ID、店铺标识和商品数据明细",
|
"校验广告主 ID、店铺标识和商品数据明细",
|
||||||
|
"任务中心被重定向到账户选择页时自动点击目标广告主后重试",
|
||||||
"按业务日期下载原始 Excel",
|
"按业务日期下载原始 Excel",
|
||||||
"重置千川错误的 worksheet A1 dimension 并校验日期覆盖"
|
"重置千川错误的 worksheet A1 dimension 并校验日期覆盖"
|
||||||
],
|
],
|
||||||
@@ -2231,6 +2203,54 @@
|
|||||||
},
|
},
|
||||||
"note": "每天自动采集昨天数据;控制台可手动选择任意补采日期并幂等覆盖该日。全选聚水潭全部店铺,不拆京东、抖音、天猫;每次只采集一个业务日期。"
|
"note": "每天自动采集昨天数据;控制台可手动选择任意补采日期并幂等覆盖该日。全选聚水潭全部店铺,不拆京东、抖音、天猫;每次只采集一个业务日期。"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "product.hk_anpei_public_inventory.daily",
|
||||||
|
"module": "product_commerce",
|
||||||
|
"trigger": "scheduled",
|
||||||
|
"execution": {
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": "query_and_refresh_sheet",
|
||||||
|
"name": "刷新香港安沛 SKU 公有可用数",
|
||||||
|
"description": "校验计划书固定 SKU 与现有库存工作表中的商品编码对应关系,调用聚水潭 /open/inventory/query 查询库存及锁定量,并将公有可用数写入仓库现有库存列。",
|
||||||
|
"entry": "public_sku_inventory_sheet.py",
|
||||||
|
"timeout_seconds": 900,
|
||||||
|
"replay_policy": "idempotent",
|
||||||
|
"data_flow": {
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"system": "光影行星香港安沛2026Q2计划书",
|
||||||
|
"label": "现有库存表的 51 个唯一 SKU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": "聚水潭开放 API",
|
||||||
|
"label": "/open/inventory/query",
|
||||||
|
"detail": "请求 has_lock_qty=true 以取得库存锁定量"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"processing": [
|
||||||
|
"校验整批 SKU 库存响应后再写入飞书",
|
||||||
|
"按商品编码(光影)匹配行;公有可用数 = qty - order_lock - lock_qty + virtual_qty",
|
||||||
|
"没有库存记录的 SKU 清空仓库现有库存单元格",
|
||||||
|
"仅写入仓库现有库存列,不覆盖图片、备注、在途生产或其它字段"
|
||||||
|
],
|
||||||
|
"destinations": [
|
||||||
|
{
|
||||||
|
"system": "飞书电子表格",
|
||||||
|
"label": "光影行星香港安沛2026Q2計劃書",
|
||||||
|
"detail": "現有庫存工作表;按商品编码(光影)更新 D 列仓库现有库存"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"provenance": {
|
||||||
|
"source_project": "data-hub",
|
||||||
|
"task_name": "HongKongAnpeiPublicSkuInventoryDaily"
|
||||||
|
},
|
||||||
|
"note": "每天 Asia/Shanghai 09:00 执行。聚水潭应用凭据从 GYXX 调度进程环境读取;不得使用开发配置默认值。"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "product.video_upload",
|
"id": "product.video_upload",
|
||||||
"module": "product_commerce",
|
"module": "product_commerce",
|
||||||
@@ -2350,7 +2370,6 @@
|
|||||||
"args": [
|
"args": [
|
||||||
"--anchor-record-id",
|
"--anchor-record-id",
|
||||||
"recvrXTRWWFXeJ",
|
"recvrXTRWWFXeJ",
|
||||||
"--manual-login",
|
|
||||||
"--execute"
|
"--execute"
|
||||||
],
|
],
|
||||||
"timeout_seconds": 21600
|
"timeout_seconds": 21600
|
||||||
@@ -2607,6 +2626,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "supply.purchase_confirmation.daily",
|
"id": "supply.purchase_confirmation.daily",
|
||||||
|
"required_accounts": [
|
||||||
|
"erp-jushuitan"
|
||||||
|
],
|
||||||
"module": "supply_chain",
|
"module": "supply_chain",
|
||||||
"trigger": "scheduled",
|
"trigger": "scheduled",
|
||||||
"execution": {
|
"execution": {
|
||||||
@@ -2657,6 +2679,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "supply.replenishment.weekly",
|
"id": "supply.replenishment.weekly",
|
||||||
|
"required_accounts": [
|
||||||
|
"erp-jushuitan"
|
||||||
|
],
|
||||||
"module": "supply_chain",
|
"module": "supply_chain",
|
||||||
"trigger": "scheduled",
|
"trigger": "scheduled",
|
||||||
"execution": {
|
"execution": {
|
||||||
@@ -2712,6 +2737,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "supply.replenishment_alert.daily",
|
"id": "supply.replenishment_alert.daily",
|
||||||
|
"required_accounts": [
|
||||||
|
"erp-jushuitan"
|
||||||
|
],
|
||||||
"module": "supply_chain",
|
"module": "supply_chain",
|
||||||
"trigger": "scheduled",
|
"trigger": "scheduled",
|
||||||
"execution": {
|
"execution": {
|
||||||
@@ -2811,6 +2839,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "product.market_rank",
|
"id": "product.market_rank",
|
||||||
|
"required_accounts": [
|
||||||
|
"douyin-shop"
|
||||||
|
],
|
||||||
"module": "product_commerce",
|
"module": "product_commerce",
|
||||||
"trigger": "scheduled",
|
"trigger": "scheduled",
|
||||||
"execution": {
|
"execution": {
|
||||||
|
|||||||
@@ -46,6 +46,27 @@ Scrapling 安装和管理。具体边界与验证命令见
|
|||||||
`EnvironmentFile=` 或当前进程环境优先于文件中的同名值,避免本地文件意外覆盖密钥系统
|
`EnvironmentFile=` 或当前进程环境优先于文件中的同名值,避免本地文件意外覆盖密钥系统
|
||||||
注入值。
|
注入值。
|
||||||
|
|
||||||
|
## 香港安沛 SKU 公有库存同步
|
||||||
|
|
||||||
|
`product.hk_anpei_public_inventory.daily` 每天按 `Asia/Shanghai` 09:00 查询计划书中的固定 SKU 清单,
|
||||||
|
调用聚水潭 `/open/inventory/query`,并按商品编码(光影)更新飞书电子表格「現有庫存」工作表的
|
||||||
|
「仓库现有库存」列。它要求调度进程环境提供
|
||||||
|
`JUSHUITAN_APP_KEY` 和 `JUSHUITAN_APP_SECRET`;`JUSHUITAN_BASE_URL` 可选,默认使用聚水潭官方 OpenAPI
|
||||||
|
地址。没有密钥时工作流会在查询前失败,不会把库存留成 0 或改写多维表格。
|
||||||
|
|
||||||
|
Windows NSSM 调度服务从受保护的运行时环境文件读取凭据。安装器默认读取通用运行环境文件
|
||||||
|
`D:\product-collector-analyze-flow\.env`,以及独立的聚水潭密钥文件
|
||||||
|
`D:\product-collector-analyze-flow\private-secrets\jushuitan.env`。两个文件都按服务账户实际身份限制 ACL;
|
||||||
|
可分别通过 `-RuntimeEnvFile` 和 `-JushuitanEnvFile` 指定其他路径:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\deploy\windows-service\install.ps1 -ProjectRoot D:\gyxx-flow -DataRoot D:\gyxx-flow-data -RuntimeEnvFile D:\product-collector-analyze-flow\.env -JushuitanEnvFile D:\product-collector-analyze-flow\private-secrets\jushuitan.env
|
||||||
|
```
|
||||||
|
|
||||||
|
仅将这两个变量的正式聚水潭应用值写入服务账户可读的环境文件,并限制文件 ACL;不要使用开发配置中的默认值,
|
||||||
|
也不要把真实值写进 Git、PowerShell 命令参数、服务日志或文档。已安装的服务需要让调度进程重新读取环境文件后,
|
||||||
|
新计划才会使用这些值。Linux 部署则将相同变量放入权限为 `0640` 的 `EnvironmentFile`。
|
||||||
|
|
||||||
## 三平台电商费用日报部署边界
|
## 三平台电商费用日报部署边界
|
||||||
|
|
||||||
`product.ecommerce_costs.daily` 是包含天猫万相台、京东京准通和抖音千川分支的电商费用工作流;三个
|
`product.ecommerce_costs.daily` 是包含天猫万相台、京东京准通和抖音千川分支的电商费用工作流;三个
|
||||||
@@ -197,3 +218,12 @@ sudo systemctl start gyxx-flow.service
|
|||||||
## Windows 兼容入口
|
## Windows 兼容入口
|
||||||
|
|
||||||
`deploy/windows-service/` 暂时保留为 legacy NSSM 兼容入口。NSSM 只守护同一个 `gyxx schedule run` 进程,不注册 Windows Task Scheduler,也不保存业务时间规则。新服务器部署以 Linux systemd 为准。
|
`deploy/windows-service/` 暂时保留为 legacy NSSM 兼容入口。NSSM 只守护同一个 `gyxx schedule run` 进程,不注册 Windows Task Scheduler,也不保存业务时间规则。新服务器部署以 Linux systemd 为准。
|
||||||
|
|
||||||
|
## Windows 工作站保活(控制台 + 调度器自检自启动)
|
||||||
|
|
||||||
|
交互式 Windows 工作站(无法用 systemd/NSSM 常驻服务时)使用 `deploy/watchdog/gyxx_keepalive.py` 做进程外保活:
|
||||||
|
|
||||||
|
- **控制台自检**:控制台进程内置看门狗线程(`console.py` `_console_watchdog`),周期探测自身 `GET /api/health`;连续失败(默认 3 次,`GYXX_CONSOLE_WATCHDOG_FAILURES` / `GYXX_CONSOLE_WATCHDOG_INTERVAL_SECONDS` 可调,`GYXX_CONSOLE_WATCHDOG=0` 关闭)即以退出码 70 自杀,交给保活拉起。`/api/health` 不鉴权、只报进程 PID、uptime 与调度锁存活,供探针使用;API 500 现在会把完整堆栈落到 stderr(随启动重定向进 `var/logs/console-restarts/`)。
|
||||||
|
- **保活脚本**:登录自启动(Startup 目录 `gyxx-keepalive.vbs` → `pythonw gyxx_keepalive.py`)后常驻,每 30 秒一轮:控制台探测连续失败或端口空置则结束僵死进程树并按 `--env-file` 同款参数拉起;读 `var/state/scheduler/service.lock` 判定调度器存活,锁主死亡即拉起 `gyxx schedule run`(调度器单实例锁保证不双跑)。单目标冷却 120 秒;30 分钟内 5 次重启触发熔断,暂停拉起 1 小时并记日志待人工检查。日志在 `var/logs/keepalive/keepalive.log`。
|
||||||
|
- **验收**:`python deploy/watchdog/gyxx_keepalive.py --once` 单轮探测+处置;健康态应只输出 `keepalive-start`/`keepalive-once-done`。
|
||||||
|
- 该脚本只做进程可用性守护,不承载任何业务时间规则(唯一调度路径仍是 `gyxx schedule run`);未注册 Windows Task Scheduler。注意:Startup 方式要求用户已登录——机器重启后需人工登录一次,之后控制台与调度器会在约 2 分钟内自动就位。
|
||||||
+24
-2
@@ -190,16 +190,38 @@ uv run gyxx schedule run --env-file /etc/gyxx-flow/gyxx-flow.env
|
|||||||
- `tmall-ozko`:商品经营日报 ozko 账号,900 秒;
|
- `tmall-ozko`:商品经营日报 ozko 账号,900 秒;
|
||||||
- `xiaohongshu-content`:小红书,900 秒;
|
- `xiaohongshu-content`:小红书,900 秒;
|
||||||
- `douyin-content`:抖音内容侧,900 秒;
|
- `douyin-content`:抖音内容侧,900 秒;
|
||||||
- `xingtu`:星图,900 秒;
|
- `chanmama`:蝉妈妈,900 秒(脚本自带账密登录);
|
||||||
- `chanmama`:蝉妈妈,900 秒;
|
|
||||||
- `bilibili-content`:B 站,900 秒。
|
- `bilibili-content`:B 站,900 秒。
|
||||||
|
|
||||||
|
星图/百应采集(`xingtu_scraper_v2.py`)的登录态挂在 `douyin-shop` 账号下;`xingtu` 账号当前无成员脚本,仅保留 vault。
|
||||||
|
|
||||||
商品经营日报 `product_commerce:taobao_sycm_collect.py` 会在同一次运行中按品牌路由账号:光影行星使用 `tmall-shop`,ozko 使用 `tmall-ozko`,两边各自使用独立 Profile 和账号 vault。`tmall-ozko` 没有静态成员脚本是有意设计,因为它由这个按品牌拆分的包装工作流动态选择;登录态成功后仍会回写 `state/accounts/tmall-ozko/`,由同一套 keepalive 负责续期。
|
商品经营日报 `product_commerce:taobao_sycm_collect.py` 会在同一次运行中按品牌路由账号:光影行星使用 `tmall-shop`,ozko 使用 `tmall-ozko`,两边各自使用独立 Profile 和账号 vault。`tmall-ozko` 没有静态成员脚本是有意设计,因为它由这个按品牌拆分的包装工作流动态选择;登录态成功后仍会回写 `state/accounts/tmall-ozko/`,由同一套 keepalive 负责续期。
|
||||||
|
|
||||||
账号需要在 `config/runtime-bindings.json` 的 `accounts.<account_id>.keepalive` 中显式开启续期,并配置一个不会产生业务副作用的已登录页面 URL。所有账号由唯一的 `gyxx schedule run` 常驻进程在后台维护;每个账号使用独立 CDP 端口、独立 Profile 和账号级 Cookie vault,启动后按 `initial_delay_seconds` 错峰访问,避免同时打开多个登录页面。访问成功且没有跳转到登录页时,Scrapling 才会把最新 Cookie/storage state 原子写回 `state/accounts/<account_id>/`,并同步给成员脚本;不需要另建 Windows 任务计划、cron 或 timer。
|
账号需要在 `config/runtime-bindings.json` 的 `accounts.<account_id>.keepalive` 中显式开启续期,并配置一个不会产生业务副作用的已登录页面 URL。所有账号由唯一的 `gyxx schedule run` 常驻进程在后台维护;每个账号使用独立 CDP 端口、独立 Profile 和账号级 Cookie vault,启动后按 `initial_delay_seconds` 错峰访问,避免同时打开多个登录页面。访问成功且没有跳转到登录页时,Scrapling 才会把最新 Cookie/storage state 原子写回 `state/accounts/<account_id>/`,并同步给成员脚本;不需要另建 Windows 任务计划、cron 或 timer。
|
||||||
|
|
||||||
Cookie 续期只能延长站点支持滑动续期的会话,不能突破服务端硬过期、主动退出、风控或验证码。keepalive 检测到登录页/无效 Cookie 时会停止发布,不会用失效状态覆盖最后一份有效 vault;此时按 `gyxx accounts list` 检查 `keepalive.status`,必要时重新执行 `gyxx accounts login <account_id>`。
|
Cookie 续期只能延长站点支持滑动续期的会话,不能突破服务端硬过期、主动退出、风控或验证码。keepalive 检测到登录页/无效 Cookie 时会停止发布,不会用失效状态覆盖最后一份有效 vault;此时按 `gyxx accounts list` 检查 `keepalive.status`,必要时重新执行 `gyxx accounts login <account_id>`。
|
||||||
|
|
||||||
|
### 工作流执行前的 Cookie 健康网关
|
||||||
|
|
||||||
|
每个正式执行(非 dry-run)的工作流在采集步骤前会自动注入"每账号一个"的 `cookie-gate.<account_id>` 关键步骤,由 `src/gyxx_flow/cookie_gate.py` 实现按账号分三级的状态机:
|
||||||
|
|
||||||
|
- **blocking**(纯扫码/验证码账号:`douyin-shop`、`xiaohongshu-content`、`douyin-content`、`bilibili-content` 等):先看 keepalive 近期成功状态(默认 1 小时内视为新鲜,可用 `GYXX_COOKIE_GATE_MAX_STATUS_AGE_SECONDS` 调整),新鲜且 vault 静态有效则直接放行;否则用 keepalive 同款无头浏览器真实探活。探活失败即判定过期:gate 步骤失败,依赖它的采集步骤记 `dependency-failed` 跳过、工作流失败退出,并通过飞书通知登录负责人(`config/notification-routing.json` 中手机号为 `+8613663785145` 的人员,默认 30 分钟节流,可用 `GYXX_LOGIN_ALERT_OPEN_IDS` 覆盖收件人)执行 `uv run gyxx accounts login <account_id>` 完成扫码/验证码重登。
|
||||||
|
- **recover**(配置了 `login_command_id` 的账密自动重登账号:`erp-jushuitan`):过期时先自动跑账密登录恢复,恢复成功放行;失败才阻断并飞书通知。
|
||||||
|
- **defer**(成员脚本自带账号密码登录的账号:京东系、天猫系、`chanmama`):不注入 gate,保持脚本内账密自动登录流程不变。
|
||||||
|
|
||||||
|
按账号划分的持久状态在 `state/accounts/<id>/cookie_state.json`(`healthy` ↔ `awaiting_login`)。编排器型工作流(入口脚本本身无账号绑定、由子采集脚本使用账号)在 `config/workflows.json` 的 `required_accounts` 字段声明依赖账号。`--rerun-step`/`--resume-from` 切片和 shadow 运行不发送通知;dry-run 完全不注入 gate。设置 `GYXX_COOKIE_GATE=0` 可临时关闭整网关。
|
||||||
|
|
||||||
|
采集脚本不再内嵌扫码/验证码登录:运行中检测到登录态失效会以 `CookieExpiredError` 快速失败并按退出码 75 记为 cookie 跳过;`self_xhs_scraper.py --login-only` 是唯一保留的人工登录引导(该脚本不在任何账号成员内,自管绑定状态)。
|
||||||
|
|
||||||
|
### 登录恢复后的自动重跑
|
||||||
|
|
||||||
|
gate 阻断工作流时会把待重跑清单(工作流 + 业务日期,shadow/验收运行不记录,同工作流同日期去重,上限 20 条)写进 `state/accounts/<id>/cookie_state.json` 的 `pending_reruns`。常驻调度器(`gyxx schedule run`)的维护循环里挂着 `CookieGateRecoveryManager`:
|
||||||
|
|
||||||
|
- 每个心跳检查有待重跑记录的账号:keepalive 近期成功 + vault 静态有效 → 立即视为恢复;否则每 5 分钟做一次真实探活(mode C 账号会自动账密重登,重登成功同样触发恢复)。
|
||||||
|
- 恢复确认后,用调度器同款启动器按**原业务日期**自动重跑全部待重跑工作流(日志在 `data/logs/scheduler/`),清空清单并飞书通知"登录态已恢复 + 已自动重跑清单"。
|
||||||
|
- 启动失败的任务保留在清单里下一轮重试;同一账号两次重跑启动之间有 5 分钟冷却;控制台手动脚本类临时运行 id(`script.*`)不会自动重跑。
|
||||||
|
- 与正常调度并发安全:工作流级锁 + EffectLedger 幂等保证同一天不会重复产生副作用。
|
||||||
|
|
||||||
项目不会自动搬动或删除已有 `var/`。迁移数据根时应停止调度器,完整备份和复制原目录,修改环境变量后依次运行 doctor、dry-run 和一次指定日期的对账执行。
|
项目不会自动搬动或删除已有 `var/`。迁移数据根时应停止调度器,完整备份和复制原目录,修改环境变量后依次运行 doctor、dry-run 和一次指定日期的对账执行。
|
||||||
|
|
||||||
## 清理和保留
|
## 清理和保留
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ uv run gyxx console
|
|||||||
uv run gyxx console --env-file D:\secure\gyxx-flow.env
|
uv run gyxx console --env-file D:\secure\gyxx-flow.env
|
||||||
```
|
```
|
||||||
|
|
||||||
|
香港客服库存工作流还需要聚水潭应用密钥。Windows 生产控制台必须额外加载受保护的聚水潭环境文件,
|
||||||
|
这样控制台发起的手动执行也能取得凭据;调度器和控制台应加载相同的环境文件集合:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run gyxx console `
|
||||||
|
--env-file D:\product-collector-analyze-flow\.env `
|
||||||
|
--env-file D:\product-collector-analyze-flow\private-secrets\jushuitan.env
|
||||||
|
```
|
||||||
|
|
||||||
商品经营正式执行会在启动子进程前校验云端 PostgreSQL 和该工作流实际需要的运行时配置。
|
商品经营正式执行会在启动子进程前校验云端 PostgreSQL 和该工作流实际需要的运行时配置。
|
||||||
需要 Hermes 分析或业务通知的工作流仍会校验 `hermes-analyzer` lark-cli user 身份和负责人
|
需要 Hermes 分析或业务通知的工作流仍会校验 `hermes-analyzer` lark-cli user 身份和负责人
|
||||||
通知路由;天猫/京东视频上传则校验 `STYLE_ANALYSIS_LLM_BASE_URL`(或
|
通知路由;天猫/京东视频上传则校验 `STYLE_ANALYSIS_LLM_BASE_URL`(或
|
||||||
@@ -144,3 +153,14 @@ uv run gyxx console --host 0.0.0.0 --port 8765
|
|||||||
|
|
||||||
控制台不提供工作流入口、参数、运行时绑定或凭据的在线编辑能力;这些仍由受版本控制的
|
控制台不提供工作流入口、参数、运行时绑定或凭据的在线编辑能力;这些仍由受版本控制的
|
||||||
项目配置和代码维护。
|
项目配置和代码维护。
|
||||||
|
|
||||||
|
## 自检与保活
|
||||||
|
|
||||||
|
- `GET /api/health` 返回进程 PID、uptime 与调度器锁存活状态,不鉴权、不触外部依赖,专供探针使用。
|
||||||
|
- 控制台进程内置看门狗线程:周期自探测 `/api/health`,连续失败(默认 3 次)即以退出码 70 退出,
|
||||||
|
由 `deploy/watchdog/gyxx_keepalive.py`(登录自启动常驻)负责拉起;保活同时守护调度器进程,
|
||||||
|
详见 `docs/deployment.md` 的「Windows 工作站保活」章节。环境开关:
|
||||||
|
`GYXX_CONSOLE_WATCHDOG=0` 关闭、`GYXX_CONSOLE_WATCHDOG_INTERVAL_SECONDS`(默认 30)、
|
||||||
|
`GYXX_CONSOLE_WATCHDOG_FAILURES`(默认 3)。
|
||||||
|
- API 返回「控制台处理请求失败」时,完整堆栈在控制台 stderr 日志
|
||||||
|
(`var/logs/console-restarts/console-*.stderr.log`)中可查。
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
| `08:30` | 2 | 1 | 1 | 达人月报生成 [`content.creator_report.monthly`](暂停);京东与天猫主图周采集 [`product.main_image.weekly`] |
|
| `08:30` | 2 | 1 | 1 | 达人月报生成 [`content.creator_report.monthly`](暂停);京东与天猫主图周采集 [`product.main_image.weekly`] |
|
||||||
| `08:40` | 1 | 1 | 0 | 商品经营日报采集 [`product.daily`] |
|
| `08:40` | 1 | 1 | 0 | 商品经营日报采集 [`product.daily`] |
|
||||||
| `09:00` | 6 | 6 | 0 | 笔记清单与合作同步 [`content.notes_master.daily`];京东自营业绩日采集 [`shop.jd_self_operated.daily`];抖音待改价申诉;聚水潭全店铺款式日报 [`product.erp_all_shop_daily`];天猫百亿补贴批量报名;补货建议预计到货同步 [`supply.replenishment_arrival_sync.daily`] |
|
| `09:00` | 6 | 6 | 0 | 笔记清单与合作同步 [`content.notes_master.daily`];京东自营业绩日采集 [`shop.jd_self_operated.daily`];抖音待改价申诉;聚水潭全店铺款式日报 [`product.erp_all_shop_daily`];天猫百亿补贴批量报名;补货建议预计到货同步 [`supply.replenishment_arrival_sync.daily`] |
|
||||||
| `10:00` | 8 | 5 | 3 | 营销日报生成与发送 [`content.marketing_report.daily`](暂停);内容平台登录态刷新 [`content.relogin.weekly`](暂停);内容周度汇总 [`content.summary.weekly`](暂停);抖音待改价申诉;三平台电商费用采集 [`product.ecommerce_costs.daily`];商品月度销量表 [`product.sales_sheet.daily`];天猫百亿补贴批量报名;市场排行周采集 [`product.market_rank`] |
|
| `10:00` | 8 | 5 | 3 | 营销日报生成与发送 [`content.marketing_report.daily`](暂停);内容周度汇总 [`content.summary.weekly`](暂停);抖音待改价申诉;三平台电商费用采集 [`product.ecommerce_costs.daily`];商品月度销量表 [`product.sales_sheet.daily`];天猫百亿补贴批量报名;市场排行周采集 [`product.market_rank`] |
|
||||||
| `11:00` | 3 | 3 | 0 | 抖音待改价申诉;款式周期分析 [`product.style_analysis.interval`];天猫百亿补贴批量报名 |
|
| `11:00` | 3 | 3 | 0 | 抖音待改价申诉;款式周期分析 [`product.style_analysis.interval`];天猫百亿补贴批量报名 |
|
||||||
| `12:00` | 5 | 5 | 0 | 多平台评论周采集 [`content.comments.weekly`];店铺经营指标周采集 [`shop.metrics.weekly`];抖音待改价申诉;商品人群画像日采集 [`product.persona.daily`];天猫百亿补贴批量报名 |
|
| `12:00` | 5 | 5 | 0 | 多平台评论周采集 [`content.comments.weekly`];店铺经营指标周采集 [`shop.metrics.weekly`];抖音待改价申诉;商品人群画像日采集 [`product.persona.daily`];天猫百亿补贴批量报名 |
|
||||||
| `12:30` | 1 | 1 | 0 | 竞店指标周采集 [`shop.competitor.weekly`] |
|
| `12:30` | 1 | 1 | 0 | 竞店指标周采集 [`shop.competitor.weekly`] |
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
|
|
||||||
- 指标采集:`content.metrics.daily`(内容指标日报采集)、`content.metrics.backfill`(内容指标补采)。
|
- 指标采集:`content.metrics.daily`(内容指标日报采集)、`content.metrics.backfill`(内容指标补采)。
|
||||||
- 报告与汇总:`content.marketing_report.daily`(营销日报生成与发送)、`content.creator_report.monthly`(达人月报生成)、`content.summary.monthly`(内容月度汇总)、`content.summary.weekly`(内容周度汇总)。
|
- 报告与汇总:`content.marketing_report.daily`(营销日报生成与发送)、`content.creator_report.monthly`(达人月报生成)、`content.summary.monthly`(内容月度汇总)、`content.summary.weekly`(内容周度汇总)。
|
||||||
- 内容同步与互动:`content.notes_master.daily`(笔记清单与合作同步)、`content.comments.weekly`(多平台评论周采集)、`content.relogin.weekly`(内容平台登录态刷新)。
|
- 内容同步与互动:`content.notes_master.daily`(笔记清单与合作同步)、`content.comments.weekly`(多平台评论周采集)。
|
||||||
|
|
||||||
### 4.2 商品经营(`product_commerce`,12 条)
|
### 4.2 商品经营(`product_commerce`,12 条)
|
||||||
|
|
||||||
@@ -124,7 +124,6 @@
|
|||||||
| 1 | `content.metrics.daily` | 内容指标日报采集 | 内容营销 | 每天 `01:00`、`05:00` | 0 | 启用 | 5 |
|
| 1 | `content.metrics.daily` | 内容指标日报采集 | 内容营销 | 每天 `01:00`、`05:00` | 0 | 启用 | 5 |
|
||||||
| 2 | `content.metrics.backfill` | 内容指标补采 | 内容营销 | 每周一 `14:00` | 0 | 暂停 | 2 |
|
| 2 | `content.metrics.backfill` | 内容指标补采 | 内容营销 | 每周一 `14:00` | 0 | 暂停 | 2 |
|
||||||
| 3 | `content.marketing_report.daily` | 营销日报生成与发送 | 内容营销 | 每天 `10:00` | 0 | 暂停 | 1 |
|
| 3 | `content.marketing_report.daily` | 营销日报生成与发送 | 内容营销 | 每天 `10:00` | 0 | 暂停 | 1 |
|
||||||
| 4 | `content.relogin.weekly` | 内容平台登录态刷新 | 内容营销 | 每周五 `10:00` | 0 | 暂停 | 1 |
|
|
||||||
| 5 | `content.creator_report.monthly` | 达人月报生成 | 内容营销 | 每月 1 日 `08:30` | 0 | 启用 | 1 |
|
| 5 | `content.creator_report.monthly` | 达人月报生成 | 内容营销 | 每月 1 日 `08:30` | 0 | 启用 | 1 |
|
||||||
| 6 | `content.summary.monthly` | 内容月度汇总 | 内容营销 | 每月 1 日 `08:00` | 0 | 启用 | 1 |
|
| 6 | `content.summary.monthly` | 内容月度汇总 | 内容营销 | 每月 1 日 `08:00` | 0 | 启用 | 1 |
|
||||||
| 7 | `content.notes_master.daily` | 笔记清单与合作同步 | 内容营销 | 每天 `09:00` | 0 | 启用 | 1 |
|
| 7 | `content.notes_master.daily` | 笔记清单与合作同步 | 内容营销 | 每天 `09:00` | 0 | 启用 | 1 |
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# 工作流总览(未隐藏)
|
||||||
|
|
||||||
|
> 生成日期:2026-10-08。数据来源:`config/workflows.json`、`config/schedules.json`、控制台显示名(`var/state/console/workflow-display-names.json`);数据库表提取自各入口脚本及共享 DB 辅助模块的内联 SQL。
|
||||||
|
> 范围:控制台中未被隐藏的工作流共 **30 个**(全部 39 个中剔除浏览器端隐藏的 10 个,其中 `content.relogin.weekly` 已不在当前配置中)。
|
||||||
|
> 定时时间均为 **Asia/Shanghai**,由常驻调度器 `gyxx schedule run` 按 `config/schedules.json` 触发;标注「未启用」的仅支持控制台手动运行。
|
||||||
|
> 启动脚本为仓库根目录相对路径,由工作流引擎相对 `src/gyxx_flow/modules/<模块>/` 解析执行;`×N` 表示同一脚本被 N 个步骤以不同参数复用。
|
||||||
|
>
|
||||||
|
> **关联数据库表说明**:均指云端 PostgreSQL(`GYXX_POSTGRES_DSN`),(写)= INSERT/UPDATE/DELETE,(读)= SELECT,(建)= CREATE TABLE IF NOT EXISTS。三点公共约定:
|
||||||
|
> - 内容营销用到款式映射的脚本会经 `feishu_mapping` → `adapters/workflow_config.py` 间接读动态配置表 `workflow_dynamic_styles`、`workflow_dynamic_style_platforms`(并合并读 `cmt_styles`,首跑建 `workflow_dynamic_*` 表),下表不逐一列出。
|
||||||
|
> - 店铺情报各入库脚本启动时执行 `shop_intelligence/db/schema.sql`、商品电商多数入库脚本执行 `product_commerce/db/schema.sql`,幂等建该模块全量表(21 / 26 张);下表只列该工作流**实际读写**的表。
|
||||||
|
> - 读写 `information_schema` / `pg_catalog` 系统目录的通用校验不列出。
|
||||||
|
|
||||||
|
## 内容营销(content_marketing,4 个)
|
||||||
|
|
||||||
|
| 工作流名称 | ID | 功能 | 定时执行 | 启动脚本 | 关联数据库表 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 合作达人内容指标日报采集 | `content.metrics.daily` | 按日采集合作达人在 B站、小红书蒲公英、星图(未命中用抖店达人广场补采)的曝光互动指标并同步 PostgreSQL;05:00 档补采 01:00 未完成任务 | 每日 01:00、05:00 | `src/gyxx_flow/modules/content_marketing/run_all.py`<br>`src/gyxx_flow/modules/content_marketing/data/tools/sync_metrics_to_cmt_notes.py` | `cmt_note_metric_snapshots`(写)<br>`cmt_notes_master`(读写)<br>`cmt_creators`(读写)<br>`cmt_styles`(读) |
|
||||||
|
| 自营内容指标日报采集 | `content.metrics.self_operated.daily` | 刷新自营款式映射,采集自营 B站、蝉妈妈、小红书 ARK 指标;飞书曝光回填并将三平台全量内容指标按日快照入库 | 每日 06:00 | `…/content_marketing/data/tools/refresh_self_mapping.py`<br>`…/content_marketing/self_bilibili_scraper.py`<br>`…/content_marketing/chanmama_scraper.py`<br>`…/content_marketing/self_xhs_scraper.py`<br>`…/content_marketing/data/tools/sync_metrics_to_cmt_notes.py`<br>`…/content_marketing/data/tools/sync_self_video_metrics.py` | `cmt_self_video_metrics`(写)<br>`cmt_note_metric_snapshots`(写)<br>`cmt_notes_master`(读写)<br>`cmt_creators`(读写)<br>`cmt_styles`(读) |
|
||||||
|
| 笔记清单与合作同步 | `content.notes_master.daily` | 一次飞书拉取同时落笔记身份与合作字段;笔记数量按去重链接口径维护主表 | 每日 09:00 | `src/gyxx_flow/modules/content_marketing/data/tools/sync_notes_master.py` | `cmt_notes_master`(建+写)<br>`cmt_note_metric_snapshots`(建)<br>`cmt_styles`(读写)<br>`cmt_creators`(读写) |
|
||||||
|
| 多平台评论周采集 | `content.comments.weekly` | B站、小红书、抖音三平台评论并行采集,任一平台失败则工作流失败 | 每周日 12:00 | `…/content_marketing/data/tools/batch_rescrape_bilibili.py`<br>`…/content_marketing/data/tools/batch_rescrape_xiaohongshu.py`<br>`…/content_marketing/data/tools/batch_rescrape_douyin.py` | `cmt_comments`(写)<br>`cmt_notes_master`(读写) |
|
||||||
|
|
||||||
|
## 店铺情报(shop_intelligence,12 个)
|
||||||
|
|
||||||
|
| 工作流名称 | ID | 功能 | 定时执行 | 启动脚本 | 关联数据库表 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 店铺经营指标周采集 | `shop.metrics.weekly` | 三平台作为独立节点并行采集店铺经营指标并写 PostgreSQL(不写飞书 Base) | 每周一 12:00 | `src/gyxx_flow/modules/shop_intelligence/runners/run_shop.py` ×3 | `jd_shop_weekly`、`dy_shop_weekly`、`tm_shop_weekly`(各平台步骤分别写) |
|
||||||
|
| 竞店指标周采集 | `shop.competitor.weekly` | 京东、抖音并行采集竞店指标并入库,单平台失败不阻断另一平台 | 每周一 12:30 | `src/gyxx_flow/modules/shop_intelligence/runners/run_peer_store.py` ×2 | `jd_peer_store_ranking`、`dy_peer_store_ranking`(各平台步骤分别写) |
|
||||||
|
| 京东自营业绩日采集 | `shop.jd_self_operated.daily` | 采集京东自营品牌与商品业绩(业务日期 = 运行日 −2) | 每日 09:00 | `…/shop_intelligence/collectors/jd_self_operated_brand_daily.py`<br>`…/shop_intelligence/collectors/jd_self_operated_product_daily.py` | `jd_self_operated_brand_daily`(写)<br>`jd_self_operated_product_daily`(写) |
|
||||||
|
| 京东客服消息采集 | `shop.jd_customer_service.daily` | 按业务日在商智消息视图采集顾客会话,单日须完成全部顾客,失败保留进度可重试 | 每月 21 日 03:00 | `src/gyxx_flow/modules/shop_intelligence/collectors/jd_customer_service_messages.py` | `jd_customer_service_conversations`、`jd_customer_service_messages`(写) |
|
||||||
|
| 抖音客服消息采集 | `shop.douyin_customer_service.daily` | 按业务日采集抖音客服会话,失败保留进度可安全重试 | 每月 21 日 03:20 | `src/gyxx_flow/modules/shop_intelligence/collectors/dy_customer_service_messages.py` | `dy_customer_service_conversations`、`dy_customer_service_messages`、`dy_customer_service_conversation_orders`(写)<br>`dy_customer_service_products`(读写) |
|
||||||
|
| 京东差评采集 | `shop.jd_negative_reviews.daily` | 按店铺选择日期范围逐日采集京东负面评价,按评价 ID 幂等 | 每月 22 日 03:00 | `src/gyxx_flow/modules/shop_intelligence/collectors/jd_negative_reviews.py` | `jd_negative_reviews`(写) |
|
||||||
|
| 抖音差评采集 | `shop.dy_negative_reviews.daily` | 逐日采集抖音负面评价(评价历史仅保留近 30 天),按订单 ID 幂等 | 每月 22 日 03:20 | `src/gyxx_flow/modules/shop_intelligence/collectors/dy_negative_reviews.py` | `dy_negative_reviews`(写) |
|
||||||
|
| 京东品退问题采集 | `shop.jd_after_sales_issues.daily` | 在商智 31 天限制下按连续区间采集售后服务单,按申请日期写业务日,幂等更新 | 每月 23 日 03:00 | `src/gyxx_flow/modules/shop_intelligence/collectors/jd_after_sales_issues.py` | `jd_after_sales_issues`(写,含范围外残留 DELETE 清理) |
|
||||||
|
| 抖音品退采集 | `shop.douyin_after_sales_quality.daily` | 采集抖音售后单,按申请时间归属业务日,账号 + 售后编号幂等更新 | 每月 23 日 03:20 | `src/gyxx_flow/modules/shop_intelligence/collectors/dy_after_sales_quality.py` | `dy_after_sales_quality_orders`、`dy_after_sales_quality_negotiations`(写) |
|
||||||
|
| 天猫客服消息采集 | `shop.tmall_customer_service.daily` | 按每次最多 31 天的窗口顺序采集天猫客服聊天记录,失败窗口保留数据 | 每月 24 日 03:40(未启用,仅手动) | `src/gyxx_flow/modules/shop_intelligence/collectors/tmall_customer_service_messages.py` | `tmall_customer_service_conversations`、`tmall_customer_service_messages`(写) |
|
||||||
|
| 天猫差评采集 | `shop.tmall_negative_reviews.daily` | 固定筛选负面评价并按评价时间范围采集后入库 | 每月 25 日 03:50(未启用,仅手动) | `src/gyxx_flow/modules/shop_intelligence/collectors/tmall_negative_reviews.py` | `tmall_negative_reviews`(写) |
|
||||||
|
| 抖音待改价申诉 | `shop.douyin_price_appeal` | 每个时点重新扫描可申诉并发起价格申诉;已申诉商品不重复出现 | 每日每小时整点(00:00–23:00) | `src/gyxx_flow/modules/shop_intelligence/collectors/dy_store_competitor_store_scraping.py` | 无(浏览器采集 + 本地 JSON) |
|
||||||
|
|
||||||
|
## 商品电商(product_commerce,12 个)
|
||||||
|
|
||||||
|
| 工作流名称 | ID | 功能 | 定时执行 | 启动脚本 | 关联数据库表 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 商品人群画像周采集 | `product.persona.daily` | 天猫、抖音、京东人群画像并行采集并写入对应飞书多维表 | 每周二 12:00 | `src/gyxx_flow/modules/product_commerce/run_daily_persona.py` | `daily_persona_metrics`(写) |
|
||||||
|
| 商品经营日报采集 | `product.daily` | 按业务日期(运行日 −1)采集 ERP 与三平台数据,幂等写 PostgreSQL,汇总分析后导出并写飞书日报 | 每日 08:40 | `src/gyxx_flow/modules/product_commerce/orchestrate_daily_collection.py` | 写:`product_reviews`、`product_daily_metrics`、`daily_style_metrics`、`fact_style_analysis_reports`、`decline_events`、`fact_alert_history`<br>读:`weekly_style_metrics`、`daily_persona_metrics`、`main_image_creatives`、`cmt_styles`、`cmt_notes_master`、`cmt_comments`、`cmt_creators`(及上述写表) |
|
||||||
|
| 私域库存预警 | `product.online_stock_alert.daily` | 查询线上渠道全部商品,库存低于 3 时汇总 SKU 与库存发飞书预警 | 每日 09:30 | `src/gyxx_flow/modules/product_commerce/online_stock_alert.py` | 无(企业数据交互 HTTP API + 飞书) |
|
||||||
|
| 三平台电商费用采集 | `product.ecommerce_costs.daily` | 并行采集天猫万相台、京东、抖音千川业务日前一天的费用,各平台先下载再导入,单平台失败不阻断其他 | 每日 10:00 | `…/product_commerce/collect_tmall_wanxiang_ads.py`<br>`…/product_commerce/import_tmall_product_ads.py`<br>`…/product_commerce/collect_jd_ad_costs.py`<br>`…/product_commerce/import_jd_ad_costs.py`<br>`…/product_commerce/collect_douyin_qianchuan_ads.py`<br>`…/product_commerce/import_douyin_qianchuan_ads.py` | `product_daily_metrics`(写,天猫原始费用并入 raw_data JSONB)<br>`jd_product_ad_daily_metrics`、`dy_product_ad_daily_metrics`(建+写)<br>`dim_style`(读) |
|
||||||
|
| 京东与天猫主图周采集 | `product.main_image.weekly` | 京东与天猫万相台主图并行采集,各自完成 PG 入库和飞书插入,互不影响 | 每周日 08:30 | `…/product_commerce/run_weekly_jd_main_image.py`<br>`…/product_commerce/run_weekly_main_image.py` | `main_image_creatives`(写) |
|
||||||
|
| 飞书商品月度销量表填写 | `product.sales_sheet.daily` | 抓取并校验全部 ERP 报表后填写飞书月度销量表,同名月份页幂等覆盖,无 ERP 编码产品按 0 写入 | 每月 1 日 10:00 | `src/gyxx_flow/modules/product_commerce/sync_monthly_sales_sheet.py` | 无(聚水潭浏览器采集 + 飞书写入) |
|
||||||
|
| 聚水潭全店铺款式日报 | `product.erp_all_shop_daily` | 聚水潭商品主题分析全选全部店铺,采集业务日前一天所有 ERP 款式的自定义销量与退货量并幂等写 PostgreSQL | 每日 09:00 | `src/gyxx_flow/modules/product_commerce/backfill_erp_all_shop_daily.py` | `erp_all_shop_style_daily_metrics`(读写) |
|
||||||
|
| 客服香港库存工作流 | `product.hk_anpei_public_inventory.daily` | 校验计划书 SKU 与库存表商品编码对应关系,调聚水潭库存接口查询并将公有可用数写入仓库现有库存列 | 每日 09:00 | `src/gyxx_flow/modules/product_commerce/public_sku_inventory_sheet.py` | 无(聚水潭 API + 飞书表格) |
|
||||||
|
| 天猫视频上传 | `product.video_upload` | 按店铺规则扫描飞书视频附件,凭附件级发布账本防重,仅在存在可领取附件时登录光合账号上传 | 每 3 天 14:00 | `src/gyxx_flow/modules/product_commerce/upload_video_to_guanghe.py` | `fact_video_publications`、`fact_video_publication_attempts`(建+读写,附件级发布账本)<br>`dim_style`、`product_daily_metrics`(读) |
|
||||||
|
| 京东视频上传 | `product.jd_video_upload` | 从凌云air基线扫描到视图末尾识别新增视频,以 record_id + file_token 幂等发布到京东 | 每 3 天 14:00 | `src/gyxx_flow/modules/product_commerce/upload_video_to_jd.py` | `fact_video_publications`、`fact_video_publication_attempts`(建+读写,发布账本)<br>`dim_style`、`product_daily_metrics`(读) |
|
||||||
|
| 天猫百亿补贴批量报名 | `product.tmall_baibu_apply` | 每次从飞书下载报名模板逐入口提交;默认仅导入 3 个不带超链入口(含老天猫全渠道 3C 后置完善草稿),可切换全部 6 个入口 | 每日每小时整点(00:00–23:00) | `…/product_commerce/tmall_baibu_apply_old_chain_bag.py`<br>`…/product_commerce/tmall_baibu_apply_old_all_bag.py`<br>`…/product_commerce/tmall_baibu_apply_old_all_3c.py`<br>`…/product_commerce/tmall_baibu_apply_old_chain_3c.py`<br>`…/product_commerce/tmall_baibu_apply_new_chain_bag.py`<br>`…/product_commerce/tmall_baibu_apply_new_all_bag.py` | 无(浏览器操作 + 飞书) |
|
||||||
|
| 三平台市场排行采集(用于行军图) | `product.market_rank` | 并行采集天猫、京东、抖音市场排行,按月份写入品牌行军图数据库并复用已有人工分类 | 每月 1 日 10:00 | `src/gyxx_flow/modules/product_commerce/orchestrate_market_rank_collection.py` | `market_product`、`market_product_classification`(读写)<br>`fact_market_product_snapshot`(写,重跑先 DELETE)<br>`fact_market_rank_reports`、`fact_market_rank_notifications`(建+读写)<br>`dim_market_style`、`dim_market_scene`(读) |
|
||||||
|
|
||||||
|
## 供应链(supply_chain,2 个)
|
||||||
|
|
||||||
|
| 工作流名称 | ID | 功能 | 定时执行 | 启动脚本 | 关联数据库表 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 补货预警日处理 | `supply.replenishment_alert.daily` | 依据配置 SKU 与库存阈值生成预警并写 PostgreSQL,仅存在预警时发送业务通知 | 每日 07:00 | `src/gyxx_flow/modules/supply_chain/run.py` | `fact_threshold_alert`(读写)<br>`dim_product`(写)<br>`workflow_runs`(读写,运行账本)<br>`bitable_insert_log`(读写,通知去重账本) |
|
||||||
|
| 同步聚水潭到货时间 | `supply.replenishment_arrival_sync.daily` | 读取到货同步待处理队列,按商品编码在聚水潭采购单页面更新预计到货时间并回写状态,失败行下次自动重试 | 每日 09:00 | `src/gyxx_flow/modules/supply_chain/orchestrator/scripts/collect_replenishment_arrival_sync.py` | `erp_replenishment_arrival_sync`(读写,队列表由 ERP 应用写入) |
|
||||||
|
|
||||||
|
## 附:已隐藏的工作流(未收录)
|
||||||
|
|
||||||
|
浏览器端隐藏的 10 个 ID:`content.creator_report.monthly`、`content.marketing_report.daily`、`content.metrics.backfill`、`content.relogin.weekly`(配置中已不存在)、`content.summary.monthly`、`content.summary.weekly`、`product.alert.daily`、`product.style_analysis.interval`、`supply.purchase_confirmation.daily`、`supply.replenishment.weekly`。
|
||||||
@@ -20,6 +20,7 @@ PRODUCT_SCHEDULED_WORKFLOW_IDS = (
|
|||||||
"product.main_image.weekly",
|
"product.main_image.weekly",
|
||||||
"product.sales_sheet.daily",
|
"product.sales_sheet.daily",
|
||||||
"product.erp_all_shop_daily",
|
"product.erp_all_shop_daily",
|
||||||
|
"product.hk_anpei_public_inventory.daily",
|
||||||
"product.market_rank",
|
"product.market_rank",
|
||||||
"product.tmall_baibu_apply",
|
"product.tmall_baibu_apply",
|
||||||
"product.video_upload",
|
"product.video_upload",
|
||||||
@@ -45,6 +46,7 @@ _WORKFLOW_SCOPED_RESOURCE_IDS = {
|
|||||||
"product.daily",
|
"product.daily",
|
||||||
"product.persona.daily",
|
"product.persona.daily",
|
||||||
"product.style_analysis.interval",
|
"product.style_analysis.interval",
|
||||||
|
"product.hk_anpei_public_inventory.daily",
|
||||||
"product.ecommerce_costs.daily",
|
"product.ecommerce_costs.daily",
|
||||||
# Tmall and JD video publication use separate browser profiles, accounts,
|
# Tmall and JD video publication use separate browser profiles, accounts,
|
||||||
# and channel-scoped PostgreSQL ledgers; do not let a long publication on
|
# and channel-scoped PostgreSQL ledgers; do not let a long publication on
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from gyxx_flow.modules.content_marketing.data.tools import (
|
|||||||
analyze_comments,
|
analyze_comments,
|
||||||
analyze_note,
|
analyze_note,
|
||||||
daily_report_card,
|
daily_report_card,
|
||||||
friday_relogin_parallel,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -138,15 +137,3 @@ def test_note_report_sends_only_to_wang_yunlong(
|
|||||||
|
|
||||||
assert calls[0]["user_id"] == ANALYZER_ACCEPTANCE_RECIPIENT_OPEN_ID
|
assert calls[0]["user_id"] == ANALYZER_ACCEPTANCE_RECIPIENT_OPEN_ID
|
||||||
assert calls[0]["profile"] == "hermes-analyzer"
|
assert calls[0]["profile"] == "hermes-analyzer"
|
||||||
|
|
||||||
|
|
||||||
def test_relogin_notifications_resolve_only_to_wang_yunlong(
|
|
||||||
monkeypatch,
|
|
||||||
tmp_path: Path,
|
|
||||||
) -> None:
|
|
||||||
_enable_acceptance(monkeypatch, tmp_path)
|
|
||||||
|
|
||||||
assert (
|
|
||||||
friday_relogin_parallel.recipient_for_platform("pgy", "ou_other")
|
|
||||||
== ANALYZER_ACCEPTANCE_RECIPIENT_OPEN_ID
|
|
||||||
)
|
|
||||||
@@ -416,11 +416,9 @@ def test_acceptance_invalid_session_skips_before_qr_login(
|
|||||||
)
|
)
|
||||||
monkeypatch.setenv("GYXX_ACCEPTANCE_EVIDENCE_FILE", str(evidence_file))
|
monkeypatch.setenv("GYXX_ACCEPTANCE_EVIDENCE_FILE", str(evidence_file))
|
||||||
monkeypatch.setattr(module, "is_logged_in", lambda _page: False)
|
monkeypatch.setattr(module, "is_logged_in", lambda _page: False)
|
||||||
monkeypatch.setattr(
|
# 扫码登录函数已整体移除;采集器不得重新引入交互登录。
|
||||||
module,
|
assert not hasattr(module, "wait_for_scan_login")
|
||||||
"wait_for_scan_login",
|
assert not hasattr(module, "open_scan_login")
|
||||||
lambda *_args, **_kwargs: pytest.fail("QR login must remain disabled"),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Page:
|
class Page:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ def test_catalog_composition_registers_all_workflows() -> None:
|
|||||||
"content.metrics.self_operated.daily",
|
"content.metrics.self_operated.daily",
|
||||||
"content.metrics.backfill",
|
"content.metrics.backfill",
|
||||||
"content.marketing_report.daily",
|
"content.marketing_report.daily",
|
||||||
"content.relogin.weekly",
|
|
||||||
"content.creator_report.monthly",
|
"content.creator_report.monthly",
|
||||||
"content.summary.monthly",
|
"content.summary.monthly",
|
||||||
"content.notes_master.daily",
|
"content.notes_master.daily",
|
||||||
@@ -65,6 +64,7 @@ def test_catalog_composition_registers_all_workflows() -> None:
|
|||||||
"product.persona.daily",
|
"product.persona.daily",
|
||||||
"product.daily",
|
"product.daily",
|
||||||
"product.online_stock_alert.daily",
|
"product.online_stock_alert.daily",
|
||||||
|
"product.hk_anpei_public_inventory.daily",
|
||||||
"product.alert.daily",
|
"product.alert.daily",
|
||||||
"product.ecommerce_costs.daily",
|
"product.ecommerce_costs.daily",
|
||||||
"product.style_analysis.interval",
|
"product.style_analysis.interval",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ def test_content_module_registers_only_scheduled_entries() -> None:
|
|||||||
|
|
||||||
definitions = module.workflow_definitions()
|
definitions = module.workflow_definitions()
|
||||||
assert tuple(item.workflow_id for item in definitions) == CONTENT_WORKFLOW_IDS
|
assert tuple(item.workflow_id for item in definitions) == CONTENT_WORKFLOW_IDS
|
||||||
assert len(CONTENT_SCHEDULED_WORKFLOW_IDS) == 10
|
assert len(CONTENT_SCHEDULED_WORKFLOW_IDS) == 9
|
||||||
|
|
||||||
|
|
||||||
def test_content_workflows_use_project_owned_module_commands() -> None:
|
def test_content_workflows_use_project_owned_module_commands() -> None:
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ REQUIRED_SOURCES = {
|
|||||||
"chanmama_scraper.py",
|
"chanmama_scraper.py",
|
||||||
"daily_creator_exposure_scope.py",
|
"daily_creator_exposure_scope.py",
|
||||||
"daily_marketing_report.py",
|
"daily_marketing_report.py",
|
||||||
"data/tools/friday_relogin_parallel.py",
|
|
||||||
"data/tools/schema_gyxx_super_data.sql",
|
"data/tools/schema_gyxx_super_data.sql",
|
||||||
"data/tools/migrations/005_style_product_profile.sql",
|
"data/tools/migrations/005_style_product_profile.sql",
|
||||||
"data/config/db.env.example",
|
"data/config/db.env.example",
|
||||||
@@ -71,7 +70,7 @@ def test_content_source_snapshot_manifest_is_complete_and_well_formed() -> None:
|
|||||||
assert manifest["module"] == "content_marketing"
|
assert manifest["module"] == "content_marketing"
|
||||||
files = manifest["files"]
|
files = manifest["files"]
|
||||||
assert isinstance(files, list)
|
assert isinstance(files, list)
|
||||||
assert len(files) == 66
|
assert len(files) == 59
|
||||||
|
|
||||||
sources = [entry["source_relative_path"] for entry in files]
|
sources = [entry["source_relative_path"] for entry in files]
|
||||||
targets = [entry["target_relative_path"] for entry in files]
|
targets = [entry["target_relative_path"] for entry in files]
|
||||||
|
|||||||
@@ -0,0 +1,771 @@
|
|||||||
|
"""Cookie gate: per-account login-state machine injected before workflows."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from gyxx_flow.adapters.browser import BrowserCookieStore
|
||||||
|
from gyxx_flow.adapters.integration import AccountBinding
|
||||||
|
from gyxx_flow.catalog import WorkflowCatalog, WorkflowEntry
|
||||||
|
from gyxx_flow.cookie_gate import (
|
||||||
|
GATE_MODE_BLOCKING,
|
||||||
|
GATE_MODE_DEFER,
|
||||||
|
GATE_MODE_RECOVER,
|
||||||
|
CookieGate,
|
||||||
|
CookieGateRecoveryManager,
|
||||||
|
apply_cookie_gates,
|
||||||
|
gate_enabled,
|
||||||
|
)
|
||||||
|
from gyxx_flow.core.config import Settings
|
||||||
|
from gyxx_flow.core.context import RunContext
|
||||||
|
from gyxx_flow.core.layout import DataLayout
|
||||||
|
from gyxx_flow.core.locks import LockManager
|
||||||
|
from gyxx_flow.core.records import RunJournal
|
||||||
|
from gyxx_flow.workflow.engine import WorkflowEngine
|
||||||
|
from gyxx_flow.workflow.model import StepDefinition, WorkflowDefinition
|
||||||
|
from gyxx_flow.workflow.steps import StepExecution
|
||||||
|
|
||||||
|
_NOW = datetime(2026, 10, 8, 12, 0, 0, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def _settings(tmp_path: Path) -> Settings:
|
||||||
|
return Settings(project_root=tmp_path, data_root=tmp_path / "var")
|
||||||
|
|
||||||
|
|
||||||
|
def _account(tmp_path: Path, account_id: str, **overrides: object) -> AccountBinding:
|
||||||
|
vault = tmp_path / "state" / "accounts" / account_id
|
||||||
|
values: dict[str, object] = {
|
||||||
|
"account_id": account_id,
|
||||||
|
"cdp_port": 22000,
|
||||||
|
"cdp_url": "http://127.0.0.1:22000",
|
||||||
|
"profile_dir": vault / "profile",
|
||||||
|
"cookie_file": vault / "cookies.json",
|
||||||
|
"storage_state_file": vault / "storage_state.json",
|
||||||
|
"required_cookie_domains": ("example.com",),
|
||||||
|
"required_cookie_names": ("sessionid",),
|
||||||
|
"login_identity_markers": ("示例店铺",),
|
||||||
|
"keepalive_enabled": True,
|
||||||
|
"keepalive_url": "https://example.com/home",
|
||||||
|
"keepalive_interval_seconds": 900,
|
||||||
|
}
|
||||||
|
values.update(overrides)
|
||||||
|
return AccountBinding(**values) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
|
def _fake_catalog(*accounts: AccountBinding, bindings: dict[str, str] | None = None):
|
||||||
|
by_id = {account.account_id: account for account in accounts}
|
||||||
|
binding_accounts = bindings or {}
|
||||||
|
account_tuple = tuple(accounts)
|
||||||
|
|
||||||
|
class _Catalog:
|
||||||
|
accounts = account_tuple
|
||||||
|
|
||||||
|
def account_for(self, account_id: str) -> AccountBinding:
|
||||||
|
return by_id[account_id]
|
||||||
|
|
||||||
|
def binding_for(self, binding_id: str):
|
||||||
|
return SimpleNamespace(account=binding_accounts.get(binding_id, ""))
|
||||||
|
|
||||||
|
return _Catalog()
|
||||||
|
|
||||||
|
|
||||||
|
def _write_valid_vault(account: AccountBinding) -> None:
|
||||||
|
store = BrowserCookieStore(account.cookie_file, account.storage_state_file)
|
||||||
|
store.save_cookies(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "sessionid",
|
||||||
|
"value": "live-session",
|
||||||
|
"domain": ".example.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": _NOW.timestamp() + 86_400,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_keepalive_status(
|
||||||
|
account: AccountBinding,
|
||||||
|
payload: dict[str, object] | None,
|
||||||
|
) -> None:
|
||||||
|
path = account.cookie_file.with_name("keepalive_status.json")
|
||||||
|
if payload is None:
|
||||||
|
path.unlink(missing_ok=True)
|
||||||
|
return
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(json.dumps(payload), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _gate(
|
||||||
|
tmp_path: Path,
|
||||||
|
*accounts: AccountBinding,
|
||||||
|
probe: dict[str, object] | None = None,
|
||||||
|
environment: dict[str, str] | None = None,
|
||||||
|
sent: list[dict[str, object]] | None = None,
|
||||||
|
bindings: dict[str, str] | None = None,
|
||||||
|
) -> CookieGate:
|
||||||
|
probes = probe or {"status": "expired"}
|
||||||
|
keepalive = SimpleNamespace(
|
||||||
|
refresh_account=lambda account_id: dict(probes, account_id=account_id)
|
||||||
|
)
|
||||||
|
sender_calls = sent if sent is not None else []
|
||||||
|
|
||||||
|
def _sender(**kwargs: object) -> dict[str, object]:
|
||||||
|
sender_calls.append(dict(kwargs))
|
||||||
|
return {"message_id": "om_fake"}
|
||||||
|
|
||||||
|
catalog = _fake_catalog(*accounts, bindings=bindings)
|
||||||
|
gate = CookieGate(
|
||||||
|
_settings(tmp_path),
|
||||||
|
catalog=catalog,
|
||||||
|
keepalive=keepalive, # type: ignore[arg-type]
|
||||||
|
environment=environment or {},
|
||||||
|
now=lambda: _NOW,
|
||||||
|
sender=_sender,
|
||||||
|
)
|
||||||
|
gate._workflow_catalog = WorkflowCatalog(timezone="UTC", workflows=(), schedules=())
|
||||||
|
return gate
|
||||||
|
|
||||||
|
|
||||||
|
def _context(**overrides: object) -> RunContext:
|
||||||
|
values: dict[str, object] = {
|
||||||
|
"workflow_id": "shop.demo.daily",
|
||||||
|
"business_date": "2026-10-08",
|
||||||
|
"shadow": False,
|
||||||
|
"now": _NOW,
|
||||||
|
"random_suffix": "aaaa",
|
||||||
|
}
|
||||||
|
values.update(overrides)
|
||||||
|
return RunContext.create(**values) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
|
def _noop_outcome(*, context, timeout_seconds, dry_run) -> StepExecution: # noqa: ANN001
|
||||||
|
return StepExecution(exit_code=0)
|
||||||
|
|
||||||
|
|
||||||
|
def _deny_outcome(*, context, timeout_seconds, dry_run) -> StepExecution: # noqa: ANN001
|
||||||
|
raise AssertionError("resolution must not run actions")
|
||||||
|
|
||||||
|
|
||||||
|
class _PassAction:
|
||||||
|
execute = staticmethod(_noop_outcome)
|
||||||
|
|
||||||
|
|
||||||
|
class _BoundAction:
|
||||||
|
"""Duck-typed DeferredModuleCommandStep for account resolution tests."""
|
||||||
|
|
||||||
|
def __init__(self, command_entry: str = "collect.py") -> None:
|
||||||
|
self.entry = SimpleNamespace(module="shop_intelligence", entry="fallback.py")
|
||||||
|
self.command_entry = command_entry
|
||||||
|
self.execute = _deny_outcome
|
||||||
|
|
||||||
|
|
||||||
|
class _RecordingAction:
|
||||||
|
def __init__(self, name: str, sink: list[str], command_entry: str | None = None) -> None:
|
||||||
|
def run(*, context, timeout_seconds, dry_run) -> StepExecution: # noqa: ANN001
|
||||||
|
sink.append(name)
|
||||||
|
return StepExecution(exit_code=0)
|
||||||
|
|
||||||
|
self.execute = run
|
||||||
|
if command_entry is not None:
|
||||||
|
self.entry = SimpleNamespace(
|
||||||
|
module="shop_intelligence", entry="fallback.py"
|
||||||
|
)
|
||||||
|
self.command_entry = command_entry
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Mode classification
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_gate_mode_classifies_accounts(tmp_path: Path) -> None:
|
||||||
|
gate = _gate(
|
||||||
|
tmp_path,
|
||||||
|
_account(tmp_path, "qr-only"),
|
||||||
|
_account(tmp_path, "cred-scripts", credential_env_names=("A_USER", "A_PASS")),
|
||||||
|
_account(
|
||||||
|
tmp_path,
|
||||||
|
"cred-vault",
|
||||||
|
login_mode="C",
|
||||||
|
login_command_id="product_commerce:erp_login.py",
|
||||||
|
),
|
||||||
|
_account(tmp_path, "cred-mode-b", login_mode="B"),
|
||||||
|
)
|
||||||
|
assert gate.gate_mode("qr-only") == GATE_MODE_BLOCKING
|
||||||
|
assert gate.gate_mode("cred-scripts") == GATE_MODE_DEFER
|
||||||
|
assert gate.gate_mode("cred-vault") == GATE_MODE_RECOVER
|
||||||
|
assert gate.gate_mode("cred-mode-b") == GATE_MODE_DEFER
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Workflow account resolution
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _bound_workflow(workflow_id: str = "shop.demo.daily") -> WorkflowDefinition:
|
||||||
|
return WorkflowDefinition(
|
||||||
|
workflow_id,
|
||||||
|
(
|
||||||
|
StepDefinition("collect", _BoundAction("collect.py")),
|
||||||
|
StepDefinition("sync", _BoundAction("sync.py"), depends_on=("collect",)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_accounts_maps_bindings_and_skips_defer(tmp_path: Path) -> None:
|
||||||
|
gate = _gate(
|
||||||
|
tmp_path,
|
||||||
|
_account(tmp_path, "douyin-shop"),
|
||||||
|
_account(tmp_path, "jd-shop", credential_env_names=("JD_SHOP", "JD_PASSWORD")),
|
||||||
|
bindings={
|
||||||
|
"shop_intelligence:collect.py": "douyin-shop",
|
||||||
|
"shop_intelligence:other.py": "jd-shop",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
mapping = gate.collect_workflow_accounts(_bound_workflow())
|
||||||
|
# sync.py 无账号绑定,jd-shop 属 defer 模式:两者都不进映射
|
||||||
|
assert mapping == {"douyin-shop": {"collect"}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_accounts_includes_declared_required_accounts(tmp_path: Path) -> None:
|
||||||
|
gate = _gate(tmp_path, _account(tmp_path, "douyin-shop"))
|
||||||
|
gate._workflow_catalog = WorkflowCatalog(
|
||||||
|
timezone="UTC",
|
||||||
|
workflows=(
|
||||||
|
WorkflowEntry(
|
||||||
|
workflow_id="shop.demo.daily",
|
||||||
|
module="shop_intelligence",
|
||||||
|
trigger="manual",
|
||||||
|
entry="collect.py",
|
||||||
|
required_accounts=("douyin-shop",),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
schedules=(),
|
||||||
|
)
|
||||||
|
mapping = gate.collect_workflow_accounts(_bound_workflow())
|
||||||
|
assert mapping == {"douyin-shop": {"collect", "sync"}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_cookie_gates_rewrites_dependencies(tmp_path: Path) -> None:
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
gate = _gate(
|
||||||
|
tmp_path,
|
||||||
|
account,
|
||||||
|
bindings={"shop_intelligence:collect.py": "douyin-shop"},
|
||||||
|
)
|
||||||
|
gated = apply_cookie_gates(
|
||||||
|
_bound_workflow(), settings=_settings(tmp_path), gate=gate
|
||||||
|
)
|
||||||
|
ids = [step.step_id for step in gated.steps]
|
||||||
|
assert "cookie-gate.douyin-shop" in ids
|
||||||
|
collect = next(step for step in gated.steps if step.step_id == "collect")
|
||||||
|
sync = next(step for step in gated.steps if step.step_id == "sync")
|
||||||
|
assert collect.depends_on == ("cookie-gate.douyin-shop",)
|
||||||
|
assert sync.depends_on == ("collect",)
|
||||||
|
assert gated.ordered_steps() # patched DAG stays valid
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_cookie_gates_returns_workflow_without_accounts(tmp_path: Path) -> None:
|
||||||
|
gate = _gate(tmp_path, _account(tmp_path, "douyin-shop"))
|
||||||
|
workflow = _bound_workflow()
|
||||||
|
assert (
|
||||||
|
apply_cookie_gates(workflow, settings=_settings(tmp_path), gate=gate) is workflow
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_gate_enabled_env_kill_switch() -> None:
|
||||||
|
assert gate_enabled({})
|
||||||
|
assert not gate_enabled({"GYXX_COOKIE_GATE": "0"})
|
||||||
|
assert not gate_enabled({"GYXX_COOKIE_GATE": "off"})
|
||||||
|
assert gate_enabled({"GYXX_COOKIE_GATE": "1"})
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# State machine: check()
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_fast_path_accepts_fresh_keepalive_without_probe(tmp_path: Path) -> None:
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
gate = _gate(tmp_path, account, probe={"status": "expired"})
|
||||||
|
_write_valid_vault(account)
|
||||||
|
_write_keepalive_status(
|
||||||
|
account,
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"last_success_at": (_NOW - timedelta(minutes=10)).isoformat(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
verdict = gate.check("douyin-shop", context=_context())
|
||||||
|
assert not verdict.blocked
|
||||||
|
assert "keepalive" in verdict.message
|
||||||
|
state = json.loads(
|
||||||
|
(account.cookie_file.parent / "cookie_state.json").read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
assert state["state"] == "healthy"
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_stale_status_triggers_probe(tmp_path: Path) -> None:
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
gate = _gate(tmp_path, account, probe={"status": "success"})
|
||||||
|
_write_valid_vault(account)
|
||||||
|
_write_keepalive_status(
|
||||||
|
account,
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"last_success_at": (_NOW - timedelta(hours=6)).isoformat(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
verdict = gate.check("douyin-shop", context=_context())
|
||||||
|
assert not verdict.blocked
|
||||||
|
assert verdict.message.endswith("探活通过)")
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_expired_blocks_and_sends_feishu_alert(tmp_path: Path) -> None:
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
sent: list[dict[str, object]] = []
|
||||||
|
gate = _gate(
|
||||||
|
tmp_path,
|
||||||
|
account,
|
||||||
|
probe={"status": "expired"},
|
||||||
|
environment={"GYXX_LOGIN_ALERT_OPEN_IDS": "ou_alert_target"},
|
||||||
|
sent=sent,
|
||||||
|
)
|
||||||
|
_write_keepalive_status(account, None)
|
||||||
|
verdict = gate.check("douyin-shop", context=_context())
|
||||||
|
assert verdict.blocked
|
||||||
|
assert verdict.notified
|
||||||
|
assert len(sent) == 1
|
||||||
|
text = str(sent[0]["text"])
|
||||||
|
assert "uv run gyxx accounts login douyin-shop" in text
|
||||||
|
assert "shop.demo.daily" in text
|
||||||
|
assert sent[0]["profile"] == "hermes-analyzer"
|
||||||
|
state = json.loads(
|
||||||
|
(account.cookie_file.parent / "cookie_state.json").read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
assert state["state"] == "awaiting_login"
|
||||||
|
assert state["last_notified_at"] == _NOW.isoformat(timespec="seconds")
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_notification_is_throttled_per_interval(tmp_path: Path) -> None:
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
sent: list[dict[str, object]] = []
|
||||||
|
gate = _gate(
|
||||||
|
tmp_path,
|
||||||
|
account,
|
||||||
|
probe={"status": "expired"},
|
||||||
|
environment={"GYXX_LOGIN_ALERT_OPEN_IDS": "ou_alert_target"},
|
||||||
|
sent=sent,
|
||||||
|
)
|
||||||
|
_write_keepalive_status(account, None)
|
||||||
|
first = gate.check("douyin-shop", context=_context())
|
||||||
|
second = gate.check("douyin-shop", context=_context())
|
||||||
|
assert first.notified and second.blocked
|
||||||
|
assert not second.notified
|
||||||
|
assert len(sent) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_shadow_run_never_notifies(tmp_path: Path) -> None:
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
sent: list[dict[str, object]] = []
|
||||||
|
gate = _gate(
|
||||||
|
tmp_path,
|
||||||
|
account,
|
||||||
|
probe={"status": "expired"},
|
||||||
|
environment={"GYXX_LOGIN_ALERT_OPEN_IDS": "ou_alert_target"},
|
||||||
|
sent=sent,
|
||||||
|
)
|
||||||
|
verdict = gate.check("douyin-shop", context=_context(shadow=True))
|
||||||
|
assert verdict.blocked
|
||||||
|
assert not verdict.notified
|
||||||
|
assert sent == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_mode_c_recovery_success_and_failure_paths(tmp_path: Path) -> None:
|
||||||
|
account = _account(
|
||||||
|
tmp_path,
|
||||||
|
"erp-jushuitan",
|
||||||
|
login_mode="C",
|
||||||
|
login_command_id="product_commerce:erp_login.py",
|
||||||
|
)
|
||||||
|
recovered = _gate(
|
||||||
|
tmp_path, account, probe={"status": "success", "login_recovered": True}
|
||||||
|
)
|
||||||
|
_write_keepalive_status(account, None)
|
||||||
|
verdict = recovered.check("erp-jushuitan", context=_context())
|
||||||
|
assert not verdict.blocked
|
||||||
|
assert verdict.recovered
|
||||||
|
assert "自动账密重登成功" in verdict.message
|
||||||
|
|
||||||
|
sent: list[dict[str, object]] = []
|
||||||
|
failed = _gate(
|
||||||
|
tmp_path,
|
||||||
|
account,
|
||||||
|
probe={"status": "not_logged_in"},
|
||||||
|
environment={"GYXX_LOGIN_ALERT_OPEN_IDS": "ou_alert_target"},
|
||||||
|
sent=sent,
|
||||||
|
)
|
||||||
|
_write_keepalive_status(
|
||||||
|
failed.catalog.account_for("erp-jushuitan"),
|
||||||
|
{
|
||||||
|
"status": "not_logged_in",
|
||||||
|
"login_attempted": True,
|
||||||
|
"login_error_type": "TimeoutExpired",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
verdict = failed.check("erp-jushuitan", context=_context())
|
||||||
|
assert verdict.blocked
|
||||||
|
assert verdict.notified
|
||||||
|
assert "自动账密重登已尝试但未成功" in str(sent[0]["text"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_busy_falls_back_to_static_evidence(tmp_path: Path) -> None:
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
gate = _gate(tmp_path, account, probe={"status": "busy"})
|
||||||
|
_write_valid_vault(account)
|
||||||
|
_write_keepalive_status(account, None)
|
||||||
|
verdict = gate.check("douyin-shop", context=_context())
|
||||||
|
assert not verdict.blocked
|
||||||
|
assert "探活锁忙" in verdict.message
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_probe_error_with_static_valid_vault_passes(tmp_path: Path) -> None:
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
gate = _gate(tmp_path, account, probe={"status": "timeout"})
|
||||||
|
_write_valid_vault(account)
|
||||||
|
_write_keepalive_status(account, None)
|
||||||
|
verdict = gate.check("douyin-shop", context=_context())
|
||||||
|
assert not verdict.blocked
|
||||||
|
assert "探活异常" in verdict.message
|
||||||
|
|
||||||
|
|
||||||
|
def test_acceptance_mode_is_static_only_and_never_alerts(tmp_path: Path) -> None:
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
sent: list[dict[str, object]] = []
|
||||||
|
gate = _gate(
|
||||||
|
tmp_path,
|
||||||
|
account,
|
||||||
|
probe={"status": "expired"},
|
||||||
|
environment={
|
||||||
|
"GYXX_WORKFLOW_ACCEPTANCE": "1",
|
||||||
|
"GYXX_LOGIN_ALERT_OPEN_IDS": "ou_alert_target",
|
||||||
|
},
|
||||||
|
sent=sent,
|
||||||
|
)
|
||||||
|
_write_keepalive_status(account, None)
|
||||||
|
verdict = gate.check("douyin-shop", context=_context())
|
||||||
|
assert verdict.blocked
|
||||||
|
assert sent == []
|
||||||
|
assert "验收模式" in verdict.message
|
||||||
|
|
||||||
|
|
||||||
|
def test_alert_recipient_falls_back_to_constant(tmp_path: Path) -> None:
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
gate = _gate(tmp_path, account)
|
||||||
|
recipients = gate._alert_recipients()
|
||||||
|
assert recipients == [
|
||||||
|
("ou_7ad5fc8012e2f741afc5346e05ffd447", "hermes-analyzer")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Engine integration
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _engine(tmp_path: Path) -> WorkflowEngine:
|
||||||
|
return WorkflowEngine(LockManager(tmp_path / "state" / "locks"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_engine_blocks_dependents_when_gate_fails(tmp_path: Path) -> None:
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
sent: list[dict[str, object]] = []
|
||||||
|
gate = _gate(
|
||||||
|
tmp_path,
|
||||||
|
account,
|
||||||
|
probe={"status": "expired"},
|
||||||
|
environment={"GYXX_LOGIN_ALERT_OPEN_IDS": "ou_alert_target"},
|
||||||
|
sent=sent,
|
||||||
|
bindings={"shop_intelligence:collect.py": "douyin-shop"},
|
||||||
|
)
|
||||||
|
executed: list[str] = []
|
||||||
|
workflow = WorkflowDefinition(
|
||||||
|
"shop.demo.daily",
|
||||||
|
(
|
||||||
|
StepDefinition(
|
||||||
|
"collect", _RecordingAction("collect", executed, "collect.py")
|
||||||
|
),
|
||||||
|
StepDefinition(
|
||||||
|
"sync", _RecordingAction("sync", executed), depends_on=("collect",)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
gated = apply_cookie_gates(workflow, settings=_settings(tmp_path), gate=gate)
|
||||||
|
context = RunContext.create(
|
||||||
|
"shop.demo.daily", "2026-10-08", now=_NOW, random_suffix="dddd"
|
||||||
|
)
|
||||||
|
journal = RunJournal.create(DataLayout(_settings(tmp_path).data_root), context)
|
||||||
|
engine = _engine(tmp_path)
|
||||||
|
run_workflow = engine.execute
|
||||||
|
result = run_workflow(gated, context=context, journal=journal)
|
||||||
|
|
||||||
|
assert result.status == "failed"
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert result.steps["cookie-gate.douyin-shop"].status == "failed"
|
||||||
|
assert result.steps["collect"].reason == "dependency-failed"
|
||||||
|
assert result.steps["sync"].reason == "dependency-failed"
|
||||||
|
assert executed == []
|
||||||
|
assert len(sent) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_engine_runs_collect_steps_when_gate_passes(tmp_path: Path) -> None:
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
gate = _gate(
|
||||||
|
tmp_path,
|
||||||
|
account,
|
||||||
|
probe={"status": "success"},
|
||||||
|
bindings={"shop_intelligence:collect.py": "douyin-shop"},
|
||||||
|
)
|
||||||
|
_write_valid_vault(account)
|
||||||
|
_write_keepalive_status(account, None)
|
||||||
|
executed: list[str] = []
|
||||||
|
workflow = WorkflowDefinition(
|
||||||
|
"shop.demo.daily",
|
||||||
|
(
|
||||||
|
StepDefinition(
|
||||||
|
"collect", _RecordingAction("collect", executed, "collect.py")
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
gated = apply_cookie_gates(workflow, settings=_settings(tmp_path), gate=gate)
|
||||||
|
context = RunContext.create(
|
||||||
|
"shop.demo.daily", "2026-10-08", now=_NOW, random_suffix="eeee"
|
||||||
|
)
|
||||||
|
journal = RunJournal.create(DataLayout(_settings(tmp_path).data_root), context)
|
||||||
|
engine = _engine(tmp_path)
|
||||||
|
run_workflow = engine.execute
|
||||||
|
result = run_workflow(gated, context=context, journal=journal)
|
||||||
|
|
||||||
|
assert result.status == "success"
|
||||||
|
assert result.steps["cookie-gate.douyin-shop"].status == "success"
|
||||||
|
assert result.steps["collect"].status == "success"
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Recovery: automatic rerun after the login recovers
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeLauncher:
|
||||||
|
def __init__(self, fail: bool = False) -> None:
|
||||||
|
self.launched: list[tuple[str, str]] = []
|
||||||
|
self.closed: list[int] = []
|
||||||
|
self.fail = fail
|
||||||
|
|
||||||
|
def launch(self, workflow_id, slot, business_date): # noqa: ANN001
|
||||||
|
if self.fail:
|
||||||
|
raise RuntimeError("spawn denied")
|
||||||
|
process = SimpleNamespace(pid=9000 + len(self.launched), poll=lambda: None)
|
||||||
|
self.launched.append((workflow_id, business_date.isoformat()))
|
||||||
|
return process
|
||||||
|
|
||||||
|
def close(self, process) -> None: # noqa: ANN001
|
||||||
|
self.closed.append(process.pid)
|
||||||
|
|
||||||
|
|
||||||
|
def _state_file(account: AccountBinding) -> Path:
|
||||||
|
return account.cookie_file.parent / "cookie_state.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _read_state(account: AccountBinding) -> dict:
|
||||||
|
return json.loads(_state_file(account).read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def _recovery_workflow_catalog() -> WorkflowCatalog:
|
||||||
|
return WorkflowCatalog(
|
||||||
|
timezone="UTC",
|
||||||
|
workflows=(
|
||||||
|
WorkflowEntry(
|
||||||
|
workflow_id="shop.demo.daily",
|
||||||
|
module="shop_intelligence",
|
||||||
|
trigger="scheduled",
|
||||||
|
entry="collect.py",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
schedules=(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _blocking_gate(
|
||||||
|
tmp_path: Path,
|
||||||
|
account: AccountBinding,
|
||||||
|
sent: list[dict[str, object]],
|
||||||
|
) -> CookieGate:
|
||||||
|
gate = _gate(
|
||||||
|
tmp_path,
|
||||||
|
account,
|
||||||
|
probe={"status": "expired"},
|
||||||
|
environment={"GYXX_LOGIN_ALERT_OPEN_IDS": "ou_alert_target"},
|
||||||
|
sent=sent,
|
||||||
|
)
|
||||||
|
return gate
|
||||||
|
|
||||||
|
|
||||||
|
def _healthy_gate(
|
||||||
|
tmp_path: Path,
|
||||||
|
account: AccountBinding,
|
||||||
|
sent: list[dict[str, object]],
|
||||||
|
) -> CookieGate:
|
||||||
|
gate = _gate(
|
||||||
|
tmp_path,
|
||||||
|
account,
|
||||||
|
probe={"status": "success"},
|
||||||
|
environment={"GYXX_LOGIN_ALERT_OPEN_IDS": "ou_alert_target"},
|
||||||
|
sent=sent,
|
||||||
|
)
|
||||||
|
gate._workflow_catalog = _recovery_workflow_catalog()
|
||||||
|
return gate
|
||||||
|
|
||||||
|
|
||||||
|
def _simulate_manual_login(account: AccountBinding) -> None:
|
||||||
|
_write_valid_vault(account)
|
||||||
|
_write_keepalive_status(
|
||||||
|
account,
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"last_success_at": (_NOW - timedelta(seconds=60)).isoformat(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_expire_records_pending_reruns(tmp_path: Path) -> None:
|
||||||
|
sent: list[dict[str, object]] = []
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
gate = _blocking_gate(tmp_path, account, sent)
|
||||||
|
gate.check("douyin-shop", context=_context())
|
||||||
|
gate.check("douyin-shop", context=_context()) # 同工作流同日期去重
|
||||||
|
gate.check("douyin-shop", context=_context(business_date="2026-10-09"))
|
||||||
|
state = _read_state(account)
|
||||||
|
assert [
|
||||||
|
(item["workflow_id"], item["business_date"])
|
||||||
|
for item in state["pending_reruns"]
|
||||||
|
] == [
|
||||||
|
("shop.demo.daily", "2026-10-08"),
|
||||||
|
("shop.demo.daily", "2026-10-09"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# shadow 运行的阻断不进入待重跑清单
|
||||||
|
gate.check(
|
||||||
|
"douyin-shop", context=_context(business_date="2026-10-10", shadow=True)
|
||||||
|
)
|
||||||
|
state = _read_state(account)
|
||||||
|
assert all(
|
||||||
|
item["business_date"] != "2026-10-10" for item in state["pending_reruns"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_recovery_reruns_blocked_workflows_after_login(tmp_path: Path) -> None:
|
||||||
|
sent: list[dict[str, object]] = []
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
blocked = _blocking_gate(tmp_path, account, sent)
|
||||||
|
blocked.check("douyin-shop", context=_context())
|
||||||
|
|
||||||
|
_simulate_manual_login(account)
|
||||||
|
healthy = _healthy_gate(tmp_path, account, sent)
|
||||||
|
launcher = _FakeLauncher()
|
||||||
|
outcomes = CookieGateRecoveryManager(healthy, launcher=launcher).tick()
|
||||||
|
|
||||||
|
assert launcher.launched == [("shop.demo.daily", "2026-10-08")]
|
||||||
|
assert launcher.closed # 父进程日志句柄立即关闭
|
||||||
|
assert outcomes and outcomes[0]["launched"]
|
||||||
|
state = _read_state(account)
|
||||||
|
assert state["pending_reruns"] == []
|
||||||
|
assert state["state"] == "healthy"
|
||||||
|
assert state["last_rerun_launch_at"]
|
||||||
|
assert state["rerun_history"][0]["workflow_id"] == "shop.demo.daily"
|
||||||
|
assert any("登录态已恢复" in str(item["text"]) for item in sent)
|
||||||
|
|
||||||
|
|
||||||
|
def test_recovery_skips_while_login_still_expired(tmp_path: Path) -> None:
|
||||||
|
sent: list[dict[str, object]] = []
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
blocked = _blocking_gate(tmp_path, account, sent)
|
||||||
|
blocked.check("douyin-shop", context=_context())
|
||||||
|
_write_keepalive_status(account, None)
|
||||||
|
|
||||||
|
still_expired = _gate(
|
||||||
|
tmp_path,
|
||||||
|
account,
|
||||||
|
probe={"status": "expired"},
|
||||||
|
environment={"GYXX_LOGIN_ALERT_OPEN_IDS": "ou_alert_target"},
|
||||||
|
sent=sent,
|
||||||
|
)
|
||||||
|
still_expired._workflow_catalog = _recovery_workflow_catalog()
|
||||||
|
launcher = _FakeLauncher()
|
||||||
|
outcomes = CookieGateRecoveryManager(still_expired, launcher=launcher).tick()
|
||||||
|
|
||||||
|
assert launcher.launched == []
|
||||||
|
assert outcomes == []
|
||||||
|
state = _read_state(account)
|
||||||
|
assert len(state["pending_reruns"]) == 1
|
||||||
|
assert state["last_recovery_probe_at"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_recovery_drops_adhoc_script_ids(tmp_path: Path) -> None:
|
||||||
|
sent: list[dict[str, object]] = []
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
blocked = _blocking_gate(tmp_path, account, sent)
|
||||||
|
blocked.check(
|
||||||
|
"douyin-shop", context=_context(workflow_id="script.shop.abcdef123456")
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
_read_state(account)["pending_reruns"][0]["workflow_id"]
|
||||||
|
== "script.shop.abcdef123456"
|
||||||
|
)
|
||||||
|
|
||||||
|
_simulate_manual_login(account)
|
||||||
|
healthy = _healthy_gate(tmp_path, account, sent)
|
||||||
|
launcher = _FakeLauncher()
|
||||||
|
CookieGateRecoveryManager(healthy, launcher=launcher).tick()
|
||||||
|
|
||||||
|
assert launcher.launched == []
|
||||||
|
assert _read_state(account)["pending_reruns"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_recovery_launch_failure_keeps_pending_for_retry(tmp_path: Path) -> None:
|
||||||
|
sent: list[dict[str, object]] = []
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
blocked = _blocking_gate(tmp_path, account, sent)
|
||||||
|
blocked.check("douyin-shop", context=_context())
|
||||||
|
_simulate_manual_login(account)
|
||||||
|
|
||||||
|
healthy = _healthy_gate(tmp_path, account, sent)
|
||||||
|
launcher = _FakeLauncher(fail=True)
|
||||||
|
CookieGateRecoveryManager(healthy, launcher=launcher).tick()
|
||||||
|
|
||||||
|
state = _read_state(account)
|
||||||
|
assert len(state["pending_reruns"]) == 1
|
||||||
|
assert "error" in state["pending_reruns"][0]
|
||||||
|
assert state["state"] == "awaiting_login"
|
||||||
|
assert not any("已恢复" in str(item["text"]) for item in sent)
|
||||||
|
|
||||||
|
|
||||||
|
def test_recovery_respects_launch_cooldown(tmp_path: Path) -> None:
|
||||||
|
sent: list[dict[str, object]] = []
|
||||||
|
account = _account(tmp_path, "douyin-shop")
|
||||||
|
blocked = _blocking_gate(tmp_path, account, sent)
|
||||||
|
blocked.check("douyin-shop", context=_context())
|
||||||
|
_simulate_manual_login(account)
|
||||||
|
state = _read_state(account)
|
||||||
|
state["last_rerun_launch_at"] = _NOW.isoformat(timespec="seconds")
|
||||||
|
_state_file(account).write_text(json.dumps(state), encoding="utf-8")
|
||||||
|
|
||||||
|
healthy = _healthy_gate(tmp_path, account, sent)
|
||||||
|
launcher = _FakeLauncher()
|
||||||
|
CookieGateRecoveryManager(healthy, launcher=launcher).tick()
|
||||||
|
|
||||||
|
assert launcher.launched == []
|
||||||
@@ -67,15 +67,21 @@ def test_baseline_contains_all_provided_people_and_routable_capabilities(
|
|||||||
) -> None:
|
) -> None:
|
||||||
payload, revision = NotificationRoutingStore(routing_settings).snapshot()
|
payload, revision = NotificationRoutingStore(routing_settings).snapshot()
|
||||||
|
|
||||||
assert len(payload["people"]) == 16
|
assert len(payload["people"]) == 18
|
||||||
assert payload["app_profile"] == "hermes-analyzer"
|
assert payload["app_profile"] == "hermes-analyzer"
|
||||||
assert (
|
assert (
|
||||||
payload["app_profiles"]["hermes-analyzer"]["app_id"]
|
payload["app_profiles"]["hermes-analyzer"]["app_id"]
|
||||||
== ANALYZER_NOTIFICATION_APP_ID
|
== ANALYZER_NOTIFICATION_APP_ID
|
||||||
)
|
)
|
||||||
assert all(person["mobile"] is None for person in payload["people"].values())
|
mobiles = {
|
||||||
|
person_id: person["mobile"]
|
||||||
|
for person_id, person in payload["people"].items()
|
||||||
|
}
|
||||||
|
assert mobiles["wang_yunlong"] == "+8613663785145"
|
||||||
|
assert mobiles["stock_alert_user_19832771651"] == "+8619832771651"
|
||||||
assert set(payload["capabilities"]) == set(NOTIFICATION_CAPABILITIES)
|
assert set(payload["capabilities"]) == set(NOTIFICATION_CAPABILITIES)
|
||||||
assert payload["routes"] == {}
|
assert set(payload["routes"]) == {"product.online_stock_alert.daily"}
|
||||||
|
assert payload["routes"]["product.online_stock_alert.daily"]["enabled"] is True
|
||||||
assert len(revision) == 64
|
assert len(revision) == 64
|
||||||
|
|
||||||
wang = payload["people"]["wang_yunlong"]
|
wang = payload["people"]["wang_yunlong"]
|
||||||
@@ -85,10 +91,15 @@ def test_baseline_contains_all_provided_people_and_routable_capabilities(
|
|||||||
assert wang["bindings"]["hermes-analyzer"]["open_id"] == (
|
assert wang["bindings"]["hermes-analyzer"]["open_id"] == (
|
||||||
"ou_7ad5fc8012e2f741afc5346e05ffd447"
|
"ou_7ad5fc8012e2f741afc5346e05ffd447"
|
||||||
)
|
)
|
||||||
|
_mobile_lookup_people = {
|
||||||
|
"wang_yunlong",
|
||||||
|
"online_stock_alert_recipient",
|
||||||
|
"stock_alert_user_19832771651",
|
||||||
|
}
|
||||||
provided = {
|
provided = {
|
||||||
person["name"]: binding["open_id"]
|
person["name"]: binding["open_id"]
|
||||||
for person_id, person in payload["people"].items()
|
for person_id, person in payload["people"].items()
|
||||||
if person_id != "wang_yunlong"
|
if person_id not in _mobile_lookup_people
|
||||||
for profile, binding in person["bindings"].items()
|
for profile, binding in person["bindings"].items()
|
||||||
if profile == "hermes-analyzer"
|
if profile == "hermes-analyzer"
|
||||||
}
|
}
|
||||||
@@ -112,7 +123,7 @@ def test_baseline_contains_all_provided_people_and_routable_capabilities(
|
|||||||
assert all(
|
assert all(
|
||||||
binding["verified"] is False and binding["source"] == "provided"
|
binding["verified"] is False and binding["source"] == "provided"
|
||||||
for person_id, person in payload["people"].items()
|
for person_id, person in payload["people"].items()
|
||||||
if person_id != "wang_yunlong"
|
if person_id not in _mobile_lookup_people
|
||||||
for profile, binding in person["bindings"].items()
|
for profile, binding in person["bindings"].items()
|
||||||
if profile == "hermes-analyzer"
|
if profile == "hermes-analyzer"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ def test_product_module_registers_scheduled_and_manual_entries() -> None:
|
|||||||
WorkflowCatalog.load(PROJECT_ROOT / "config")
|
WorkflowCatalog.load(PROJECT_ROOT / "config")
|
||||||
)
|
)
|
||||||
assert tuple(item.workflow_id for item in module.workflow_definitions()) == PRODUCT_WORKFLOW_IDS
|
assert tuple(item.workflow_id for item in module.workflow_definitions()) == PRODUCT_WORKFLOW_IDS
|
||||||
assert len(PRODUCT_SCHEDULED_WORKFLOW_IDS) == 12
|
assert len(PRODUCT_SCHEDULED_WORKFLOW_IDS) == 14
|
||||||
assert PRODUCT_MANUAL_WORKFLOW_IDS == ()
|
assert PRODUCT_MANUAL_WORKFLOW_IDS == ()
|
||||||
|
|
||||||
|
|
||||||
@@ -78,7 +78,10 @@ def test_product_workflows_use_project_owned_module_commands() -> None:
|
|||||||
assert definition.steps
|
assert definition.steps
|
||||||
for step in definition.steps:
|
for step in definition.steps:
|
||||||
assert isinstance(step.action, DeferredModuleCommandStep)
|
assert isinstance(step.action, DeferredModuleCommandStep)
|
||||||
assert step.timeout_seconds == PRODUCT_TIMEOUT_SECONDS == 6 * 60 * 60
|
assert step.timeout_seconds == PRODUCT_TIMEOUT_SECONDS == 6 * 60 * 60 or (
|
||||||
|
definition.workflow_id == "product.hk_anpei_public_inventory.daily"
|
||||||
|
and step.timeout_seconds == 900.0
|
||||||
|
)
|
||||||
assert step.max_attempts == 1
|
assert step.max_attempts == 1
|
||||||
if definition.workflow_id == "product.ecommerce_costs.daily":
|
if definition.workflow_id == "product.ecommerce_costs.daily":
|
||||||
assert step.resources == (
|
assert step.resources == (
|
||||||
@@ -93,6 +96,7 @@ def test_product_workflows_use_project_owned_module_commands() -> None:
|
|||||||
"product.video_upload",
|
"product.video_upload",
|
||||||
"product.jd_video_upload",
|
"product.jd_video_upload",
|
||||||
"product.tmall_baibu_apply",
|
"product.tmall_baibu_apply",
|
||||||
|
"product.hk_anpei_public_inventory.daily",
|
||||||
}:
|
}:
|
||||||
assert step.resources == (
|
assert step.resources == (
|
||||||
f"{PRODUCT_RESOURCE}:{definition.workflow_id}",
|
f"{PRODUCT_RESOURCE}:{definition.workflow_id}",
|
||||||
@@ -219,7 +223,6 @@ def test_jd_video_upload_workflow_allows_idempotent_rescans() -> None:
|
|||||||
assert step.action.command_args == (
|
assert step.action.command_args == (
|
||||||
"--anchor-record-id",
|
"--anchor-record-id",
|
||||||
"recvrXTRWWFXeJ",
|
"recvrXTRWWFXeJ",
|
||||||
"--manual-login",
|
|
||||||
"--execute",
|
"--execute",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -53,10 +53,10 @@ def test_catalog_and_scripts_work_after_project_and_data_roots_move(
|
|||||||
|
|
||||||
assert workflows.returncode == 0, workflows.stderr
|
assert workflows.returncode == 0, workflows.stderr
|
||||||
assert scripts.returncode == 0, scripts.stderr
|
assert scripts.returncode == 0, scripts.stderr
|
||||||
assert len(json.loads(workflows.stdout)) == 35
|
assert len(json.loads(workflows.stdout)) == 39
|
||||||
commands = json.loads(scripts.stdout)
|
commands = json.loads(scripts.stdout)
|
||||||
assert len(commands) == 55
|
assert len(commands) == 60
|
||||||
assert len({row["command_id"] for row in commands}) == 55
|
assert len({row["command_id"] for row in commands}) == 60
|
||||||
assert all(row["legacy_script_id"].startswith(f"{row['module']}:") for row in commands)
|
assert all(row["legacy_script_id"].startswith(f"{row['module']}:") for row in commands)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -80,10 +80,10 @@ def test_default_registry_covers_every_executable_workflow_entry() -> None:
|
|||||||
for entry in entries:
|
for entry in entries:
|
||||||
workflow_entries.add(f"{workflow['module']}:{entry}")
|
workflow_entries.add(f"{workflow['module']}:{entry}")
|
||||||
|
|
||||||
assert scheduled_workflows == 35
|
assert scheduled_workflows == 39
|
||||||
assert manual_workflows == 0
|
assert manual_workflows == 0
|
||||||
assert len(workflow_entries) == 50
|
assert len(workflow_entries) == 55
|
||||||
assert len(catalog.scripts) == 55
|
assert len(catalog.scripts) == 60
|
||||||
assert workflow_entries < set(catalog.script_ids)
|
assert workflow_entries < set(catalog.script_ids)
|
||||||
assert {
|
assert {
|
||||||
"content.mapping.rebuild",
|
"content.mapping.rebuild",
|
||||||
@@ -179,10 +179,6 @@ def test_default_registry_covers_every_executable_workflow_entry() -> None:
|
|||||||
catalog.get("content.marketing_report.generate").notification_workflow_id
|
catalog.get("content.marketing_report.generate").notification_workflow_id
|
||||||
== "content.marketing_report.daily"
|
== "content.marketing_report.daily"
|
||||||
)
|
)
|
||||||
assert (
|
|
||||||
catalog.get("content.login.refresh").notification_workflow_id
|
|
||||||
== "content.relogin.weekly"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -284,7 +280,7 @@ def test_cli_lists_and_dry_runs_registered_commands(tmp_path: Path) -> None:
|
|||||||
|
|
||||||
assert main(["scripts", "list", "--json"], settings=settings, stdout=output) == 0
|
assert main(["scripts", "list", "--json"], settings=settings, stdout=output) == 0
|
||||||
rows = json.loads(output.getvalue())
|
rows = json.loads(output.getvalue())
|
||||||
assert len(rows) == 55
|
assert len(rows) == 60
|
||||||
assert {row["module"] for row in rows} == {
|
assert {row["module"] for row in rows} == {
|
||||||
"content_marketing",
|
"content_marketing",
|
||||||
"product_commerce",
|
"product_commerce",
|
||||||
|
|||||||
@@ -57,6 +57,6 @@ def test_every_workflow_and_manual_script_resolves_inside_new_project() -> None:
|
|||||||
assert target.is_relative_to(PROJECT_ROOT)
|
assert target.is_relative_to(PROJECT_ROOT)
|
||||||
|
|
||||||
commands = ScriptCatalog.load_default()
|
commands = ScriptCatalog.load_default()
|
||||||
assert len(commands.scripts) == 55
|
assert len(commands.scripts) == 60
|
||||||
assert len(commands.command_ids) == len(set(commands.command_ids))
|
assert len(commands.command_ids) == len(set(commands.command_ids))
|
||||||
assert all(command.path.is_relative_to(PROJECT_ROOT) for command in commands.scripts)
|
assert all(command.path.is_relative_to(PROJECT_ROOT) for command in commands.scripts)
|
||||||
Reference in new issue
Block a user