feat: complete production workflow migration

This commit is contained in:
2026-08-06 14:29:57 +08:00
parent 7f215e79c4
commit 8df5266abb
448 changed files with 56937 additions and 14619 deletions
+139
View File
@@ -0,0 +1,139 @@
# GYXX Flow 工作流合并验收报告
验收日期:2026-08-01
## 验收结论
项目工作流目录已收敛为 21 个真实且启用的调度任务。每个调度任务对应一个 LangGraph 工作流;补采、重试、映射刷新和受保护写操作不再重复注册为工作流,统一通过显式命令执行。
重复且原本禁用的 `content.self_operated.weekly` 已删除,其自营映射、B 站、蝉妈妈和指标同步脚本继续由每日 `content.metrics.daily` 调用。京东与天猫主图周采集收敛为唯一工作流 `product.main_image.weekly`,其余时间规则和失败隔离边界保持不变。
## 数量总览
| 项目 | 数量 | 说明 |
|---|---:|---|
| 业务模块 | 4 | 内容营销、商品经营、店铺洞察、供应链 |
| 调度工作流 | 21 | 内容 8、商品 7、店铺 3、供应链 3 |
| 启用状态 | 21 / 21 | 所有当前调度均启用 |
| LangGraph 节点 | 29 | 所有调度工作流均声明显式节点 |
| 公开命令 | 31 | 覆盖工作流节点及手动补偿入口 |
| 浏览器状态绑定 | 136 | 唯一 CDP、Profile、Cookie 与 storage state |
| 源同步清单 | 233 | 运行时不依赖外部源码目录 |
## 工作流清单
所有时间均使用 `Asia/Shanghai`
### 内容营销(8
| 工作流 | 时间 | 用途 |
|---|---|---|
| `content.cooperations.daily` | 每日 09:00 | 刷新业务映射并同步合作记录。 |
| `content.marketing_report.daily` | 每日 10:00 | 生成营销日报并按现有飞书策略发送。 |
| `content.metrics.daily` | 每日 22:00 | 依次完成合作达人、自营映射、自营 B 站、蝉妈妈和指标同步。 |
| `content.relogin.weekly` | 周五 10:00 | 维护各内容平台可复用登录状态。 |
| `content.comments.weekly` | 周日 12:00 | 并行补采 B 站、小红书和抖音评论。 |
| `content.summary.weekly` | 周二 10:00 | 生成跨平台内容周汇总。 |
| `content.summary.monthly` | 每月 1 日 08:00 | 生成跨平台内容月汇总。 |
| `content.creator_report.monthly` | 每月 1 日 08:30 | 生成达人月度报告。 |
### 店铺洞察(4
| 工作流 | 时间 | 用途 |
|---|---|---|
| `shop.metrics.weekly` | 周一 12:00 | 并行采集京东、抖音和天猫店铺经营指标。 |
| `shop.competitor.weekly` | 周一 12:30 | 采集京东、抖音竞店数据并形成对比结果。 |
| `shop.douyin_price_appeal` | 每日 08:00、16:00、22:00 | 复用店铺周采集登录态,扫描全部待改价列表并对可申诉商品提交固定原因申诉。 |
| `shop.jd_self_operated.daily` | 每日 16:00,业务日 -1 | 京东自营数据从下午开始推送;预留刷新窗口后先采集品牌,再采集商品,品牌失败后商品仍继续。 |
### 商品经营(7
| 工作流 | 时间 | 用途 |
|---|---|---|
| `product.daily` | 每日 08:40,业务日 -1 | 编排 ERP 与三平台采集、分析、导出和入库。 |
| `product.persona.daily` | 每日 10:00 | 并行采集天猫、抖音和京东商品画像。 |
| `product.import.daily` | 每日 19:00 | 幂等导入三平台商品日报。 |
| `product.alert.daily` | 每日 23:00 | 独立检测商品异常并按条件通知。 |
| `product.style_analysis.interval` | 每 3 天 11:00 | 使用本地 Hermes 分析到期款式。 |
| `product.main_image.weekly` | 周日 08:30 | 并行启动 JD、TM 两个独立主图分支,各自写入本地 PG 和飞书;一个平台失败不阻断另一平台,全部结束后汇总整体状态和平台级具体错误。 |
| `product.market_rank` | 周一 10:00 | 并行采集天猫、京东、抖音市场排行并汇总归档。 |
### 供应链(3
| 工作流 | 时间 | 用途 |
|---|---|---|
| `supply.replenishment_alert.daily` | 每日 07:00 | 检测补货风险并执行告警链路。 |
| `supply.purchase_confirmation.daily` | 每日 08:00 | 采集采购确认数据,经本地 Hermes 分析后通知。 |
| `supply.replenishment.weekly` | 周一 08:00 | 采集并分析周度补货数据。 |
## 手动能力合并结果
| 原目录入口 | 当前入口 | 合并结果 |
|---|---|---|
| `content.mapping.refresh` | `content.mapping.rebuild` | 改为内容映射重建命令。 |
| `content.retry_failed` | `content.failed.retry` | 改为失败任务重试命令。 |
| `content.metrics.backfill` | `gyxx backfill content.metrics.daily` | 合并到标准内容日报图,按日期范围重跑。 |
| `shop.jd_self_operated.history` | `shop.jd_self_operated.collect_brand` | 保留品牌单日回采;`--date` 自动渲染起止日期。 |
| `product.backfill` | `product.backfill.run` | 保留商品补采语义;`--date` 自动渲染 `--from/--to`。 |
| `product.review_collection` | `product.review.orchestrate` | 保留评价补采;`--date` 自动渲染目标日期。 |
| `supply.purchase_order_update` | `supply.workflow.run` | 保留受保护 ERP 更新;自动选择 `mcp-run purchase-order-update`。 |
`content.self_operated.weekly` 因与每日指标工作流重复且原调度已禁用而删除;底层自营脚本继续保留。`product.weekly_aggregate.documented_missing` 没有对应脚本和调度任务,也只在历史证据中保留说明。
## 整体架构
```mermaid
flowchart TD
S["Python Scheduler"] --> C["21 Scheduled Workflows"]
U["CLI Manual Commands"] --> M["31 Explicit Commands"]
C --> G["LangGraph StateGraph"]
G --> B1["content_marketing"]
G --> B2["product_commerce"]
G --> B3["shop_intelligence"]
G --> B4["supply_chain"]
M --> B1
M --> B2
M --> B3
M --> B4
B1 --> A["Shared Adapters"]
B2 --> A
B2 --> MI["product.main_image.weekly"]
MI -->|"parallel independent branch"| JD["JD collect + PG + Feishu"]
MI -->|"parallel independent branch"| TM["TM collect + PG + Feishu"]
B3 --> A
B4 --> A
A --> P["Local PostgreSQL"]
A --> H["Local Hermes Collector / Analyzer"]
A --> F["Existing Feishu Integration"]
A --> R["Per-script Browser State"]
G --> J["Journal / Locks / Effect Ledger"]
G --> D["Layered Data Layout"]
```
业务模块只依赖共享 `core``workflow``adapters``ops` 契约,不跨模块导入内部实现。时间规则集中在 `config/schedules.json`,生产环境只守护一个 `gyxx schedule run` 进程。
## 数据与外部系统边界
- PostgreSQL`127.0.0.1:5432/gyxx_super_data`,非回环地址会被拒绝。
- Hermes:本机 analyzer 与 collector 两个角色,分别使用独立 API 和 gateway。
- 飞书:保持现有身份、应用和业务调用方式,凭据只从运行环境注入。
- 浏览器:每个脚本独立 CDP 端口、Profile、Cookie 和 storage state。
- 数据:JSON、CSV、Markdown、Excel、下载文件和截图按模块及处理阶段写入 `GYXX_DATA_ROOT`
## 工程验收
全量回归结果为 `940 passed, 1 skipped`
本次主图合并、失败语义与双 sink 静态定向回归为 `21 passed`;自动化测试没有连接生产 PostgreSQL,也没有执行真实飞书写入。另已在本地 `gyxx_super_data``main_image_creatives` 主键迁移为四列,3681 条存量数据数量不变;同键 JD/TM 双行事务探针成功并已回滚测试数据。
- [x] 工作流目录仅包含 21 个有效调度任务,且与 21 条时间规则一一对应。
- [x] 21 个当前调度全部启用,重复禁用项已删除。
- [x] 主图调度仅保留 `product.main_image.weekly`,JD、TM 以独立资源并行编排;分支失败隔离、成功写入保留、最终状态汇总和平台级错误明细已有配置和自动化测试覆盖。
- [x] 京东、天猫入口默认均调用飞书插入与本地 PG upsert 链路;相关 21 项定向测试通过,不代表已执行真实外部写入。
- [x] 本地 PG 存量主键已完成平台隔离迁移;同日、同款、同图片名的京东与天猫记录可同时存在。
- [x] 7 个手动能力已转为命令或合并进标准工作流,默认仍为 dry-run。
- [x] 单日回采、商品补采、评价补采和采购单更新具备安全默认参数。
- [x] 不可执行历史项不再参与目录、注册和数量统计。
- [x] 最终全量测试、Ruff、构建、doctor、调度 dry-run 与敏感信息扫描通过。
真实浏览器登录、飞书写入、Hermes 分析和 ERP 修改不由本次结构合并自动触发。各业务链路最近一次实跑结果与未通过原因见 [工作流验收测试报告](workflow-acceptance-test-report.md)。
+106
View File
@@ -0,0 +1,106 @@
# GYXX Flow 架构
## 目标
GYXX Flow 是一个 Python 3.12 模块化单体。项目在同一部署单元中提供工作流目录、LangGraph 编排、Python 定时调度、外部系统适配和运行审计,同时保持业务模块之间互不依赖。
核心约束:
- 每个定时任务都是一个独立工作流,并编译为 LangGraph `StateGraph`
- 业务模块只能依赖共享契约,不能导入其他业务模块的内部实现。
- PostgreSQL 使用运行时注入的云端连接;Hermes 使用本机回环地址;飞书保持现有身份和调用方式。
- 手动执行默认 dry-run,真实外部副作用必须显式使用 `--execute`
- 代码、配置和运行数据分离;生产数据根目录必须位于项目目录之外。
## 项目边界
```text
gyxx-flow/
├─ src/gyxx_flow/
│ ├─ core/ 配置、上下文、产物、记录和锁
│ ├─ workflow/ LangGraph 构建、步骤协议和执行引擎
│ ├─ adapters/ 进程、浏览器和外部服务适配
│ ├─ modules/ 四个业务模块,生产代码直接位于模块目录
│ ├─ source_sync/ 可选的业务源码漂移检查
│ └─ scheduler_service.py 跨平台 Python 常驻调度器
├─ config/ 工作流、时间规则和运行绑定
├─ deploy/ PostgreSQL 与进程守护定义
├─ docs/ 架构、部署和运维主文档
└─ tests/ 开发和验收测试,不进入生产运行包
```
`var/` 只是开发环境的默认运行目录,不是源码。生产环境通过 `GYXX_DATA_ROOT` 使用独立持久化目录。
## 运行链路
```mermaid
flowchart TD
A["CLI / Python Scheduler"] --> B["WorkflowCatalog"]
B --> C["Workflow Registry + Factory"]
C --> D["LangGraph StateGraph"]
D --> E1["content_marketing"]
D --> E2["product_commerce"]
D --> E3["shop_intelligence"]
D --> E4["supply_chain"]
E1 --> F["Shared Adapters"]
E2 --> F
E3 --> F
E4 --> F
F --> G1["Cloud PostgreSQL"]
F --> G2["Local Hermes collector / analyzer"]
F --> G3["Existing Feishu identity"]
F --> G4["Per-script CDP and browser state"]
D --> H["Journal / Locks / EffectLedger"]
D --> I["Layered DataLayout"]
```
## 工作流与调度
`config/workflows.json` 只保存由 Python 调度器托管的工作流,记录稳定 ID、所属模块、入口、显式步骤、依赖和失败策略。每个目录项必须在 `config/schedules.json` 中有且只有一条时间规则。模块工厂负责注入超时、重试、资源锁和副作用策略。
`config/commands.json` 保存可手动执行的脚本白名单,也承载补采、重试、映射刷新和受保护写操作。命令不是新的定时工作流;`--date` 可渲染命令声明的 `{business_date}` 默认参数,`--arg` 用于追加脚本参数。
`config/schedules.json` 只保存时间规则。`scheduler_service.py` 负责 `Asia/Shanghai` 时区计算、业务日期偏移、时间槽防重、同工作流不重叠、有限错过触发补偿和优雅停止。
服务器只守护一个 `gyxx schedule run` 进程。systemd 或兼容的 NSSM 只负责进程生命周期,不保存业务时间规则;不得再向 Windows Task Scheduler 或多条 cron 复制工作流时间。
## 外部系统
- PostgreSQL:云端模式不提供地址、数据库、用户或密码的源码默认值;由 `GYXX_POSTGRES_DSN` 在运行时注入,并拒绝回环数据库地址。
- Hermes:保留本机 `data-analyzer``data-collector` 两个角色,以及各自 API 和 gateway。
- 飞书:复用现有 lark-cli profile、应用身份、表格和消息调用方式。
- 浏览器:每个脚本从 `config/runtime-bindings.json` 获得唯一 CDP 端口及独立 Profile、Cookie、storage state 路径,不共享可写 Profile。
## 数据布局
所有运行路径由 `GYXX_DATA_ROOT` 推导:
```text
data/raw 原始响应、JSON、CSV、Excel、下载文件和截图
data/normalized 清洗和标准化数据
data/curated 聚合事实和业务结果
data/exports Markdown、Excel 等交付文件
data/evidence 对账、验收和追溯证据
state 调度、图状态、锁、账本和浏览器状态
logs 调度器与工作流日志
tmp 可清理临时文件
```
原始数据采用追加式保存。迁移部署时停止调度器,完整复制数据根目录并重新注入 `GYXX_DATA_ROOT`,不修改代码中的路径。
## 扩展方式
新增定时任务时:
1. 在所属 `modules/<module>/` 中实现可独立测试的业务入口。
2. 在工作流目录声明 LangGraph 步骤和依赖,并同步增加唯一时间规则。
3. 为工作流使用的脚本登记公开命令和独立浏览器绑定。
4. 通过共享适配器访问数据库、Hermes、飞书和数据目录。
5. 增加 dry-run、失败边界、业务日期和模块回归测试。
新增补采或维护能力时,只在命令目录注册,不增加工作流或调度规则;只有形成新的独立定时任务后才升级为工作流。
新增模块不应要求修改其他业务模块;新增步骤不应要求修改调度器或执行引擎。
四个模块下的 `runtime/__init__.py` 仅用于兼容历史 Python 导入路径,不承载生产
代码。项目内部代码和新增实现必须使用模块正式路径。
+132 -27
View File
@@ -1,39 +1,144 @@
# 部署手册
# GYXX Flow 部署
## 推荐部署方式
生产环境推荐使用 Linux 主机:
- PostgreSQL 使用云端服务,完整 DSN 由服务器密钥环境注入。
- Python 调度器、本机 Hermes 和需要登录状态的浏览器运行在宿主机。
- systemd 只守护一个 Python 调度进程,所有业务时间规则仍来自 `config/schedules.json`
- 运行数据使用 `/var/lib/gyxx-flow`,不得把生产 `GYXX_DATA_ROOT` 指向源码目录中的 `var/`
这种方式能够直接访问两个本机 Hermes 角色和浏览器 CDP,也不会把业务定时规则复制到 systemd timer、cron 或 Windows Task Scheduler。
## 环境要求
- Windows 10/11 或 Windows Server,系统时区 `China Standard Time`
- Python 3.12、`uv`、Windows Task Scheduler
- 凭据由环境变量或外部密钥系统提供,不写入源码或清单
- Python 3.12 和 `uv`
- 可访问的 PostgreSQL 13+ 云端实例及运行时注入的 `GYXX_POSTGRES_DSN`
- 本机 Hermes `data-analyzer``data-collector`
- Chrome/Playwright,以及个别业务入口仍需要的 PowerShell 运行条件
- 可访问现有飞书身份的专用系统用户
## 安装
创建生产目录和服务账户:
```powershell
cd D:\gyxx-flow
uv sync --python 3.12 --extra test
$env:GYXX_DATA_ROOT = 'D:\gyxx-flow\var'
.\.venv\Scripts\python.exe -m gyxx_flow doctor --json
.\.venv\Scripts\python.exe -m pytest
```bash
sudo useradd --system --create-home --shell /usr/sbin/nologin gyxx-flow
sudo install -d -o gyxx-flow -g gyxx-flow /opt/gyxx-flow
sudo install -d -o gyxx-flow -g gyxx-flow /var/lib/gyxx-flow
sudo install -d -o root -g gyxx-flow -m 0750 /etc/gyxx-flow
```
不再配置任何 `GYXX_LEGACY_*_ROOT`。运行代码和资源随 `gyxx_flow` 包部署,
四个旧项目可以不挂载。商品模块需要独立配置时,可设置 `GYXX_PRODUCT_CONFIG`
该变量只指向新部署的配置文件。
将代码发布到 `/opt/gyxx-flow` 后安装锁定依赖:
## 生成候选调度计划
```powershell
.\.venv\Scripts\python.exe -m gyxx_flow schedule plan `
--output D:\gyxx-flow\var\schedule-plan\candidate `
--start-date 2026-07-27 `
--python-executable D:\gyxx-flow\.venv\Scripts\python.exe
```bash
cd /opt/gyxx-flow
sudo -u gyxx-flow uv sync --python 3.12 --no-group dev --frozen
```
输出包含 21 个 XML、`install.ps1``plan.json``drift.json`。生成计划不会
注册任务。只有生产门禁通过并获得明确授权后,才能人工审阅并逐个使用
`install.ps1 -Apply -WorkflowId <id>`;安装器强制一次只处理一个任务。
凭据放入 `/etc/gyxx-flow/gyxx-flow.env`,权限设为 `0640`。该文件不提交到 Git,至少按实际环境注入数据库密码、飞书身份和可选 Hermes 密钥。
## 可迁移部署
控制台和调度器都支持可重复的 `--env-file`,只加载显式列出的文件。systemd 的
`EnvironmentFile=` 或当前进程环境优先于文件中的同名值,避免本地文件意外覆盖密钥系统
注入值。
复制项目或安装 wheel 后,只需重新设置 `GYXX_DATA_ROOT` 和凭据。禁止在生产任务
命令中出现旧项目盘符、用户目录解释器或旧项目工作目录。
## PostgreSQL
从受限环境文件加载云端数据库连接:
```bash
cd /opt/gyxx-flow
set -a
source /etc/gyxx-flow/gyxx-flow.env
set +a
uv run gyxx doctor --json
```
生产环境必须设置 `GYXX_POSTGRES_DSN`,且 DSN 主机必须是非回环地址。项目不会把云端地址、用户名或密码写入源码;`deploy/postgres.compose.yml` 仅保留为开发和恢复场景的可选本地工具,不是当前生产数据库入口。
## 本机 Hermes
启动并验证两个独立角色:
```text
data-analyzer: API base http://127.0.0.1:8642/v1
data-collector: API base http://127.0.0.1:8643/v1
```
`28790/28791` 不作为工作流业务端点。
运行时配置必须保持回环地址。Hermes 不可用时,纯采集、文件处理和数据库同步仍可运行;依赖 Hermes 分析或通知的工作流应保持停用或手工执行,不得静默改用远程 AI。
## 上线前验证
使用生产服务账户运行:
```bash
cd /opt/gyxx-flow
sudo -u gyxx-flow env GYXX_DATA_ROOT=/var/lib/gyxx-flow uv run gyxx doctor --json
sudo -u gyxx-flow env GYXX_DATA_ROOT=/var/lib/gyxx-flow uv run gyxx schedule run --dry-run --once
sudo -u gyxx-flow env GYXX_DATA_ROOT=/var/lib/gyxx-flow uv run gyxx list --json
```
开发或发布流水线另外执行:
```bash
uv run ruff check src tests
uv run pytest
uv build
uv run gyxx acceptance status --json
```
## systemd 调度服务
项目提供 `deploy/gyxx-flow.service`。安装并启动:
```bash
sudo cp /opt/gyxx-flow/deploy/gyxx-flow.service /etc/systemd/system/gyxx-flow.service
sudo systemctl daemon-reload
sudo systemctl enable --now gyxx-flow.service
sudo systemctl status gyxx-flow.service
```
unit 的唯一业务入口是:
```text
/opt/gyxx-flow/.venv/bin/python -m gyxx_flow schedule run
```
若 unit 不使用 systemd `EnvironmentFile=`,入口必须显式追加:
```text
--env-file /etc/gyxx-flow/gyxx-flow.env
```
调度状态写入 `/var/lib/gyxx-flow/state/scheduler`,日志和子进程产物写入同一外置数据根。修改 `config/schedules.json` 后先运行一次 dry-run,再重启服务:
```bash
sudo -u gyxx-flow env GYXX_DATA_ROOT=/var/lib/gyxx-flow \
/opt/gyxx-flow/.venv/bin/python -m gyxx_flow schedule run --dry-run --once
sudo systemctl restart gyxx-flow.service
```
不要为单个工作流创建 systemd timer 或 cron 条目,也不得同时运行两个调度器实例。
## 发布更新
```bash
sudo systemctl stop gyxx-flow.service
cd /opt/gyxx-flow
# 切换到已验收版本后:
sudo -u gyxx-flow uv sync --python 3.12 --no-group dev --frozen
sudo -u gyxx-flow env GYXX_DATA_ROOT=/var/lib/gyxx-flow uv run gyxx doctor --json
sudo -u gyxx-flow env GYXX_DATA_ROOT=/var/lib/gyxx-flow uv run gyxx schedule run --dry-run --once
sudo systemctl start gyxx-flow.service
```
代码发布和回滚都不得覆盖 `/var/lib/gyxx-flow`
## Docker 边界
当前 Compose 只负责 PostgreSQL。完整应用若进入容器,容器内 `127.0.0.1` 不再指向宿主机的两个 Hermes 和浏览器 CDP;同时部分工作流仍可能依赖可见桌面登录或 PowerShell。因此在完成网络、安全、浏览器 Profile 持久化和目标工作流验收前,不将完整生产应用声明为纯容器部署。
## Windows 兼容入口
`deploy/windows-service/` 暂时保留为 legacy NSSM 兼容入口。NSSM 只守护同一个 `gyxx schedule run` 进程,不注册 Windows Task Scheduler,也不保存业务时间规则。新服务器部署以 Linux systemd 为准。
@@ -0,0 +1,6 @@
# Archived content-marketing Windows launchers
These BAT and PowerShell files are retained only as migration history. They are
not production commands, are not registered by GYXX Flow, and are not used by
the Python scheduler. The active workflow steps are declared in
`config/workflows.json` and scheduled through the Python scheduler service.
@@ -0,0 +1,44 @@
@echo off
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
REM Daily SKU marketing operations report: Hermes analysis + Feishu dashboard and full report
REM Historical launcher retained for provenance; production scheduling is owned by gyxx schedule run.
setlocal
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
if not defined GYXX_DATA_ROOT (
if not defined GYXX_PROJECT_ROOT (
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
exit /b 3
)
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
)
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
set PYTHON=%GYXX_PYTHON%
where %PYTHON% >nul 2>&1 || set PYTHON=python
set PYTHONIOENCODING=utf-8
set LARK_CLI_NO_PROXY=1
set LARKSUITE_CLI_NO_UPDATE_NOTIFIER=1
set LARKSUITE_CLI_NO_SKILLS_NOTIFIER=1
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
set TS=%TS: =0%
set LOG_FILE=%LOG_DIR%\daily_marketing_report_%TS%.log
echo === Daily marketing report started at %date% %time% === > "%LOG_FILE%"
echo PROJECT_DIR=%PROJECT_DIR% >> "%LOG_FILE%"
REM Exposure collection finishes at 22:00. The 10:00 report reads the latest
REM completed database snapshot and must not wait for a same-day collection.
cd /d "%PROJECT_DIR%"
call %PYTHON% -u -X utf8 daily_marketing_report.py --send >> "%LOG_FILE%" 2>&1
set RC=%ERRORLEVEL%
echo daily_marketing_report exit=%RC% >> "%LOG_FILE%"
echo === Daily marketing report finished at %date% %time% (rc=%RC%) === >> "%LOG_FILE%"
endlocal & exit /b %RC%
@@ -0,0 +1,67 @@
@echo off
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
REM Daily 22:00 task: collect collaborator + self-operated exposure, write back to Feishu, then sync cmt_notes.
REM Historical launcher retained for provenance; production scheduling is owned by gyxx schedule run.
setlocal
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
if not defined GYXX_DATA_ROOT (
if not defined GYXX_PROJECT_ROOT (
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
exit /b 3
)
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
)
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
REM The Python scheduler service may inject a project-specific interpreter.
set PYTHON=%GYXX_PYTHON%
where %PYTHON% >nul 2>&1 || set PYTHON=python
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
set TS=%TS: =0%
set LOG_FILE=%LOG_DIR%\daily_run_%TS%.log
echo === Daily run started at %date% %time% === > "%LOG_FILE%"
echo PROJECT_DIR=%PROJECT_DIR% >> "%LOG_FILE%"
cd /d "%PROJECT_DIR%"
echo --- [step 1] run_all.py (collaborator only; includes collaborator mapping refresh) --- >> "%LOG_FILE%"
call %PYTHON% run_all.py --daily-scope >> "%LOG_FILE%" 2>&1
set RC_RUN=%ERRORLEVEL%
echo run_all exit=%RC_RUN% >> "%LOG_FILE%"
echo --- [step 2] refresh_self_mapping.py --- >> "%LOG_FILE%"
call %PYTHON% data\tools\refresh_self_mapping.py >> "%LOG_FILE%" 2>&1
set RC_SELF_MAP=%ERRORLEVEL%
echo refresh_self_mapping exit=%RC_SELF_MAP% >> "%LOG_FILE%"
echo --- [step 3] self_bilibili_scraper.py --- >> "%LOG_FILE%"
call %PYTHON% self_bilibili_scraper.py >> "%LOG_FILE%" 2>&1
set RC_SELF_BILI=%ERRORLEVEL%
echo self_bilibili exit=%RC_SELF_BILI% >> "%LOG_FILE%"
echo --- [step 4] chanmama_scraper.py (self-operated Douyin) --- >> "%LOG_FILE%"
call %PYTHON% chanmama_scraper.py >> "%LOG_FILE%" 2>&1
set RC_SELF_DOUYIN=%ERRORLEVEL%
echo chanmama exit=%RC_SELF_DOUYIN% >> "%LOG_FILE%"
echo --- [step 5] sync_metrics_to_cmt_notes.py --- >> "%LOG_FILE%"
call %PYTHON% data\tools\sync_metrics_to_cmt_notes.py >> "%LOG_FILE%" 2>&1
set RC_SYNC=%ERRORLEVEL%
echo sync exit=%RC_SYNC% >> "%LOG_FILE%"
set FINAL_RC=0
if not "%RC_RUN%"=="0" set FINAL_RC=1
if not "%RC_SELF_MAP%"=="0" set FINAL_RC=1
if not "%RC_SELF_BILI%"=="0" set FINAL_RC=1
if not "%RC_SELF_DOUYIN%"=="0" set FINAL_RC=1
if not "%RC_SYNC%"=="0" set FINAL_RC=1
echo === Daily run finished at %date% %time% (run=%RC_RUN%, self_map=%RC_SELF_MAP%, self_bili=%RC_SELF_BILI%, self_douyin=%RC_SELF_DOUYIN%, sync=%RC_SYNC%, final=%FINAL_RC%) === >> "%LOG_FILE%"
endlocal & exit /b %FINAL_RC%
@@ -0,0 +1,49 @@
@echo off
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
REM Monday task: run 3-platform V2 + sync metrics to cmt_notes
REM Register: schtasks /Create /SC WEEKLY /D MON /TN YingxiaoYunying_MondayBackfill /TR %PROJECT_DIR%\data\tools\daily_run_with_backfill.bat /ST 06:00 /F
setlocal
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
if not defined GYXX_DATA_ROOT (
if not defined GYXX_PROJECT_ROOT (
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
exit /b 3
)
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
)
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
REM Task Scheduler (SYSTEM account) has no user PATH -> lock to venv python
set PYTHON=%GYXX_PYTHON%
where %PYTHON% >nul 2>&1 || set PYTHON=python
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
set TS=%TS: =0%
set LOG_FILE=%LOG_DIR%\monday_run_%TS%.log
echo === Monday run started at %date% %time% === > "%LOG_FILE%"
echo PROJECT_DIR=%PROJECT_DIR% >> "%LOG_FILE%"
cd /d "%PROJECT_DIR%"
echo --- [step 1] run_all.py (includes feishu_mapping refresh) --- >> "%LOG_FILE%"
call %PYTHON% run_all.py --daily-scope >> "%LOG_FILE%" 2>&1
set RC_RUN=%ERRORLEVEL%
echo run_all exit=%RC_RUN% >> "%LOG_FILE%"
echo --- [step 2] sync_metrics_to_cmt_notes.py --- >> "%LOG_FILE%"
call %PYTHON% data\tools\sync_metrics_to_cmt_notes.py >> "%LOG_FILE%" 2>&1
set RC_SYNC=%ERRORLEVEL%
echo sync exit=%RC_SYNC% >> "%LOG_FILE%"
set FINAL_RC=0
if not "%RC_RUN%"=="0" set FINAL_RC=1
if not "%RC_SYNC%"=="0" set FINAL_RC=1
echo === Monday run finished at %date% %time% (run=%RC_RUN%, sync=%RC_SYNC%, final=%FINAL_RC%) === >> "%LOG_FILE%"
endlocal & exit /b %FINAL_RC%
@@ -0,0 +1,39 @@
@echo off
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
REM Friday relogin: 4 QR scans — B站, 蒲公英, 星图(+同步到抖音评论), 小红书评论
REM 星图登录完成后自动把 cookie 同步到抖音评论 scraper,不需要单独再扫抖音码
REM Register: schtasks /Create /SC WEEKLY /D FRI /TN YingxiaoYunying_FridayRelogin /TR %PROJECT_DIR%\data\tools\friday_relogin.bat /ST 10:00 /F
setlocal
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
if not defined GYXX_DATA_ROOT (
if not defined GYXX_PROJECT_ROOT (
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
exit /b 3
)
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
)
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
set PYTHON=%GYXX_PYTHON%
where %PYTHON% >nul 2>&1 || set PYTHON=python
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
set TS=%TS: =0%
set LOG_FILE=%LOG_DIR%\friday_relogin_%TS%.log
echo === Friday relogin started at %date% %time% === > "%LOG_FILE%"
echo PROJECT_DIR=%PROJECT_DIR% >> "%LOG_FILE%"
cd /d "%PROJECT_DIR%"
echo --- Launch 5 QR relogins in parallel; send screenshots; retry up to 3 rounds --- >> "%LOG_FILE%"
call %PYTHON% data\tools\friday_relogin_parallel.py --max-attempts 3 --round-timeout 300 --screenshot-delay 60 >> "%LOG_FILE%" 2>&1
set FINAL_RC=%ERRORLEVEL%
echo === Friday relogin finished at %date% %time% (exit=%FINAL_RC%) === >> "%LOG_FILE%"
endlocal & exit /b %FINAL_RC%
@@ -0,0 +1,7 @@
$moduleRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$dataRoot = if ($env:GYXX_DATA_ROOT) { $env:GYXX_DATA_ROOT } else { Join-Path $moduleRoot 'var' }
$profileRoot = Join-Path $dataRoot 'state\content_marketing\browser-profiles'
Get-WmiObject Win32_Process | Where-Object { $_.Name -eq 'chrome.exe' -and $_.CommandLine -like "*$profileRoot*" } | ForEach-Object { Write-Host "Killing PID $($_.ProcessId)"; Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
Start-Sleep -Seconds 3
$procs = Get-WmiObject Win32_Process | Where-Object { $_.Name -eq 'chrome.exe' -and $_.CommandLine -like "*$profileRoot*" }
if ($procs) { Write-Host 'Remaining project chrome processes:'; $procs | Select-Object ProcessId,CommandLine } else { Write-Host 'No project chrome processes remaining' }
@@ -0,0 +1,4 @@
$moduleRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$dataRoot = if ($env:GYXX_DATA_ROOT) { $env:GYXX_DATA_ROOT } else { Join-Path $moduleRoot 'var' }
$profileRoot = Join-Path $dataRoot 'state\content_marketing\browser-profiles'
Get-WmiObject Win32_Process | Where-Object { $_.Name -eq 'chrome.exe' -and $_.CommandLine -like "*$profileRoot*" } | Select-Object ProcessId, ParentProcessId, @{Name='CmdStart';Expression={$_.CommandLine.Substring(0, [Math]::Min(200, $_.CommandLine.Length))}}
@@ -0,0 +1,55 @@
@echo off
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
REM Monday 13:00 self-operated pipeline: Bilibili + Chanmama -> sync to cmt_notes (with INSERT)
REM Feishu write-back happens inside each scraper; sync step writes PostgreSQL.
REM Register: schtasks /Create /SC WEEKLY /D MON /TN YingxiaoYunying_MondaySelf /TR %PROJECT_DIR%\data\tools\monday_self_run.bat /ST 13:00 /F
setlocal
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
if not defined GYXX_DATA_ROOT (
if not defined GYXX_PROJECT_ROOT (
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
exit /b 3
)
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
)
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
set PYTHON=%GYXX_PYTHON%
if not exist "%PYTHON%" set PYTHON=python
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
set TS=%TS: =0%
set LOG_FILE=%LOG_DIR%\monday_self_%TS%.log
echo === Monday self-operated run started at %date% %time% === > "%LOG_FILE%"
echo PROJECT_DIR=%PROJECT_DIR% >> "%LOG_FILE%"
cd /d "%PROJECT_DIR%"
echo --- [step 1] self_bilibili_scraper.py --- >> "%LOG_FILE%"
call %PYTHON% self_bilibili_scraper.py >> "%LOG_FILE%" 2>&1
set RC_BILI=%ERRORLEVEL%
echo self_bilibili exit=%RC_BILI% >> "%LOG_FILE%"
echo --- [step 2] chanmama_scraper.py --- >> "%LOG_FILE%"
call %PYTHON% chanmama_scraper.py >> "%LOG_FILE%" 2>&1
set RC_CM=%ERRORLEVEL%
echo chanmama exit=%RC_CM% >> "%LOG_FILE%"
echo --- [step 3] sync_metrics_to_cmt_notes.py (with INSERT) --- >> "%LOG_FILE%"
call %PYTHON% data\tools\sync_metrics_to_cmt_notes.py >> "%LOG_FILE%" 2>&1
set RC_SYNC=%ERRORLEVEL%
echo sync exit=%RC_SYNC% >> "%LOG_FILE%"
set FINAL_RC=0
if not "%RC_BILI%"=="0" set FINAL_RC=1
if not "%RC_CM%"=="0" set FINAL_RC=1
if not "%RC_SYNC%"=="0" set FINAL_RC=1
echo === Monday self-operated run finished at %date% %time% (bili=%RC_BILI%, cm=%RC_CM%, sync=%RC_SYNC%, final=%FINAL_RC%) === >> "%LOG_FILE%"
endlocal & exit /b %FINAL_RC%
@@ -0,0 +1,41 @@
@echo off
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
REM Monthly creator report: run on 1st of each month at 08:30
REM 1. Read cmt_cooperations + cmt_creators + cmt_styles from PG
REM 2. Generate 9-section analysis report (Markdown)
REM 3. Create Feishu doc + save to cmt_creator_report
REM Register: schtasks /Create /SC MONTHLY /D 1 /TN YingxiaoYunying_MonthlyCreatorReport /TR %PROJECT_DIR%\data\tools\monthly_creator_report.bat /ST 08:30 /F
setlocal
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
if not defined GYXX_DATA_ROOT (
if not defined GYXX_PROJECT_ROOT (
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
exit /b 3
)
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
)
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
set PYTHON=%GYXX_PYTHON%
where %PYTHON% >nul 2>&1 || set PYTHON=python
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
set TS=%TS: =0%
set LOG_FILE=%LOG_DIR%\monthly_creator_report_%TS%.log
echo === Monthly creator report started at %date% %time% === > "%LOG_FILE%"
echo PROJECT_DIR=%PROJECT_DIR% >> "%LOG_FILE%"
cd /d "%PROJECT_DIR%"
call %PYTHON% -u -X utf8 data/tools/generate_creator_report.py >> "%LOG_FILE%" 2>&1
set RC=%ERRORLEVEL%
echo monthly_creator_report exit=%RC% >> "%LOG_FILE%"
echo === Monthly creator report finished at %date% %time% (rc=%RC%) === >> "%LOG_FILE%"
endlocal
@@ -0,0 +1,41 @@
@echo off
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
REM Monthly summary: run on 1st of each month at 08:00
REM 1. Compute last month date range
REM 2. Query PG for notes per style
REM 3. For each style: note analysis + LLM cross-compare + Feishu doc + write back + save to DB
REM Register: schtasks /Create /SC MONTHLY /D 1 /TN YingxiaoYunying_MonthlySummary /TR %PROJECT_DIR%\data\tools\monthly_summary.bat /ST 08:00 /F
setlocal
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
if not defined GYXX_DATA_ROOT (
if not defined GYXX_PROJECT_ROOT (
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
exit /b 3
)
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
)
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
set PYTHON=%GYXX_PYTHON%
where %PYTHON% >nul 2>&1 || set PYTHON=python
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
set TS=%TS: =0%
set LOG_FILE=%LOG_DIR%\monthly_summary_%TS%.log
echo === Monthly summary started at %date% %time% === > "%LOG_FILE%"
echo PROJECT_DIR=%PROJECT_DIR% >> "%LOG_FILE%"
cd /d "%PROJECT_DIR%"
call %PYTHON% -u -X utf8 monthly_summary_all.py --max-workers 4 >> "%LOG_FILE%" 2>&1
set RC=%ERRORLEVEL%
echo monthly_summary exit=%RC% >> "%LOG_FILE%"
echo === Monthly summary finished at %date% %time% (rc=%RC%) === >> "%LOG_FILE%"
endlocal
@@ -0,0 +1,37 @@
@echo off
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
REM Sync creator attributes and cooperation records from Feishu to PostgreSQL
REM Runs after daily scrape to pick up new creators/notes/cooperations
REM Register: schtasks /Create /SC DAILY /TN YingxiaoYunying_SyncCooperations /TR %PROJECT_DIR%\data\tools\sync_cooperations.bat /ST 09:00 /F
setlocal
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
if not defined GYXX_DATA_ROOT (
if not defined GYXX_PROJECT_ROOT (
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
exit /b 3
)
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
)
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
set PYTHON=%GYXX_PYTHON%
where %PYTHON% >nul 2>&1 || set PYTHON=python
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
set TS=%TS: =0%
set LOG_FILE=%LOG_DIR%\sync_cooperations_%TS%.log
echo === sync_cooperations started at %date% %time% === > "%LOG_FILE%"
cd /d "%PROJECT_DIR%"
call %PYTHON% data\tools\sync_cooperations.py --refresh-mapping >> "%LOG_FILE%" 2>&1
set RC=%ERRORLEVEL%
echo === sync_cooperations finished at %date% %time% exit=%RC% === >> "%LOG_FILE%"
endlocal & exit /b %RC%
@@ -0,0 +1,80 @@
@echo off
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
REM Weekly three-platform comment scraper wrapper.
REM Runs bilibili, xiaohongshu, douyin in parallel via PowerShell.
REM Register: schtasks /Create /SC WEEKLY /D SUN /TN YingxiaoYunying_WeeklyCommentScrape /TR %PROJECT_DIR%\data\tools\weekly_comment_scrape.bat /ST 12:00 /F
setlocal
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
if not defined GYXX_DATA_ROOT (
if not defined GYXX_PROJECT_ROOT (
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
exit /b 3
)
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
)
set "PYTHON_EXE=%GYXX_PYTHON%"
set "LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing"
set "TMP_DIR=%GYXX_DATA_ROOT%\tmp\content_marketing"
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
if not exist "%TMP_DIR%" mkdir "%TMP_DIR%"
for /f "tokens=2 delims==" %%I in ('wmic os get localdatetime /value') do set "NOW=%%I"
set "STAMP=%NOW:~0,4%-%NOW:~4,2%-%NOW:~6,2%_%NOW:~8,2%-%NOW:~10,2%-%NOW:~12,2%"
set "MASTER_LOG=%LOG_DIR%\weekly_%STAMP%.log"
echo === weekly scrape started %STAMP% > "%MASTER_LOG%"
set "DOUYIN_LOG=%LOG_DIR%\weekly_%STAMP%_douyin.log"
set "XHS_LOG=%LOG_DIR%\weekly_%STAMP%_xiaohongshu.log"
set "BILI_LOG=%LOG_DIR%\weekly_%STAMP%_bilibili.log"
echo [%STAMP%] launching douyin, xiaohongshu, bilibili in parallel >> "%MASTER_LOG%"
set "PS_SCRIPT=%GYXX_DATA_ROOT%\tmp\content_marketing\weekly_scrape_%STAMP%.ps1"
> "%PS_SCRIPT%" echo $ErrorActionPreference = 'Continue'
>> "%PS_SCRIPT%" echo $env:PYTHONIOENCODING = 'utf-8'
>> "%PS_SCRIPT%" echo $env:PYTHONUTF8 = '1'
>> "%PS_SCRIPT%" echo Set-Location -LiteralPath '%PROJECT_DIR%'
>> "%PS_SCRIPT%" echo $py = '%GYXX_PYTHON%'
>> "%PS_SCRIPT%" echo $stamp = '%STAMP%'
>> "%PS_SCRIPT%" echo $logDir = '%GYXX_DATA_ROOT%\logs\content_marketing'
>> "%PS_SCRIPT%" echo $jobs = @(
>> "%PS_SCRIPT%" echo [pscustomobject]@{ Name='bilibili'; Script='%PROJECT_DIR%\data\tools\batch_rescrape_bilibili.py'; Log=Join-Path $logDir ("weekly_${stamp}_bilibili.log") },
>> "%PS_SCRIPT%" echo [pscustomobject]@{ Name='xiaohongshu'; Script='%PROJECT_DIR%\data\tools\batch_rescrape_xiaohongshu.py'; Log=Join-Path $logDir ("weekly_${stamp}_xiaohongshu.log") },
>> "%PS_SCRIPT%" echo [pscustomobject]@{ Name='douyin'; Script='%PROJECT_DIR%\data\tools\batch_rescrape_douyin.py'; Log=Join-Path $logDir ("weekly_${stamp}_douyin.log") }
>> "%PS_SCRIPT%" echo )
>> "%PS_SCRIPT%" echo $started = @{}
>> "%PS_SCRIPT%" echo foreach ($job in $jobs) {
>> "%PS_SCRIPT%" echo $logPath = $job.Log
>> "%PS_SCRIPT%" echo $header = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] === $($job.Name) launch ==="
>> "%PS_SCRIPT%" echo $cmd = "`"$py`" -X utf8 `"$($job.Script)`" >> `"$logPath`" 2>&1"
>> "%PS_SCRIPT%" echo Add-Content -LiteralPath $logPath -Value $header -Encoding utf8
>> "%PS_SCRIPT%" echo $proc = Start-Process -FilePath cmd.exe -ArgumentList '/c', "`"$cmd`"" -PassThru -WindowStyle Hidden
>> "%PS_SCRIPT%" echo if ($null -eq $proc) {
>> "%PS_SCRIPT%" echo Add-Content -LiteralPath $logPath -Value "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] !!! Start-Process returned null for $($job.Name) !!!" -Encoding utf8
>> "%PS_SCRIPT%" echo continue
>> "%PS_SCRIPT%" echo }
>> "%PS_SCRIPT%" echo $started[$job.Name] = @{ Proc=$proc; Log=$logPath }
>> "%PS_SCRIPT%" echo }
>> "%PS_SCRIPT%" echo $exitCodes = @{}
>> "%PS_SCRIPT%" echo foreach ($job in $jobs) {
>> "%PS_SCRIPT%" echo $entry = $started[$job.Name]
>> "%PS_SCRIPT%" echo $entry.Proc.WaitForExit()
>> "%PS_SCRIPT%" echo $rc = $entry.Proc.ExitCode
>> "%PS_SCRIPT%" echo $exitCodes[$job.Name] = $rc
>> "%PS_SCRIPT%" echo Add-Content -LiteralPath $entry.Log -Value "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] === $($job.Name) done, exit=$rc ===" -Encoding utf8
>> "%PS_SCRIPT%" echo }
>> "%PS_SCRIPT%" echo foreach ($k in $exitCodes.Keys) { Write-Output ("{0}={1}" -f $k, $exitCodes[$k]) }
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%PS_SCRIPT%" >> "%MASTER_LOG%" 2>&1
set "RC=%ERRORLEVEL%"
del "%PS_SCRIPT%" 2>nul
echo [%STAMP%] all three scrapers finished, ps exit=%RC% >> "%MASTER_LOG%"
echo === weekly scrape done %STAMP% >> "%MASTER_LOG%"
endlocal & exit /b %RC%
@@ -0,0 +1,42 @@
@echo off
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
REM Weekly summary: 每周一 10:00 跑上周各款周笔记汇总
REM 1. 动态算上周一到上周日时间范围
REM 2. PG 查上周有笔记的款
REM 3. 对每个款:跑单品分析 + LLM 4 维度综合 + 创建飞书文档 + 写入生命进程表
REM Register: schtasks /Create /SC WEEKLY /D TUE /TN YingxiaoYunying_WeeklySummary /TR %PROJECT_DIR%\data\tools\weekly_summary.bat /ST 10:00 /F
setlocal
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
if not defined GYXX_DATA_ROOT (
if not defined GYXX_PROJECT_ROOT (
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
exit /b 3
)
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
)
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
set PYTHON=%GYXX_PYTHON%
where %PYTHON% >nul 2>&1 || set PYTHON=python
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
set TS=%TS: =0%
set LOG_FILE=%LOG_DIR%\weekly_summary_%TS%.log
echo === Weekly summary started at %date% %time% === > "%LOG_FILE%"
echo PROJECT_DIR=%PROJECT_DIR% >> "%LOG_FILE%"
cd /d "%PROJECT_DIR%"
REM 跑周汇总(4 路并发,动态时间范围)
call %PYTHON% -u -X utf8 weekly_summary_all.py --max-workers 4 >> "%LOG_FILE%" 2>&1
set RC=%ERRORLEVEL%
echo weekly_summary exit=%RC% >> "%LOG_FILE%"
echo === Weekly summary finished at %date% %time% (rc=%RC%) === >> "%LOG_FILE%"
endlocal
@@ -0,0 +1,15 @@
# Product-commerce migration history
Product-commerce production code now lives directly in
`src/gyxx_flow/modules/product_commerce/`. The module is fully owned by
`gyxx-flow`; it does not import executable code from the legacy checkout.
`launchers_reference/` is provenance only. Those files document the old task
arguments and must never be registered as final scheduled-task actions.
The migrated regression tests live in `tests/modules/product_commerce/` and are
part of the repository's normal pytest collection.
The authoritative source/target hashes, transformations, and six intentionally
excluded diagnostic entries are recorded in
`config/source-manifests/product_commerce.json`.
@@ -0,0 +1,8 @@
# Launcher provenance only
These launchers preserve the arguments and sequencing discovered during the
read-only audit. Their project-root literals were replaced by the inert
`GYXX_RUNTIME_ROOT` placeholder during migration.
Do not register or invoke these files as production entry points. Final tasks
must call the native `gyxx-flow` CLI and its workflow registry.
@@ -0,0 +1,35 @@
@echo off
REM Daily 23:00 sales-decline alert -- independent of 08:40 daily collect.
REM
REM Detection window: previous 9 days from today, split into 3 segments
REM (strict decline: seg1 > seg2 > seg3 > 0).
REM Data source: data/<plat>/<safe_name>_<date>/data.json (PG first, JSON fallback).
REM Notify: POST Hermes analyzer chat completions, analyzer @ recipient by openid.
REM Recipient: ou_7ad5fc8012e2f741afc5346e05ffd447.
REM
REM Independence rationale:
REM - daily collect at 08:40 uses "yesterday" as window end (9d back: 5.31-6.8)
REM - this job at 23:00 uses "today" (9d back: 6.1-6.9) to see the latest trend
REM - even if daily collect fails, the alert still triggers
REM
REM Exit codes:
REM 0 = 0 events, or successful POST
REM 1 = detection logic failed
REM 3 = Hermes POST failed (events still persisted)
setlocal
cd /d %GYXX_RUNTIME_ROOT%
set "PY=%GYXX_RUNTIME_ROOT%\.venv\Scripts\python.exe"
if not exist data\logs mkdir data\logs
for /f %%i in ('powershell -NoProfile -Command "Get-Date -Format yyyy-MM-dd"') do set TODAY=%%i
echo [%TODAY% 23:00] === sales alert job start === >> data\logs\alerts.log
"%PY%" run_alerts_with_retry.py --end-date %TODAY% --openid ou_7ad5fc8012e2f741afc5346e05ffd447 >> data\logs\alerts.log 2>&1
set RC=%ERRORLEVEL%
echo [%TODAY% 23:00] === sales alert job end (rc=%RC%) === >> data\logs\alerts.log
exit /b %RC%
@@ -0,0 +1,17 @@
@echo off
REM Daily 08:40 trigger -- collect (ERP + 3 platforms) + analyze (date-first summary)
REM + export (data/ready_for_bitable/<date>/bitable_records.json)
REM + insert (推飞书多维表格)
REM Skips notify/decline (告警由 run_alerts.bat 23:00 跑; decline 暂未自动触发)
REM Uses project venv Python to ensure scrapling/playwright are available
REM 08:40 选在 JD/TM 当日数据已刷新、且赶在工作日开始前完成批跑
setlocal
cd /d %GYXX_RUNTIME_ROOT%
set "PY=%GYXX_RUNTIME_ROOT%\.venv\Scripts\python.exe"
if not exist data\logs mkdir data\logs
"%PY%" orchestrate_daily_collection.py --stages collect,analyze,export,insert >> data\logs\daily_collect.log 2>&1
exit /b %ERRORLEVEL%
@@ -0,0 +1,38 @@
@echo off
REM run_daily_import.bat
REM Triggered by Windows Task Scheduler daily at 19:00.
REM Imports previous day's data via import_product_daily.py.
REM Log goes to logs/daily_import_<timestamp>.log.
REM Note: file is ASCII-only on purpose - cmd's default codepage 936 (GBK)
REM misparses UTF-8 Chinese in REM/echo lines as commands.
setlocal EnableDelayedExpansion
set "PROJECT_DIR=%GYXX_RUNTIME_ROOT%"
set "PY=%PROJECT_DIR%\.venv\Scripts\python.exe"
set "SCRIPT=%PROJECT_DIR%\import_product_daily.py"
set "LOG_DIR=%PROJECT_DIR%\logs"
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
REM yesterday's date (cmd has no date math, use PowerShell with usebackq to avoid quote conflict)
for /f "usebackq" %%D in (`powershell -NoProfile -Command "(Get-Date).AddDays(-1).ToString('yyyy-MM-dd')"`) do set "YESTERDAY=%%D"
REM timestamp YYYYMMDD_HHMMSS
set "STAMP=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%"
set "STAMP=%STAMP: =0%"
set "LOG_FILE=%LOG_DIR%\daily_import_%STAMP%.log"
REM banner (ASCII only)
echo [%date% %time:~0,8%] START daily import target_date=%YESTERDAY% log=%LOG_FILE%> "%LOG_FILE%"
cd /d "%PROJECT_DIR%"
"%PY%" "%SCRIPT%" --date %YESTERDAY% >> "%LOG_FILE%" 2>&1
set "EXIT_CODE=%ERRORLEVEL%"
echo [%date% %time:~0,8%] exit_code=%EXIT_CODE% >> "%LOG_FILE%"
REM keep 30 days of logs, delete older
forfiles /p "%LOG_DIR%" /m "daily_import_*.log" /d -30 /c "cmd /c del @file" 2>nul
endlocal & exit /b %EXIT_CODE%
@@ -0,0 +1,49 @@
# run_daily_import.ps1
# 每日 19:00 由 Windows Task Scheduler 触发。
# 计算昨天的日期, 调 import_product_daily.py 入库, 日志落到 logs/。
$ErrorActionPreference = "Continue"
$OutputEncoding = [System.Text.Encoding]::UTF8
$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8'
$ProjectDir = $env:GYXX_RUNTIME_ROOT
$LogDir = Join-Path $ProjectDir "logs"
if (-not (Test-Path $LogDir)) {
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
}
$Stamp = Get-Date -Format "yyyyMMdd_HHmmss"
$Yesterday = (Get-Date).AddDays(-1).ToString("yyyy-MM-dd")
$LogFile = Join-Path $LogDir ("daily_import_" + $Stamp + ".log")
# 把 stdout/stderr 全部 >> 到日志 (用 cmd 重定向, 避免 PowerShell pipe 跟 Python 的编码冲突)
@"
[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] | =$Yesterday | =$LogFile
"@ | Out-File -FilePath $LogFile -Encoding utf8 -Append
Set-Location $ProjectDir
$Python = Join-Path $ProjectDir ".venv\Scripts\python.exe"
$Script = Join-Path $ProjectDir "import_product_daily.py"
if (-not (Test-Path $Python)) {
Add-Content -Path $LogFile -Value "[FATAL] 找不到 python: $Python" -Encoding utf8
exit 2
}
if (-not (Test-Path $Script)) {
Add-Content -Path $LogFile -Value "[FATAL] 找不到脚本: $Script" -Encoding utf8
exit 2
}
# 用 cmd 重定向 (2>&1 把 stderr 合并) - 兼容 Python 输出
cmd /c "`"$Python`" `"$Script`" --date $Yesterday >> `"$LogFile`" 2>&1"
$Exit = $LASTEXITCODE
Add-Content -Path $LogFile -Value "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] 退出码: $Exit" -Encoding utf8
# 保留 30 天日志, 删旧的
Get-ChildItem -Path $LogDir -Filter "daily_import_*.log" |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } |
ForEach-Object { Remove-Item $_.FullName -Force }
exit $Exit
@@ -0,0 +1,16 @@
@echo off
REM Daily 10:00 trigger -- 三平台人群画像并行采集 + 回填飞书表
REM 天猫: collect_persona_to_bitable.py (达摩盘)
REM 抖音: collect_dy_persona_to_bitable.py (罗盘成交人群)
REM 京东: collect_jd_persona_to_bitable.py (商智人群画像)
REM 三个脚本并行启动,各自独立写日志
setlocal
cd /d %GYXX_RUNTIME_ROOT%
set "PY=%GYXX_RUNTIME_ROOT%\.venv\Scripts\python.exe"
if not exist data\logs mkdir data\logs
"%PY%" run_daily_persona.py >> data\logs\daily_persona_launcher.log 2>&1
exit /b %ERRORLEVEL%
@@ -0,0 +1,13 @@
@echo off
REM Runs independently every 3 days at 11:00. The analyzer defaults to yesterday
REM as the window end, so each run covers the latest three complete calendar days.
setlocal
cd /d %GYXX_RUNTIME_ROOT%
set "PY=%GYXX_RUNTIME_ROOT%\.venv\Scripts\python.exe"
if not exist data\logs mkdir data\logs
"%PY%" -u analyze_style_with_hermes.py --all-styles --days 3 --min-interval-days 3 --skip-existing >> data\logs\style_analysis_3d.log 2>&1
exit /b %ERRORLEVEL%
@@ -0,0 +1,12 @@
@echo off
REM Weekly JD main image pipeline. Keep this file ASCII-only for Task Scheduler.
setlocal
cd /d %GYXX_RUNTIME_ROOT%
set "PY=%GYXX_RUNTIME_ROOT%\.venv\Scripts\python.exe"
set "PYTHONUNBUFFERED=1"
if not exist data\logs mkdir data\logs
"%PY%" -u run_weekly_jd_main_image.py --headless >> data\logs\weekly_jd_main_image.log 2>&1
exit /b %ERRORLEVEL%
@@ -0,0 +1,12 @@
@echo off
REM Weekly main image pipeline. Keep this file ASCII-only for Task Scheduler.
setlocal
cd /d %GYXX_RUNTIME_ROOT%
set "PY=%GYXX_RUNTIME_ROOT%\.venv\Scripts\python.exe"
set "PYTHONUNBUFFERED=1"
if not exist data\logs mkdir data\logs
"%PY%" -u run_weekly_main_image.py --headless >> data\logs\weekly_main_image.log 2>&1
exit /b %ERRORLEVEL%
@@ -0,0 +1,14 @@
@echo off
REM Weekly Monday 10:00 -- collect TM/JD/DY market ranks, create Feishu docs,
REM and archive each platform's latest document URL in PostgreSQL.
setlocal
cd /d %GYXX_RUNTIME_ROOT%
set "PY=%GYXX_RUNTIME_ROOT%\.venv\Scripts\python.exe"
set "PYTHONUNBUFFERED=1"
if not exist data\logs mkdir data\logs
"%PY%" orchestrate_market_rank_collection.py >> data\logs\market_rank_weekly.log 2>&1
exit /b %ERRORLEVEL%
@@ -0,0 +1,21 @@
#Requires -RunAsAdministrator
param(
[string]$ProjectRoot = "",
[string]$NssmPath = "nssm.exe"
)
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($ProjectRoot)) {
$ProjectRoot = if ($env:GYXX_PROJECT_ROOT) {
$env:GYXX_PROJECT_ROOT
} else {
(Resolve-Path (Join-Path $PSScriptRoot "..\..\..\..\..\..")).Path
}
}
$uninstaller = Join-Path $ProjectRoot "deploy\windows-service\uninstall.ps1"
if (-not (Test-Path -LiteralPath $uninstaller -PathType Leaf)) {
throw "Server scheduler uninstaller is missing: $uninstaller"
}
& $uninstaller -NssmPath $NssmPath
exit $LASTEXITCODE
@@ -0,0 +1,22 @@
#Requires -RunAsAdministrator
param(
[string]$ProjectRoot = "",
[string]$DataRoot = "",
[string]$NssmPath = "nssm.exe"
)
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($ProjectRoot)) {
$ProjectRoot = if ($env:GYXX_PROJECT_ROOT) {
$env:GYXX_PROJECT_ROOT
} else {
(Resolve-Path (Join-Path $PSScriptRoot "..\..\..\..\..\..")).Path
}
}
$installer = Join-Path $ProjectRoot "deploy\windows-service\install.ps1"
if (-not (Test-Path -LiteralPath $installer -PathType Leaf)) {
throw "Server scheduler installer is missing: $installer"
}
& $installer -ProjectRoot $ProjectRoot -DataRoot $DataRoot -NssmPath $NssmPath
exit $LASTEXITCODE
@@ -0,0 +1,190 @@
---
name: data-analyzer
description: 分析端。接收用户消息触发采购单更新,发送采购单更新通知,处理编排器发来的工作流分析指令。
metadata:
hermes:
skillKey: data-analyzer
profile: analyzer
os: ["win32"]
requires:
bins: ["python"]
---
# Data Analyzer
## Mission
你是 `data-analyzer`,职责:
1. 读取 `shared-data` 产物做分析/汇总。
2. 发送最终业务通知(给业务对象和 owner)。
3. 接收用户飞书消息,触发采购单日期更新工作流。
## 采购单日期更新触发
当用户发来包含 **8位SKU编码****日期** 的消息时,触发采购单更新工作流。
### 触发条件
消息中同时包含:
- 至少一个 8 位数字 SKU(如 `10439030`
- 一个日期(如 `5月25日``2026-05-25``明天`
### 触发方式
使用终端工具执行(**必须传递 --sender-open-id**,脚本内部校验白名单):
```bash
python -m gyxx_flow.modules.supply_chain.runtime.orchestrator.scripts.trigger_purchase_order_update --message "<用户消息原文>" --sender-open-id "<发送人open_id>"
```
允许触发的 sender_open_id 在 `orchestrator/config.py``FEISHU_CONFIG.trigger.purchase_order_update.allowed_sender_open_ids` 中配置。当前允许:`<ANALYZER_OWNER_OPEN_ID>`
### 执行后
- 工作流触发后,采集端执行 ERP 操作,结果写入 `shared-data/purchase-order-update/update_result.json`
- **通知发送需要分析端手动执行**(见下节),工作流本身不自动发送飞书消息
- 如果触发成功,回复用户"采购单更新已触发,SKU: xxx,目标日期: xxx"
- 如果触发失败(权限不足、解析失败等),回复用户具体原因
- 如果消息不包含 SKU 或日期,按普通对话处理,不触发工作流
## 采购单更新后发送通知
工作流执行完成后,分析端需要手动发送通知到两个目标:
1. **审单群**chat_id: `oc_6e95333db779b07524e1c361099c7aec`
2. **owner 个人**open_id: `ou_7ad5fc8012e2f741afc5346e05ffd447`
### 前置条件
- 分析端飞书机器人(`cli_aa8c4fc918b85cce`)必须已是审单群成员,才能发送到群
- 如果机器人不在群里,会返回 `code=230002: Bot/User can NOT be out of the chat`
### 发送方式(直接调用飞书 Open API)
```python
import requests, json
from datetime import datetime
APP_ID = "cli_aa8c4fc918b85cce"
APP_SECRET = "${GYXX_SUPPLY_ANALYZER_APP_SECRET}"
FEISHU_OPENAPI_BASE = "https://open.feishu.cn/open-apis"
SHENDAN_GROUP_CHAT_ID = "oc_6e95333db779b07524e1c361099c7aec"
MY_OPEN_ID = "ou_7ad5fc8012e2f741afc5346e05ffd447"
# 获取 token
token_resp = requests.post(
f"{FEISHU_OPENAPI_BASE}/auth/v3/tenant_access_token/internal/",
json=dict((("app_id", APP_ID), ("app_secret", APP_SECRET))), timeout=10
)
token = token_resp.json()["tenant_access_token"]
# 构建交互式卡片 (兼容写法:top-level elements + header,避开 body 嵌套翻车)
# records 每条形如 {"sku": "...", "name": "...", "spec": "..."}
header = {
"template": "blue",
"title": {"tag": "plain_text", "content": "📌 采购单更新通知"},
}
elements = [
{
"tag": "div",
"fields": [
{"is_short": True, "text": {"tag": "lark_md", "content": f"**更新时间**\n{datetime.now().strftime('%Y-%m-%d %H:%M')}"}},
{"is_short": True, "text": {"tag": "lark_md", "content": f"**到货日期**\n{target_date}"}},
],
},
{"tag": "div", "text": {"tag": "lark_md", "content": f"✅ **成功更新 {total_updated} 条采购单记录**"}},
{"tag": "hr"},
]
for r in records:
elements.append({
"tag": "div",
"text": {"tag": "lark_md", "content": f"- **SKU:** {r['sku']} **品名:** {r['name']} **规格:** {r['spec']}"},
})
card = {
"config": {"wide_screen_mode": True},
"header": header,
# ⚠️ 2026-06-29:不要把 elements 嵌套在 body 下!Feishu 存盘会 strip 掉 body,
# 用户收到空卡。用 top-level elements(详见上文避坑)。
"elements": elements,
}
# 发送到审单群
requests.post(
f"{FEISHU_OPENAPI_BASE}/im/v1/messages",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
params={"receive_id_type": "chat_id"},
json={"receive_id": SHENDAN_GROUP_CHAT_ID, "msg_type": "interactive",
"content": json.dumps(card, ensure_ascii=False)},
timeout=10
)
# 发送到个人
requests.post(
f"{FEISHU_OPENAPI_BASE}/im/v1/messages",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
params={"receive_id_type": "open_id"},
json={"receive_id": MY_OPEN_ID, "msg_type": "interactive",
"content": json.dumps(card, ensure_ascii=False)},
timeout=10
)
```
### 通知内容要求(来自工作流规则)
必须包含:SKU列表、商品名称、规格颜色、到货日期、更新记录数。
### 如果机器人不在审单群
1. 先发送通知到个人账户告知结果
2. 告知用户需要将分析端机器人加入审单群
### 备选:lark-cli 发送(不推荐主用)
如果不想手写 `requests.post` token 流程,可改用 `lark-cli`(已配 `hermes-analyzer` profile):
```bash
# 1. 把 card 写到临时文件
cat > /tmp/card.json << 'JSON'
{
"config": {"wide_screen_mode": true},
"header": {"template": "blue", "title": {"tag": "plain_text", "content": "📌 采购单更新通知"}},
"elements": [...]
}
JSON
# 2. 发给个人(analyzer ownerbot 身份在 hermes-analyzer profile 下)
lark-cli --profile hermes-analyzer im +messages-send \
--as bot --user-id ou_7ad5fc8012e2f741afc5346e05ffd447 \
--msg-type interactive --content "$(cat /tmp/card.json)"
# 3. 发给审单群(用 --chat-id + bot 身份)
lark-cli --profile hermes-analyzer im +messages-send \
--as bot --chat-id oc_6e95333db779b07524e1c361099c7aec \
--msg-type interactive --content "$(cat /tmp/card.json)"
```
**关键点**
- 必须在命令前加 `--profile hermes-analyzer`lark-cli 默认 `currentApp` 是采集端 `cli_aa8c4fb4c4f81cd3`,发给 analyzer owner 会 `code: 99992361 "open_id cross app"`)。
- 卡片 JSON 用 **top-level `elements`**(不要再用 `body.elements` 嵌套,详见上文 Hard Rules 避坑段)。`header.template` + `header.title` 控制标题颜色和文字。
- `--as bot` 不需要额外权限;`--as user` 需要 `im:message.send_as_user` scope,分析端应用未配置。
## 工作流分析(由编排器调用)
当收到编排器发来的工作流分析指令时:
1. 读取 `shared-data` 下对应工作流的输出文件
2. 分析数据并发送最终业务通知
3. 不发送过程/进度通知
## 参考资料
- `references/purchase-order-update-config.md` — 飞书应用配置、审单群 chat_id、触发白名单、常见错误码
## Hard Rules
- 不运行采集脚本(`collect_*.ps1`)。
- 不发送采集心跳、进度播报给业务用户。
- 最终业务通知如果内容过长,必须拆分为多条消息,使用 `[1/N]...[N/N]` 前缀。
- 禁止截断通知内容。
- **禁止用本地 Python/PowerShell 脚本发送飞书业务通知**,必须通过 `execute_code` 直接调飞书 Open API。
- 例外:**Schema 2.0 卡片发送**统一走 `orchestrator/scripts/send_card_notification.py`(2026-06-29 起,LLM 直接调 lark-cli im +messages-send --file 翻车)。其它通知场景仍按上面铁律。
- **业务通知必须使用 interactive card**`msg_type="interactive"`, `content` 为卡片 JSON 字符串),header 配色按场景选 blue/green/red;不要发 plain text 或 post 富文本。
- **【避坑 2026-06-29】卡片 elements 不要用 `body.elements` 嵌套!** 实测 Feishu `/im/v1/messages` POST 返回 200 OK,但存盘时把整个 `body` 字段丢掉,用户收到"只有标题的空卡片"。**正确写法:top-level elements + header(template+title)**,`send_card_notification.py` 已加 body→top-level 兜底,但 LLM 应该直接发对。
```json
// ✓ 推荐 (兼容写法,header 控色,elements 在 top-level)
{"config": {"wide_screen_mode": true},
"header": {"template": "blue", "title": {"tag": "plain_text", "content": "📌 通知标题"}},
"elements": [{"tag": "div", "text": {"tag": "lark_md", "content": "**正文**"}}]}
// ✗ 会翻车 (Schema 2.0 嵌套,body 存盘被 strip)
{"header": {...}, "body": {"elements": [...]}}
```
- 用 lark-cli 发卡时必须加 `--profile hermes-analyzer`,否则 lark-cli 用采集端 bot 身份,发给分析端用户会 `code: 99992361 "open_id cross app"`。卡片 JSON 用 **top-level elements**(不要再用 `body.elements` 嵌套,见上面避坑)。
- **【避坑 2026-06-29】不要在卡片里用 `tag: "table"` 元素**。`/im/v1/messages` 报 400 ErrCode 200906 「table columns is empty」(加 column_widths 也不解决);改用 cells/td 又触发 200621 「parse card json err」;改用 column name 映射 rows 能 send(200)但飞书客户端显示「请升级至最新版本」的占位图。**结论:表格用 `div + lark_md` 包 ```markdown|...|...|``` 代码块渲染,稳定可靠。**
@@ -0,0 +1,151 @@
---
name: purchase-order-update
description: 采购单日期更新工作流 — 触发工作流更新ERP中SKU的协议到货日期,然后发送飞书通知到审单群和用户
---
# Purchase Order Update Workflow(采购单日期更新工作流)
## 触发条件
用户发送消息包含 **8位SKU****到货日期**,支持多种自然语言格式。
### 支持的消息格式示例
| 示例消息 | 说明 |
|---------|------|
| `10465007 更新到货日期 5月19日` | ✅ |
| `10465007 到货日期改为 2026-05-19` | ✅ |
| `采购单 10465007 5月19日到货` | ✅ |
| `10465007 明天到货` | ✅(明天自动换算) |
| `104650075月19日,采购到货` | ✅ |
| `10439048 10439047 更新到货日期 5月19日` | ✅(多SKU |
| `10465007 更新 5月19日` | ✅ |
### 日期格式支持
- `YYYY-MM-DD``2026-05-19`
- `YYYY/MM/DD``2026/05/19`
- `5月19日` / `5月19号`
- `今天` / `明天` / `后天` / `大后天`
### 关键词忽略
`采购``到货``日期``更新``协议` 等字词会被自动过滤,不影响解析。
## 执行步骤
### 1. 准备任务文件
由于 `orchestrator.config` 模块当前不可用,直接写入 task.json:
```bash
mkdir -p ${GYXX_PROJECT_ROOT}/shared-data/purchase-order-update
# 写入 task.json
{
"sku_list": ["<SKU1>", "<SKU2>"],
"target_date": "2026-05-19",
"remark": "",
"created_at": "<ISO时间>",
"source": "analyzer_message_trigger"
}
```
### 2. 直接运行采购单更新脚本
```bash
cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe orchestrator/scripts/PurchaseOrderUpdate.py --task-file ${GYXX_PROJECT_ROOT}/shared-data/purchase-order-update/task.json
```
- 超时:300秒
- 成功标志:日志含 `[LOG] SCRIPT_COMPLETED`
- 结果写入:`orchestrator/scripts/data/update_result.json`
### 3. 发送飞书通知
#### 3.1 获取 tenant_access_token
```python
import requests
APP_ID = "cli_aa8c4fc918b85cce"
APP_SECRET = "${GYXX_SUPPLY_ANALYZER_APP_SECRET}"
token = requests.post(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/",
json=dict((("app_id", APP_ID), ("app_secret", APP_SECRET))),
timeout=10
).json()["tenant_access_token"]
```
#### 3.2 构建通知内容
```python
import json
from datetime import datetime
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
# Schema 2.0 交互式卡片
header = {
"template": "blue",
"title": {"tag": "plain_text", "content": "📌 采购单更新通知"},
}
elements = [
{
"tag": "div",
"fields": [
{"is_short": True, "text": {"tag": "lark_md", "content": f"**更新时间**\n{current_time}"}},
{"is_short": True, "text": {"tag": "lark_md", "content": f"**到货日期**\n{target_date}"}},
],
},
{"tag": "div", "text": {"tag": "lark_md", "content": f"✅ **成功更新 {total_updated} 条采购单记录**"}},
{"tag": "hr"},
]
# 每条记录一行:- SKU:{sku},品名:{name},规格:{spec}
for r in records:
elements.append({
"tag": "div",
"text": {"tag": "lark_md", "content": f"- **SKU:** {r['sku']} **品名:** {r['name']} **规格:** {r['spec']}"},
})
card = {
"config": {"wide_screen_mode": True},
"header": header,
"body": {"elements": elements}, # Schema 2.0 规范:elements 嵌套在 body 下
}
```
#### 3.3 发送消息
```python
SHENDAN_GROUP_CHAT_ID = "oc_6e95333db779b07524e1c361099c7aec"
MY_OPEN_ID = "ou_7ad5fc8012e2f741afc5346e05ffd447"
# 发送审单群
requests.post(
"https://open.feishu.cn/open-apis/im/v1/messages",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
params={"receive_id_type": "chat_id"},
json={
"receive_id": SHENDAN_GROUP_CHAT_ID,
"msg_type": "interactive",
"content": json.dumps(card, ensure_ascii=False)
},
timeout=10
)
# 发送本人
requests.post(
"https://open.feishu.cn/open-apis/im/v1/messages",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
params={"receive_id_type": "open_id"},
json={
"receive_id": MY_OPEN_ID,
"msg_type": "interactive",
"content": json.dumps(card, ensure_ascii=False)
},
timeout=10
)
```
## 白名单配置
`${GYXX_PROJECT_ROOT}\orchestrator\config.py``FEISHU_CONFIG.trigger.purchase_order_update.allowed_sender_open_ids`
- `ou_7ad5fc8012e2f741afc5346e05ffd447`(用户)
- `ou_339c1d396b397c97b08e1bf377963d40`
- `ou_54141d75b24d22ddc8c1c4d66e00b6e9`
## 注意事项
- `orchestrator.config` 模块缺失(WORKFLOWS / FEISHU_CONFIG 未定义),trigger 脚本因此不可用;改用直接脚本执行可正常工作
- 发送审单群时需确认分析端机器人已在群中
- 工作流会自动更新 ERP 系统中对应 SKU 采购单的协议到货日期
- Playwright 浏览器依赖若未安装(`Executable doesn't exist`),需先运行:`.venv\Scripts\python.exe -m playwright install chromium`
- 卡片 JSON 用 `body.elements` 嵌套(Schema 2.0 规范);top-level `elements` 也兼容但官方不推荐
- 若改用 lark-cli 发送,命令前必须加 `--profile hermes-analyzer`lark-cli 默认采集端 bot 身份),否则 `code: 99992361 "open_id cross app"`
@@ -0,0 +1,88 @@
---
name: data-collector-guardrail
description: "Guardrail for collector: collect + heartbeat only."
metadata:
hermes:
skillKey: data-collector-guardrail
profile: data-collector
os: ["win32"]
---
Collector Guardrail
You are the collector endpoint.
Do:
- Run collection scripts.
- Publish fresh artifacts to shared-data.
- (Heartbeat is now sent automatically by `orchestrator/mcp_workflow.py` as a Schema 2.0 interactive card to `ou_8ee224968aa26a74c7d30ba27fed5eeb`. Do NOT call send_message / curl / any Feishu API to relay heartbeats.)
- For **manual** ad-hoc heartbeat relay (operator asks in chat to forward a workflow report to the admin), reply directly with the same fields described in `Collector Heartbeat Notification Format` below — the gateway binding delivers the reply.
Do not:
- Send final business notifications.
- Run analyzer-stage actions.
- Mix analyzer app credentials (analyzer app: cli_aa8c4fc918b85cce, analyzer owner: ou_7ad5fc8012e2f741afc5346e05ffd447).
- Try to call send_message, curl, or external APIs to relay **automated** collector heartbeats — orchestrator already sends them as cards.
Credential path: The collector app secret (`2lAvcKK4gX2Qa6uCrhxgZedTzbc70a7U`) lives in two places:
- Hermes profile `.env`: `${HERMES_STATE_ROOT}\profiles\data-collector\.env` under `FEISHU_APP_SECRET` (for cron / LLM-direct paths).
- `orchestrator/config.py``FEISHU_CONFIG.apps.collector.app_secret` (env override `AUTOFLOW_COLLECTOR_APP_SECRET`). Used by `mcp_workflow.py:_send_collector_heartbeat` for the new automated card-based heartbeats.
It is NOT in `orchestrator/scripts/config.json``feishu.app_secret` / `feishu_analyzer.app_secret` (those are analyzer app credentials). Scripts like `send_collector_notify.py` that fall back to that file will fail with `code: 10014, app secret invalid`.
**Automated heartbeat (post-2026-06-27):** The orchestrator sends Schema 2.0 interactive cards via `mcp_workflow.py:_send_collector_heartbeat``requests.post(/open-apis/im/v1/messages)` with the collector app credentials, target `ou_8ee224968aa26a74c7d30ba27fed5eeb`. Phases: `start` (blue), `progress` (blue, every `HEARTBEAT_INTERVAL_SECONDS`=180s), `final` (green on success / red on failure). The collector LLM is NOT involved — do not duplicate.
**Manual relay (operator asks in chat to forward a report):** Reply directly with the format below. The gateway binding delivers it. No tool calls needed.
Pitfalls:
| Collector app owner open_id | `ou_8ee224968aa26a74c7d30ba27fed5eeb` — the admin/reporter who receives collector heartbeat notifications via collector Feishu app |
| Notification target (admin/reporter) | `ou_8ee224968aa26a74c7d30ba27fed5eeb` — this profile receives workflow heartbeat reports (NOT the analyzer app owner) |
| Analyzer app owner open_id | `ou_7ad5fc8012e2f741afc5346e05ffd447` — NOT the collector heartbeat target; this belongs to the analyzer app scope (`cli_aa8c4fc918b85cce`) |
- UTF-8 BOM manifest error: If a workflow fails with `Invalid run manifest: Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1`, the manifest JSON file was saved with a BOM (common on Windows editors like Notepad). Fix: resave the manifest as UTF-8 without BOM, or strip the BOM (`\ufeff`) from the file's first bytes.
- batch_process.py scripts must be saved as UTF-8 encoding. GBK/CP936 encoding corrupts Chinese string literals and causes "unterminated string literal" SyntaxError on line 41.
- Yuanbao toolsets are listed in platform_toolsets.cli but the `yb` CLI binary and `yb_send_dm`/`yb_query_group_members` functions are NOT available on data-collector — `yb_send_dm: command not found` is the expected result. Do not search for the `yb` binary, attempt `hermes tools enable yuanbao` (it will succeed but expose nothing usable), or try to curl localhost:8123/yuanbao/send_dm. Yuanbao adapter is also absent from channel_directory.json (yuanbao: []). Always fall back to direct reply immediately — the API server delivers collector heartbeat reports without any Yuanbao integration.
- Cron jobs running collector workflows (deliver: local): output IS the notification — do NOT try to call Feishu REST APIs from cron. The cron output goes to the gateway chat; the gateway relays to Feishu. If you need to send Feishu from cron, you need the app_secret accessible in the cron env (`FEISHU_APP_SECRET`) — collector app secrets are often gateway-runtime-injected and NOT stored in the codebase. Use `FEISHU_APP_SECRET` (not `COLLECTOR_FEISHU_APP_SECRET`).
- **`execute_code` filesystem access is limited to the sandbox directory.** `execute_code` runs in an isolated sandbox — it cannot access host paths like `${GYXX_PROJECT_ROOT}\shared-data\` or `${HERMES_STATE_ROOT}\...`. Attempting to open host paths via `Path()` or `read_bytes()` will raise `FileNotFoundError`. Use `terminal` (which inherits the MSYS/Git-Bash environment with proper path mapping) for all file operations on host paths. The `execute_code` sandbox is only suitable for pure in-memory computation or accessing paths that the sandbox itself creates.
- **`execute_code` can make outbound HTTP calls** — the "cannot call Feishu REST APIs" restriction is outdated. `requests` + `FEISHU_APP_SECRET` works fine from `execute_code` in the agent process. The failure scenario is cron job scripts running in a subprocess (where `FEISHU_APP_SECRET` is not injected). Always check `os.environ.get("FEISHU_APP_SECRET")` before assuming it works or fails.
- **`terminal` + inline Python script** is the most reliable pattern for Feishu REST calls. `python -c "..."` via terminal inherits the shell env, works in both agent and cron contexts.
- **Manual relay only (operator asks in chat to forward a report) — reply DIRECTLY with no tool calls.** Your reply text IS the notification. Do NOT query channel_directory, search for yuanbao group codes, call send_message, or look up credentials. This is for **manual / ad-hoc** forwarding only — automated heartbeats are sent by the orchestrator as cards.
- Common mistake: trying to relay an automated heartbeat (orchestrator already sent it) or calling send_message for manual relay. Both are unnecessary.
- Cron jobs with **gateway Feishu binding** (deliver: origin/local): reply directly — the gateway's own Feishu binding delivers the reply. (For automated heartbeats the orchestrator sends the card itself, so this is now mostly relevant for operator-side messages, not workflow status.)
- Scripts run via **terminal** without gateway binding: use `python -c "..."` to call Feishu REST APIs with `FEISHU_APP_SECRET`. (Same caveat — only relevant for paths outside the orchestrator's automated heartbeat flow.)
**Cron job skills that don't exist cause a skip warning but don't fail the job.** When a cron job's `skills` list contains a nonexistent skill, the scheduler logs `WARNING Cron job 'X': skill not found, skipping — Skill 'feishu_doc' not found.` The job still runs with the remaining skills or no skills. If the cron job silently does nothing or hits an unexpected code path, check `logs/errors.log` for this warning — the missing skill is the likely cause. To fix: remove the nonexistent skill from the cron job's `skills` list, or create the skill.
- Example: A `purchase-confirmation` workflow monitor cron job listed `feishu_doc` in skills but the skill was deleted. The job's `feishu_doc_read` tool call failed because the skill wasn't loaded, producing a partial/incomplete result with no error surfaced to the user.
## Collector Heartbeat Notification Format
**Automated heartbeats are sent by the orchestrator as Schema 2.0 cards** (see "Automated heartbeat" under Credential path). The LLM does not need to format or send them.
**Manual relay only** (operator asks in chat to forward a workflow report) — include all key fields:
```
[采集端心跳] <工作流名称>
Workflow ID: <id>
运行 ID: <run_id>
状态: <status>
当前阶段: <phase>
采集尝试次数: <n>
分析尝试次数: <n>
执行结论: <conclusion>
```
Manual relay pattern (for owner-only reports):
- User asks: "notify me only, not business targets" → reply directly as your response text (API server delivers it).
- The reply content IS the notification — no extra tools needed.
- Include all key fields: workflow ID, run ID, phase, status, attempts, error details, collection summary.
- Do NOT also call send_message / lark-cli / lark-cli / Feishu REST API — that would double-send.
Reference: `references/feishu-open-id-cross-app.md` — Feishu open_id cross-app failure and correct routing. Collector heartbeat target is `ou_8ee224968aa26a74c7d30ba27fed5eeb` via collector app (`cli_aa8c4fb4c4f81cd3`). `ou_7ad5fc8012e2f741afc5346e05ffd447` is the analyzer app owner — NOT the collector heartbeat target. Full REST call pattern included.
Reference: `references/collector-notification-credentials.md` — Collector Feishu notification credential sources: direct-reply pattern vs REST API pattern, and the known bug where `send_collector_notify.py` reads the wrong app secret from `config.json`.
Reference: `references/workflow-state-querying.md` — How to query collector workflow state. **Primary source: `shared-data/` artifacts + agent logs.** The orchestrator's `state.db` (LangChain checkpoints) only stores `replenishment-alert` runs — NOT collector runs. See the reference file for the full state querying strategy.
**Reusable monitoring script:** `scripts/monitor_purchase_confirmation.py` — drop-in script that queries collector session DB + shared-data, then sends Feishu admin notification. Run with `python scripts/monitor_purchase_confirmation.py <RUN_ID>`. Handles `orchestrator/state.db` ≠ collector workflow source correctly.
Final check:
- Collection only.
- No business-user final notify.
- Shared-data contains only this run outputs.
@@ -0,0 +1,58 @@
---
name: purchase-confirmation-workflow
description: 触发采购确认通知工作流 (purchase-confirmation)
triggers:
- manual: 用户要求"执行/触发 purchase-confirmation"
- scheduled: 通过 cron 定时触发
---
# Purchase Confirmation Workflow
Trigger the `purchase-confirmation` (采购确认通知工作流) from the `${GYXX_PROJECT_ROOT}` orchestrator.
## ⚠️ Important Execution Notes
- **执行时间长**:此工作流包含浏览器自动化(Chrome CDP),采集脚本执行时间约 **5-15 分钟**,请耐心等待,**不要**超时后随意清理锁和进程
- **不要主动终止**:看到超时不要清理锁/杀进程,工作流可能仍在正常执行
- **判断完成**:检查 `shared-data/purchase-confirmation/` 目录中是否有 CSV 文件(如 `采购确认通知汇总_YYYYMMDD_HHMMSS.csv`
## Workflow Command
```bash
cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run purchase-confirmation
```
工作流通过 Hermes MCP 网关执行,包含采集端和分析端两个阶段。
## 创建定时任务
```python
cronjob(action='create',
prompt='执行 purchase-confirmation 工作流。运行命令:cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run purchase-confirmation',
schedule='0 9 * * *', # 每天早上9点
name='purchase-confirmation-daily')
```
## 通知规则(关键)
⚠️ **采集端通知规则**:采集端 Hermes 只通知运营,不通知业务目标用户。
- 采集端通知对象:运营(飞书 OpenID `ou_7ad5fc8012e2f741afc5346e05ffd447`
- 业务最终通知:由分析端 Hermes 负责
- 通知方式:通过当前会话直接响应(不调用 send_message/yuanbao 派)
当用户请求"只通知我(不要通知业务目标用户)"时:
1. 直接在当前会话中以文本形式输出报告
2. 说明通知范围:仅运营,业务目标用户由分析端 Hermes 负责
## 工作流配置参考
- **工作流名称**purchase-confirmation
- **显示名称**:采购确认通知工作流
- **采集脚本**`${GYXX_PROJECT_ROOT}/orchestrator/scripts/collect_confirmation.ps1`
- **输出文件**`采购确认通知汇总_*.csv`xlsx 格式)
- **通知目标**(业务用户):
- `ou_7ad5fc8012e2f741afc5346e05ffd447`
- `ou_339c1d396b397c97b08e1bf377963d40`
- `ou_54141d75b24d22ddc8c1c4d66e00b6e9`
- **采集端绑定应用**`cli_aa8c4fb4c4f81cd3`
- **分析端绑定应用**`cli_aa8c4fc918b85cce`
@@ -0,0 +1,34 @@
---
name: purchase-order-update-workflow
description: 触发采购单日期更新工作流 (purchase-order-update)
category: workflow
triggers:
- manual: 用户要求"执行/触发 purchase-order-update"
- scheduled: 通过 cron 定时触发
---
# Purchase Order Update Workflow
Trigger the `purchase-order-update`(采购单日期更新工作流)from the `${GYXX_PROJECT_ROOT}` orchestrator.
## Workflow Command
```bash
cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run purchase-order-update
```
## 工作流配置(来源 bytecode 反向提取)
| 字段 | 值 |
|------|-----|
| **工作流名称** | purchase-order-update |
| **显示名称** | 采购单日期更新工作流 |
| **采集脚本** | `collect_purchase_order_update.ps1` |
| **输出文件** | `update_result.json` |
| **分析端通知方式** | `plugin:feishu.im.send_message` |
| **采集端绑定应用** | `cli_aa8c4fb4c4f81cd3` |
| **分析端绑定应用** | `cli_aa8c4fc918b85cce` |
## ⚠️ config.py 源文件已丢失
此工作流配置从 bytecode 反向提取,如需修改通知目标,需先重建 `config.py`
@@ -0,0 +1,41 @@
---
name: replenishment-alert-workflow
description: 触发库存预警通知工作流 (replenishment-alert)
triggers:
- manual: 用户要求"执行/触发 replenishment-alert"
- scheduled: 通过 cron 定时触发
---
# Replenishment Alert Workflow
Trigger the `replenishment-alert` (库存预警通知工作流) from the `${GYXX_PROJECT_ROOT}` orchestrator.
## Trigger Conditions
- **手动触发**:用户明确要求"执行/触发 replenishment-alert"
- **定时触发**:通过 cron 任务定期运行
## Workflow Command
```bash
cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run replenishment-alert
```
工作流通过 Hermes MCP 网关执行,包含采集端和分析端两个阶段。
## 已知问题
⚠️ **通知重复问题**:如需修复,从 `config.py``notification_targets.threshold_alert` 中移除 analyzer owner。但注意:`config.py` 源文件已丢失(仅 pyc 缓存),修改前需先重建配置。
⚠️ **config.py 源文件已丢失**:仅存 `__pycache__/config.cpython-311.pyc`,如需修改通知目标,需从 bytecode 反向提取常量后再重建文件。
## 工作流配置参考
- **工作流名称**replenishment-alert
- **显示名称**:库存预警通知工作流
- **采集脚本**`${GYXX_PROJECT_ROOT}/orchestrator/scripts/collect_replenishment.ps1`
- **输出文件**`threshold_alert_data.json`
- **通知目标**(业务用户,仅 `threshold_alert`,来源 bytecode const[53]):
- `ou_b76e4cbdb24fe28cebd45ad091b60224`(黄坤平)
- **采集端绑定应用**`cli_aa8c4fb4c4f81cd3`
- **分析端绑定应用**`cli_aa8c4fc918b85cce`
@@ -0,0 +1,54 @@
---
name: replenishment-workflow
description: 触发补货建议工作流 (replenishment)
triggers:
- manual: 用户要求"执行/触发 replenishment"
- scheduled: 通过 cron 定时触发
---
# Replenishment Workflow
Trigger the `replenishment` (补货建议工作流) from the `${GYXX_PROJECT_ROOT}` orchestrator.
## ⚠️ Important Execution Notes
- **执行时间长**:此工作流包含浏览器自动化(Chrome CDP),采集脚本执行时间约 **5-15 分钟**,请耐心等待
- **判断完成**:检查 `shared-data/replenishment/` 目录中是否有 `pending_insert.json``notify_data.json`
- **后台执行**:建议使用 `background=true` + `notify_on_complete=true` 执行,避免超时
## Workflow Command
```bash
cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run replenishment
```
## 创建定时任务
```python
cronjob(action='create',
prompt='执行 replenishment 工作流。运行命令:cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run replenishment',
schedule='0 9 * * *', # 每天早上9点
name='replenishment-daily')
```
## 通知目标(分析端)
仅通知运营(`ou_7ad5fc8012e2f741afc5346e05ffd447`)。不通知黄坤平,不通知审单群。
## 工作流配置参考
- **工作流名称**replenishment
- **显示名称**:补货建议工作流
- **采集脚本**`${GYXX_PROJECT_ROOT}/orchestrator/scripts/collect_replenishment.ps1`
- **输出文件**`pending_insert.json`, `notify_data.json`
- **多维表**:应用 `cli_aa8c4fc918b85cce` 需对多维表 `Th0jbMHHQa7a8Lse3CicfSMBnxb` 有写入权限
- **采集端绑定应用**`cli_aa8c4fb4c4f81cd3`
- **分析端绑定应用**`cli_aa8c4fc918b85cce`
## 已知问题
⚠️ **多维表写入权限**:如遇以下错误,需在飞书开放平台为应用 `cli_aa8c4fc918b85cce` 开启多维表写入权限:
- `91403 Forbidden` — 无权限访问多维表
- `99992402 field validation failed` — 字段验证失败(常见于 app 无写入权限或字段类型不匹配)
写入失败**不影响通知发送**(符合 HARD RULES),但待处理数据需手动补充。
@@ -0,0 +1,57 @@
---
name: workflow-trigger-only
description: 工作流触发原则:只执行既有命令,不自主生成代码修改流程
triggers:
- manual: 用户要求将某个操作原则沉淀为 skill 时创建
---
# Workflow Trigger Only
## 核心原则
**只触发既有工作流,不自主生成或修改流程代码。**
## 执行方式
```
cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run <工作流名>
```
支持的 `<工作流名>`
- `replenishment-alert` — 库存预警通知
- `purchase-confirmation` — 采购确认通知
- `replenishment` — 补货建议
## 规则
1. **只运行既有命令** — 使用 `run.py mcp-run` 触发,不写 Python 脚本绕过后续流程
2. **不自主修改流程代码** — 不修改 `mcp_workflow.py`、通知脚本、采集脚本等,除非用户明确要求
3. **不自主生成新代码** — 不创建新脚本自行完成通知/写表等操作
4. **编排逻辑由既有代码决定** — 采集→分析→通知的分段流程和通知对象由 `mcp_workflow.py` 和分析端 Hermes 控制
## 例外:手动转发心跳/执行报告
"只触发既有命令"规则**仅适用于 orchestrator 工作流执行**。当用户要求将某个工作流的心跳/执行报告**手动转发**给运营时:
1. 构造心跳消息内容(包含工作流 ID、运行 ID、状态、错误信息等)
2. 通过飞书采集端应用(`cli_aa8c4fb4c4f81cd3`)将报告以 **Feishu DM** 形式发送给运营(`ou_8ee224968aa26a74c7d30ba27fed5eeb`
3. **不**通知业务目标用户,不走 yuanbao group
4. 使用 `feishu` skill 中的 temp 脚本模式发送(写临时脚本 → terminal 运行 → 删除),或直接 reply 让 gateway 转发
> 注意:**自动心跳已由 `orchestrator/mcp_workflow.py` 用 Schema 2.0 interactive card 发送**target 同样是 `ou_8ee224968aa26a74c7d30ba27fed5eeb`),本例外仅适用于**人工额外转发**场景(比如重发、转发给第三方等),不要重复发送已自动发出的心跳。
这是人工操作员在对话中转发报告的合法场景,与 orchestrator 自动执行工作流是两回事。
## 触发命令参考
| 工作流 | 命令 |
|--------|------|
| 库存预警通知 | `cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run replenishment-alert` |
| 采购确认通知 | `cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run purchase-confirmation` |
| 补货建议 | `cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run replenishment` |
## 为什么这样做
- 工作流的编排逻辑(采集、分析、通知分段执行)由 `mcp_workflow.py` 控制
- 通知对象由 `config.py` 和分析端 prompt 决定,不应在触发层硬编码
- 自主生成代码容易出错且难以追踪
@@ -0,0 +1,6 @@
@echo off
setlocal
if defined GYXX_PROJECT_ROOT (set "PROJECT_ROOT=%GYXX_PROJECT_ROOT%") else (set "PROJECT_ROOT=%~dp0\..\..\..\..\..\..\..")
for %%I in ("%PROJECT_ROOT%") do set "PROJECT_ROOT=%%~fI"
"%PROJECT_ROOT%\.venv\Scripts\python.exe" -m gyxx_flow.cli run supply.purchase_confirmation.daily
endlocal & exit /b %ERRORLEVEL%
@@ -0,0 +1,6 @@
@echo off
setlocal
if defined GYXX_PROJECT_ROOT (set "PROJECT_ROOT=%GYXX_PROJECT_ROOT%") else (set "PROJECT_ROOT=%~dp0\..\..\..\..\..\..\..")
for %%I in ("%PROJECT_ROOT%") do set "PROJECT_ROOT=%%~fI"
"%PROJECT_ROOT%\.venv\Scripts\python.exe" -m gyxx_flow.cli run supply.replenishment_alert.daily
endlocal & exit /b %ERRORLEVEL%
@@ -0,0 +1,6 @@
@echo off
setlocal
if defined GYXX_PROJECT_ROOT (set "PROJECT_ROOT=%GYXX_PROJECT_ROOT%") else (set "PROJECT_ROOT=%~dp0\..\..\..\..\..\..\..")
for %%I in ("%PROJECT_ROOT%") do set "PROJECT_ROOT=%%~fI"
"%PROJECT_ROOT%\.venv\Scripts\python.exe" -m gyxx_flow.cli run supply.replenishment.weekly
endlocal & exit /b %ERRORLEVEL%
-29
View File
@@ -1,29 +0,0 @@
# 迁移手册
## 当前边界
- 四个旧项目的源码、数据和现有计划任务保持原状。
- 工作流及其业务脚本已复制到 `src/gyxx_flow/modules/*/runtime`,运行时不引用旧项目。
- 21 个定时工作流可生成候选系统任务;默认不应用。
- 所有发现到的本地可执行脚本可通过 `gyxx scripts` 手工 dry-run 或执行。
- 历史数据副本按模块落入新数据根,并保留数量、字节和 SHA-256 对账证据。
## 工程验收
1. 四份 `config/source-manifests/*.json` 的目标文件和 SHA-256 全部通过。
2. 扫描源码、配置和启动器,确认无四个旧项目根目录或 `GYXX_LEGACY_*_ROOT`
3. 在不设置旧项目环境变量的进程中导入目录、列出脚本并 dry-run 21 个工作流。
4. 执行 `uv run pytest``uv run gyxx doctor --json` 和 wheel 构建检查。
5. 只生成调度计划,不注册、不禁用任何系统任务。
## 逐任务生产切换
1. 轮换历史明文凭据,并将新凭据放入外部密钥系统或环境变量。
2. 对目标 workflow 做 dry-run 和 shadow 对账。
3. 审核 run journal、artifact manifest、effect ledger 和 outbox。
4. 取得负责人明确授权,只安装一个新任务并禁用对应旧任务;旧任务不删除。
5. 日任务连续观察 7 天,周任务观察 2 个周期,月任务完成指定历史月回放。
6. 数据库、飞书、文件产物和通知对账通过后,才迁移下一任务。
推荐顺序:`shop_intelligence``supply_chain``content_marketing`
`product_commerce`
-29
View File
@@ -1,29 +0,0 @@
# 日常运维手册
## 常用命令
```powershell
gyxx list --json
gyxx scripts list --json
gyxx doctor --json
gyxx acceptance status --json
gyxx run shop.metrics.weekly --date 2026-07-27
gyxx backfill shop.metrics.weekly --from 2026-07-21 --to 2026-07-27
gyxx scripts run shop_intelligence:runners/run_shop.py --date 2026-07-27
```
`run``backfill``scripts run` 默认 dry-run。`--execute` 才启动本地迁入脚本;
Task Scheduler 使用 `--scheduled`,按 Asia/Shanghai 当日执行。
## 数据与证据
- `var/runs/.../<run_id>/run.json`:步骤、尝试、退出码和 trace
- `var/state/ops/run-index/`:按 run_id/workflow/date/status 查询
- `var/state/ops/effects/`:生产 Sink 幂等回执
- `var/state/ops/outbox/messages/`:外部写入消息
- `var/logs/``var/data/evidence/`:日志和截图证据
- `var/state/profiles/`:浏览器 Profile
- `var/state/locks/`:工作流、Profile 和共享资源锁
effect 为 `ambiguous` 或长时间 `in_progress` 时,先人工核对外部系统;不要删除回执
或强制重跑。手工脚本执行同样会产生统一 run journal 和 effect 记录。
+77
View File
@@ -0,0 +1,77 @@
# GYXX Flow 重构执行计划
验收规则:只有在具备代码、配置或自动化测试证据后才能勾选。本次验收统一使用 dry-run;手动业务入口默认 dry-run,调度预演显式使用 `--dry-run --once`。本轮不启动真实采集,不写飞书、PostgreSQL、Hermes 或业务浏览器会话。
## P0 业务代码同步
- [x] P0.1 只读比较 4 个业务源码目录与项目清单,识别最新更新、目标适配和冲突。
- [x] P0.2 将最新业务源码完整复制到对应模块,不通过导入、挂载或子进程引用外部项目代码。
- [x] P0.3 对需要统一路径、数据库、Hermes 和浏览器状态的文件执行目标项目适配。
- [x] P0.4 更新 233 个来源/目标清单 SHA-256;同步状态为 233 个 `in_sync`
## P1 LangGraph 工作流
- [x] P1.1 建立 4 个高内聚业务模块和统一模块契约,禁止业务模块互相导入内部实现。
- [x] P1.2 将每个定时任务声明为独立工作流,并为每个定时工作流显式声明图节点。
- [x] P1.3 21 个定时工作流全部编译为 LangGraph `StateGraph` 并启用。
- [x] P1.4 工作流目录收敛为 21 个有效调度项;7 个手动能力转为命令,重复或失效历史项不再注册。
- [x] P1.5 京东自营每日工作流按品牌、商品两节点执行;品牌失败后商品仍运行,最终状态保留失败。
- [x] P1.6 统一重试、超时、依赖、fan-in、日志、run_id、资源锁和外部副作用幂等契约。
## P2 Python 调度
- [x] P2.1 使用单一 Python 常驻调度器读取 `config/schedules.json`
- [x] P2.2 支持日、周、月、间隔日、Asia/Shanghai、错过触发补偿和优雅停止。
- [x] P2.3 持久化时间槽并提供单实例锁、同工作流不重叠和重启防重复。
- [x] P2.4 京东自营每日工作流 10:00 运行,业务日期固定为调度日期前一天。
- [x] P2.5 退出 Windows Task Scheduler 执行链;服务器只托管一个 Python 调度进程。
## P3 外部系统与数据目录
- [x] P3.1 PostgreSQL 统一使用运行时注入的云端 DSN,源码不设置地址、数据库、用户或密码默认值,回环地址立即拒绝。
- [x] P3.2 Hermes 保持本机采集端和分析端两个角色,统一配置 2 个 API 和 2 个 gateway。
- [x] P3.3 飞书保持既有身份、应用和调用方式,密钥只从运行时环境注入。
- [x] P3.4 31 个公开命令显式注册;136 个浏览器状态键各自使用唯一 CDP 端口、Profile、Cookie 和 storage state。
- [x] P3.5 JSON、Markdown、CSV、Excel、下载文件和截图按模块及数据阶段分目录保存。
- [x] P3.6 运行状态、日志、临时文件与业务数据分离,整体可随 `GYXX_DATA_ROOT` 迁移。
## P4 可部署性与低耦合
- [x] P4.1 项目运行不依赖外部源码目录、固定盘符或 Windows 定时任务。
- [x] P4.2 生产代码直接位于 `modules/<module>/`,业务模块只依赖共享 workflow、adapters、core 契约。
- [x] P4.3 提供本地 PostgreSQL Compose,初始化 4 个模块 schema 且只监听回环地址。
- [x] P4.4 手动命令保留 dry-run 默认值;调度器提供显式 dry-run 预演。
## P5 自动化验收
- [x] P5.1 工作流目录、模块注册、LangGraph、fan-in、调度日期和失败语义测试通过。
- [x] P5.2 全量项目回归 940 passed、1 skipped。
- [x] P5.3 Ruff、compileall、构建和差异格式检查通过。
- [x] P5.4 `gyxx doctor``gyxx list`、调度 dry-run 和验收状态通过。
- [x] P5.5 敏感信息扫描、本地服务策略、31 个公开命令、136 个浏览器绑定和可迁移性测试通过。
- [x] P5.6 最终验收报告记录完整工作流清单、用途、架构和验证证据。
## P6 目录精简
- [x] P6.1 四个模块生产代码上移到模块根,`runtime/` 只保留单文件历史导入兼容层。
- [x] P6.2 47 个模块内测试统一迁入 `tests/modules/`,生产包不再夹带测试目录。
- [x] P6.3 Windows 启动器和历史技能说明归档到 `docs/history/`,不参与命令发现或服务器调度。
- [x] P6.4 脚本发现改为 `config/commands.json` 显式白名单,不再递归暴露内部工具和历史脚本。
- [x] P6.5 项目文档收口为架构、部署、运维、计划和验收报告,删除重复跳转文档。
- [x] P6.6 四个模块的新路径、命令别名、浏览器状态键和源同步哈希均完成回归校验。
## P7 工作流合并
- [x] P7.1 核对实际调度来源,确认 21 条有效调度定义并全部启用。
- [x] P7.2 从工作流目录移出 7 个手动入口,并删除 1 个不可执行历史目录项。
- [x] P7.3 通过命令注册保留补采、重试、映射刷新和采购单更新能力。
- [x] P7.4 为单日回采、商品补采、评价补采和采购单更新增加业务日期或固定子命令默认参数。
- [x] P7.5 更新项目文档并完成定向、全量、Lint、构建、doctor 和调度 dry-run 验收。
- [x] P7.6 删除重复且禁用的 `content.self_operated.weekly`,保留每日指标工作流仍依赖的底层自营脚本并完成回归。
- [x] P7.7 将京东、天猫主图周任务合并为 `product.main_image.weekly` 单图,周日 08:30 并行启动两个无依赖、资源隔离的分支;任一平台失败都不阻断另一平台采集、PG upsert 和飞书插入,全部结束后汇总失败状态及平台级具体错误。
- [x] P7.8 核对两个主图入口默认均调用飞书插入和本地 PG upsert,并以目录、图依赖、失败语义和入口测试完成双 sink 静态验收;相关 21 项定向测试通过,未将自动化测试表述为真实外部写入。
- [x] P7.9 将本地 `main_image_creatives` 存量主键迁移为日期、款式、平台、图片四列;迁移前后均为 3681 行,并用同键 JD/TM 双行事务探针验证后回滚测试数据。
## 生产启用条件
以下是部署后的运维门禁,不属于本次无副作用代码验收:配置真实凭据、初始化本地数据库、启动两个本机 Hermes 角色、准备浏览器首次登录态,以及逐工作流执行真实数据与外部写入对账。
-17
View File
@@ -1,17 +0,0 @@
# 回滚手册
## 单任务回滚
1. 记录失败的新任务名、workflow ID、run_id 和业务日期。
2. 禁用对应 `\GYXX\<workflow-id>`,保留任务定义和运行证据。
3. 重新启用原计划任务,核对触发时间、账号和旧工作目录仍与基线一致。
4. 检查 `state/ops/effects``in_progress``ambiguous` 必须先做外部对账。
5. 检查 outbox;已 `sent` 的消息不得重发,`failed` 只能以同一幂等键 replay。
6. 记录回滚结果和恢复时间,保留新旧日志。
## 原则
- 一次只切换或回滚一个 workflow。
- 旧任务只禁用/启用,不删除;旧项目在全部观察期完成前保持只读可回滚。
- 不删除新系统数据、run journal、effect receipt 或 outbox 消息。
- 未完成对账时,不允许用直接运行脚本绕过 effect ledger。
+149
View File
@@ -0,0 +1,149 @@
# GYXX Flow 运维手册
## 日常检查
```bash
uv run gyxx doctor --json
uv run gyxx schedule status
uv run gyxx list
```
Linux 服务同时检查:
```bash
systemctl status gyxx-flow.service
journalctl -u gyxx-flow.service --since today
```
工作流失败时,使用 `workflow_id`、业务日期和 `run_id` 对照 `<GYXX_DATA_ROOT>/logs`、运行记录、节点产物和副作用账本。
## 手工执行
```bash
uv run gyxx run <workflow_id> --date 2026-08-01
uv run gyxx run <workflow_id> --date 2026-08-01 --execute
uv run gyxx scripts list --json
uv run gyxx scripts run <command_id> --date 2026-08-01
uv run gyxx scripts run <command_id> --date 2026-08-01 --execute
```
`gyxx run` 只接受 22 个调度工作流;补采和维护操作使用 `gyxx scripts run`。两类入口都默认 dry-run,只有业务日期、凭据、浏览器登录态、PostgreSQL 和对应 Hermes 角色全部确认后才使用 `--execute`
| 手动场景 | 命令 ID | 日期行为 |
|---|---|---|
| 内容映射重建 | `content.mapping.rebuild` | 业务日期只用于运行审计 |
| 内容失败任务重试 | `content.failed.retry` | 业务日期只用于运行审计 |
| 内容日报重跑 | `gyxx backfill content.metrics.daily` | 使用 `--from/--to` 指定日期范围 |
| 京东自营品牌单日回采 | `shop.jd_self_operated.collect_brand` | `--date` 自动成为起止日期 |
| 商品历史补采 | `product.backfill.run` | `--date` 自动成为 `--from/--to` |
| 商品评价补采 | `product.review.orchestrate` | `--date` 自动成为目标日期 |
| 采购单更新 | `supply.workflow.run` | 自动选择 `mcp-run purchase-order-update` |
命令的额外脚本参数使用可重复的 `--arg=<值>` 传入。`supply.workflow.run --execute` 会进入真实 ERP 修改链路,必须在任务文件、目标范围和回滚条件全部复核后执行。
### 主图周工作流
主图调度的唯一工作流 ID 是 `product.main_image.weekly`,每周日 08:30 启动。图同时运行 JD、TM 两个无依赖分支;一个平台失败不会取消、跳过或回滚另一个平台的采集、PG upsert 和飞书插入。两个分支全部结束后再汇总工作流状态:任一平台失败时整体状态为失败,并在错误摘要中标明具体平台和错误,另一平台已经成功的结果继续保留。
两个平台默认都在采集后执行飞书主图表插入和本地 PostgreSQL `main_image_creatives` upsert。执行前必须同时确认两套浏览器登录态、飞书身份和本地数据库;排障时分别检查 `jd``tmall` 节点以及对应插入脚本日志,不能只以采集文件存在判断双 sink 已完成。
## 调度操作
```bash
uv run gyxx schedule run --dry-run --once
uv run gyxx schedule status
sudo systemctl restart gyxx-flow.service
```
调度服务也只加载显式指定的受控环境文件:
```bash
uv run gyxx schedule run --env-file /etc/gyxx-flow/gyxx-flow.env
```
业务时间只在 `config/schedules.json` 中维护。systemd 负责进程守护,Python scheduler 负责全部业务定时;不要重复注册 cron、systemd timer 或 Windows 任务计划。
需要停用单个工作流时,优先修改该工作流的调度启用状态,先 dry-run,再重启服务。其他模块继续运行。
## 数据和浏览器状态
生产 `GYXX_DATA_ROOT` 必须位于源码目录之外,Linux 推荐 `/var/lib/gyxx-flow`
- `data/raw/<module>`:原始采集结果,不按临时文件清理。
- `data/normalized``data/curated`:清洗、标准化和聚合数据。
- `data/exports`Markdown、Excel 等交付文件。
- `state/browser/<module>/<script>`:独立 Cookie、Profile 和 storage state。
- `state/scheduler``state/locks`、运行账本:调度和幂等状态。
- `tmp`:仅存放可重建临时文件。
项目不会自动搬动或删除已有 `var/`。迁移数据根时应停止调度器,完整备份和复制原目录,修改环境变量后依次运行 doctor、dry-run 和一次指定日期的对账执行。
## 清理和保留
可以在确认没有任务运行后清理缓存、`tmp`、dry-run 目录和可重建构建产物。以下内容不得作为普通缓存删除:
- `data/raw` 原始数据;
- `state/scheduler` 和资源锁;
- EffectLedger、运行 journal 和浏览器状态;
- 尚未核对或尚未导入数据库的 Excel、JSON、Markdown 导出。
旧 Windows 任务计划 XML 只属于历史部署证据,确认 Python scheduler 已接管且完成备份后再归档。
## 故障处理
1.`workflow_id`、业务日期和 `run_id` 定位失败节点。
2. 检查本机 PostgreSQL、对应 Hermes 角色、飞书身份和脚本专属 CDP 端口。
3. 检查 Cookie/storage state 是否存在且未失效。
4. 修复后按同一业务日期重跑,依靠唯一键和 EffectLedger 防止重复写入。
5. 仍失败时停用该工作流,不停止其他模块。
### 副作用账本对账
正式节点失败后,EffectLedger 会保留 `ambiguous` 回执,避免不确定状态下重复写库或重复
发送飞书。不要删除或手工修改回执文件。只有先从 PostgreSQL、飞书消息回执和目标文档
确认该节点是否已产生外部效果,才可执行审计式对账:
```bash
uv run gyxx effects reconcile <workflow_id> \
--date 2026-08-04 \
--step <step_id> \
--expected-run-id <原失败run_id> \
--action retry \
--operator <操作人> \
--reason <重试原因> \
--evidence <已核对的证据> \
--execute
```
`retry` 仅在证明确实没有完成外部写入时允许同日重试;若证据表明外部写入已经完成,使用
`--action applied`。命令只接受当前仍为 `ambiguous``run_id` 精确匹配的回执,并保留
操作人、原因、证据和时间,不能用于绕过正在执行的节点。
## 回滚
代码回滚时先停止 `gyxx-flow.service`,切换到上一个已验收版本,重新安装锁定依赖并执行 doctor 和调度 dry-run;确认后再启动服务。不得用代码回滚覆盖 `GYXX_DATA_ROOT`
数据撤销必须针对明确表、业务键和 `run_id`,先备份 PostgreSQL,再执行经过复核的 SQL。不得删除整个数据库、数据卷或数据根。
浏览器状态异常时,只备份并处理目标脚本的 `state/browser/<module>/<script>`,不要清理其他脚本状态。调度状态异常时先停止唯一调度进程并备份 `state/scheduler`,不要同时启动第二实例。
## 业务源码同步
外部业务源码目录只用于发现更新,GYXX Flow 运行时不导入或挂载这些目录。只读检查:
```powershell
uv run gyxx sources status `
--source-root content_marketing=<内容源码目录> `
--source-root product_commerce=<商品源码目录> `
--source-root shop_intelligence=<店铺源码目录> `
--source-root supply_chain=<供应链源码目录> `
--json
```
只有未转换、无冲突的 `source_changed` 可以显式执行:
```powershell
uv run gyxx sources apply ... --execute --json
```
经过统一路径、数据库、Hermes、飞书或浏览器适配的文件必须人工合并并运行回归测试。新增可执行脚本还需登记工作流、调度和独立 CDP 绑定。同步完成后执行测试、doctor、acceptance 和 source status;真实采集不在源码同步过程中自动触发。
+197
View File
@@ -0,0 +1,197 @@
# GYXX Flow 全工作流验收测试报告
测试日期:2026-08-03
测试时区:Asia/Shanghai
测试顺序:供应链 → 店铺 → 商品 → 内容
## 1. 最终结论
当前架构共有 21 个定时工作流并全部启用,模块分布为内容 8、商品 7、店铺 3、供应链 3。13 项达到 `PASS``PASS_WITH_SKIPS`,合并后的 `product.main_image.weekly` 已完成工程结构验证但尚无新两节点图的业务实跑,因此业务通过和规则完成均为 13/21,未通过或待复测为 8/21。7 个手动入口、2 个已删除/废弃项以及 2 条合并前主图子链实跑证据均单列,不进入当前工作流分母。
| 结果 | 数量 | 打勾 | 解释 |
|---|---:|---|---|
| `PASS` | 4 | 是 | 允许执行的采集、处理和本地写入成功;包含合法空结果 |
| `PASS_WITH_SKIPS` | 9 | 是 | 主链路或受控入口验证成功,禁止/无需执行的外部副作用已明确跳过 |
| `FAIL_AUTH_OR_UI` | 1 | 否 | 登录、配置或目标页面控件条件不满足 |
| `PARTIAL_FAILED` | 2 | 否 | 有部分有效结果,但必要子链明确失败 |
| `PARTIAL_TIMEOUT` | 2 | 否 | 有部分有效结果,但整体在有界等待内未收敛 |
| `BLOCKED_EXTERNAL_PERMISSION` | 2 | 否 | 登录成功后被外部系统权限阻断 |
| `PENDING_RETEST_AFTER_MERGE` | 1 | 否 | 合并后的结构已验证,但新拓扑尚无受控业务实跑证据 |
| 合计 | 21 | 13/21 | 13 项业务通过且规则完成,8 项未通过或待复测 |
手动命令历史场景另计:`PASS` 3、`PARTIAL_TIMEOUT` 3、`EXCLUDED_BY_REQUEST` 1,共 7 项;`content.self_operated.weekly``product.weekly_aggregate.documented_missing` 仅作为已删除/废弃历史证据保存。合并前京东 `PASS` 和天猫 `SKIPPED_COOKIE` 的 dated run 只作子链历史追溯,不能计入上表。
## 2. 云端数据库迁移与本地数据基线
云端 `8.148.185.119/data_hub` 已在 2026-08-01 16:31Asia/Shanghai)完成一致性快照恢复到本地 `127.0.0.1:5432/gyxx_super_data`,本地所有者和运行角色均为 `gyxx_flow`。迁移凭据只在进程环境中临时注入,没有写入仓库、dump 元数据或报告。
- 云端 PostgreSQL 17.6,本地 PostgreSQL 17.10;源端和目标端均为 359 张业务表。
- 恢复后校验到 5,815 个可见列、606 个索引、433 个约束、337 个序列、7 个触发器、1 个业务函数,语义结构一致。
- 359 张表全部完成行数和双指纹校验:357 张与云端实时状态完全匹配;`qrtz_scheduler_state` 的调度心跳、`system_oauth2_access_token` 及其序列的新增属于一致性 dump 完成后的正常在线漂移,不是恢复丢失。
- 本机没有 PostGIS,迁移只排除了扩展自有表;359 张业务表均无 PostGIS 列,因此不影响业务数据完整性。
- 云端一致性 dump 保存在 `var/migrations/cloud_data_hub_to_gyxx_super_data_20260801T162327/cloud_data_hub.dump`;恢复前本地库 dump 保存在同目录的 `local_gyxx_super_data_before.dump`
- 恢复前数据库仍保留为 `gyxx_super_data_before_20260801_162327`,已禁止新连接;旧库和两个 custom dump 共同构成可回滚点。
- 恢复使用单事务;临时 `pg_hba.conf` 规则已恢复。连接、schema usage/create 及“建表 → 插入 → 读取 → 更新 → 回滚”探针全部通过,探针未持久化。
- 会产生本地验收写入的实际工作流均在上述迁移和一致性校验完成后才执行;因此 357+2 的迁移结论没有被后续验收数据污染。
权威迁移证据为 `var/migrations/cloud_data_hub_to_gyxx_super_data_20260801T162327/migration-report.json`
## 3. 隔离、安全与运行环境结论
### 飞书与通知
- 店铺复测中曾因一次命令漏设验收总开关误建 1 条京东周数据记录;随后通过飞书 Base 精确读取核对后只删除该 record id,并回读确认 `record_not_found`。除此之外飞书 Base/Bitable/Sheets 写动作均由验收门禁记录为 `feishu_write_skipped`,最终业务表未留下本轮测试记录。
- 通知策略只允许王云龙作为收件人,其他收件人和业务群在验收态被拒绝。
- 商品告警因无符合阈值的数据没有通知;营销日报未启用发送参数。采购确认 journal 仅记录通用 `step:collect_analyze_notify:production_sink`,没有独立消息正文或投递回执,因此不声称该通用副作用的具体内容或投递结果。
### Hermes
- 分析端 `127.0.0.1:8642`、采集端 `127.0.0.1:8643` 均保持本机回环绑定,项目没有回退到远程 AI。店铺指标采集的分析端请求因当前运行进程未注入 `GYXX_HERMES_API_KEY` 返回 401,AI 分析结果为空,但采集、文件与 PostgreSQL 主链路均成功。
- 营销日报已真实调用本地分析 Hermes 并生成 Markdown/PNG。
- 项目客户端全部使用回环地址;两个 Hermes 进程当前仍监听 `0.0.0.0`,服务器部署前应通过服务配置或防火墙收紧。这是部署风险,不改变本轮本地调用结论。
### 浏览器、Cookie 与来源只读
- 136 个运行绑定保留各自唯一的 CDP/Profile/Cookie/storage-state/state-key 组合。
- 15/15 个已识别的来源状态映射已复制到本项目独立目录,共 5,037,233,753 B、31,213 个文件;运行配置不引用来源路径。
- 实跑先复用本项目/已复制状态,再使用脚本明确支持的账号密码回退;仍不可用才 exit 75。验收过程中未扫码、未人工过验证码、未覆盖来源 Cookie。
- 来源项目和来源浏览器状态保持只读,未被本轮命令写入;来源清单检查为 233/233 `in_sync`
- JSON、Markdown、CSV、Excel、图片、日志和 journal 均落入本项目 `GYXX_DATA_ROOT` 分层目录;没有把来源目录当运行时依赖。
### 凭据清理
- 历史遗留的实际 `db.env``analyze.env` 已删除;项目只保留无凭据示例和统一运行时环境变量入口。
- 全仓 secret scan 结果为 0;报告、验收事件和迁移清单均未记录账号、密码、Cookie、token 或密钥。
## 4. 框架安全修复(6 项)
以下为框架层修复,与下一节业务逻辑对齐分开统计:
1. `CommandStep` 超时时完整终止进程树:Windows 使用 Job ObjectPOSIX 使用 process group,避免父进程退出后浏览器或子脚本继续产生副作用。
2. secret scanner 收紧动态引用、`getenv` fallback 和 lambda 边界;最终全仓扫描命中 0。
3. 商品配置路径优先级固定为 `GYXX_PRODUCT_CONFIG > AUTO_FLOW_CONFIG > AUTOFLOW_CONFIG_PATH > canonical`,避免旧兼容变量覆盖显式配置。
4. `NamedResourceLock` 增加原子发布、PID create-time/lease 校验和 OS reclaim guard,对陈旧锁采取保守恢复,避免误夺仍存活进程的锁。
5. acceptance evidence 增加跨线程/跨进程文件锁:Windows 使用 `msvcrt`、POSIX 使用 `flock``register_at_fork` 后重置状态,锁等待超时按 fail-closed 处理。
6. `_safe_details``Mapping``list``tuple` 递归脱敏并检测循环引用,避免嵌套验收细节泄露敏感值或因自引用崩溃。
## 5. 业务逻辑与编排对齐
- 商品日报显式下传 `--target-date {business_date}`,08:40 定时任务使用前一业务日;评价采集同样显式下传目标日期。
- 内容评论恢复 B 站、小红书、抖音三平台并发、全部等待和最终聚合失败语义。
- 内容指标回补恢复“采集失败仍执行同步、最后返回聚合失败”的原逻辑。
- 商品主图收敛为 `product.main_image.weekly`:周日 08:30 并行启动 JD、Tmall 两个独立资源分支,两个子链均执行本地 PG 入库与飞书插入逻辑;任一受控节点失败都不阻断另一平台完成采集和写入,末尾聚合工作流失败状态和平台级具体错误。
- 蝉妈妈保留分页兜底、逐页异常处理及“获取数据 → 最长等待 10 分钟 → 重新进入导出”。
- 商品告警使用统一通知配置,验收态只允许王云龙。
- 供应链补货输出先写本轮目录再归档,停产 SKU 状态改为项目路径;批处理只消费本轮 Excel,空补货结果按合法业务结果处理。
- 供应链外层增加 Cookie/CDP 和 ERP 修改门禁,修复 PowerShell 在 Python 防护前启动浏览器的窗口。
- 21 个定时 `workflow_id` 均编译为 LangGraph `StateGraph`,模块分布为内容 8、商品 7、店铺 3、供应链 3;手动命令继续通过 `config/commands.json` 显式注册,不再混入定时工作流目录,也没有恢复递归脚本发现或 Windows 定时任务。
## 6. 逐定时工作流最终结果
### 供应链
| 完成 | 工作流 | 调度 | 用途 | 最终状态、run_id 与关键证据 |
|---|---|---|---|---|
| ✅ | `supply.purchase_confirmation.daily` | 每天 08:00 | 采购确认采集、分析和通知 | `PASS``supply.purchase_confirmation.daily__20260729__20260801T092309Z__5daace`;约 443 秒,ERP 原始 Excel、汇总 CSV、本地 PG 状态均成功。通用 `production_sink` 不作为消息内容/回执证明 |
| ⬜ | `supply.replenishment.weekly` | 周一 08:00 | 计算并输出补货建议 | `BLOCKED_EXTERNAL_PERMISSION``supply.replenishment.weekly__20260728__20260801T094413Z__743f4a`;登录成功后 ERP 拒绝“普通商品资料”,exit 1、外部写入为空 |
| ⬜ | `supply.replenishment_alert.daily` | 每天 07:00 | 指定 SKU 库存阈值预警 | `BLOCKED_EXTERNAL_PERMISSION``supply.replenishment_alert.daily__20260727__20260801T094546Z__500e91`,复现 `supply.replenishment_alert.daily__20260728__20260801T094100Z__4b2da2`;同一 ERP 权限阻断 |
### 店铺
| 完成 | 工作流 | 调度 | 用途 | 最终状态、run_id 与关键证据 |
|---|---|---|---|---|
| ✅ | `shop.metrics.weekly` | 周一 12:00 | 并行采集京东、抖音、天猫店铺指标 | `PASS``shop.metrics.weekly__20260804__20260804T065204Z__bb3b69`;三节点 3/3 成功,产物与云端 PG 写入完成;京东、抖音真实更新飞书 Base,天猫按原逻辑不写店铺表;抖音采用确认后的净销售额 `294,892.98` |
| ✅ | `shop.competitor.weekly` | 周一 12:30 | 并行采集京东、抖音竞店指标 | `PASS``shop.competitor.weekly__20260804__20260804T075530Z__4fd7c1`;京东/抖音 2/2 成功并分别 upsert 49/24 行,飞书 Base 分别更新 7/9 条品牌记录 |
| ✅ | `shop.jd_self_operated.daily` | 每天 16:00,业务日 -1 | 京东自营品牌后接商品采集 | `PASS``shop.jd_self_operated.daily__20260801__20260804T082226Z__255cfc`;品牌/商品 2/2 成功并分别采集 1/50 行,收入合计均为 `19,974.27`;同日真实重跑 `shop.jd_self_operated.daily__20260801__20260804T082722Z__0ced7e` 再次 2/2 成功且仍为 1/50 行,CDP 22132/22133 均关闭 |
### 商品
| 完成 | 工作流 | 调度 | 用途 | 最终状态、run_id 与关键证据 |
|---|---|---|---|---|
| ⬜ | `product.persona.daily` | 每天 10:00 | 三平台商品人群画像采集 | `PARTIAL_FAILED``product.persona.daily__20260815__20260801T103908Z__2d6028`;天猫缺 DMP、抖音缺搜索控件、京东完成 19/30 后浏览器崩溃 |
| ⬜ | `product.daily` | 每天 08:40,业务日 -1 | ERP/三平台采集、分析、导出、入库 | `PARTIAL_TIMEOUT``product.daily__20260731__20260801T113513Z__b59304`ERP/JD/TM 完成,DY 超时,raw 257;独立下游完成分析、导出 32、本地 upsert 与 32 个飞书写跳过 |
| ✅ | `product.alert.daily` | 每天 23:00 | 商品异常检测和条件通知 | `PASS``script.product_commerce.955b1dfb0cdf__20260801__20260801T085614Z__4fefb3`;真实读取本地库,无符合阈值异常,未产生通知 |
| ✅ | `product.import.daily` | 每天 19:00 | 三平台日报幂等导入本地 PG | `PASS``script.product_commerce.9137239938d5__20260727__20260801T085541Z__59c16c`;抖音/京东/天猫 163/90/173 行,无重复 |
| ✅ | `product.style_analysis.interval` | 每 3 天 11:00 | 本地 Hermes 分析到期款式 | `PASS_WITH_SKIPS``product.style_analysis.interval__20260801__20260801T084137Z__952204`;数据库/Hermes 检查和实现入口 dry-run 成功,外部写关闭 |
| ⬜ | `product.main_image.weekly` | 周日 08:30 | JD、Tmall 主图并行采集,两边独立 PG 入库与飞书插入 | `PENDING_RETEST_AFTER_MERGE`;尚无合并后新图的业务 run_id。工程结构要求为两个无依赖且资源隔离的分支;任一分支受控失败不阻断另一分支,全部结束后整体失败并报告平台级具体错误。合并前 run 仅见下方历史证据表 |
| ⬜ | `product.market_rank` | 周一 10:00 | 三平台周市场排行采集、归档 | `FAIL_AUTH_OR_UI``product.market_rank__20260814__20260801T103628Z__5a9024`;天猫认证、京东类目配置、抖音控件分别失败;目标与来源脚本哈希一致 |
### 内容
| 完成 | 工作流 | 调度 | 用途 | 最终状态、run_id 与关键证据 |
|---|---|---|---|---|
| ⬜ | `content.metrics.daily` | 每天 22:00 | 达人/自营/B 站/蝉妈妈采集和指标同步 | `PARTIAL_FAILED``content.metrics.daily__20260813__20260801T101312Z__28d505`;71 分 05 秒后完整度检查失败,已完成证据和飞书写跳过保留 |
| ✅ | `content.marketing_report.daily` | 每天 10:00 | 从本地数据生成营销日报 | `PASS_WITH_SKIPS``script.content_marketing.62b3b647254f__20260804__20260801T091245Z__f20319`;真实 PG + 本地 Hermes,生成 Markdown/PNG,未启用发送 |
| ✅ | `content.relogin.weekly` | 周五 10:00 | 并行刷新内容平台登录态 | `PASS_WITH_SKIPS``content.relogin.weekly__20260811__20260801T100706Z__45a8db`;验收态按设计禁止 QR/交互登录并留跳过证据,未修改 Cookie |
| ✅ | `content.creator_report.monthly` | 每月 1 日 08:30 | 生成达人月报 | `PASS_WITH_SKIPS``content.creator_report.monthly__20260801__20260801T084140Z__bd9fdd`;数据库检查和入口 dry-run 成功,飞书写关闭 |
| ✅ | `content.summary.monthly` | 每月 1 日 08:00 | 内容月度汇总 | `PASS_WITH_SKIPS``content.summary.monthly__20260801__20260801T084142Z__9d73c1`;数据库检查和入口 dry-run 成功,外部写关闭 |
| ✅ | `content.cooperations.daily` | 每天 09:00 | 刷新映射并同步合作记录 | `PASS_WITH_SKIPS``content.cooperations.daily__20260801__20260801T084143Z__02b89a`;真实飞书读取与本地 PG 读取,内部 dry-run 禁止写入 |
| ⬜ | `content.comments.weekly` | 周日 12:00 | 三平台评论并行补采和汇总 | `PARTIAL_TIMEOUT``content.comments.weekly__20260812__20260801T100904Z__530989`;60 分 56 秒,B 站完成,XHS/DY 有界停止,229 CSV + 229 JSON、约 5.45 MB |
| ✅ | `content.summary.weekly` | 周二 10:00 | 内容周度汇总 | `PASS_WITH_SKIPS``content.summary.weekly__20260801__20260801T084145Z__7e6081`;数据库检查和入口 dry-run 成功,外部写关闭 |
### 手动命令/历史验收场景(不计当前工作流分母)
| 旧验收标识 | 合并后的入口与用途 | 历史状态、run_id 与关键证据 |
|---|---|---|
| `supply.purchase_order_update` | `supply.workflow.run`;默认参数自动选择 `mcp-run purchase-order-update` | `EXCLUDED_BY_REQUEST`;无 run_id,按要求未执行、未访问 ERP |
| `shop.jd_self_operated.history` | `shop.jd_self_operated.collect_brand``--date``{business_date}` 自动渲染起止日期 | `PASS``shop.jd_self_operated.history__20260728__20260801T103803Z__22f456`;约 43 秒,品牌 JSON 533 B,本地库 +1 |
| `product.backfill` | `product.backfill.run``--date``{business_date}` 自动渲染 `--from/--to` | `PARTIAL_TIMEOUT``product.backfill__20260730__20260801T112223Z__af7f00`;抖音第二轮未收敛 |
| `product.review_collection` | `product.review.orchestrate``--date``{business_date}` 自动渲染目标日期 | `PASS`(空结果);`product.review_collection__20260731__20260801T111838Z__e99652`;约 92 秒,JSON/Markdown 成功,当日新增 0 条 |
| `content.mapping.refresh` | `content.mapping.rebuild`;飞书只读并生成本地内容/款式映射 | `PASS``content.mapping.refresh__20260818__20260801T112822Z__917ed7`;约 25 秒,35 张表、117,505 B 本地 JSON |
| `content.retry_failed` | `content.failed.retry`;根据失败清单重试采集 | `PARTIAL_TIMEOUT``content.retry_failed__20260820__20260801T113412Z__e0c8af`20 分 56 秒,B 站 4/4、蒲公英 14/22、星图 0 |
| `content.metrics.backfill` | `gyxx backfill content.metrics.daily`;合并到标准日报图并按日期范围重跑 | `PARTIAL_TIMEOUT``content.metrics.backfill__20260821__20260801T115644Z__1cb626`;15 分 30 秒,采集有界停止、同步成功,133/133 验收证据完整 |
历史场景分类为 `PASS` 3、`PARTIAL_TIMEOUT` 3、`EXCLUDED_BY_REQUEST` 1,共 7 项;这些状态不改变当前 21 个定时工作流的统计。
### 已废弃历史证据(不计任何当前分母)
- `content.self_operated.weekly`:原禁用兼容工作流与 `content.metrics.daily` 重复,已删除工作流和调度定义;底层脚本仍由日报调用。历史状态 `PASS_WITH_SKIPS`run_id `content.self_operated.weekly__20260819__20260801T113119Z__b71197`
- `product.weekly_aggregate.documented_missing``NOT_EXECUTABLE`;无 run_id;已确认没有脚本、注册项或调度入口,仅保留原审计结论。
### 合并前主图子链实跑证据(不计任何当前分母)
| 合并前工作流/版本 | 历史状态、run_id 与关键证据 |
|---|---|
| `product.main_image.jd.weekly` | `PASS``product.main_image.jd.weekly__20260812__20260801T102149Z__b2d3b7`;京东旧独立工作流生成 120 个文件、约 15.5 MB,本地 PG 288,82 个飞书写动作由验收门禁跳过 |
| `product.main_image.weekly`(合并前单平台版本) | `SKIPPED_COOKIE``product.main_image.weekly__20260813__20260801T103413Z__b48e34`;天猫旧独立工作流无法建立有效非交互登录态,exit 75,未扫码、未覆盖来源状态 |
以上 dated `run_id` 只证明合并前子链的历史表现,不证明当前 JD、Tmall 并行两节点图已经实跑,且不增加当前目录数量。
### 合并后的旧→新映射
- 当前 21 条有效调度定义对应 21 个顶层 LangGraph 工作流并全部启用;原两个主图调度已合并为一个周日 08:30 的 `product.main_image.weekly`,旧 run 仅作非加总历史证据。
- `shop.jd_self_operated.history``product.backfill``product.review_collection` 由命令声明的 `default_args``{business_date}` 自动渲染日期,`supply.purchase_order_update` 自动注入固定子命令;调用者无需重复传日期范围或 `mcp-run purchase-order-update`
- `content.metrics.backfill` 收敛为 `gyxx backfill content.metrics.daily``content.mapping.refresh``content.retry_failed` 分别映射到 `content.mapping.rebuild``content.failed.retry`
- `product.weekly_aggregate.documented_missing` 不再注册、不再调度,只保留上述历史证据。
## 7. 证据与自动化校验
- 现行 21 个定时工作流均已通过隔离 `GYXX_DATA_ROOT` 下的 LangGraph 注册/编排验证;合并主图的 JD、Tmall 并行启动、独立资源、失败隔离、成功写入保留和最终错误聚合已通过工程测试,但尚未完成受控业务实跑。7 个手动命令保留原实跑或排除证据;真实验收状态以第 6 节为准,工程验证或 dry-run 不替代业务实跑。
- 数据库阻塞已解除:8 个原数据库阻塞工作流均完成表/列/权限检查和事务回滚探针;商品导入、商品告警、营销日报随后执行了真实安全分支。
- 本次合并后的最终全量回归数量由交付汇总统一更新;Ruff、构建、`gyxx doctor --json` 和来源状态检查仍是必验项,不在此预写新的工程测试数量。
- 全仓 secret scan 命中 0;飞书误建的 1 条记录已精确删除并确认不存在,其余业务表写入均跳过;来源项目实际写入 0。
主要证据位置:
- 数据库迁移:`var/migrations/cloud_data_hub_to_gyxx_super_data_20260801T162327/`
- 浏览器状态复制:`var/migrations/browser-state/20260801T085006Z-5c217b14/`
- 验收事件:`var/reports/workflow-acceptance/evidence.jsonl`
- 店铺实跑:`var/reports/workflow-acceptance/shop-live-20260801.jsonl`
- 店铺指标三平台复测:`var/reports/workflow-acceptance/shop-metrics-full-20260909/`
- 店铺竞店两平台复测:`var/reports/workflow-acceptance/shop-competitor-full-20260913/`
- 京东自营日采集复测:`var/reports/workflow-acceptance/shop-jd-self-operated-full-20260729/`
- 商品京东主图(合并前子链证据):`var/reports/workflow-acceptance/product-main-image-jd-20260812.jsonl`
- 商品日报下游复核:`var/reports/workflow-acceptance/product-daily-downstream-20260731-retry.jsonl`
- 内容长任务:`var/reports/workflow-acceptance/runs/`
- 工作流 journal`var/runs/<workflow_id>/<business-date>/<run_id>/run.json`
- 逐项执行清单:`docs/workflow-acceptance-test-requirements.md`
## 8. 未通过项的下一步
1. 为供应链测试账号授予 ERP“普通商品资料”权限后,只复测补货周任务和补货告警;采购单更新仍保持排除,除非另行授权并提供可回滚测试数据。
2. 服务器运行店铺指标工作流前注入有效 `GYXX_HERMES_API_KEY`,复核可选 AI 分析输出;该项不阻塞三个店铺工作流的采集、文件和云端 PostgreSQL 验收结论。
3. 补齐商品天猫 DMP、京东类目配置,更新抖音页面控件定位;处理京东画像浏览器崩溃与抖音采集未收敛。
4. 为天猫万相提供有效且可复用的非交互登录态后,对合并后的主图两节点工作流执行受控业务复测:验证周日 08:30 并行启动、两个分支资源隔离、两边 PG/飞书边界,以及任一受控失败时另一边完成写入、整体失败并给出平台级具体错误;仍不得弹出扫码流程,飞书保持零写门禁。
5. 对内容完整度、评论、失败重试和指标回补设置可分段续跑的检查点,复测时继续保持飞书表零写入和唯一通知收件人策略。
最终结论是:工作流框架、数据库迁移、本地服务绑定和安全隔离已达到验收要求;当前 21 个定时工作流中,13 项业务通过且规则完成,7 项未通过,1 项合并后待受控业务复测。另有 7 个手动命令历史场景、2 个已删除/废弃历史证据和 2 条合并前主图子链实跑证据单列追溯,不能表述为“全部生产链路通过”。
@@ -0,0 +1,130 @@
# GYXX Flow 全工作流验收测试需求
测试日期:2026-08-03
测试时区:Asia/Shanghai
测试顺序:供应链 → 店铺 → 商品 → 内容
## 1. 验收目标与范围
当前架构的验收分母是 21 个定时工作流并全部启用,按模块分为内容 8、商品 7、店铺 3、供应链 3。原先独立调度的 `product.main_image.jd.weekly` 已与原单平台 `product.main_image.weekly` 合并为一个两节点工作流;合并前两个 dated `run_id` 只作为子链历史证据,不得冒充新图实跑。原目录中的另外 7 个可执行项现归类为手动命令/历史验收场景,不进入当前分母;`content.self_operated.weekly``product.weekly_aggregate.documented_missing` 继续仅保留历史证据。
验收覆盖工作流注册、LangGraph 编排、业务日期、浏览器状态、数据采集、云端 PostgreSQL、文件落盘、Hermes、飞书隔离和副作用账本。真实外部链路只在安全门禁允许时执行;失败、超时、权限阻塞和安全跳过必须如实保留,不能用 dry-run 冒充业务通过。
## 2. 强制隔离与数据标准
1. PostgreSQL 只允许使用运行时注入的云端 DSN;回环数据库立即失败,源码、报告和示例文件不得保存真实凭据。
2. 开始供应链工作流前,先验证云端目标库的连接、schema、读写权限和事务回滚能力;测试数据必须使用唯一 run_id,避免覆盖既有业务记录。
3. 飞书 Base、Bitable、Sheets 的新增、更新、删除全部物理跳过并留证;飞书读取可以执行。
4. 通知收件人只允许王云龙;不得向原业务群、原收件人或其他用户发送。通用 `production_sink` 只能证明进入副作用边界,不能据此推断消息正文或投递回执。
5. 每个浏览器入口保留独立 CDP、Profile、Cookie 和 storage state。登录策略固定为“本项目状态 → 已复制的来源状态 → 受支持的账号密码回退 → 安全跳过”;不弹出二维码、不人工通过验证码。
6. 本机 Hermes 分析端和采集端只通过回环地址访问,不得回退到远程 AI。
7. JSON、Markdown、CSV、Excel、图片、日志和 journal 必须按模块、工作流、业务日期落入 `GYXX_DATA_ROOT`;来源目录只读且不得成为运行时依赖。
8. 凭据只允许运行时注入。历史遗留 `db.env``analyze.env` 必须删除,仓库和验收报告不得记录账号、密码、Cookie、token 或密钥。
## 3. 状态与打勾规则
| 状态 | 含义 | 打勾 |
|---|---|---|
| `PASS` | 原逻辑、采集、处理和本轮允许的本地写入成功;允许出现业务空结果 | 是 |
| `PASS_WITH_SKIPS` | 已验证的主链路成功,本轮禁止或无需执行的外部副作用被明确跳过 | 是 |
| `SKIPPED_COOKIE` | 状态复用和受支持的非交互登录均不能建立有效登录态,且按规则安全退出 | 是,但不代表生产采集通过 |
| `NOT_EXECUTABLE` | 确认没有脚本、注册项或调度入口;仅用于已废弃历史证据 | 是,但不计当前工作流分母 |
| `PENDING_RETEST_AFTER_MERGE` | 合并后的工作流结构、调度和失败传播已完成工程验证,但新拓扑尚无受控业务实跑证据 | 否 |
| `FAIL_AUTH_OR_UI` | 登录、授权、配置或目标页面控件不满足,业务链路未完成 | 否 |
| `PARTIAL_FAILED` | 部分子链路已有有效结果,但至少一个必要子链路明确失败 | 否 |
| `PARTIAL_TIMEOUT` | 部分子链路已有有效结果,但整体在有界等待内未收敛 | 否 |
| `BLOCKED_EXTERNAL_PERMISSION` | 登录成功后,外部系统明确拒绝目标菜单或资源权限 | 否 |
| `EXCLUDED_BY_REQUEST` | 用户明确排除,本轮没有执行;仅用于手动命令历史场景 | 否,不计当前工作流分母 |
打勾表示“本轮验收计划已得到规则允许的确定结论”,不等同于生产链路全部无条件成功。当前 21 个定时工作流中,业务通过仅包括 `PASS``PASS_WITH_SKIPS``PENDING_RETEST_AFTER_MERGE` 不计业务通过或规则完成。`SKIPPED_COOKIE``NOT_EXECUTABLE` 在本文仍可用于历史证据,但当前结果表没有这两类状态。
## 4. 执行计划与验收方法
1. 冻结来源与验收范围:核对工作流清单、调度、入口、业务日期和源文件基线,来源目录保持只读。
2. 建立数据基线:确认云端目标库、schema、表数量和读权限,并使用可回滚事务验证写权限。
3. 建立安全门禁:统一云端 PG、本地 Hermes、唯一浏览器绑定、Cookie 复用、飞书写隔离、收件人白名单和 effect ledger。
4. 按“供应链 → 店铺 → 商品 → 内容”逐项执行;为每项保存 `run_id`、节点结果、退出码、产物、数据库变化和副作用证据。
5. 对超时任务执行有界观察;已经成功的独立下游只作为局部证据,不把部分成功升级为整体通过。
6. 汇总分类时分别核对“21 个定时工作流”“7 个手动命令场景”“2 个已删除/废弃历史证据”和“2 条合并前主图子链实跑证据”;最后一类只作非加总追溯,不进入任何当前状态分母。
## 5. 供应链工作流
- [x] `supply.purchase_confirmation.daily`:采购确认采集、分析和通知。`PASS`run_id `supply.purchase_confirmation.daily__20260729__20260801T092309Z__5daace`,ERP 采集、原始 Excel、汇总 CSV 和本地 PG `workflow_runs=completed` 均成功。journal 仅记录通用 `production_sink`,不扩展解释其消息内容或回执。
- [ ] `supply.replenishment.weekly`:按销量、库存和在途生成补货建议。`BLOCKED_EXTERNAL_PERMISSION`run_id `supply.replenishment.weekly__20260728__20260801T094413Z__743f4a`,账号登录成功后 ERP 明确拒绝“普通商品资料”权限,exit 1,阻断前外部写入为空。
- [ ] `supply.replenishment_alert.daily`:指定 SKU 库存阈值预警。`BLOCKED_EXTERNAL_PERMISSION`;主要 run_id `supply.replenishment_alert.daily__20260727__20260801T094546Z__500e91`,复现 run_id `supply.replenishment_alert.daily__20260728__20260801T094100Z__4b2da2`;均在登录后被同一菜单权限阻断,外部写入为空。
## 6. 店铺工作流
- [x] `shop.metrics.weekly`:并行采集京东、抖音、天猫店铺经营指标。`PASS`run_id `shop.metrics.weekly__20260804__20260804T065204Z__bb3b69`,京东、抖音、天猫 3/3 节点成功,产物落盘并写入云端 PostgreSQL;京东和抖音按原逻辑真实更新飞书 Base,天猫按原逻辑不写店铺表。抖音销售额采用确认后的净销售额 `294,892.98`,毛销售额保留在原始产物和 PostgreSQL。
- [x] `shop.competitor.weekly`:并行采集京东、抖音竞店指标;当前没有天猫竞店采集任务。`PASS`run_id `shop.competitor.weekly__20260804__20260804T075530Z__4fd7c1`,京东和抖音 2/2 节点成功,分别 upsert 49/24 行;飞书 Base 分别更新 7/9 条品牌记录,无法排名的类目只在页面明确显示“暂无数据”时记为未上榜。
- [x] `shop.jd_self_operated.daily`:按前一业务日依次采集京东自营品牌和商品。`PASS`run_id `shop.jd_self_operated.daily__20260801__20260804T082226Z__255cfc`,品牌和商品 2/2 节点成功,分别采集 1/50 行,收入合计均为 `19,974.27`。Cookie 复用时从京东 `pin` 识别账号 `gyxx2022`,云端 PostgreSQL 按 `account + data_date + brand/product_code` UPSERT;同日真实重跑 `shop.jd_self_operated.daily__20260801__20260804T082722Z__0ced7e` 再次 2/2 成功且产物仍为 1/50 行,CDP 22132/22133 均已关闭。
## 7. 商品工作流
- [ ] `product.persona.daily`:并行采集天猫、抖音、京东商品人群画像。`PARTIAL_FAILED`run_id `product.persona.daily__20260815__20260801T103908Z__2d6028`。天猫缺少 DMP 入口,抖音缺少搜索控件,京东完成 19/30 后浏览器崩溃,部分结果不能代表三平台完成。
- [ ] `product.daily`:ERP 与三平台采集、分析、导出和入库。`PARTIAL_TIMEOUT`run_id `product.daily__20260731__20260801T113513Z__b59304`。ERP、京东、天猫完成,抖音超时;保留 257 个 raw 文件。独立下游验证完成分析、32 条导出、本地 PG upsert 和 32 次飞书写跳过,但不把下游成功升级为整体通过。
- [x] `product.alert.daily`:读取商品数据并检测销量异常、按条件通知。`PASS`;实际入口 run_id `script.product_commerce.955b1dfb0cdf__20260801__20260801T085614Z__4fefb3`。本地库读取成功,当次无符合阈值的异常,因此没有告警新增或通知。
- [x] `product.import.daily`:把三平台商品日报幂等导入本地 PG。`PASS`;实际入口 run_id `script.product_commerce.9137239938d5__20260727__20260801T085541Z__59c16c`。抖音、京东、天猫分别处理 163、90、173 行,无重复。
- [x] `product.style_analysis.interval`:使用本地 Hermes 分析到期款式。`PASS_WITH_SKIPS`workflow run_id `product.style_analysis.interval__20260801__20260801T084137Z__952204`。数据库表/权限与 Hermes 健康检查通过,单款实现入口内部 dry-run 成功,未写外部副作用目标。
- [ ] `product.main_image.weekly`:周日 08:30 并行启动 JD、Tmall 两个无依赖且资源隔离的主图分支,两个子链均独立执行本地 PG 入库与飞书插入逻辑。`PENDING_RETEST_AFTER_MERGE`;当前没有可归属于合并后新图的业务 run_id。工程结构要求为:任一分支受控失败时,另一分支仍完成自己的采集和双 sink 写入;全部结束后整体聚合为失败,并在错误摘要中标明具体平台和错误。下一轮需在现有飞书零写门禁下完成受控业务复测。
- [ ] `product.market_rank`:并行采集天猫、京东、抖音三平台周市场排行。`FAIL_AUTH_OR_UI`run_id `product.market_rank__20260814__20260801T103628Z__5a9024`。天猫认证失败、京东缺配置类目、抖音目标控件缺失;目标脚本与来源哈希一致,当前证据不支持判定为迁移改坏。
## 8. 内容工作流
- [ ] `content.metrics.daily`:采集合作达人、自营 B 站、蝉妈妈数据并同步指标。`PARTIAL_FAILED`run_id `content.metrics.daily__20260813__20260801T101312Z__28d505`,运行 71 分 05 秒后完整度检查失败;已完成子链和飞书写跳过证据保留,但必要采集链未全部完成。
- [x] `content.marketing_report.daily`:从本地数据生成营销日报并按条件通知。`PASS_WITH_SKIPS`;实际入口 run_id `script.content_marketing.62b3b647254f__20260804__20260801T091245Z__f20319`。真实读取本地 PG、调用本地 Hermes,生成 Markdown/PNG;未启用发送参数。
- [x] `content.relogin.weekly`:并行刷新内容平台登录态。`PASS_WITH_SKIPS`run_id `content.relogin.weekly__20260811__20260801T100706Z__45a8db`。LangGraph 和验收门禁按设计阻止二维码/交互登录并安全记录跳过;没有扫码、没有覆盖 Cookie 或来源状态。
- [x] `content.creator_report.monthly`:生成达人月报。`PASS_WITH_SKIPS`workflow run_id `content.creator_report.monthly__20260801__20260801T084140Z__bd9fdd`。本地数据库检查和实现入口 dry-run 成功,飞书表写入关闭。
- [x] `content.summary.monthly`:生成内容月度汇总。`PASS_WITH_SKIPS`workflow run_id `content.summary.monthly__20260801__20260801T084142Z__9d73c1`。本地数据库检查和实现入口 dry-run 成功,外部写入关闭。
- [x] `content.cooperations.daily`:刷新映射并同步合作记录。`PASS_WITH_SKIPS`workflow run_id `content.cooperations.daily__20260801__20260801T084143Z__02b89a`。真实完成飞书读取和本地数据库读取,内部 dry-run 阻止数据库/飞书写入。
- [ ] `content.comments.weekly`:并行补采 B 站、小红书、抖音评论并汇总。`PARTIAL_TIMEOUT`run_id `content.comments.weekly__20260812__20260801T100904Z__530989`,运行 60 分 56 秒。B 站完成,小红书/抖音有界停止;保留 229 个 CSV、229 个 JSON,合计约 5.45 MB。
- [x] `content.summary.weekly`:生成内容周度汇总。`PASS_WITH_SKIPS`workflow run_id `content.summary.weekly__20260801__20260801T084145Z__7e6081`。本地数据库检查和实现入口 dry-run 成功,外部写入关闭。
## 9. 手动命令与历史验收场景
下列 7 项保留手动执行能力和既有验收证据,但不再注册为定时工作流,也不进入 21 个工作流分母。
| 旧验收标识 | 合并后的入口与用途 | 历史状态、run_id 与关键证据 |
|---|---|---|
| `supply.purchase_order_update` | `supply.workflow.run`;受保护的 ERP 更新命令,默认参数自动选择 `mcp-run purchase-order-update` | `EXCLUDED_BY_REQUEST`;无 run_id,按要求未测试、未访问 ERP |
| `shop.jd_self_operated.history` | `shop.jd_self_operated.collect_brand``--date``{business_date}` 自动渲染起止日期 | `PASS``shop.jd_self_operated.history__20260728__20260801T103803Z__22f456`;约 43 秒,生成 533 B 品牌 JSON,本地库新增 1 条 |
| `product.backfill` | `product.backfill.run``--date``{business_date}` 自动渲染 `--from/--to` | `PARTIAL_TIMEOUT``product.backfill__20260730__20260801T112223Z__af7f00`;抖音第二轮等待未收敛,保留已完成平台证据 |
| `product.review_collection` | `product.review.orchestrate``--date``{business_date}` 自动渲染目标日期 | `PASS`(空结果);`product.review_collection__20260731__20260801T111838Z__e99652`;约 92 秒,JSON/Markdown 产物成功,当日数据库新增 0 条 |
| `content.mapping.refresh` | `content.mapping.rebuild`;读取内容/款式映射并生成本地 JSON | `PASS``content.mapping.refresh__20260818__20260801T112822Z__917ed7`;约 25 秒,35 张表、117,505 B 本地映射文件,飞书只读 |
| `content.retry_failed` | `content.failed.retry`;按失败清单重试采集并汇总 | `PARTIAL_TIMEOUT``content.retry_failed__20260820__20260801T113412Z__e0c8af`20 分 56 秒,B 站 4/4、蒲公英 14/22、星图 0 |
| `content.metrics.backfill` | `gyxx backfill content.metrics.daily`;合并到标准日报图并按日期范围重跑 | `PARTIAL_TIMEOUT``content.metrics.backfill__20260821__20260801T115644Z__1cb626`;15 分 30 秒,采集有界停止、同步成功,133/133 条验收证据完整 |
手动场景历史分类为:`PASS` 3、`PARTIAL_TIMEOUT` 3、`EXCLUDED_BY_REQUEST` 1,合计 7。
## 10. 当前分母外的历史证据
### 已删除/废弃项
- `content.self_operated.weekly`:原禁用兼容工作流与 `content.metrics.daily` 的自营链重复,现已删除工作流和调度定义;底层自营脚本仍由日报工作流调用。历史结论为 `PASS_WITH_SKIPS`run_id `content.self_operated.weekly__20260819__20260801T113119Z__b71197`,仅作追溯。
- `product.weekly_aggregate.documented_missing`:历史文档中的商品周聚合说明项。`NOT_EXECUTABLE`;无 run_id;已确认来源和目标均不存在脚本、注册项或调度入口。该项不进入当前工作流或手动命令分母。
### 合并前主图子链实跑证据
| 合并前工作流/版本 | 历史状态、run_id 与关键证据 |
|---|---|
| `product.main_image.jd.weekly` | `PASS``product.main_image.jd.weekly__20260812__20260801T102149Z__b2d3b7`;京东旧独立工作流生成 120 个文件、约 15.5 MB,本地 PG 记录 288,82 个飞书写动作由验收门禁跳过 |
| `product.main_image.weekly`(合并前单平台版本) | `SKIPPED_COOKIE``product.main_image.weekly__20260813__20260801T103413Z__b48e34`;天猫旧独立工作流无法建立有效非交互会话,exit 75 安全退出,未扫码、未覆盖来源状态 |
这两个 dated `run_id` 只能说明合并前京东、天猫子链各自的历史表现,不是合并后 JD、Tmall 并行两节点图的实跑结果,也不参与当前 21 项状态统计。
## 11. 合并口径与旧→新映射
- 当前 21 条有效调度定义对应 21 个顶层 LangGraph 工作流并全部启用,模块分布为内容 8、商品 7、店铺 3、供应链 3;重复且禁用的 `content.self_operated.weekly` 已删除,其主要采集链继续由 `content.metrics.daily` 执行。
-`product.main_image.jd.weekly` 与原单平台 `product.main_image.weekly` 已收敛为当前 `product.main_image.weekly`:周日 08:30 并行启动 JD、Tmall 两个独立分支,两边均进入各自 PG 与飞书写入边界;受控失败不阻断另一边,末尾聚合失败状态和平台级具体错误。当前仅工程结构验证完成,业务状态保持 `PENDING_RETEST_AFTER_MERGE`
- `shop.jd_self_operated.history``product.backfill``product.review_collection` 使用命令层 `default_args``{business_date}` 渲染日期;`supply.purchase_order_update` 使用固定默认子命令,调用者无需重复传日期范围或固定子命令。`content.metrics.backfill` 合并为标准日报工作流的 `gyxx backfill` 入口,其余内容项为显式维护/补偿命令。
- `content.self_operated.weekly``product.weekly_aggregate.documented_missing` 仅作为历史审计证据保存,不再出现在工作流注册、调度或当前数量统计中。
## 12. 最终验收标准与完成度
- 21/21 个定时工作流均有用途和状态;20 项保留既有 dated 业务证据,合并后的主图工作流尚无新图 run_id,明确列为待受控业务复测。当前调度全部启用。
- 当前工作流结果分类:`PASS` 4、`PASS_WITH_SKIPS` 9、`FAIL_AUTH_OR_UI` 1、`PARTIAL_FAILED` 2、`PARTIAL_TIMEOUT` 2、`BLOCKED_EXTERNAL_PERMISSION` 2、`PENDING_RETEST_AFTER_MERGE` 1,合计 21。
- 当前工作流业务通过和规则完成均为 13/21;未通过或待复测为 8/21。
- 7 个手动命令历史场景、2 个已删除/废弃历史证据和 2 条合并前主图子链实跑证据均单列保存,不进入当前工作流分母;主图历史证据是非加总 run 记录,不能机械增加目录项数量。
- 店铺复测中曾因一次命令漏设验收总开关误建 1 条京东周数据记录;已按精确 record id 删除并回读确认不存在,除此之外飞书业务表写入均由门禁跳过。通知策略唯一允许王云龙,但供应链通用 `production_sink` 没有独立消息正文或投递回执,不作过度结论。
- 云端快照迁移、旧库/dump 回滚、本地读写、Hermes、浏览器状态复制、来源只读、凭据清理和 secret scan 证据必须在最终报告中单列。
- 当前工作流未通过或待复测项保留真实边界:2 项外部 ERP 权限、1 项认证/UI、2 项部分失败、2 项部分超时、1 项合并后受控业务复测;手动命令另有 3 项部分超时、1 项用户排除。主图项是由拓扑合并产生的明确复测要求,不沿用旧的模糊“待浏览器复测”表述。
+69
View File
@@ -0,0 +1,69 @@
# 工作流控制台
GYXX Flow 自带一个只依赖 Python 运行时的工作流控制台。页面从当前工作流目录、定时配置、
调度状态和运行索引动态取数,不维护第二套工作流清单。
## 启动
```powershell
cd D:\gyxx-flow
uv run gyxx console
```
生产凭据不会从源码或任意 `.env` 自动发现。需要真实运行时,必须显式指定受控环境文件;
已经由服务环境注入的同名变量优先,不会被文件覆盖:
```powershell
uv run gyxx console --env-file D:\secure\gyxx-flow.env
```
商品经营正式执行会在启动子进程前校验云端 PostgreSQL、`hermes-analyzer` 飞书身份、
所需 Hermes 密钥和负责人 `open_id`。缺少任一必需项时,前端会返回明确的配置错误,
不会创建一个注定失败的“正式运行”。
默认地址为 `http://127.0.0.1:8765`。控制台进程与调度进程职责分离;服务器仍只运行一个
项目内调度服务:
```powershell
uv run gyxx schedule run
```
页面可以:
- 按内容营销、商品经营、店铺洞察和供应链分别展示全部工作流;
- 查看工作流定义、执行步骤、定时规则和下一次启动时间;
- 修改定时类型、一个或多个时间、日期规则、启停状态和业务日期偏移;
- 以安全预演或正式执行方式手动触发已注册工作流;
- 查看最近运行状态、步骤统计和经过脱敏的错误详情。
定时配置保存到 `config/schedules.json`。常驻 Python 调度器会在下一次轮询时重新验证并
加载配置,不需要创建或修改 Windows Task Scheduler 任务。若新规则在错过触发补偿窗口
内已经到期,下一次轮询可能立即启动该工作流。
## 执行安全
手动运行默认选择“安全预演”,不会产生真实外部副作用。只有在页面中选择“正式执行”并
确认影响后,控制台才会通过固定的 `python -m gyxx_flow run ... --execute` 入口启动独立
子进程。浏览器不能提交脚本路径、命令参数、环境变量或凭据。
控制台沿用 `GYXX_DATA_ROOT`,因此运行日志、工作流锁、运行索引和副作用账本与 CLI、调度
服务保持同一边界。生产部署必须为所有进程设置同一个外部数据根。
页面中的“预演完成”和“正式成功”是两种不同状态。预演全部跳过外部写入时不会再显示为
最近正式成功;运行详情和历史记录会保留 `dry_run``execute` 模式。
## 远程访问
默认只监听回环地址。若需要绑定非回环地址,必须先通过安全环境注入不少于 24 个字符的
访问令牌:
```powershell
$env:GYXX_CONSOLE_TOKEN = '<由密钥系统注入的随机令牌>'
uv run gyxx console --host 0.0.0.0 --port 8765
```
令牌不会写入源码、配置、URL 或日志。页面会在当前浏览器会话中临时保存令牌。生产环境
应再通过 HTTPS 反向代理或 SSH 隧道访问,不应直接把明文 HTTP 控制端口暴露到公网。
控制台不提供工作流入口、参数、运行时绑定或凭据的在线编辑能力;这些仍由受版本控制的
项目配置和代码维护。