Files
gyxx-flow/docs/runbook.md
T
wangyunlong 6f1c193f4b feat(product-commerce): 款式主键统一 style_id 并下线评价采集
采集端与 data-hub 统一用 style_id (= dim_style.id) 关联款式,style_name 降级为显示冗余。

款式主键:
- 14 张采集表加 style_id BIGINT + style_match_method + *_style_id_idx 索引
- 一律逻辑外键:不建 FOREIGN KEY,早期 10 个 *_style_id_fk 在 schema.sql 里幂等 DROP
- loader 下发 dim_style.id;db 层 attach_style_ids / resolve_style_ids_by_product 兜底
- 新增 backfill_style_id.py 三段回填(款名 / 日报按商品 ID / 广告表按 spu_id)
- 新增视图 v_style_id_coverage、v_style_unmatched_product、v_style_unmatched_source
- 实库补录 199 个商品 ID 到 67 款 spus;日报 style_id 覆盖 49.1% -> 79.0%,
  tm 广告 99.4% / jd 92.4% / dy 83.2%,其余 11 张 100%

评价采集下线:
- product_reviews 归档为 product_reviews_archived_20261101
- 差评改由 shop.{tmall,jd,dy}_negative_reviews.daily 采集,消费方读 v_negative_reviews
- 删除 import_product_reviews.py / orchestrate_review_collection.py 及相关命令与绑定
- dy_product_scraping 默认流程摘除评论采集,保留 --reviews-only

天猫广告:
- 迁移到独立表 tm_product_ad_daily_metrics,不再写 product_daily_metrics.raw_data

同批附带:
- 光合上传支持天猫新店 new_bag(仅黑曜石系列)
- 京东商智双月日历排除 -diff-month 补位格
- 万相主体搜索词覆盖 STYLE_SEARCH_KEYWORDS
- ERP 部分编码失败时用已成功编码销量兜底,避免整款归零
2026-11-01 16:43:27 +08:00

38 KiB
Raw Blame History

GYXX Flow 运维手册

日常检查

uv run gyxx doctor --json
uv run gyxx schedule status
uv run gyxx list
lark-cli --profile hermes-analyzer auth status --json --verify
lark-cli --profile hermes-analyzer auth check --scope "im:message.send_as_user im:message" --json

两条 lark-cli 检查必须在实际运行调度服务的系统账户下执行。业务消息统一由该 profile 的 user 身份投递;只有日报卡片的图片预上传因接口限制使用同 profile 的 bot 身份。不要用 交互登录账户的授权结果代替 NSSM/systemd 服务账户验证。

Linux 服务同时检查:

systemctl status gyxx-flow.service
journalctl -u gyxx-flow.service --since today

工作流失败时,使用 workflow_id、业务日期和 run_id 对照 <GYXX_DATA_ROOT>/logs、运行记录、节点产物和副作用账本。

手工执行

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 接受 23 个调度工作流和已注册的手动工作流;补采和维护操作使用 gyxx scripts run。两类入口都默认 dry-run,只有业务日期、凭据、浏览器登录态、PostgreSQL、lark-cli user 身份,以及工作流确实需要大模型时对应的 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 自动成为目标日期
天猫旗舰店、箱包店与天猫新店视频上传 product.video_upload.run 业务日期用于运行审计;--store all 顺序扫描三家,由 PostgreSQL 附件级账本防重
京东旗舰店视频上传 product.jd_video_upload.run 业务日期只用于运行审计;从固定凌云air记录锚点按飞书视图顺序扫描,日期可为空
采购单更新 supply.workflow.run 自动选择 mcp-run purchase-order-update

命令的额外脚本参数使用可重复的 --arg=<值> 传入。supply.workflow.run --execute 会进入真实 ERP 修改链路,必须在任务文件、目标范围和回滚条件全部复核后执行。

天猫视频上传工作流

天猫视频上传注册为幂等定时工作流 product.video_upload,按 config/schedules.json 以 2026-09-04 为起始日每 3 天 14:00(Asia/Shanghai)执行;前端仍可手动选择“光影行星旗舰店 / 光影行星鑫华达专卖店:龙虾仔 / 天猫新店(gyxx箱包旗舰店:龙虾仔)”并立即运行,也可填写天猫话题完整名称或关键词。正式执行只处理所选店铺,话题搜索结果按包含关系选中。默认 dry-run 只生成运行记录并跳过浏览器节点;确认飞书待传记录、对应淘宝光合店铺的登录态和 PostgreSQL 后,再显式正式执行:

uv run gyxx run product.video_upload --date 2026-08-07
uv run gyxx run product.video_upload --date 2026-08-07 --execute

前端把店铺选择作为受限运行参数传给脚本,只允许 flagship、luggage 或 new_bag。脚本先读取飞书并应用所选店铺路由,再写入/读取附件级账本;只有存在可领取的未上传附件时才启动有头 Chrome、登录对应账号并上传。旗舰店只选择“天猫上传”未勾选且有附件的光影行星男/女记录;箱包店只选择女性、全部款式命中箱包店白名单且有附件的记录;天猫新店不区分性别,只要求一条记录的全部「代拍产品」款式都属黑曜石系列(关键词 黑曜石,如黑曜石托特、黑曜石吐司、黑曜石电脑包、黑曜石饺子),组合款里掺了其它系列整条记录都不进新店,视频源与其它店铺为同一张飞书表。三店分别使用 GUANGHE_USERNAME / GUANGHE_PASSWORD、GUANGHE_LUGGAGE_USERNAME / GUANGHE_LUGGAGE_PASSWORD 和 GUANGHE_NEW_BAG_USERNAME / GUANGHE_NEW_BAG_PASSWORD,箱包店当前账号为 光影行星鑫华达专卖店:龙虾仔,天猫新店当前账号为 gyxx箱包旗舰店:龙虾仔,密码配置保持不变,各自保留独立浏览器 Profile(-luggage / -new-bag)。发生登录或页面错误时保留窗口供人工定位,排查完成后手动关闭。

每次正式扫描都会把视频附件写入 PostgreSQL 附件级发布账本,防重身份由视频 file_token + 目标账号 + 飞书记录 构成。账本状态为 published 的附件在下次执行时自动跳过,因此同一业务日期可以安全重跑并发现新增附件。旗舰店仍只在一条飞书记录的全部附件发布成功后回写“天猫上传”;箱包店和天猫新店不借用该勾选状态,是否已上传完全以自己的目标账号账本(guanghe-luggage / guanghe-new-bag)为准,同一视频可以分别发到多家店铺。

首次正式执行会读取 config/tmall-video-publication-backfill.json,将 2026-07-26 箱包店旧日志中可按 record_id + file_token + 文件名 再次核验的 25 个明确成功附件回填为 published。配置与当前飞书任一身份不一致都会停止,不会静默猜测。该回填不依赖部署机保留旧 var/ 日志文件;证据路径与成功行号已固化在配置中供审计。

只有淘宝光合页面出现明确成功回执后才把附件记为 published。点击发布后超时、浏览器中断或回执无法确认时,必须记为 ambiguous 并停止自动重发;先在对应店铺的作品管理中按账号、附件和飞书记录人工核对,再做审计式对账,不能通过更换业务日期或重复运行绕过。

单条续跑或指定附件使用公开命令,并通过可重复的 --arg 传参。下面的命令会停在正式发布前,核对无误后才能去掉 --stop-before-publish:

uv run gyxx scripts run product.video_upload.run --date 2026-08-07 --arg=--record-id --arg=RECORD_ID --arg=--stop-before-publish --execute

京东视频上传工作流

京东视频上传注册为幂等定时工作流 product.jd_video_upload,按 config/schedules.json 以 2026-09-04 为起始日每 3 天 14:00(Asia/Shanghai)执行;前端仍可手动立即运行,同一业务日期可以重复执行。每次都从飞书视图中的固定凌云air记录 recvrXTRWWFXeJ(含)扫描到视图末尾,不依赖“日期”字段。真正的防重键是 PostgreSQL 中的 视频 file_token + 目标账号 + 飞书记录:

uv run gyxx run product.jd_video_upload --date 2026-08-07
uv run gyxx run product.jd_video_upload --date 2026-08-07 --execute

框架 dry-run 不启动子进程,不下载附件、不写数据库也不打开京东。前端控制台点击“京东视频上传”会重新扫描锚点之后的视图后缀:新建的记录即使日期为空也能被发现;既有后缀记录后来新增第二个视频附件时,新 file_token 也会形成独立任务。已入账的同一 record_id + file_token 不会重复发布。锚点丢失或重复时脚本拒绝退化为全表扫描,避免误发历史内容。组合款在下载素材前直接跳过。正式执行按视图顺序逐条处理:有图片时设置飞书封面;没有图片时保留京东从视频生成的默认封面;生成 5-27 字标题、关联同款同色商品、选择相关话题和标签。只有页面出现明确成功回执后才把账本写为 published;点击发布后超时或浏览器中断会写为 ambiguous 并停止,必须先在京东内容管理中人工核对,不能自动重发。

商品关联只按页面商品标题中的款式名做受控模糊匹配,再由直连多模态大模型判断包体颜色;价格和 SPU 不参与查询或勾选。同款同色商品可多选,整个表单最多 10 个。SPU 仅作为可选审计信息;组合款视频当前主动跳过。

首次登录或遇到验证码时,使用公开命令限定一条记录并停在发布按钮前:

uv run gyxx scripts run product.jd_video_upload.run --date 2026-08-07 --arg=--record-id --arg=RECORD_ID --arg=--manual-login --arg=--stop-before-publish --execute

首次部署或多京东账号部署必须显式配置稳定且不可变的 JD_VIDEO_ACCOUNT_KEY;已有单账号账本时,前端运行会从 JD 渠道唯一账号 cohort 自动解析,数据库不保存登录账号明文。浏览器由 Scrapling 隐身会话启动真实 Chrome,默认使用该命令独立的 Profile、Cookie 和 Storage State,不能改成其他京东采集任务的共享目录。需要复用一个已启动且已验证的京麦 Chrome 会话时,使用运行时绑定注入的 GYXX_BROWSER_CDP_URL;脚本只借用唯一的现有 context,并在退出时保留原浏览器及其页面。

主图周工作流

主图调度的唯一工作流 ID 是 product.main_image.weekly,每周日 08:30 启动。图同时运行 JD、TM 两个无依赖分支;一个平台失败不会取消、跳过或回滚另一个平台的采集、PG upsert 和飞书插入。两个分支全部结束后再汇总工作流状态:任一平台失败时整体状态为失败,并在错误摘要中标明具体平台和错误,另一平台已经成功的结果继续保留。

两个平台默认都在采集后执行飞书主图表插入和本地 PostgreSQL main_image_creatives upsert。执行前必须同时确认两套浏览器登录态、飞书身份和本地数据库;排障时分别检查 jd、tmall 节点以及对应插入脚本日志,不能只以采集文件存在判断双 sink 已完成。

三平台电商费用日报

唯一工作流 ID 是 product.ecommerce_costs.daily,每天 10:00 由项目内 Python 调度器运行, 业务日期为前一天;天猫、京东、抖音三个下载分支并行启动,各自完成下载后再导入。天猫流程先进入万相台商品报表选择昨日,提交下载任务;任务在下载列表显示“生成成功” 后下载 ZIP、做路径安全校验并解压 CSV,随后将商品级广告指标幂等写入独立表 tm_product_ad_daily_metrics(每日每商品一行,主键 (stat_date, product_id))。花费、直接/间接/归因 成交金额和投产比走标量列,报表其余指标(展现、点击、点击率、转化率、预售成交等)原样存在 metrics JSONB。商品款式先按 dim_style.tm_spus 的商品 ID 匹配,匹配不到再按商品名称做精确包含匹配,结果 写进 style_name 与 style_match_method(product_id/product_name/ambiguous/unmatched)。 天猫广告数据不再写入 product_daily_metrics.raw_data.tm_ad_metrics,投放与经营日报分表,经营数据 重新同步不会覆盖投放数据;data-hub 产品生命进程从新表读取花费和广告归因成交金额并计算投产比、电商费比。

历史数据迁移(2026-11-01 一次性执行):src/gyxx_flow/modules/product_commerce/scripts/ migrate_tm_ad_metrics_to_table.py,默认 dry-run,--execute 先备份旧节点到运行时 TMP_ROOT 再回填, 校验行数与花费合计一致后才允许 --clear-source 清除 raw_data 里的旧节点。

正式服务必须注入 WANXIANG_ACCOUNT、WANXIANG_PASSWORD、GYXX_DATA_ROOT 和 GYXX_POSTGRES_DSN。默认浏览器 Profile 是 <GYXX_DATA_ROOT>\state\browser-profiles\wanxiang-ads,登录失效时不能在另一个 Profile 中登录后 期待服务自动获得状态。

手工验证或补采某一天时,先 dry-run,再显式执行完整工作流:

uv run gyxx run product.ecommerce_costs.daily --date 2026-08-17
uv run gyxx run product.ecommerce_costs.daily --date 2026-08-17 --execute

如果登录态失效,使用有头的采集命令(不要给它加 --headless)完成验证码/滑块,再单独导入:

uv run gyxx scripts run product.tmall_wanxiang_ads.collect --date 2026-08-17 --execute
uv run gyxx scripts run product.import.tmall_ads --date 2026-08-17 --execute

原始文件位于 <GYXX_DATA_ROOT>\data\raw\product_commerce\tmall_wanxiang_ads\<date>\,其中 manifest.json 记录 ZIP 和解压出的 CSV。定时工作流每次都会进入商品报表点击“下载报表”,在日期范围 选择“昨日”并点击“确定”,再到下载列表等待“生成成功”。若只需重新解析已下载文件,可以跳过采集步骤 重跑导入;默认导入以 manifest 列出的本次 CSV 为准,避免同一日期目录中历史 CSV 被重复合并;重复导入同一日期和商品不会产生重复行。采集命令手工重试时若不加 --force-request,仍可 复用下载列表中已有的“生成成功”任务。

排障顺序:

  1. 检查 gyxx schedule status、服务日志和对应 run_id,确认失败发生在申请、下载、解压还是入库。
  2. 检查同一 Profile 是否仍能打开万相台商品报表;登录失效时在有头命令中人工完成验证。
  3. 检查日期目录中的 manifest.json、CSV 表头和 日期/主体ID/主体名称;ZIP 存在不代表数据库已导入。
  4. 检查 PostgreSQL 的 tm_product_ad_daily_metrics 是否存在目标日期与商品 ID 的行,spend、 attributed_deal_amount 是否为目标值,style_name 是否已匹配;确认 product_daily_metrics 同日期经营数据(访客、成交)仍完整。
  5. 只有在外部状态核对清楚后才按同一业务日期重跑,不能用换日期或重复申请掩盖下载结果不明。

调度操作

uv run gyxx schedule run --dry-run --once
uv run gyxx schedule status
sudo systemctl restart gyxx-flow.service

调度服务也只加载显式指定的受控环境文件:

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,再重启服务。其他模块继续运行。

开机自启(Windows 生产机)

重启/关机开机后的自动恢复链路:Windows 自动登录 → Startup\gyxx-keepalive.vbs → deploy/watchdog/gyxx_keepalive.py → 控制台(8765) + gyxx schedule run 调度器。keepalive 每 30 秒巡检并负责拉起死掉的控制台/调度器(单实例锁防双跑,冷却 120s、熔断保护),日志在 var/logs/keepalive/keepalive.log,验收用 python deploy/watchdog/gyxx_keepalive.py --once。

采集依赖浏览器 CDP,必须运行在交互式用户会话中,因此不能改成 Windows 服务或 Task Scheduler SYSTEM 任务(也不允许注册 Windows 任务计划)。免登录自启的唯一环节是自动登录,用脚本启用(密码交互输入、写入 HKLM\...\Winlogon,仅 Administrators 可读):

powershell -ExecutionPolicy Bypass -File D:\gyxx-flow\deploy\watchdog\autologon.ps1           # 启用
powershell -ExecutionPolicy Bypass -File D:\gyxx-flow\deploy\watchdog\autologon.ps1 -Disable # 撤销

启用后重启验证:开机自动进桌面,约 2 分钟内控制台与调度器就位;停机窗内到期任务由调度器 catch-up 自动补跑。

数据和浏览器状态

生产 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/accounts/<account>:账号级登录态主库(同一账号多个脚本共用,runtime-bindings.json 中声明 account 的脚本每次运行前自动合并同步)。
  • state/scheduler、state/locks、运行账本:调度和幂等状态。
  • tmp:仅存放可重建临时文件。

账号级登录态管理:

  • gyxx accounts list:查看账号、登录态有效性、成员脚本。
  • gyxx accounts login <account_id>:打开该账号的登录浏览器(独立 Profile),人工扫码/验证码登录后自动保存 cookies.json/storage_state.json 到账号主库,并同步到全部成员脚本;--timeout 控制等待时长,--force 强制重登。
  • gyxx accounts sync [<account_id>]:把账号主库 cookie 推送到成员脚本(不打开浏览器)。
  • gyxx accounts seed <account_id> --source-binding <binding_id>:一次性把已有脚本的登录态按平台域名过滤后导入账号主库;仅用于迁移,不替代后续的账号级登录。

同一账号只需登录一次:之后任何成员脚本运行前都会从账号主库合并最新登录态到自己的独立 Cookie 文件,无需逐脚本登录。

当前配置按登录身份拆分为以下账号,避免同一平台不同店铺互相覆盖 Cookie:

  • douyin-shop:抖店,600 秒;
  • jd-shop、jd-self-operated、jd-market-rank:京东三个独立登录身份,各 900 秒;
  • tmall-shop:天猫/淘宝/万相台光影行星账号,900 秒;
  • tmall-ozko:商品经营日报 ozko 账号,900 秒;
  • xiaohongshu-content:小红书,900 秒;
  • douyin-content:抖音内容侧,900 秒;
  • chanmama:蝉妈妈,900 秒(脚本自带账密登录);
  • bilibili-content:B 站,900 秒。

星图/百应采集(xingtu_scraper_v2.py)的登录态挂在 douyin-shop 账号下;xingtu 账号当前无成员脚本,仅保留 vault。

商品经营日报 product_commerce:taobao_sycm_collect.py 会在同一次运行中按品牌路由账号:光影行星使用 tmall-shop,ozko 使用 tmall-ozko,两边各自使用独立 Profile 和账号 vault。tmall-ozko 没有静态成员脚本是有意设计,因为它由这个按品牌拆分的包装工作流动态选择;登录态成功后仍会回写 state/accounts/tmall-ozko/,由同一套 keepalive 负责续期。

账号需要在 config/runtime-bindings.json 的 accounts.<account_id>.keepalive 中显式开启续期,并配置一个不会产生业务副作用的已登录页面 URL。所有账号由唯一的 gyxx schedule run 常驻进程在后台维护;每个账号使用独立 CDP 端口、独立 Profile 和账号级 Cookie vault,启动后按 initial_delay_seconds 错峰访问,避免同时打开多个登录页面。访问成功且没有跳转到登录页时,Scrapling 才会把最新 Cookie/storage state 原子写回 state/accounts/<account_id>/,并同步给成员脚本;不需要另建 Windows 任务计划、cron 或 timer。

Cookie 续期只能延长站点支持滑动续期的会话,不能突破服务端硬过期、主动退出、风控或验证码。keepalive 检测到登录页/无效 Cookie 时会停止发布,不会用失效状态覆盖最后一份有效 vault;此时按 gyxx accounts list 检查 keepalive.status,必要时重新执行 gyxx accounts login <account_id>。

每个正式执行(非 dry-run)的工作流在采集步骤前会自动注入"每账号一个"的 cookie-gate.<account_id> 关键步骤,由 src/gyxx_flow/cookie_gate.py 实现按账号分三级的状态机:

  • blocking(纯扫码/验证码账号:douyin-shop、xiaohongshu-content、douyin-content、bilibili-content 等):先看 keepalive 近期成功状态(默认 1 小时内视为新鲜,可用 GYXX_COOKIE_GATE_MAX_STATUS_AGE_SECONDS 调整),新鲜且 vault 静态有效则直接放行;否则用 keepalive 同款无头浏览器真实探活。探活失败即判定过期:gate 步骤失败,依赖它的采集步骤记 dependency-failed 跳过、工作流失败退出,并通过飞书通知登录负责人(config/notification-routing.json 中手机号为 +8613663785145 的人员,默认 30 分钟节流,可用 GYXX_LOGIN_ALERT_OPEN_IDS 覆盖收件人)执行 uv run gyxx accounts login <account_id> 完成扫码/验证码重登。
  • recover(配置了 login_command_id 的账密自动重登账号:erp-jushuitan):过期时先自动跑账密登录恢复,恢复成功放行;失败才阻断并飞书通知。
  • defer(成员脚本自带账号密码登录的账号:京东系、天猫系、chanmama):不注入 gate,保持脚本内账密自动登录流程不变。

按账号划分的持久状态在 state/accounts/<id>/cookie_state.json(healthy ↔ awaiting_login)。编排器型工作流(入口脚本本身无账号绑定、由子采集脚本使用账号)在 config/workflows.json 的 required_accounts 字段声明依赖账号。--rerun-step/--resume-from 切片和 shadow 运行不发送通知;dry-run 完全不注入 gate。设置 GYXX_COOKIE_GATE=0 可临时关闭整网关。

采集脚本不再内嵌扫码/验证码登录:运行中检测到登录态失效会以 CookieExpiredError 快速失败并按退出码 75 记为 cookie 跳过;self_xhs_scraper.py --login-only 是唯一保留的人工登录引导(该脚本不在任何账号成员内,自管绑定状态)。

登录恢复后的自动重跑

gate 阻断工作流时会把待重跑清单(工作流 + 业务日期,shadow/验收运行不记录,同工作流同日期去重,上限 20 条)写进 state/accounts/<id>/cookie_state.json 的 pending_reruns。常驻调度器(gyxx schedule run)的维护循环里挂着 CookieGateRecoveryManager:

  • 每个心跳检查有待重跑记录的账号:keepalive 近期成功 + vault 静态有效 → 立即视为恢复;否则每 5 分钟做一次真实探活(mode C 账号会自动账密重登,重登成功同样触发恢复)。
  • 恢复确认后,用调度器同款启动器按原业务日期自动重跑全部待重跑工作流(日志在 data/logs/scheduler/),清空清单并飞书通知"登录态已恢复 + 已自动重跑清单"。
  • 启动失败的任务保留在清单里下一轮重试;同一账号两次重跑启动之间有 5 分钟冷却;控制台手动脚本类临时运行 id(script.*)不会自动重跑。
  • 与正常调度并发安全:工作流级锁 + EffectLedger 幂等保证同一天不会重复产生副作用。

项目不会自动搬动或删除已有 var/。迁移数据根时应停止调度器,完整备份和复制原目录,修改环境变量后依次运行 doctor、dry-run 和一次指定日期的对账执行。

清理和保留

可以在确认没有任务运行后清理缓存、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、飞书消息回执和目标文档 确认该节点是否已产生外部效果,才可执行审计式对账:

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,不要同时启动第二实例。

商品日报采集开关(product.daily)

采集开关是 dim_style.collect_enabled(2026-10-29 起),两层语义(2026-10-30 起):

  1. 锚点是系列总开关:以该系列 is_series_anchor = TRUE 那一行为准;系列没有锚点时按款名 排序第一个。锚点 FALSE → 整条系列一款都不采,同系列其余款式跟随锚点。
  2. 锚点 TRUE 时,子款开关只决定自己的 ERP 编码是否参与聚合:子款 TRUE 的 ERP 款式编码 并入锚点一起去聚水潭取数;子款 FALSE 的编码不并入(既不由它自己采,也不让锚点代采)。 被排除的子款列在 series.erp_excluded,运行时打 [INFO] 系列「X」子款 Y 已关闭采集,ERP 编码不纳入「Z」聚合。平台商品编码仍按整系列聚合, 不受子款开关影响。

旧的独立表 product_daily_collect_scope 已迁移并删除,备份在 var/tmp/collect_scope_before_drop_20261029.json。

默认值是 TRUE(采集),因此产品生命进程在 dim_style 新增的款式无需任何操作就会进入日报; 配件、包装、耗材、停售款才需要显式关掉。

关掉整条系列(改锚点那一行即可,子款自动跟随):

-- 单系列
UPDATE dim_style SET collect_enabled = FALSE WHERE style_name = '黑曜石托特包2';

-- 批量:把若干系列的所有行一起关(无锚点的系列也覆盖到)
UPDATE dim_style SET collect_enabled = FALSE
WHERE COALESCE(NULLIF(btrim(series_name), ''), style_name)
      IN ('飞机盒', '香片', '雨衣', '鞋袋');

恢复采集把 FALSE 改成 TRUE。改完无需重启控制台或调度器:StyleConfigLoader.load() 每次运行都直接读库,下一轮 product.daily(每天 08:40,业务日 T-1)即生效。

两点注意:

  • 开关落在锚点行上才控制整条系列。 ERP 编码与三平台商品编码都由 _apply_series_sharing() 按整系列聚合并去重后挂到锚点(子款误填的编码也会并入,但 collect_enabled = FALSE 的子款 ERP 编码除外),锚点是系列唯一的采集单元;子款自己的 编码保留在 series.own_erp_codes 与 series.platform_ids 备查。
  • 关掉只影响商品日报(tm/jd/dy 平台段 + ERP 段)。天猫/京东主图周采集、聚水潭全店铺日报、 三平台电商费用不受此开关影响。

放行不等于一定会采:采集门仍要求「ERP 款式编码 + 各平台商品编码」齐备;飞书目标表地址 (下一节的 9 类 bitable_* 列)只决定能否写飞书,缺地址的款照常采集入库。缺口写在 var/logs/product_commerce/<date>_skipped_styles.json 的 skipped.reason。

改款式名时要同步改 series_name/锚点标记,否则旧开关会变成孤儿行、新名字按默认 TRUE 采集。

飞书表地址维护(dim_style)

2026-10-29 起,9 类飞书多维表格地址全部维护在 dim_style,而且只写在系列锚点行上。 workflow_dynamic_styles / workflow_dynamic_style_platforms 里的同名字段已废弃, 代码不再读取;旧值备份在 var/tmp/bitable_urls_before_move_20261029.json。

用途 dim_style 列 原动态配置字段
销量表 bitable_weekly_url sales_bitable_url
主图表 bitable_main_image_url main_image_bitable_url
合作达人表 bitable_creator_url creator_bitable_url
自营合作达人表 bitable_self_creator_url self_creator_bitable_url
每周/每月笔记分析 bitable_weekly_note_url weekly_note_analysis_url
平台单品分析 bitable_style_analysis_url style_analysis_bitable_url
人群画像(天猫) bitable_persona_tm_url tm_persona_bitable_url
人群画像(京东) bitable_persona_jd_url jd_persona_bitable_url
人群画像(抖音) bitable_persona_dy_url dy_persona_bitable_url

子款留空即可:两处代码会把整条系列的地址聚合去重后统一下发—— workflow_config._propagate_series_targets() 在合并建档数据时按「锚点优先、其余按款名」 收集整系列的非空地址,去重后写回每一行;style_config_loader._apply_series_sharing() 在聚合配置时再做一次同样的兜底。因此子款误填的地址不会丢、也不会让同系列出现两套表。 一个表位只能落一张表:同一字段填了多个不同地址时,保留优先级最高的那条,并在日志里打 [WARN] 系列「X」<字段> 有 N 个不同表,提示到锚点手动裁决。

-- 改某个系列的主图表(只改锚点一行,子款自动跟随)
UPDATE dim_style
SET bitable_main_image_url = 'https://xxx.feishu.cn/base/<base>?table=<tbl>'
WHERE style_name = '逐星';   -- 必须是该系列的锚点行

-- 查看哪些锚点还缺地址
SELECT style_name, series_name,
       bitable_weekly_url, bitable_main_image_url, bitable_creator_url
FROM dim_style
WHERE is_series_anchor AND COALESCE(bitable_weekly_url, '') = ''
ORDER BY style_name;

改完不需要重启:StyleConfigLoader.load() 每次运行都读库,下一轮工作流即生效。 若某类地址为空,对应环节会跳过写表但照常采集入库(例如主图缺表地址打 [NO-TABLE])。

地址读取是 fail-closed 的(2026-10-30):写表脚本拿不到 dim_style 地址时会打 [FAIL] … 拒绝回退本地 JSON 并以退出码 1 结束,不会改用 bitable_style_map.json / bitable_main_image_map.json 里的旧地址。所以看到这类 [FAIL] 先去查库里的地址, 不要去改本地 JSON(这两个文件已无读取方)。

新增系列时,地址写在锚点行。若某款业务上确实是独立一套表(历史上 逐星GT 这种情形), 必须把它拆成独立系列(单独的 series_name + 锚点标记)并把地址写在自己那行;只写地址 而不拆系列的话,聚合会把它并入原系列,只在日志里留一条告警,不会真的生效。

款式主键统一(style_id = dim_style.id,2026-11-01)

采集端不再只用 style_name 关联款式。唯一关联键是 style_id(= dim_style.id, 与 data-hub 的 22 张表同一套主键),style_name 降级为显示冗余。这样在 dim_style 改款名时,历史指标表不会变成孤儿。

数据从哪里来

环节 机制
loader 下发 merge_lifecycle_style_records() 输出 style_id + style_match_method,经 style_config_loader 进入每条款式配置
db 层兜底 db.attach_style_ids()(按款名)/ db.resolve_style_ids_by_product()(按平台商品 ID)在 upsert 内补齐
历史回填 backfill_style_id.py

回填与验收

# 只报数,不写库
.venv\Scripts\python.exe backfill_style_id.py --dry-run
# 执行(幂等,只补 style_id IS NULL 的行)
.venv\Scripts\python.exe backfill_style_id.py
# 只补款名表,跳过按商品 ID 的两步
.venv\Scripts\python.exe backfill_style_id.py --skip-product-table --skip-ad-tables

回填分三段,缺一段覆盖率就上不去:

段 表 依据
按款名 13 张(daily_style_metrics / weekly_style_metrics / erp_all_shop_style_daily_metrics 等) t.style_name = dim_style.style_name
按商品 ID product_daily_metrics platform + product_id 反查 *_spus
按商品 ID(二次) 三张广告表 同上;顺带把空的 style_name 补成 dim_style 的权威款名

广告表必须走第三段:平台的报表不给款式名,jd_product_ad_daily_metrics 有 27%、 dy_product_ad_daily_metrics 有 40% 的行 style_name 为空,只按款名回填完全无效。 补完后 data-hub 现有按款名的 join 立刻多认回一批行,不必等主键切换。

-- 各表回填进度;除 product_daily_metrics 与三张广告表外都应 100%
SELECT * FROM v_style_id_coverage ORDER BY tbl;
-- 未建档的平台商品 ID,仅统计经营日报口径
SELECT * FROM v_style_unmatched_product ORDER BY platform, rows DESC;
-- 同上,但把三张广告表也算进来(纯投流商品只在这里出现)
SELECT * FROM v_style_unmatched_source ORDER BY platform, rows DESC;

style_match_method 取值:dim_style(按款名精确)、product_id(按平台商品 ID)、 product_name(按商品名包含)、unmatched(确实没建档)。

补 spus 的正确做法

product_daily_metrics.product_no 存的是款名(不是 ERP 编码),天猫填充率 100%、 京东 97.8%、抖音 0%。补 spus 时按这个优先级,命中率远高于猜商品标题:

  1. product_no 精确等于 dim_style.style_name;
  2. product_no 与款名互为前后缀(长度差 ≤ 3,如「蓝鹊pro」→「蓝鹊」、「知更大号」→「知更」);
  3. 商品标题包含完整款名(抖音只能用这一条),并用「命中位置后的版本后缀」排除串款 —— 例如标题「盖亚斜挎包3」不能归到「盖亚斜挎」;
  4. 编码与标题互相矛盾时不要自动写,列出来人工定(实测有天猫单品编码写「理岩」、 标题写「阿波罗 air」,支付额 50 万,猜错代价极大)。

物料类(数据线、内胆包、补邮费、配件包、礼品袋、定制服务)不该进 spus,应留 unmatched。dim_style 里本身建成款的物料(配件包、飞机盒、电泵 等)也不写。

必须知道的边界

  • product_daily_metrics 表里没有款名列,只能拿 platform + product_id 去 tm_spus / jd_spus / dy_spus 反查。未建档的商品 ID 回填不了,要靠建档页补 spus 才能涨,不是脚本 bug。
  • 2026-11-01 补过一轮:product_daily_metrics 49.1% → 86.0%(jd)/ 77.3%(tm)/ 76.7%(dy),整体 79.0%;广告表 tm 99.4% / jd 92.4% / dy 83.2%。 剩余 259 个商品 ID(dy 107 / jd 52 / tm 100)未建档,清单见 v_style_unmatched_source。
  • 不建物理外键:14 张表的 style_id 都只做逻辑外键(与 data-hub 侧一致)。 库里没有任何指向 dim_style(id) 的 FK 约束,完整性靠写入端(loader 下发 + attach_style_ids 兜底)、backfill_style_id.py --dry-run、v_style_id_coverage 三道保证。理由:采集语义是「宁可留 NULL 待回填,也不要整批写入失败」, 物理外键会在传错 id 时直接中断整天采集;此外拆系列/合并同款要改数据时, 还得先删约束。早期 P4 建过的 10 个 <表名>_style_id_fk 已在 schema.sql 里 幂等 DROP CONSTRAINT IF EXISTS 清掉,老库重跑 schema 即自动回到无 FK 状态。
  • 主键一律没动:daily_style_metrics 仍是 (metric_date, style_name, platform)。 data-hub 有 6 处按 style_name join,改主键收益小、回归面大。

商品评价采集下线(product_reviews,2026-11-01)

product_reviews 已停止写入并归档为 product_reviews_archived_20261101。 原因是 JD 侧 style_name 全靠 sku_id 反查,而映射表不存在,214 行全部为 NULL, 两个消费方都是 WHERE style_name = ?,等于入库了读不到。

差评统一由三个独立工作流采集:shop.tmall_negative_reviews.daily、 shop.jd_negative_reviews.daily、shop.dy_negative_reviews.daily。

消费方改读视图

SELECT platform, comment_date, rating, content, style_id, style_name, style_match_method
FROM v_negative_reviews;   -- tm + jd + dy 三张差评表 UNION,款式按 ID 优先、商品名兜底

改动点:analyze_style.py:pull_reviews、content_marketing/daily_marketing_report.py:_review_context。

口径变化(必须知道)

  • 数据源只有差评一档,不再有好评/中评。风格分析的「评价等级分布」因此只剩差评。
  • 营销日报的 sample_size = 差评条数,negative_rate 恒为 1.0;门槛从「至少 5 条评价」 改成「至少 1 条差评」(旧门槛在纯差评源上会导致永不产出)。
  • 覆盖面反而更全:天猫差评 97 条(旧表天猫为 0)、京东 32 条(旧表 8 月断更)。
  • 若产品侧确实需要好评样本,应把差评采集器扩成「按等级采集」,不要恢复 product_reviews。
  • dy_product_scraping.py 默认流程里的评论采集已摘除(只产出无人读取的本地 JSON); 需要历史样本时用 --reviews-only 显式触发。

业务源码同步

外部业务源码目录只用于发现更新,GYXX Flow 运行时不导入或挂载这些目录。只读检查:

uv run gyxx sources status `
  --source-root content_marketing=<内容源码目录> `
  --source-root product_commerce=<商品源码目录> `
  --source-root shop_intelligence=<店铺源码目录> `
  --source-root supply_chain=<供应链源码目录> `
  --json

只有未转换、无冲突的 source_changed 可以显式执行:

uv run gyxx sources apply ... --execute --json

经过统一路径、数据库、Hermes、飞书或浏览器适配的文件必须人工合并并运行回归测试。新增可执行脚本还需登记工作流、调度和独立 CDP 绑定。同步完成后执行测试、doctor、acceptance 和 source status;真实采集不在源码同步过程中自动触发。