如何用Skills做Listing每日巡检(硬核完整技术方案)

0. 需求背景与业务目标

🎯 一句话目标: 基于领星已有运营日志,把“运营主动查异常”升级为“系统识别异常并主动告警”。

0.1 业务背景与能力缺口

评分、Review、父子体关系和购物车归属会直接影响 Listing 的转化、广告承接和销售稳定性。目前运营仍需每天逐个检查链接,监控规模扩大后容易出现耗时、漏检和无法追溯的问题。

  • 行业现状: 领星、赛狐已经能够记录 Listing 的关键运营变化。
  • 缺少规则: 现有监控模块无法针对四类业务场景配置监控对象、判定条件和接收人。
  • 缺少触达: 日志主要用于被动查询和事后追溯,异常仍要依赖运营主动发现。

有日志不等于有监控,能追溯不等于能预警。真正缺少的是从“变化数据”到“运营行动”的自动化链路。

建设思路: 整合日志接入、规则判定、状态管理、事件去重和飞书通知,形成统一的轻量级告警工具。首期由本地 Python 通过领星 Open API 与官方 Python SDK 采集,不重复建设网页抓取链路。

0.2 需要监控的四类变化

  • 新增差评: 识别新增 1~3 星 Review,不重复告警历史存量。
  • 评分变化: 记录评分旧值、新值和变化时间,升分与降分均留痕。
  • 父体成员变化: 运营希望判断同一父 ASIN 下的子 ASIN 集合和数量是否发生变化,包括成员减少、增加,以及数量不变但成员被替换。
  • 购物车丢失: 识别 Buy Box 从自有卖家切换给其他卖家或无人持有;仅价格变化不判定为掉购物车。

0.3 业务目标

  1. 降低人工巡检成本: 自动覆盖全部启用 ASIN,运营只处理异常。
  2. 实现主动触达: 在每日巡检窗口内识别变化并通过飞书告警。
  3. 控制误报与重复: 区分数据源失败和业务正常,同一事件最多发送一次。
  4. 形成追溯闭环: 告警可反查 ASIN、店铺、原始日志、状态变化和任务批次。
  5. 保持轻量可演进: 先以 Skill、Python 和 SQLite 落地,后续可平滑迁移 MySQL。

🔔 业务闭环: 运营日志 → 规则判定 → 状态比较与事件去重 → 飞书主动告警 → 运营处理

0.4 建设范围

本期建设

  • 配置化 ASIN 清单与领星自动运营日志增量读取。
  • 四类事件判定、SQLite 留痕、飞书告警及失败审计。

本期不做

  • 不建设分钟级或实时监控。
  • 不自动调价、修改 Listing、调整广告或执行其他运营动作。
  • 不接入 Sorftime 等第三方数据源,不推断数据源未提供的评论明细。

0.5 验收成功标准

  • ✅ 每日任务覆盖全部启用目标,未完成目标必须有明确失败原因。
  • ✅ 四类标准测试样例均能按本文口径正确识别。
  • ✅ 重复拉取或重复执行不会产生重复事件和重复业务告警。
  • ✅ 接口超时、返回异常或解析失败只记录技术失败,不发送运营告警,也不误判为链接正常。
  • ✅ 任一告警均可反查事件时间、变化前后值、原始日志、event_key 和任务批次。

方案结论: 采用“Codex 每日计划任务 + Listing Monitor Skill + Python 采集程序 + 领星 Open API/官方 SDK + SQLite + 飞书签名 Webhook”的轻量实现。没有常驻服务、Java/Spring、Redis 或消息队列;Python 负责采集、分页、重试、规则、落库和通知,模型只负责启动任务并读取紧凑汇总。

数据边界: 主链路只读取领星 Open API:运营日志负责差评、评分和 Buybox 事件,Listing 接口每日完整快照负责父体成员变化;不接入 Sorftime 或网页抓取。Open API 原始响应直接在本地解析并写入 SQLite,不进入模型上下文;SQLite 保存目标、状态、父体快照、事件、Outbox 和运行记录。

0.6 飞书告警效果

告警示例1

告警示例2

后续可以迭代的功能:将Listing和人员绑定,告警时可以直接 @到人;有能力的可以跟自己的ERP系统建立ACK机制联动,考核运营的响应率。

1. 需求与监控口径

运营问题 主数据源 判定口径 建议告警
是否新增差评 运营日志:Review 变动 新增 Review 中明确出现 1~3 星评价 P2;展示新增数量与 ASIN
评分是否变化 运营日志:评分变动 比较旧评分与新评分,区分下降和上升 下降:核心 P1、普通 P2;上升:按配置发送 P3
父体成员是否变化 erp_listing 每日快照;父 ASIN 变动日志仅作证据 按店铺、站点、父 ASIN 比较去重后的子 ASIN 集合 核心父体移除发 P0,普通父体移除发 P1,仅新增发 P3
是否掉购物车 运营日志:Buybox 变动 最新持有者不再命中自营卖家别名;价格变化不等于掉购物车 失去:核心 P0、普通 P1;恢复:P3

📌 关键口径: 父体规则不能只依赖运营日志中的单条“父 ASIN 变动”,也不能只比较子体数量。必须每天完整拉取 erp_listing,按父体构建去重后的子 ASIN 集合,再与上一份完整快照做集合差异比较。

2. 数据源组合与职责边界

  • 运营日志: 提供 Review 变动、评分变动和 Buybox 变动等已识别事件,适合低成本增量判断。
  • Listing 快照: erp_listing 提供 asinparent_asinstore_idmarketplace_id 等当前关系,用于重建父体成员集合。
  • 双源互补: 日志回答“发生过什么”,快照回答“当前父体下有哪些子体”;两路在统一事件模型和告警状态机处汇合。
  • 失败语义: 任一数据源调用失败、分页不完整或关键字段缺失,都记为 UNKNOWN 并写入技术失败记录;不得发送运营告警,也不得把未知状态解释成业务异常。

因此,本方案不是“只读运营日志”,而是“日志增量 + Listing 每日完整快照”的轻量组合。两者均由 Python 通过领星 Open API/官方 SDK 读取,不增加网页抓取或第三方数据源;原始响应不进入模型上下文。

领星官方说明:运营日志

3. 总体数据流

每日任务并行读取两路数据:运营日志经过合并、标准化和事件去重;Listing 经过完整分页校验、父体分组和成员集合比较。两路结果统一进入规则引擎、SQLite 事件账本、Outbox 与飞书通知。 数据流

4. 领星 Open API 与 Python SDK 采集设计

4.1 SDK 适配与采集层次

层次 对象 作用
SDK Client 领星官方 Python SDK 负责鉴权、签名、HTTP 连接和基础响应封装;凭据只从本机安全配置或环境变量读取。
Collector 本地采集器 按目标批次调度运营日志与 Listing 请求,处理分页、限流、超时、指数退避和完整性校验。
Adapter 响应适配与标准化 解包 SDK 响应,统一字段、时间、空值和错误码;保留可审计原始证据,但不把整包响应送入模型。
Store / Rules SQLite 与确定性规则 外部请求完成后再开启短事务,完成幂等写入、状态比较、事件生成和 Outbox 落库。

适配层不得把官方 SDK 的具体方法名、请求字段和返回包装泄漏到规则层。SDK 包装器是唯一接口边界;接口路径、鉴权方式、批量上限、分页字段、限流和历史时间范围须在收到领星 Open API 文档与 SDK 后逐项确认,再冻结为可测试契约。

4.2 采集契约与查询窗口

业务项 设计值 实施约束
运营日志目标 启用的 ASIN 清单 Collector 接受多个 ASIN;底层是批量请求、自动分片还是逐个请求,由官方接口的批量能力与上限决定,不在方案阶段臆测。
日志时间窗口 计划日 D-2 至 D 形成 3 天重叠窗口以覆盖迟到数据;接口是否支持历史日期、最大跨度及源时区以官方文档和联调结果为准。
查询粒度 按日 若接口支持关闭汇总,则读取明细;实际请求字段和值由 SDK 契约定义。
Listing 快照 按 sid + 站点分组 完整读取全部页,不只取第一页,不因在售过滤造成父体成员误减。
分页与限流 Collector 统一处理 校验页码、总数和关键字段;批量大小、pageSize、限流阈值和重试条件待官方文档确认后配置化。
operation_logs = collector.fetch_operation_logs(
    target_asins=target_asins,
    window_start=scheduled_date - timedelta(days=2),
    window_end=scheduled_date,
)

listing_pages = collector.fetch_listing_pages(
    sid=sid,
    marketplace_id=marketplace_id,
    require_complete=True,
)

4.3 Listing 快照查询与父体聚合

领星 Listing Open API(对应现有 erp_listing 业务能力,具体 SDK 方法名待官方文档确认)不直接返回子体数量时,可利用每条记录中的 asinparent_asinstore_idmarketplace_id 在本地构建父体成员集合。应按店铺和站点完整分页拉取目标范围,禁止只取第一页或只筛选在售状态。

处理项 规则
父体分组键 sid/store_id + marketplace_id + parent_asin
子体身份 按规范化后的 asin 去重,避免同一 ASIN 多个 MSKU 被重复计数
成员范围 parent_asin 非空且不等于自身 ASIN;排除 is_delete = 1,保留非在售状态
完整性门槛 全部分页成功且累计记录数与接口 total 一致后,才允许与上次快照比较
def build_parent_sets(rows):
    groups = defaultdict(set)
    for row in rows:
        child = normalize_asin(row.get("asin"))
        parent = normalize_asin(row.get("parent_asin"))
        if not child or not parent or child == parent:
            continue
        if str(row.get("is_delete")) == "1":
            continue
        key = (row["store_id"], row["marketplace_id"], parent)
        groups[key].add(child)
    return groups

📌 数据口径: 该规则监控的是“领星当前可见的父体成员集合”。任一分页失败、总数不一致或字段缺失时,状态必须记为 UNKNOWN,不得把不完整结果解释为子体减少。

4.4 运营日志 Open API 返回结构与兼容策略

现有链路的观测样本显示:返回结果按日期分组,每个日期项可能包含多个日志数组,B096S5YCC6 的系统自动日志出现在 operation_log_data,而不是只出现在 auto_log_data。切换官方 SDK 后必须用 Open API 原始响应重新验证字段路径,不能直接照搬旧链路的包装层结构,也不能只读某一个数组。

{
  "type_name": "表现",
  "content": "[Rating变动] 新增5个Rating",
  "gmt_modified": "2026-08-31 23:59:59",
  "asin": "B096S5YCC6",
  "sid": 13345,
  "is_detail": false
}

解析顺序:

  1. 遍历日期列表,兼容日期字段 r_date / rdate
  2. 合并 operation_log_dataauto_log_data;null 一律按空数组处理。
  3. log_data 只作版本兼容:必须确认是系统自动事件后才纳入,避免把人工备注当成告警。
  4. 只处理 Review变动、评分变动、Buybox变动三类日志;“父 ASIN 变动”仅保存为辅助证据,不作为父体成员告警的唯一依据。
  5. 按事件时间、ASIN、店铺和内容构造幂等键,重复轮询不得重复通知。

5. 统一事件模型

字段 示例 用途
event_type PARENT_MEMBERS_CHANGED 业务事件枚举;另含 NEGATIVE_REVIEW_ADDED、RATING_CHANGED、BUYBOX_LOST 等
subject_type PARENT_ASIN 区分单 ASIN 日志事件与父 ASIN 集合事件
subject_key B0DM7H5QZ8 告警主体;父体成员变化以父 ASIN 为主体
sid / marketplace_id 13345 / ATVPDKIKX0DER 隔离店铺和站点,必须参与分组与去重
event_time 2026-09-02T01:10:00Z 日志事件取源时间;快照事件取本次完整快照观测时间
old_value / new_value 6 / 5 普通事件保存前后值;父体事件保存前后子体数量
change_detail_json added / removed / hashes 保存新增、移除 ASIN 集合和前后集合哈希
source_type LISTING_SNAPSHOT 区分 OPERATION_LOG 与 LISTING_SNAPSHOT
raw_payload_json 原始日志或快照摘要 审计证据;支持规则升级后的离线重放
event_key SHA-256 日志优先使用日志 ID;快照事件使用日期、父体及前后集合哈希计算
{
  "event_type": "PARENT_MEMBERS_CHANGED",
  "subject_type": "PARENT_ASIN",
  "subject_key": "B0DM7H5QZ8",
  "old_count": 6,
  "new_count": 5,
  "added_asins": [],
  "removed_asins": ["B0XXXXXXX"],
  "source_type": "LISTING_SNAPSHOT"
}

6. 四类规则的判定细节

6.1 新增差评

  • 匹配表现类 Review变动 自动日志,并解析新增 1–3 星 Review 数量。
  • 数量大于 0 产生 NEGATIVE_REVIEW_ADDED,告警级别为 P2;只增加总 Review、没有 1~3 星信息时仅记账,不发差评告警。
  • 领星该类日志同一天最多一条,告警内容应说明“领星本日汇总新增数量”,不把它解释成单条评价详情。

6.2 评分变化

  • 匹配 评分变动,解析旧评分和新评分。
  • 新评分低于旧评分:核心链接发送 P1,普通链接发送 P2;高于旧评分默认只记账,开启正向通知时发送 P3;相等则仅记审计日志。
  • Rating变动 是评分数量变化,不等于星级分数变化,二者必须使用不同事件类型。

6.3 父体成员集合与数量变化

💡 能力边界: 领星运营日志只记录单个 ASIN 的“父 ASIN 变动”,不直接提供父体下子体数量。因此本规则必须以 erp_listing 每日完整快照为主,运营日志仅用于解释成员为何加入或移出。

  • 首次完整快照只建立基线,不发送业务告警。
  • sid + marketplace_id + parent_asin 分组,对子 ASIN 去重后生成有序集合及 SHA-256 哈希。
  • 比较集合而不是只比较数量:即使数量保持 5 → 5,只要新增或移除成员非空,仍产生事件。
  • removed_asins 非空时,核心父体发送 P0、普通父体发送 P1;只有 added_asins 时发送 P3。
  • 告警展示旧数量、新数量、新增 ASIN、移除 ASIN和本次快照时间。
  • 分页不完整、总数不一致或关键字段缺失时只记录技术失败,不发送运营告警,也不更新基线。
added = current.children - previous.children
removed = previous.children - current.children

if not added and not removed:
    return Decision.silent()

if removed:
    alert_level = "P0" if target.business_level == "CORE" else "P1"
else:
    alert_level = "P3"

return Decision.alert(
    event_type="PARENT_MEMBERS_CHANGED",
    alert_level=alert_level,
    old_count=len(previous.children),
    new_count=len(current.children),
    added_asins=sorted(added),
    removed_asins=sorted(removed),
)

6.4 掉购物车

  • 匹配 Buybox变动,区分“价格变化”和“持有者变化”。
  • 只有最新持有者不命中配置的自营卖家名称/别名时,才产生 BUYBOX_LOST:核心链接发送 P0,普通链接发送 P1;价格变化不作为掉购物车告警。
  • 最新持有者重新命中自营卖家时产生 BUYBOX_REGAINED,发送 P3 恢复通知。
  • erp_listing 不返回 Buybox 持有者,因此不能用它代替 Buybox 判定。首次接入需由运营在领星页面确认当前持有者并配置自营卖家别名;未完成时状态标为 UNKNOWN,不做“已掉购物车”的推断。

7. 告警状态机

业务异常和采集异常必须分开。运营日志或 Listing 快照失败时,状态只能是“数据未知”;不得据此推断掉购物车、评分下降或父体成员减少。只有完整快照比较产生的成员差异,才能进入父体业务告警状态。 状态机

8. 技术选型与运行形态

本方案不建设常驻 Web 服务,也不引入 Java、Spring、Redis、消息队列或独立部署环境。一次每日巡检就是一次短任务:Codex 定时任务唤起 Skill,Skill 启动本地 Python 入口;Python 通过领星 Open API/官方 SDK 获取运营日志和 Listing 完整快照,并完成批量调度、分页、重试、解析、父体聚合、规则判定、SQLite 落库与飞书投递。模型不读取原始响应,只接收紧凑运行汇总。

组件 选型 边界
每日调度 Codex Scheduled Task 每天固定时间启动本地 Python 任务,只读取最终摘要,不承担采集和业务状态存储。
流程入口 Listing Monitor Skill + Python Runner Skill 定义配置、启动方式和汇总契约;Python Runner 执行完整数据链路。
数据来源 领星 Open API + 官方 Python SDK 只读取运营日志和 Listing 能力;原始响应仅在本地运行目录、审计日志或 SQLite 中处理,不进入模型上下文。
确定性逻辑 Python 3 标准库 + 领星 SDK 负责批量、分页、重试、标准化、哈希、规则和事务;核心判定不交给大模型自由推断。
状态存储 SQLite 单文件 启用 WAL;保存目标、当前状态、原始证据、Outbox 和运行记录。
通知 飞书签名 Webhook 为主 send_feishu.py --transport auto 优先固定群 Webhook;lark-cli 仅用于配置/诊断,已有 OpenAPI 客户端作为应用身份场景的后备。

Skill 与 Plugin 的选择: 当前只有一个内部工作流,直接开发 Skill 最轻。Plugin 不是调度器,也不会替代 SQLite;它只是把 Skill、可选 SDK/通知依赖和展示资源打包,适合后续需要跨团队安装、版本发布时再做。OpenAI 官方也建议先用 Skill 迭代单个工作流,稳定并需要分发时再封装 Plugin。

OpenAI:Scheduled tasks

OpenAI:Build skills

OpenAI:Build plugins

9. Skill 与目录设计

Skill 负责定义固定启动方式、配置边界和摘要输出;本地 Python 负责通过领星官方 SDK 采集,以及所有可重复验证的业务逻辑和飞书投递。原始响应在进程内直接标准化、落库,不经模型中转,从根源上避免大批量数据占用上下文。

lingxing-listing-alert-monitor/
├── SKILL.md                         # 启动方式、边界、失败语义、摘要契约
├── agents/openai.yaml              # 展示信息;不声明领星数据工具依赖
├── scripts/
│   ├── run_daily.py                # 每日短任务入口与紧凑汇总
│   ├── lingxing_client.py          # 官方 SDK 包装、鉴权与响应解包
│   ├── collector.py                # 批量、分页、限流、重试和完整性校验
│   ├── monitor.py                  # 目标配置与本地管理 CLI
│   ├── storage.py                  # SQLite Store 实现与短事务
│   ├── parsers.py                  # 领星运营日志解析器注册表
│   ├── snapshots.py                # Listing 父体聚合与集合哈希
│   ├── rules.py                    # 日志事件、父体集合与 P0~P3 分级规则
│   ├── render_feishu.py            # 告警卡片数据生成
│   └── send_feishu.py              # Webhook 主通道与 OpenAPI 后备
├── migrations/001_init_sqlite.sql
├── references/
│   ├── event-patterns.yaml         # 已验证的脱敏日志样本与匹配规则
│   └── severity-policy.yaml        # P0~P3 默认等级与正向通知开关
└── tests/fixtures/                 # 脱敏日志、Listing 快照与 SDK 响应样本

运行产物:~/Documents/AI-Ecommerce-Runs/listing-monitor/runs/<run_slug>/
数据库:~/Documents/AI-Ecommerce-Runs/listing-monitor/listing-monitor.sqlite

脚本契约:

入口/模块 输入/输出 用途
run_daily.py --date 输入计划日期;输出紧凑 JSON 汇总 执行采集、规则、SQLite 和通知全链路;摘要仅包含成功、失败、事件和告警计数。
monitor.py target ... 维护并列出启用目标 本地配置 ASIN、父 ASIN、sid、站点、业务等级和启停状态。
lingxing_client.py SDK 请求/标准化响应 隔离官方 SDK 方法、鉴权、错误码和响应包装,便于接口升级。
collector.py 目标批次/本地标准记录 按官方能力分片或逐个查询,处理分页、限流、重试与完整性;不把原始批量响应输出给模型。
send_feishu.py 读取 Outbox;回写发送结果 默认 auto 传输:签名 Webhook 优先,应用 OpenAPI 后备;lark-cli 只用于配置与人工诊断。

10. SQLite 库表设计

数据库只保存运行状态和审计证据,不保存领星或飞书凭据。时间统一保存 ISO-8601 UTC 文本;展示时再转 Asia/Shanghai。JSON 字段在 SQLite 中保存为 TEXT,迁移 MySQL 时改为 JSON 类型。

PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;

-- 监控目标:定义需要巡检的 ASIN 或父 ASIN
CREATE TABLE IF NOT EXISTS monitor_target (
    id                      INTEGER PRIMARY KEY AUTOINCREMENT, -- 本地主键
    target_type             TEXT NOT NULL CHECK (target_type IN ('ASIN', 'PARENT_ASIN')), -- 目标类型
    target_value            TEXT NOT NULL,                    -- ASIN 或父 ASIN
    business_level          TEXT NOT NULL DEFAULT 'NORMAL' CHECK (business_level IN ('CORE', 'NORMAL')), -- 核心/普通链接
    sid                     INTEGER NOT NULL,                 -- 领星店铺 ID
    marketplace_id          TEXT NOT NULL,                    -- Amazon Marketplace ID
    own_seller_aliases_json TEXT NOT NULL DEFAULT '[]',       -- 自营卖家名称及别名 JSON 数组
    notify_chat_id          TEXT NOT NULL,                    -- 飞书告警群 chat_id
    enabled                 INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)), -- 是否启用
    created_at              TEXT NOT NULL,                    -- 创建时间,ISO-8601 UTC
    updated_at              TEXT NOT NULL,                    -- 更新时间,ISO-8601 UTC
    UNIQUE (target_type, sid, marketplace_id, target_value)
);

-- ASIN 当前状态:保存日志规则的最新可比较状态
CREATE TABLE IF NOT EXISTS listing_state (
    target_id               INTEGER PRIMARY KEY,              -- monitor_target.id,仅用于 ASIN 目标
    rating_score            REAL,                             -- 最新星级分数
    buybox_holder           TEXT,                             -- 归一化后的最新 Buybox 持有者
    buybox_owned            INTEGER CHECK (buybox_owned IN (0, 1) OR buybox_owned IS NULL), -- 1 自营,0 非自营,NULL 未知
    last_source_event_time  TEXT,                             -- 已推进状态的最新源事件时间
    updated_at              TEXT NOT NULL,                    -- 状态更新时间,ISO-8601 UTC
    FOREIGN KEY (target_id) REFERENCES monitor_target(id) ON DELETE CASCADE
);

-- 每日任务运行记录:用于幂等、统计和故障审计
CREATE TABLE IF NOT EXISTS job_run (
    id                      INTEGER PRIMARY KEY AUTOINCREMENT, -- 运行批次主键
    run_key                 TEXT NOT NULL UNIQUE,             -- 逻辑运行唯一键
    scheduled_date          TEXT NOT NULL,                    -- 计划执行日期,Asia/Shanghai,YYYY-MM-DD
    window_start            TEXT NOT NULL,                    -- 查询窗口起点,ISO-8601 UTC
    window_end              TEXT NOT NULL,                    -- 查询窗口终点,ISO-8601 UTC,左闭右开
    status                  TEXT NOT NULL CHECK (status IN ('RUNNING', 'SUCCESS', 'PARTIAL', 'FAILED')), -- 运行状态
    attempt_count           INTEGER NOT NULL DEFAULT 1,       -- 当前逻辑任务尝试次数
    target_total            INTEGER NOT NULL DEFAULT 0,       -- 本次启用目标总数
    target_success          INTEGER NOT NULL DEFAULT 0,       -- 成功目标数
    target_failed           INTEGER NOT NULL DEFAULT 0,       -- 失败目标数
    new_event_count         INTEGER NOT NULL DEFAULT 0,       -- 新增事件数
    sent_alert_count        INTEGER NOT NULL DEFAULT 0,       -- 成功发送告警数
    error_summary_json      TEXT,                             -- 失败摘要 JSON
    started_at              TEXT NOT NULL,                    -- 首次开始时间,ISO-8601 UTC
    finished_at             TEXT                              -- 最终结束时间,ISO-8601 UTC
);

-- 父体成员快照:保存每个父 ASIN 当日完整子体集合
CREATE TABLE IF NOT EXISTS parent_snapshot (
    id                      INTEGER PRIMARY KEY AUTOINCREMENT, -- 快照主键
    run_id                  INTEGER NOT NULL,                 -- 所属运行批次
    target_id               INTEGER NOT NULL,                 -- monitor_target.id,仅用于 PARENT_ASIN 目标
    snapshot_date           TEXT NOT NULL,                    -- 快照业务日期,Asia/Shanghai,YYYY-MM-DD
    child_count             INTEGER NOT NULL CHECK (child_count >= 0), -- 去重后子 ASIN 数量
    child_asins_json        TEXT NOT NULL,                    -- 排序后的子 ASIN JSON 数组
    child_set_hash          TEXT NOT NULL,                    -- 子体集合规范化 JSON 的 SHA-256
    source_total_rows       INTEGER NOT NULL,                 -- 本次店铺/站点 Listing 原始总行数
    is_complete             INTEGER NOT NULL CHECK (is_complete IN (0, 1)), -- 分页是否完整
    observed_at             TEXT NOT NULL,                    -- 观测完成时间,ISO-8601 UTC
    FOREIGN KEY (run_id) REFERENCES job_run(id) ON DELETE CASCADE,
    FOREIGN KEY (target_id) REFERENCES monitor_target(id) ON DELETE CASCADE,
    UNIQUE (run_id, target_id)
);

CREATE INDEX IF NOT EXISTS idx_parent_snapshot_lookup
    ON parent_snapshot(target_id, snapshot_date DESC);

-- 统一事件账本:保存日志事件、快照差异事件及原始证据
CREATE TABLE IF NOT EXISTS operation_event (
    id                      INTEGER PRIMARY KEY AUTOINCREMENT, -- 事件主键
    event_key               TEXT NOT NULL UNIQUE,             -- 跨重跑稳定的幂等键
    target_id               INTEGER NOT NULL,                 -- 关联监控目标
    event_type              TEXT NOT NULL,                    -- 标准业务事件类型
    alert_level             TEXT CHECK (alert_level IN ('P0', 'P1', 'P2', 'P3') OR alert_level IS NULL), -- 告警级别;NULL 表示只记事件
    parse_status            TEXT NOT NULL CHECK (parse_status IN ('PARSED', 'UNPARSED', 'IGNORED')), -- 解析状态
    parser_version          TEXT NOT NULL,                    -- 解析器规则版本
    source_type             TEXT NOT NULL CHECK (source_type IN ('OPERATION_LOG', 'LISTING_SNAPSHOT', 'SYSTEM')), -- 事件来源
    source_bucket           TEXT NOT NULL,                    -- 来源数组或工具分桶
    source_event_time       TEXT NOT NULL,                    -- 业务事件发生时间
    source_log_id           TEXT,                             -- 领星原始日志 ID,无则为空
    old_value               TEXT,                             -- 可标量表达的旧值
    new_value               TEXT,                             -- 可标量表达的新值
    change_detail_json      TEXT,                             -- 集合差异等结构化变化明细
    raw_content             TEXT NOT NULL,                    -- 原始日志正文或快照差异摘要
    raw_payload_json        TEXT NOT NULL,                    -- 原始数据或最小证据 JSON
    observed_at             TEXT NOT NULL,                    -- 系统观测时间,ISO-8601 UTC
    FOREIGN KEY (target_id) REFERENCES monitor_target(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_event_target_time
    ON operation_event(target_id, source_event_time DESC);
CREATE INDEX IF NOT EXISTS idx_event_type_time
    ON operation_event(event_type, source_event_time DESC);

-- 通知 Outbox:保证事件入库与飞书发送解耦且可重试
CREATE TABLE IF NOT EXISTS alert_outbox (
    id                      INTEGER PRIMARY KEY AUTOINCREMENT, -- Outbox 主键
    event_id                INTEGER NOT NULL,                 -- 关联事件 ID
    channel                 TEXT NOT NULL DEFAULT 'FEISHU',   -- 通知渠道
    status                  TEXT NOT NULL CHECK (status IN ('PENDING', 'SENDING', 'SENT', 'FAILED', 'DEAD')), -- 投递状态
    payload_json            TEXT NOT NULL,                    -- 待发送飞书卡片 JSON
    attempt_count           INTEGER NOT NULL DEFAULT 0,       -- 已尝试发送次数
    next_attempt_at         TEXT,                             -- 下次允许重试时间
    message_id              TEXT,                             -- 飞书发送成功后的消息 ID
    last_error              TEXT,                             -- 最近一次失败摘要
    created_at              TEXT NOT NULL,                    -- 创建时间,ISO-8601 UTC
    updated_at              TEXT NOT NULL,                    -- 状态更新时间,兼作 SENDING 租约判断
    sent_at                 TEXT,                             -- 最终发送成功时间
    FOREIGN KEY (event_id) REFERENCES operation_event(id) ON DELETE CASCADE,
    UNIQUE (event_id, channel)
);

CREATE INDEX IF NOT EXISTS idx_outbox_due
    ON alert_outbox(status, next_attempt_at);

10.1 表职责与关键约束

关键约束 设计目的
monitor_target 类型、店铺、站点、目标值唯一 ASIN 目标用于日志类规则;PARENT_ASIN 目标用于父体成员快照
listing_state target_id 主键 保存单 ASIN 最新评分和购物车状态
job_run run_key 唯一 保证每天只有一个逻辑任务,手工重跑复用同一 run
parent_snapshot UNIQUE(run_id, target_id) 保存完整成员集合、数量和哈希;不完整快照不得参与业务比较
operation_event event_key 唯一 统一保存运营日志事件和 Listing 快照差异事件
alert_outbox UNIQUE(event_id, channel) 保证事件落库与通知任务的一致性和可重试性

10.2 字段数据字典

SQLite 不支持 MySQL 的列级 COMMENT 语法,因此字段释义同时维护在本节和 SQL 行注释中。表内“迁移类型”是未来迁移到 MySQL 8 时的建议类型,不代表当前需要执行迁移。

10.2.1 monitor_target

字段 SQLite / 迁移类型 约束与默认值 字段释义
id INTEGER / BIGINT PK,自增 监控目标本地主键;仅用于系统内部关联。
target_type TEXT / VARCHAR(20) 必填;ASIN、PARENT_ASIN 目标粒度。ASIN 用于差评、评分和 Buybox;PARENT_ASIN 用于父体成员快照。
target_value TEXT / VARCHAR(20) 必填;统一大写并去空格 实际 ASIN 或父 ASIN。
business_level TEXT / VARCHAR(16) 必填;默认 NORMAL;CORE/NORMAL 链接业务等级。只有运营明确标记为 CORE 的目标,才允许因掉购物车或父体成员移除升级为 P0。
sid INTEGER / BIGINT 必填 领星店铺 ID。适配层将 erp_listing.store_id 统一映射为该字段。
marketplace_id TEXT / VARCHAR(32) 必填 Amazon Marketplace ID,例如美国站 ATVPDKIKX0DER;用于站点隔离。
own_seller_aliases_json TEXT / JSON 必填;默认 [] 自营卖家名称及别名数组,仅 Buybox 判定使用;比较前统一大小写与空白。
notify_chat_id TEXT / VARCHAR(128) 必填 该目标对应的飞书告警群 chat_id。
enabled INTEGER / TINYINT 必填;默认 1;0/1 目标启停标记。停用后不再采集,但历史数据保留。
created_at TEXT / DATETIME(3) 必填 创建时间,SQLite 保存 ISO-8601 UTC。
updated_at TEXT / DATETIME(3) 必填 最近配置更新时间,SQLite 保存 ISO-8601 UTC。

10.2.2 listing_state

字段 SQLite / 迁移类型 约束与默认值 字段释义
target_id INTEGER / BIGINT PK、FK;必填 关联 monitor_target.id;本表只保存 ASIN 类型目标。
rating_score REAL / DECIMAL(3,2) 可空 已确认的最新星级分数;NULL 表示尚未建立可信状态。
buybox_holder TEXT / VARCHAR(255) 可空 最新 Buybox 持有者的归一化名称;保留原文于事件账本。
buybox_owned INTEGER / TINYINT 可空;1/0/NULL 1 表示自营持有,0 表示非自营,NULL 表示未知;接口失败不得写成 0。
last_source_event_time TEXT / DATETIME(3) 可空 最后一次推进当前状态的源事件时间,用于阻止迟到旧日志回退状态。
updated_at TEXT / DATETIME(3) 必填 状态最近写入时间,ISO-8601 UTC。

10.2.3 job_run

字段 SQLite / 迁移类型 约束与默认值 字段释义
id INTEGER / BIGINT PK,自增 每日巡检运行批次主键。
run_key TEXT / VARCHAR(64) 必填;唯一 逻辑运行幂等键,例如 listing-monitor:2026-09-02;重跑复用同一批次。
scheduled_date TEXT / DATE 必填;YYYY-MM-DD 计划任务所属业务日期,时区固定为 Asia/Shanghai。
window_start TEXT / DATETIME(3) 必填 运营日志查询窗口起点,内部统一为 UTC。
window_end TEXT / DATETIME(3) 必填 查询窗口终点,内部统一按左闭右开区间处理。
status TEXT / VARCHAR(16) 必填;状态枚举 RUNNING 执行中;SUCCESS 全成功;PARTIAL 部分目标失败;FAILED 整体失败。
attempt_count INTEGER / INT 必填;默认 1 同一 run_key 的实际尝试次数。
target_total INTEGER / INT 必填;默认 0 本次需要处理的启用监控目标数。
target_success INTEGER / INT 必填;默认 0 已完成采集和判定的目标数。
target_failed INTEGER / INT 必填;默认 0 因调用、分页完整性或解析错误而失败的目标数。
new_event_count INTEGER / INT 必填;默认 0 本批次实际新增且通过幂等校验的事件数。
sent_alert_count INTEGER / INT 必填;默认 0 本批次已成功发送到飞书的告警数。
error_summary_json TEXT / JSON 可空 失败目标、错误类型、错误摘要和重试信息;不得保存密钥或访问令牌。
started_at TEXT / DATETIME(3) 必填 该逻辑批次首次开始时间,ISO-8601 UTC。
finished_at TEXT / DATETIME(3) 可空 批次最终结束时间;RUNNING 状态为空。

10.2.4 parent_snapshot

字段 SQLite / 迁移类型 约束与默认值 字段释义
id INTEGER / BIGINT PK,自增 父体成员快照主键。
run_id INTEGER / BIGINT FK;必填 关联产生该快照的 job_run.id
target_id INTEGER / BIGINT FK;必填 关联 PARENT_ASIN 类型的 monitor_target.id
snapshot_date TEXT / DATE 必填;YYYY-MM-DD 快照业务日期,时区为 Asia/Shanghai。
child_count INTEGER / INT 必填;大于等于 0 按 ASIN 去重后的子体数量,必须等于 child_asins_json 数组长度。
child_asins_json TEXT / JSON 必填 规范化、去重并按字典序排序后的子 ASIN 数组。
child_set_hash TEXT / CHAR(64) 必填 对子体数组规范化 JSON 计算的 SHA-256,用于快速判断集合是否变化和生成幂等键。
source_total_rows INTEGER / INT 必填 该店铺和站点本次 erp_listing 完整分页返回的原始 Listing 行数,不是该父体的子体数。
is_complete INTEGER / TINYINT 必填;0/1 分页是否全部成功且累计行数与接口 total 一致;只有 1 才能参与业务比较。
observed_at TEXT / DATETIME(3) 必填 完整拉取和聚合完成的系统时间,ISO-8601 UTC。

10.2.5 operation_event

字段 SQLite / 迁移类型 约束与默认值 字段释义
id INTEGER / BIGINT PK,自增 统一事件主键。
event_key TEXT / CHAR(64) 必填;唯一 跨重复拉取和任务重跑稳定的 SHA-256 幂等键。
target_id INTEGER / BIGINT FK;必填 事件所属监控目标。
event_type TEXT / VARCHAR(64) 必填 标准事件类型,例如 NEGATIVE_REVIEW_ADDED、RATING_CHANGED、PARENT_MEMBERS_CHANGED、BUYBOX_LOST。
alert_level TEXT / VARCHAR(8) 可空;P0/P1/P2/P3 业务告警级别;NULL 表示事件仅用于审计,不生成飞书业务告警。
parse_status TEXT / VARCHAR(16) 必填;状态枚举 PARSED 已解析;UNPARSED 格式未知;IGNORED 不在业务白名单。
parser_version TEXT / VARCHAR(32) 必填 产生标准事件的解析规则版本,支持格式变更后的重放与审计。
source_type TEXT / VARCHAR(24) 必填;来源枚举 OPERATION_LOG 运营日志;LISTING_SNAPSHOT 快照差异;SYSTEM 技术事件。
source_bucket TEXT / VARCHAR(64) 必填 原始来源分桶,例如 operation_log_data、auto_log_data 或 erp_listing。
source_event_time TEXT / DATETIME(3) 必填 业务事件发生时间;快照事件使用本次完整快照观测时间。
source_log_id TEXT / VARCHAR(128) 可空 领星原始日志 ID;快照差异事件无原始日志 ID。
old_value TEXT / VARCHAR(255) 可空 适用于评分、持有者等标量变化的旧值。
new_value TEXT / VARCHAR(255) 可空 适用于评分、持有者等标量变化的新值。
change_detail_json TEXT / JSON 可空 结构化变化明细;父体事件保存 old_count、new_count、added_asins、removed_asins。
raw_content TEXT / TEXT 必填 运营日志原文;快照事件保存可读的集合差异摘要。
raw_payload_json TEXT / JSON 必填 支撑事件判定的原始响应片段或最小快照证据;应脱敏且不得保存凭据。
observed_at TEXT / DATETIME(3) 必填 系统采集到该事件的时间,ISO-8601 UTC。

10.2.6 alert_outbox

字段 SQLite / 迁移类型 约束与默认值 字段释义
id INTEGER / BIGINT PK,自增 待发送通知主键。
event_id INTEGER / BIGINT FK;必填 关联触发通知的 operation_event.id
channel TEXT / VARCHAR(16) 必填;默认 FEISHU 通知渠道;当前只实现飞书,字段为后续渠道隔离保留。
status TEXT / VARCHAR(16) 必填;状态枚举 PENDING 待发;SENDING 发送中;SENT 成功;FAILED 可重试失败;DEAD 停止重试。
payload_json TEXT / JSON 必填 已经渲染完成、可直接发送的飞书消息或卡片 JSON。
attempt_count INTEGER / INT 必填;默认 0 实际发送尝试次数,用于退避和 DEAD 判定。
next_attempt_at TEXT / DATETIME(3) 可空 FAILED 状态下的下次允许重试时间;PENDING 可为空并立即发送。
message_id TEXT / VARCHAR(128) 可空 飞书返回的消息 ID;只有 SENT 状态必须有值。
last_error TEXT / TEXT 可空 最近一次发送失败摘要;不得保存令牌或完整敏感响应。
created_at TEXT / DATETIME(3) 必填 Outbox 创建时间,ISO-8601 UTC。
updated_at TEXT / DATETIME(3) 必填 状态更新时间;也用于识别超时未完成的 SENDING 记录并恢复为可重试状态。
sent_at TEXT / DATETIME(3) 可空 最终发送成功时间;非 SENT 状态为空。

一致性约束: child_count 必须等于子体数组长度;is_complete = 0 的快照不得参与业务比较;buybox_owned = NULL 表示未知而不是掉购物车;alert_outbox.status = SENT 时必须同时写入 message_idsent_at

11. 每日执行主流程

每日任务查询最近 3 个自然日的运营日志,同时完整分页拉取当日 Listing。日志依靠 event_key 去重;父体依靠完整快照、成员集合哈希和差集判断。外部 Open API/SDK 请求和飞书发送不得放在数据库事务内,事务只包围本地事件、状态、快照与 Outbox 写入。

def run_daily(scheduled_date):
    run = store.start_or_resume_run(
        run_key=f"listing-monitor:{scheduled_date}",
        window=last_three_days(scheduled_date),
    )
    collector = LingxingCollector.from_environment()

    # 外部请求全部在事务外完成,原始响应不进入模型上下文
    try:
        log_rows = collector.fetch_operation_logs(
            target_asins=store.list_asins(), window=run.window
        )
        ingest_operation_logs(run, log_rows)
    except ProviderError as error:
        store.record_source_failure(run.id, "operation_logs", error)

    scopes = group_parent_targets_by_scope(store.list_parent_targets())
    for scope, targets in scopes.items():
        try:
            page_result = collector.fetch_listing_pages(scope)
            require_complete_pages(page_result)
            parent_sets = build_parent_sets(page_result.rows)
        except ProviderError as error:
            store.record_scope_failure(run.id, scope, error)
            continue

        persist_parent_decisions(run, targets, parent_sets)

    dispatch_pending_outbox()
    store.finish_run(run.id)
    return build_compact_run_summary(run.id)

11.1 顺序与乱序处理

运营日志使用 3 天重叠窗口,旧事件可能晚于新事件返回;只有不早于 listing_state.last_source_event_time 的事件才能推进当前状态。父体快照按 snapshot_date 比较最近两份完整快照,不完整快照不更新基线,也不参与业务差异判断。

11.2 event_key 生成

def build_log_event_key(item, target):
    if item.get("log_id"):
        canonical = f"lingxing:{item['log_id']}"
    else:
        canonical = "|".join([
            str(target.sid), target.target_value.upper(),
            normalize_space(item.get("type_name")),
            normalize_space(item.get("content")),
            normalize_time(item.get("gmt_modified")),
        ])
    return sha256(canonical.encode()).hexdigest()

def build_snapshot_event_key(target, previous, current):
    canonical = "|".join([
        "lingxing-snapshot", current.snapshot_date,
        str(target.sid), target.marketplace_id,
        target.target_value.upper(),
        previous.child_set_hash, current.child_set_hash,
    ])
    return sha256(canonical.encode()).hexdigest()

日志事件优先使用领星 log_id;无 log_id 时使用稳定字段哈希。父体快照差异使用“目标 + 快照日期 + 旧集合哈希 + 新集合哈希”生成 event_key。技术幂等键不能简化为“同一天同类型”,否则会吞掉同日多次 Buybox 变化或不同成员替换。

12. 关键逻辑判断

12.1 自动日志筛选

  1. 按日期遍历结果,兼容 r_daterdate
  2. operation_log_dataauto_log_data 的 null 转为空数组后合并。
  3. log_data 默认不进入业务判定;只有明确存在系统标识且事件类型在白名单时才兼容纳入。
  4. 业务规则白名单仅包含 Review变动、评分变动、Buybox变动;父体成员变化由 Listing 快照路径产生。
  5. 未知格式落 UNPARSED 并记录技术失败,不发送运营告警;禁止让大模型根据自然语言猜测业务状态。

12.2 业务状态转换表

事件 判断条件 状态更新 业务告警
新增差评 Review 变动中明确出现新增 1~3 星 Review 事件记账 P2;显示新增数量
评分下降 评分变动且 new_score 小于 old_score 更新 rating_score 核心链接 P1;普通链接 P2
评分上升 new_score 大于 old_score 更新 rating_score 默认不发送;开启正向通知时为 P3
Rating 数变化 标签为 Rating变动 只记事件 不发送
父体成员移除/替换 完整快照比较后 removed_asins 非空 保存 parent_snapshot 核心父体 P0;普通父体 P1
父体仅新增成员 removed 为空且 added_asins 非空 保存 parent_snapshot P3;显示新增成员
掉购物车 持有者变动且 normalized_holder 不在自营别名中 buybox_owned = 0 核心链接 P0;普通链接 P1
购物车恢复 持有者重新命中自营别名 buybox_owned = 1 P3 恢复通知
Buybox 价格变化 只有价格变化,没有持有者变化 不改变 buybox_owned 不发送

12.3 告警等级与响应时限

飞书业务告警统一使用 P0、P1、P2、P3 四级,不再使用 INFO、TECH 或 NONE 作为告警级别。未达到业务告警条件的事件仍可写入 operation_event,但 alert_level 为空且不生成 Outbox。

级别 业务定义 默认适用场景 响应要求
P0 核心业务中断,发现后必须立即行动 仅限运营明确标记为 CORE 的链接:掉购物车,或核心父体出现子体移除/替换 15 分钟内确认,30 分钟内开始处置
P1 高优先级业务异常,需要快速处理 普通链接掉购物车、普通父体成员移除/替换、核心链接评分下降 1 小时内确认,当日完成处置方案
P2 明确负向变化,需要当日跟进 新增 1~3 星差评、普通链接评分下降 4 小时内查看,当日完成判断
P3 低优先级变化或恢复信息 父体仅新增成员、购物车恢复;评分上升仅在配置开启时发送 下一个工作日内查看,无需立即行动
def resolve_alert_level(event, target, policy):
    core = target.business_level == "CORE"

    if event.type in {"BUYBOX_LOST", "PARENT_MEMBER_REMOVED"}:
        return "P0" if core else "P1"
    if event.type == "RATING_DECREASED":
        return "P1" if core else "P2"
    if event.type == "NEGATIVE_REVIEW_ADDED":
        return "P2"
    if event.type in {"PARENT_MEMBER_ADDED", "BUYBOX_REGAINED"}:
        return "P3"
    if event.type == "RATING_INCREASED" and policy.notify_positive:
        return "P3"
    return None

P0 启用约束: 目标必须由运营显式配置 business_level = CORE。未标记的目标一律按 NORMAL 处理,程序不得根据销量、名称或模型推断核心程度。

💡 发现时效边界: 当前方案每日运行一次,因此 P0 约束的是“系统发现后的处置时限”,不等于异常发生后 15 分钟内发现。若业务要求真正的 P0 实时发现,CORE 目标的巡检频率必须提升到 5~15 分钟。

12.4 父体成员集合比较

def compare_parent_members(target, previous, current):
    if not current.is_complete:
        store.record_technical_failure("Listing 快照不完整")
        return Decision.silent()
    if previous is None:
        return Decision.baseline_only()

    added = current.children - previous.children
    removed = previous.children - current.children
    if not added and not removed:
        return Decision.silent()

    if removed:
        level = "P0" if target.business_level == "CORE" else "P1"
    else:
        level = "P3"

    return Decision.parent_members_changed(
        alert_level=level,
        old_count=len(previous.children),
        new_count=len(current.children),
        added_asins=sorted(added),
        removed_asins=sorted(removed),
    )

比较对象必须是集合,不是单独的数量字段。数量用于告警展示;集合差集才是业务判定依据。运营日志中的“父 ASIN 变动”可以附加到告警作为证据,但不得单独替代完整快照比较。

12.5 Buybox 持有者归一化

def normalize_seller(value):
    return collapse_spaces(value).strip().casefold()

def is_owned(holder, aliases):
    if holder is None or not holder.strip():
        return None
    normalized = normalize_seller(holder)
    return normalized in {normalize_seller(x) for x in aliases}

owned = is_owned(parsed.new_holder, target.own_seller_aliases)
if owned is None:
    store.record_technical_failure("Buybox 持有者为空")
    return Decision.silent()
if owned is False and previous.buybox_owned != 0:
    level = "P0" if target.business_level == "CORE" else "P1"
    return Decision.alert(level, "BUYBOX_LOST")
if owned is True and previous.buybox_owned == 0:
    return Decision.alert("P3", "BUYBOX_REGAINED")
return Decision.silent()

别名必须做精确归一化匹配,不使用模糊包含。例如卖家名 “ABC” 不能匹配 “ABC Outlet”。首次接入时 buybox_owned 为 null;如果第一条有效持有者事件显示为非自营卖家,仍按异常告警,但卡片需标记“首次观测”。

12.6 解析器版本

每类 content 使用独立解析器,并在 raw_payload_json 中保留原始数据。operation_event.parser_version 初始值为 v1;规则升级时通过离线命令重放 UNPARSED 事件,不直接修改已经发送过的业务结论。

13. 飞书告警设计与通知可靠性

13.1 通知幂等与投递一致性

风险 技术约束 结果
同日任务被重复触发 job_run.run_key 唯一 复用已有 run,不重复创建逻辑任务。
3 天窗口重复返回日志 operation_event.event_key 唯一 事件只入库一次。
事件已落库但通知任务丢失 事件、状态、Outbox 同事务提交 本地数据一致。
同一事件生成多条卡片 Outbox 的 event_id + channel 唯一 每个渠道最多一个发送任务。
进程发送成功后崩溃 通知采用 at-least-once;卡片携带 event_key 不能承诺绝对零重复,但可识别和审计。
两个本地任务并发写 SQLite 单工作进程 + BEGIN IMMEDIATE + busy_timeout 避免竞态,不需要 Redis 锁。

Outbox 状态转换: PENDING → SENDING → SENT;发送失败转 FAILED 并设置 next_attempt_at,达到 3 次后转 DEAD。每次每日任务开始时先处理上次遗留的 PENDING/FAILED,再拉取新数据。领星 Open API/SDK 调用失败只把本轮标记为 PARTIAL,不生成任何“掉购物车/评分下降”等业务事件。

13.2 告警卡片规范

飞书投递保留现有轻量方案:send_feishu.py --transport auto 对固定告警群优先使用签名 Webhook;lark-cli 只用于首次配置和人工诊断;需要应用身份能力时再走已有 OpenAPI 客户端。当前卡片仅包含 Amazon OpenLink,不需要回调,因此不强制切换官方 SDK。只有出现多群动态路由、回执查询、交互回调或统一应用治理需求时,才升级为飞书官方 SDK 主通道。飞书只主动发送业务告警,技术失败仅记录在 job_run.error_summary_json 和任务汇总中。

13.2.1 卡片通用数据协议

卡片区域 数据来源 渲染规则
标题 alert_level + event_type P0 红色、P1 橙色、P2 黄色、P3 蓝色或绿色;标题固定为“级别|监控主体 + 事件名称”。
监控主体 monitor_target 展示 ASIN 或父 ASIN,同时展示店铺名称、sid 和站点;CORE 目标增加“核心链接”标识。
变化摘要 old_value/new_value/change_detail_json 评分和持有者展示前后值;父体展示数量、新增成员和移除成员;差评展示新增低星数量。
时间 source_event_time/observed_at 优先展示业务发生时间,同时保留系统发现时间;统一转换为 Asia/Shanghai。
证据 source_type/raw_content 只展示数据类型和简短变化摘要,不展示访问令牌、完整原始响应或敏感配置。
操作按钮 Amazon 链接模板 单 ASIN 使用“查看亚马逊链接”,父体使用“查看亚马逊父体”;不提供领星跳转和不存在的运行详情入口。
追踪信息 event_key 卡片底部展示缩短后的 event_key;完整值保存在 Outbox payload 和事件账本中。
{
  "event_key": "sha256...",
  "event_type": "PARENT_MEMBERS_CHANGED",
  "alert_level": "P1",
  "subject": {"type": "PARENT_ASIN", "value": "B0DM7H5QZ8"},
  "context": {
    "business_level": "NORMAL",
    "sid": 13345,
    "marketplace_id": "ATVPDKIKX0DER"
  },
  "occurred_at": "2026-09-02T01:10:00Z",
  "observed_at": "2026-09-02T01:12:08Z",
  "change": {
    "old_count": 6,
    "new_count": 5,
    "added_asins": [],
    "removed_asins": ["B0D5CLSMFB"]
  },
  "links": {"amazon": "configured-url"}
}

13.2.2 P0~P2 业务告警

🔴 P0|B096S5YCC6 购物车丢失

业务标识: 核心链接

店铺 / 站点: 示例店铺 / 美国站

持有者: OWN STORE → OTHER SELLER

发生 / 发现: 08:55 / 09:10

处置要求: 立即检查价格、库存和跟卖情况

按钮: 查看亚马逊链接

event_key: 90c2…4aa7

💡 P1|B0DM7H5QZ8 父体成员移除

业务标识: 普通链接

店铺 / 站点: 示例店铺 / 美国站

子体数量: 6 → 5

移除: B0D5CLSMFB

新增:

快照时间: 09:12;分页完整

按钮: 查看亚马逊父体

event_key: 5ad0…1e3b

💡 P2|B096S5YCC6 新增差评

店铺 / 站点: 示例店铺 / 美国站

变化: 新增 1~3 星 Review 2 条

发生 / 发现: 08:41 / 09:10

数据类型: Review 变动

按钮: 查看亚马逊链接

event_key: 8f31…c912

💡 P2|B096S5YCC6 评分下降

业务标识: 普通链接

店铺 / 站点: 示例店铺 / 美国站

星级变化: 4.5 → 4.4(-0.1)

发生 / 发现: 08:46 / 09:10

按钮: 查看亚马逊链接

event_key: b1a8…72ef

13.2.3 P3 低优先级与恢复通知

🔵 P3|B0DM7H5QZ8 父体新增成员

店铺 / 站点: 示例店铺 / 美国站

子体数量: 5 → 6

新增: B0D5CM8VZ9

移除:

按钮: 查看亚马逊父体

event_key: 3bb1…10d9

P3|B096S5YCC6 购物车已恢复

店铺 / 站点: 示例店铺 / 美国站

持有者: OTHER SELLER → OWN STORE

恢复时间: 2026-09-02 10:15

关联异常: BUYBOX_LOST / 90c2…4aa7

按钮: 查看亚马逊链接

13.2.4 生成与发送规则

规则 实现要求
只发业务告警 只有 alert_level 为 P0~P3 的事件才生成 Outbox;采集、分页和解析失败只记录 job_run
一事件一卡片 operation_event.event_keyalert_outbox(event_id, channel) 共同保证同一渠道只生成一条逻辑卡片。
等级由代码决定 render_feishu.py 只负责展示,rules.py 根据 event_type 和 business_level 产生等级;模型不得临场调整。
长成员列表折叠 新增或移除成员最多展示前 10 个,其余显示“另有 N 个”,完整数组保留在事件账本。
关键字段缺失 不生成业务卡片,只记录技术失败并跳过基线更新;不得在卡片中用“未知”补齐业务结论。
按钮边界 MVP 只提供亚马逊 OpenLink,不提供领星入口,也不提供确认、关闭、指派或运行详情按钮。
发送结果 发送成功写入 message_id、sent_at 并置为 SENT;失败按 Outbox 规则退避重试,达到上限后转 DEAD。

14. 每日调度与配置

建议每日 09:10(Asia/Shanghai)运行一次,查询当天及前两天。该频率满足“每日看链接”需求,但不承诺 Buybox 实时发现:当天发生的多次 Buybox 变化会在下一次日任务中统一读取并逐条去重处理。

计划任务提示词应保持短且确定:

执行 Listing Monitor Skill 的每日巡检:
1. 使用计划日期启动本地 run_daily.py;
2. 由 Python 通过领星官方 SDK 读取近 3 天运营日志与当日完整 Listing 快照;
3. 由 Collector 完成批量/分片、分页、限流、重试和完整性校验;
4. 由本地规则完成去重、状态比较、SQLite 与 Outbox 写入;
5. 由 send_feishu.py 投递待发送业务告警并回写结果;
6. 只向任务返回成功、失败、新事件和告警数量等紧凑摘要;
7. 禁止把原始响应放入模型上下文,禁止把无数据或不完整分页推断为业务正常。

本地 Scheduled Task 需要电脑保持开机且 ChatGPT/Codex 桌面应用运行。监控目标通过轻量 CLI 维护:target addtarget listtarget enabletarget disable。领星 Open API/SDK 凭据和飞书凭据均由本机环境变量或安全配置管理,不写入 Skill、SQLite、日志、模型上下文或告警正文。

15. SQLite 迁移 MySQL 设计

业务层只依赖 Store 接口,不直接散落 sqlite3 SQL。当前实现为 SqliteStore;未来新增 MySqlStore,解析器、规则、Skill 和飞书通知流程无需修改。

SQLite MySQL 8 迁移说明
INTEGER AUTOINCREMENT BIGINT AUTO_INCREMENT 保持 ID 和外键关系。
TEXT 时间 DATETIME(3) 导入前将 ISO-8601 UTC 转换为 UTC datetime。
TEXT JSON JSON 迁移前逐行校验 JSON 有效性。
REAL rating_score DECIMAL(3,2) 避免浮点比较,应用层统一 Decimal。
INSERT … ON CONFLICT INSERT … ON DUPLICATE KEY 由 Store 实现封装,不暴露给规则层。
BEGIN IMMEDIATE 普通事务 + 唯一索引 并发控制仍以唯一约束为最终防线。

迁移步骤: 冻结一次调度 → SQLite 导出 CSV/JSONL → 在 MySQL 建表和唯一索引 → 按 monitor_target、listing_state、job_run、parent_snapshot、operation_event、alert_outbox 顺序导入 → 校验行数、集合哈希及唯一键 → 切换 Store 配置 → 影子执行一次不发消息的巡检 → 恢复计划任务。

16. 测试与评审门槛

测试层 输入 必须证明
日志 Parser 测试 Review、评分、Rating、父 ASIN、Buybox 脱敏样本 前后值、数量和持有者解析正确;未知格式进入 UNPARSED
快照聚合测试 重复 MSKU、自身父体、已删除记录、非在售记录 按 ASIN 去重,排除无效关系,同时不因停售造成误减
分页完整性测试 中间页失败、total 不一致、关键字段缺失 快照标记不完整,只记录技术失败,不发运营告警、不更新基线
告警分级测试 CORE/NORMAL × 四类业务事件 只产生 P0~P3;核心掉购物车/父体移除为 P0,普通事件按策略降级
集合差异测试 新增、移除、数量 5→5 但成员替换、首次快照 新增/移除集合正确;成员替换不漏报;首次只建基线
幂等测试 同一日志与同一快照连续 ingest 3 次 业务事件和 Outbox 均只新增一次
乱序与失败测试 迟到日志、Open API/SDK 超时、非法 JSON、飞书失败 状态不回退、不误报,Outbox 可重试
端到端影子测试 5~10 个 ASIN 与父体的近 30 天日志及连续快照 只生成卡片预览;人工核对无误后再启用真实通知

📌 MVP 交付边界: 一个 Skill、一组小型 Python 模块、1 个 SQLite 文件、1 个每日计划任务。不建设服务端,不维护常驻进程,不引入应用框架;Python 负责领星采集、确定性规则、落库和飞书投递,模型只负责启动任务并读取紧凑汇总。