|
|
@@ -0,0 +1,613 @@
|
|
|
+# Listing AI 优化工作台实施计划
|
|
|
+
|
|
|
+> 状态:已实施并完成首轮全量验收(2026-08-21)
|
|
|
+> 适用仓库:`Saas-voc` 前端、`Saas-voc-server` 后端
|
|
|
+> 本文是实施契约;实际落地、全量数据结果和保留门禁见 `LISTING-AI-IMPLEMENTATION-REPORT-2026-08-21.md`。任何字段、路由、表结构或验收口径变更,都应先修改本文并记录原因。
|
|
|
+
|
|
|
+## 0. 2026-08-21 AI 评分目标纠偏(本节优先于旧 MVP 描述)
|
|
|
+
|
|
|
+产品目标确认是**真实 Listing AI 评分**,不是“规则评分后由 AI 生成优化文案”。因此新增 `listing-jd-ai-v1`,并保留 `listing-jd-v2` 作为规则基线:
|
|
|
+
|
|
|
+- AI 不自由输出 0–100 总分,只对固定原子标准返回 `unknown/fail/weak/pass/strong`、原文证据、理由和置信度;
|
|
|
+- 服务端按冻结权重将等级映射为分数、校验范围并求和,模型无权修改维度、权重和总分;
|
|
|
+- 标题、核心卖点、详情和规格使用“确定性结构分 + AI 语义分”;当前文本模型不具备已验证的视觉能力,主图维度暂沿用结构规则分并明确标注,不能宣称完成视觉评分;
|
|
|
+- 正常读取使用 `sourceHash + rubricVersion + model + promptVersion` 稳定键;同键命中既有结果时不再次调用模型;模型、Prompt 或权重变化必须发布新版本,不覆盖旧结果;
|
|
|
+- AI 评分与 AI 优化是两条独立链路。本次 AI 评分不生成标题、卖点或详情候选,不创建优化版本;
|
|
|
+- 本次修正和真实验收最多选择 **10 个商品**。未选择的其余商品继续展示 `listing-jd-v2` 规则分,并在 UI 标注“规则评分,尚未执行 AI 评分”;
|
|
|
+- 当前发布门禁将单个 AI 评分任务限制为 10 个商品,禁止用筛选范围对 824 个商品直接发起模型调用。
|
|
|
+
|
|
|
+### 0.1 `listing-jd-ai-v1` 固定权重
|
|
|
+
|
|
|
+| 维度 | 确定性结构分 | AI 语义分 | 合计 |
|
|
|
+|---|---:|---:|---:|
|
|
|
+| 标题 | 8 | 搜索意图 4、信息层级 3、差异化 3、事实与合规 2 | 20 |
|
|
|
+| 核心卖点 | 4 | 客户收益 4、具体可信 4、差异化 3、购买顾虑 3、一致性 2 | 20 |
|
|
|
+| 主图 | 20(仅数量、主图标记、URL、重复和顺序) | 0;视觉语义等待多模态模型 | 20 |
|
|
|
+| 详情 | 6 | 完整度 4、结构可读性 3、卖点证据 3、顾虑处理 2、一致性 2 | 20 |
|
|
|
+| 规格 | 10 | 决策有效性 3、命名清晰 2、类目完整性 3、参数一致性 2 | 20 |
|
|
|
+
|
|
|
+等级映射固定为:`fail=0%`、`weak=35%`、`pass=70%`、`strong=100%`;`unknown` 不伪装为通过,维度标记 partial。最终分数保留 0.5 分精度。相同输入绕过缓存重复评分时,整体漂移超过 2 分即不允许扩大样本。
|
|
|
+
|
|
|
+## 1. 目标、范围与明确不做的事
|
|
|
+
|
|
|
+### 1.1 MVP 目标
|
|
|
+
|
|
|
+针对京东授权店铺当前 API 返回的全部商品,完成一个可恢复、可审计、可解释的 Listing 评分批任务,并提供:
|
|
|
+
|
|
|
+- 商品评分总览、筛选、分页和批量发起任务;
|
|
|
+- 单商品标题、核心卖点、主图、详情、规格五个维度的原文、证据、得分和优化建议;
|
|
|
+- AI 建议版本的生成、对比、采纳和审计;
|
|
|
+- 批任务进度、成功/部分/阻塞/失败数量、单项失败原因和重试;
|
|
|
+- 不在浏览器持有 JD/Parse 密钥,不把缺失数据伪装成 0 分。
|
|
|
+
|
|
|
+页面上显示的商品总数必须来自同步摘要/API,**不得写死 614、821 或本次实测的 824**。
|
|
|
+
|
|
|
+### 1.2 MVP 不做
|
|
|
+
|
|
|
+- 不调用京东写接口,不自动发布标题、详情或图片;
|
|
|
+- 不把 AI 建议直接覆盖源 Listing,只生成版本草稿;
|
|
|
+- 不把卖家 Listing 文本当作 VOC 用户证据;
|
|
|
+- 不声称做了图片审美/视觉语义评分。当前 AI 网关只有文本消息能力,MVP 主图评分仅使用数量、顺序、主图标记、URL 可访问性等确定性规则;
|
|
|
+- 不接天猫、抖音、拼多多。样例图的平台 tab 仅京东可用,其余显示“即将支持”且不可点击发起任务;
|
|
|
+- 不重写现有 VOC、行动、验证链路;
|
|
|
+- 不让 Angular 直连目标 Parse 或 JD SP-API。
|
|
|
+
|
|
|
+## 2. 已验证的输入事实
|
|
|
+
|
|
|
+1. JD `GET /sp-product/v0/products` 返回动态总数,本次为 824。
|
|
|
+2. `GET /sp-product/v0/products/{productId}?scene=pop` 已成功;`productId` 必须参与 MD5 签名。
|
|
|
+3. 目标 Parse class 资源为 `{JD_TARGET_PARSE_URL}/classes/<ClassName>`;挂载前缀根 GET 的 404 不能作为不可用证据。
|
|
|
+4. 目标 Parse 偶发 HTTP 403 / Parse code 119,有限重试可恢复;`count=1` 在当前托管环境不可靠。
|
|
|
+5. 导入器当前仍只读取 `page=1`,且 `--limit` 上限为 20,只能做联调,不能承担全量生产同步。
|
|
|
+6. JD 商品详情已观察到标题、品牌、类目、描述、主图元数据、属性、SKU、尺寸重量、价格与履约字段。
|
|
|
+7. 现有 Angular 的 `DomesticDatasetService` 加载的是 VOC 快照,不是本店实时 Listing 目录;不能用它替代 Listing 源数据。
|
|
|
+8. 现有 `listing-api.service.ts` 是停用的 Amazon 旧边界,不复用。
|
|
|
+9. 现有 AI 调用发生在浏览器,且网关 schema 只接受字符串消息。824 商品批处理必须移到服务端 worker。
|
|
|
+
|
|
|
+复测原始结论见 `JD-PRODUCT-DETAIL-PARSE-VERIFICATION-2026-08-21.md`。
|
|
|
+
|
|
|
+## 3. 总体调用链
|
|
|
+
|
|
|
+```text
|
|
|
+源 Parse EcomAuth(服务端读取最新 JD token)
|
|
|
+ -> JdTokenProvider
|
|
|
+ -> JdSpClient(path/query/header 统一签名、超时、错误分类、限流)
|
|
|
+ -> JdProductClient(列表分页 / 商品详情 / SKU / 库存)
|
|
|
+ -> ListingCatalogSyncWorker(分页、详情并发、断点、幂等、raw hash)
|
|
|
+ -> 目标 Parse ProductDetail 等源快照(现有兼容写入)
|
|
|
+ -> ListingSourceRepository(归一化为内部 DTO)
|
|
|
+ -> ListingScoreJobWorker
|
|
|
+ -> DeterministicRuleEngine(结构基线、覆盖率、硬规则证据)
|
|
|
+ -> ListingAiRubricJudge(固定原子标准、等级与证据)
|
|
|
+ -> AI Gateway(语义裁判;temperature=0;严格 JSON)
|
|
|
+ -> ServerScoreComposer(固定权重映射与总分计算)
|
|
|
+ -> ListingScoreRepository / ListingVersionRepository
|
|
|
+ -> /api/listing-ai/*(RBAC + Zod + cursor pagination)
|
|
|
+ -> Angular ListingAiApiService
|
|
|
+ -> Workbench / Product / Versions / Tasks 页面
|
|
|
+```
|
|
|
+
|
|
|
+关键决策:**AI 对固定语义标准作结构化判定,服务端确定性计算数值分。** AI 失败时不覆盖既有 `listing-jd-v2` 规则分;AI 成功后以独立 `listing-jd-ai-v1` 结果展示,且必须显示模型、Prompt 版本、评分类型和证据。
|
|
|
+
|
|
|
+## 4. 信息架构与页面流程
|
|
|
+
|
|
|
+### 4.1 路由
|
|
|
+
|
|
|
+| 路由 | 页面类型 | 职责 |
|
|
|
+|---|---|---|
|
|
|
+| `/listing-ai/workbench` | Hub | 商品总览、评分分布、筛选、批量评分入口 |
|
|
|
+| `/listing-ai/workbench/:productId` | Entity | 单商品预览、五维评分、证据、建议和采纳 |
|
|
|
+| `/listing-ai/versions` | Workspace | 优化版本列表、状态、创建人、源快照和 diff |
|
|
|
+| `/listing-ai/tasks` | Workspace | 批任务列表、进度、错误数、重试入口 |
|
|
|
+| `/listing-ai/tasks/:jobId` | Entity | 任务明细、各商品状态、错误与重试 |
|
|
|
+
|
|
|
+在 `app.routes.ts` 中新增一个受现有 `protectedRoutes` 保护的 `listing-ai` 父路由,不修改现有路由含义,不删除旧 route。
|
|
|
+
|
|
|
+### 4.2 工作台布局
|
|
|
+
|
|
|
+1. 顶部 `page-header`:标题、数据更新时间、京东状态、保存筛选、发起评分任务。
|
|
|
+2. 平台切换:京东 active;其他平台 disabled + “即将支持”,不制造空数据。
|
|
|
+3. 指标卡:商品总数、可评分、已评分、部分/阻塞、失败、平均分。每个指标带口径 tooltip。
|
|
|
+4. `page-toolbar`:搜索、类目、上下架、评分状态、分数区间、数据完整度、更新时间、排序。
|
|
|
+5. `data-table-shell`:商品、标题、类目、SKU 数、数据完整度、总分、五维分、rubric 版本、最后评分时间、状态、操作。
|
|
|
+6. 批量操作只传筛选快照或选中 ID,不把 824 条完整记录塞进 request body。
|
|
|
+7. 分页使用 cursor,默认 25,最大 100;浏览器不预载全量数据。
|
|
|
+
|
|
|
+### 4.3 单商品页
|
|
|
+
|
|
|
+- 左栏:移动端只读预览,HTML 详情必须先清洗;图片失败显示占位和原因。
|
|
|
+- 右栏:`board-tabs` 五个维度:标题、核心卖点、主图、详情、规格。
|
|
|
+- 每个 tab 使用统一结构:当前内容、覆盖度、规则证据、扣分项、AI 建议、来源字段路径、源快照时间。
|
|
|
+- “采用此版本”只写入 `ListingVersion.status=adopted` 和审计事件,不调用京东发布。
|
|
|
+- 缺失字段显示 `—`,同时显示 `not_returned`、`empty` 或 `parse_failed`,不能显示为 0。
|
|
|
+
|
|
|
+### 4.4 任务页
|
|
|
+
|
|
|
+- 状态机:`queued -> running -> completed | partial | failed | cancelled`。
|
|
|
+- 单商品状态:`queued -> rules_scored -> ai_pending -> scored | partial | blocked | failed`。
|
|
|
+- 展示总量必须来自创建任务时冻结的 scope 解析结果;进度为 terminal item 数 / frozen total。
|
|
|
+- MVP 使用 2–5 秒 polling;先不引入 SSE/WebSocket。页面刷新后凭 `jobId` 恢复。
|
|
|
+
|
|
|
+## 5. JD 字段接收与内部 DTO
|
|
|
+
|
|
|
+后端先保留受控 raw JSON 与 hash,再生成稳定 DTO。前端只接收 DTO,不感知 JD 原始层级。
|
|
|
+
|
|
|
+| 内部字段 | JD 实际路径 | 类型/处理 | 缺失语义 |
|
|
|
+|---|---|---|---|
|
|
|
+| `productId` | `productInfo.productId`;fallback 列表 `productId` | String,禁止 JS number 身份比较 | `blocked` |
|
|
|
+| `shopId` | 授权记录/同步上下文 | String | `blocked` |
|
|
|
+| `title` | `productInfo.productTitle.title` -> `productInfo.productName` | trim String | `empty` |
|
|
|
+| `titleBrandName` | `productInfo.productTitle.titleBrandName` | String/null | `not_returned` |
|
|
|
+| `brand.id/name` | `productInfo.brandInfo.*` | ID 转 String | `not_returned` |
|
|
|
+| `categoryIds` | `categoryDetail.thirdCategoryId`, `lastCategoryId` | String[],去重 | `not_returned` |
|
|
|
+| `price.jd/cost` | `productInfo.priceInfo.*` | number/null,不用 0 代替 null | `not_returned` |
|
|
|
+| `descriptions.desktopHtml` | `productDetailDesc.desc` | 原始 HTML 仅服务端留存;输出 sanitized HTML | `empty` |
|
|
|
+| `descriptions.mobileHtml` | `productDetailDesc.mobileDesc` | 同上 | `empty` |
|
|
|
+| `features[]` | `productInfo.features[].key/value` | 丢弃全空项,保留顺序 | `empty` |
|
|
|
+| `attributes[]` | `productInfo.goodsAttrInfos[]` | attrId/name/values | `empty` |
|
|
|
+| `images[]` | `material.mainImages[].imageInfoList[]` | 展平、按 orderSort 排序、去 URL 重复 | `empty` |
|
|
|
+| `images[].isPrimary` | `primaryFlag` | boolean/null | `not_returned` |
|
|
|
+| `images[].gptFlag` | `gptFlag` | boolean/null;不推导审美分 | `not_returned` |
|
|
|
+| `skus[]` | `skuList[]` | skuId String、名称、价格、库存、属性、状态 | `empty` |
|
|
|
+| `dimensions` | `length/width/height/weight` | number/null + 明确单位配置 | `not_returned` |
|
|
|
+| `logistics` | `productInfo.logisticsInfo` | 白名单字段 | `not_returned` |
|
|
|
+| `afterService` | `productInfo.afterServiceInfo`, `to7ReturnFlag` | 白名单归一化 | `not_returned` |
|
|
|
+| `sourceModifiedAt` | `productInfo.modifiedTime` | epoch 校验后 ISO | `not_returned` |
|
|
|
+| `sourceHash` | 归一化前关键内容 canonical JSON | SHA-256 | 必填 |
|
|
|
+
|
|
|
+### API DTO 草案
|
|
|
+
|
|
|
+```ts
|
|
|
+interface ListingProductSummaryDto {
|
|
|
+ productId: string;
|
|
|
+ shopId: string;
|
|
|
+ platform: 'jd';
|
|
|
+ title: string | null;
|
|
|
+ imageUrl: string | null;
|
|
|
+ categoryIds: string[];
|
|
|
+ itemStatus: string | null;
|
|
|
+ skuCount: number | null;
|
|
|
+ sourceUpdatedAt: string | null;
|
|
|
+ syncedAt: string;
|
|
|
+ sourceHash: string;
|
|
|
+ coverage: { percent: number; missing: string[]; status: 'eligible' | 'partial' | 'blocked' };
|
|
|
+ latestScore: ListingScoreSummaryDto | null;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+DTO 中 ID 一律 String;金额仍用 number 是展示折中,后续涉及财务计算再升级 decimal 字符串。
|
|
|
+
|
|
|
+## 6. 评分模型
|
|
|
+
|
|
|
+### 6.1 结果结构
|
|
|
+
|
|
|
+```ts
|
|
|
+type ListingDimension = 'title' | 'selling_points' | 'images' | 'description' | 'specifications';
|
|
|
+
|
|
|
+interface ListingDimensionScoreDto {
|
|
|
+ dimension: ListingDimension;
|
|
|
+ score: number | null;
|
|
|
+ maxScore: number;
|
|
|
+ coverage: number;
|
|
|
+ status: 'scored' | 'partial' | 'blocked';
|
|
|
+ evidence: Array<{
|
|
|
+ ruleId: string;
|
|
|
+ fieldPath: string;
|
|
|
+ outcome: 'pass' | 'fail' | 'unknown';
|
|
|
+ delta: number;
|
|
|
+ message: string;
|
|
|
+ }>;
|
|
|
+ suggestions: string[];
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- 默认五维各 20 分,总分 100;权重只存在 rubric 配置,不散落在组件。
|
|
|
+- `overallScore` 只在所有必需维度满足 rubric 的最低覆盖门槛时计算。
|
|
|
+- `coverage` 与 `quality score` 是两条独立指标。没有详情不能得“详情 0 分”,而应是 `score=null,status=blocked`。
|
|
|
+- rubric 从 `listing-jd-v1` 开始;发现 v1 将 JD 传输控制标记误判为重复卖点后,未原地修改,而是发布 `listing-jd-v2` 并完成全量重算,v1 继续保留审计。
|
|
|
+
|
|
|
+### 6.2 第一版规则边界
|
|
|
+
|
|
|
+| 维度 | 可确定性评分 | 仅建议/待确认 |
|
|
|
+|---|---|---|
|
|
|
+| 标题 | 是否存在、长度、品牌/品类/型号覆盖、重复片段、异常符号、SKU 冲突 | 类目禁限词词库需业务确认 |
|
|
|
+| 核心卖点 | features 数量、非空、重复、规格覆盖、服务信息覆盖 | 京东是否有独立卖点字段需更多样本确认 |
|
|
|
+| 主图 | 图片数、主图标识、顺序、URL 结构/可访问、重复 URL | 构图、清晰度、文字占比需多模态模型,MVP 不评分 |
|
|
|
+| 详情 | desktop/mobile 是否存在、清洗后文本长度、结构、两端一致性、危险标签 | 内容说服力由 AI 建议,不直接决定数值分 |
|
|
|
+| 规格 | SPU/SKU 属性数量、必填规格覆盖、尺寸重量、属性一致性 | 类目必填属性清单需要规则数据源 |
|
|
|
+
|
|
|
+在 20 商品 shadow benchmark 完成前,不把暂定阈值写死为产品真理。阈值由 rubric JSON 管理,并在样本评审后冻结 v1。
|
|
|
+
|
|
|
+## 7. 后端设计
|
|
|
+
|
|
|
+### 7.1 模块边界
|
|
|
+
|
|
|
+新增 `src/modules/listing-ai/`,不把评分任务塞进现有 `analysis_run`。现有 `analysis_run` 的类型和生命周期服务于 VOC 洞察,强行复用会污染语义和权限。
|
|
|
+
|
|
|
+实际落地结构(小文件按职责合并,避免无意义拆分):
|
|
|
+
|
|
|
+```text
|
|
|
+src/modules/listing-ai/
|
|
|
+ domain.ts
|
|
|
+ schemas.ts
|
|
|
+ routes.ts
|
|
|
+ listing-ai.service.ts
|
|
|
+ clients/
|
|
|
+ jd-token.provider.ts
|
|
|
+ jd-sp.client.ts
|
|
|
+ jd-product.client.ts
|
|
|
+ normalization/
|
|
|
+ domestic-dataset.adapter.ts
|
|
|
+ jd-listing.normalizer.ts
|
|
|
+ html-sanitizer.ts
|
|
|
+ repositories/
|
|
|
+ in-memory-listing-ai.repository.ts
|
|
|
+ parse-rest-listing-ai.repository.ts
|
|
|
+ postgres-listing-ai.repository.ts
|
|
|
+ scoring/
|
|
|
+ rule-engine.ts
|
|
|
+```
|
|
|
+
|
|
|
+`canonicalHash` 与五维 v1 rubric 同置于 `rule-engine.ts`,批任务领取/恢复置于 `ListingAiService`,全量目录 worker 作为显式 `scripts/sync-jd-listings.ts` 运行。这样保留清晰边界,同时不为单一实现增加空包装层。
|
|
|
+
|
|
|
+若 `postgres` 和 `parse_rest` 两种 storage driver 都必须继续支持,则 repository 定义接口,并各有实现;local demo 使用显式内存实现,不在 route 里写条件分支。
|
|
|
+
|
|
|
+### 7.2 全量商品同步必须先工程化
|
|
|
+
|
|
|
+现有 `tools/jd-import-products.mjs` 仅保留为联调工具。生产同步必须满足:
|
|
|
+
|
|
|
+1. 从 page 1 循环到 API 返回的实际末页,不依据历史总数;
|
|
|
+2. 记录每页请求、返回条数、productId 去重数和 pagination 元数据;
|
|
|
+3. 详情请求使用配置化并发(初始 3),遇限流/5xx 指数退避并尊重 retry-after;
|
|
|
+4. access token 每批获取最新值,失效时只允许一次受控刷新/重取;
|
|
|
+5. 使用 `(shopId, productId)` 作为身份,现有兼容业务键 `jd:${shopId}:${productId}` 保留;
|
|
|
+6. 每条源记录写 `sourceHash`、`sourceModifiedAt`、`syncedAt`、`detailStatus` 和错误分类;
|
|
|
+7. checkpoint 保存 page、最后成功 productId、统计值;worker 重启可恢复;
|
|
|
+8. upsert 幂等,相同 sourceHash 不重复触发评分;
|
|
|
+9. `ProductDetail`、`Product`、`AsinSkuMapping`、`JdStockSnapshot` 兼容写入先保留;迁移完成前不删类、不改业务键;
|
|
|
+10. 完成度以本次冻结的 productId 清单和逐条终态计算,不使用当前异常的 Parse count。
|
|
|
+
|
|
|
+### 7.3 持久化实体
|
|
|
+
|
|
|
+Postgres 下一迁移文件建议为 `migrations/007_listing_ai.sql`:
|
|
|
+
|
|
|
+| 实体/表 | 主键与唯一约束 | 关键字段 |
|
|
|
+|---|---|---|
|
|
|
+| `voc.listing_source_snapshot` | unique `(workspace_id, platform, shop_id, product_id, source_hash)` | normalized_json, raw_ref/raw_json, detail_status, source_modified_at, observed_at |
|
|
|
+| `voc.listing_score_job` | unique `(workspace_id, idempotency_key)` | scope_json, rubric_version, source_cutoff, totals, status, checkpoint, timestamps |
|
|
|
+| `voc.listing_score_item` | unique `(job_id, product_id, source_hash)` | status, attempts, score_result_id, error_code, error_detail_redacted |
|
|
|
+| `voc.listing_score_result` | unique `(workspace_id, product_id, source_hash, rubric_version)` | overall_score nullable, coverage, dimension_json, model_info, prompt_version |
|
|
|
+| `voc.listing_version` | unique `(workspace_id, product_id, version_no)` | base_source_hash, content_json, status, created_by, adopted_at |
|
|
|
+
|
|
|
+所有写操作追加现有 audit log:`listing.score.requested`、`listing.score.retried`、`listing.version.created`、`listing.version.adopted`。
|
|
|
+
|
|
|
+Parse REST driver 对应类名使用 `VocListingSourceSnapshot`、`VocListingScoreJob`、`VocListingScoreItem`、`VocListingScoreResult`、`VocListingVersion`,字段语义与 Postgres 对齐。已有外部目标类 `ProductDetail` 等只作为 JD 源镜像,不直接承担 SaaS 工作区/RBAC 结果存储。
|
|
|
+
|
|
|
+### 7.4 AI 执行
|
|
|
+
|
|
|
+- worker 调用 `FmodeAiClient`,前端不再逐商品请求 `/api/ai/chat/completions`;
|
|
|
+- prompt 输入只包含白名单后的 Listing DTO、规则失败项和 rubric 版本;
|
|
|
+- 返回必须通过 Zod schema;失败记录 `ai_invalid_output` 并可重试;
|
|
|
+- AI 返回只产生建议和候选版本,不修改规则 evidence 或 numeric score;
|
|
|
+- 结果记录 model、promptVersion,job 记录 requestHash;当前上游未稳定返回 usage,正式 AI 放量前在可用时补充耗时/token usage 指标,任何情况下都不记录 token/密钥;
|
|
|
+- `LISTING_AI_CONCURRENCY` 与 `LISTING_AI_MAX_ITEMS_PER_JOB` 配置并发和单任务硬预算;超限在入队前返回 429,未配置模型时规则结果保留且任务为 partial,不无限重试。跨任务每日总预算仍是全量 AI 发布门禁。
|
|
|
+
|
|
|
+## 8. 后端 API 契约
|
|
|
+
|
|
|
+统一前缀:`/api/listing-ai`。沿用现有 AuthenticationMiddleware、WorkspaceAccessService、Zod、ApiError、audit,不创造第二套鉴权。
|
|
|
+
|
|
|
+### 8.1 商品目录
|
|
|
+
|
|
|
+```http
|
|
|
+GET /api/listing-ai/products?workspaceId=&platform=jd&limit=25&cursor=&search=&categoryId=&itemStatus=&scoreStatus=&minScore=&maxScore=&coverageStatus=&sort=
|
|
|
+```
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "items": [],
|
|
|
+ "nextCursor": null,
|
|
|
+ "summary": {
|
|
|
+ "sourceTotal": 824,
|
|
|
+ "eligible": 0,
|
|
|
+ "scored": 0,
|
|
|
+ "partial": 0,
|
|
|
+ "blocked": 0,
|
|
|
+ "failed": 0,
|
|
|
+ "lastCatalogSyncAt": "2026-08-21T00:00:00.000Z"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+`sourceTotal` 来自最近一次成功同步摘要,不来自不可靠的 Parse count。
|
|
|
+
|
|
|
+```http
|
|
|
+GET /api/listing-ai/products/:productId?workspaceId=&platform=jd
|
|
|
+```
|
|
|
+
|
|
|
+返回 `source`、`coverage`、`latestScore`、`versionsSummary`。默认不返回完整 raw JD JSON。
|
|
|
+
|
|
|
+### 8.2 评分任务
|
|
|
+
|
|
|
+```http
|
|
|
+POST /api/listing-ai/score-jobs
|
|
|
+Idempotency-Key: <client generated UUID>
|
|
|
+Content-Type: application/json
|
|
|
+
|
|
|
+{
|
|
|
+ "workspaceId": "demashi",
|
|
|
+ "platform": "jd",
|
|
|
+ "scope": {
|
|
|
+ "mode": "filter",
|
|
|
+ "filter": { "itemStatus": "on_shelf", "scoreStatus": "unscored" }
|
|
|
+ },
|
|
|
+ "rubricVersion": "listing-jd-v1",
|
|
|
+ "includeAiSuggestions": true
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+也支持 `scope.mode=selected`,但 `productIds` 上限 100;全量必须用 filter scope。创建时服务端冻结 productId + sourceHash 清单,返回 202。
|
|
|
+
|
|
|
+```http
|
|
|
+GET /api/listing-ai/score-jobs?workspaceId=&status=&limit=&cursor=
|
|
|
+GET /api/listing-ai/score-jobs/:jobId?workspaceId=
|
|
|
+GET /api/listing-ai/score-jobs/:jobId/items?workspaceId=&status=&limit=&cursor=
|
|
|
+POST /api/listing-ai/score-jobs/:jobId/retry
|
|
|
+POST /api/listing-ai/score-jobs/:jobId/cancel
|
|
|
+```
|
|
|
+
|
|
|
+### 8.3 评分与版本
|
|
|
+
|
|
|
+```http
|
|
|
+GET /api/listing-ai/products/:productId/scores/latest?workspaceId=&rubricVersion=
|
|
|
+GET /api/listing-ai/products/:productId/versions?workspaceId=&limit=&cursor=
|
|
|
+POST /api/listing-ai/products/:productId/versions
|
|
|
+GET /api/listing-ai/versions/:versionId?workspaceId=
|
|
|
+POST /api/listing-ai/versions/:versionId/adopt
|
|
|
+```
|
|
|
+
|
|
|
+创建版本 body 必须包含 `baseSourceHash` 和 `baseScoreResultId`,若源商品已变化则返回 409 `listing_source_changed`,防止采纳过期建议。
|
|
|
+
|
|
|
+### 8.4 错误码与 HTTP
|
|
|
+
|
|
|
+| HTTP | error | 场景 |
|
|
|
+|---:|---|---|
|
|
|
+| 400 | `invalid_request` | Zod 校验失败 |
|
|
|
+| 401 | `unauthenticated` | 未登录/会话失效 |
|
|
|
+| 403 | `workspace_forbidden` | 无工作区权限 |
|
|
|
+| 404 | `listing_product_not_found` / `score_job_not_found` | 资源不存在 |
|
|
|
+| 409 | `listing_source_changed` / `score_job_not_retryable` / `idempotency_conflict` | 状态或幂等冲突 |
|
|
|
+| 422 | `listing_source_incomplete` | 明确请求单品评分但必要源数据缺失 |
|
|
|
+| 429 | `listing_ai_budget_exceeded` | AI 并发/预算限制 |
|
|
|
+| 502 | `jd_upstream_error` / `ai_upstream_error` | 上游明确失败 |
|
|
|
+| 503 | `listing_storage_unavailable` | 目标存储不可用 |
|
|
|
+
|
|
|
+列表中的单商品失败不得让整批 GET 500;它应成为 item 的终态和脱敏错误码。
|
|
|
+
|
|
|
+## 9. 前端设计与文件变更
|
|
|
+
|
|
|
+### 9.1 新增文件
|
|
|
+
|
|
|
+```text
|
|
|
+src/modules/listing-ai/
|
|
|
+ listing-ai.routes.ts
|
|
|
+ listing-ai.shared.scss
|
|
|
+ models/listing-ai.models.ts
|
|
|
+ services/listing-ai-api.service.ts
|
|
|
+ services/listing-ai-api.service.spec.ts
|
|
|
+ services/listing-ai-workbench.store.ts
|
|
|
+ pages/workbench/listing-ai-workbench.component.{ts,html}
|
|
|
+ pages/product/listing-ai-product.component.{ts,html}
|
|
|
+ pages/versions/listing-version-list.component.{ts,html}
|
|
|
+ pages/tasks/listing-score-job-list.component.{ts,html}
|
|
|
+ pages/task-detail/listing-score-job-detail.component.{ts,html}
|
|
|
+ components/listing-mobile-preview/*
|
|
|
+ components/listing-score-summary/*
|
|
|
+ components/listing-dimension-panel/*
|
|
|
+```
|
|
|
+
|
|
|
+source evidence 已合并到统一维度面板,version diff 已合并到版本页;页面共享样式集中在 `listing-ai.shared.scss`。API service 单测覆盖参数与写请求,页面/API 流程由后端 route 集成测试、全量 Angular suite 和 production build 共同验收。
|
|
|
+
|
|
|
+新组件使用 standalone + OnPush。状态用 Angular signals/RxJS 组合的小型 feature store;MVP 不为了单模块引入 NgRx。
|
|
|
+
|
|
|
+### 9.2 修改文件
|
|
|
+
|
|
|
+| 文件 | 修改内容 |
|
|
|
+|---|---|
|
|
|
+| `src/app/app.routes.ts` | lazy load `listing-ai` 子路由,沿用 protectedRoutes |
|
|
|
+| `src/app/core/config/runtime-config.ts` | 增加 `listingAiPath`,默认 `/api/listing-ai` |
|
|
|
+| `src/modules/shared/components/navigation/navigation.component.ts` | 在“行动”或独立组中增加 Listing AI 工作台、版本、任务入口 |
|
|
|
+| `src/app/core/services/index.ts` | 若该项目继续使用 barrel,则导出新 API service;否则不改 |
|
|
|
+| `proxy.conf.json` | 只有现有 `/api` 代理不能覆盖时才改;优先不改 |
|
|
|
+
|
|
|
+### 9.3 组件复用
|
|
|
+
|
|
|
+必须优先复用 `COMPONENT-INDEX.md` 中已有的:`page-header`、`page-toolbar`、`summary-metric-card`、`data-table-shell`、`content-card`、`board-tabs`、`entity-property-list`、loading/empty/error、`neu-button`、`split-panel`、`product-identity`。
|
|
|
+
|
|
|
+不引入 Angular Material、ng-zorro、Tailwind 或第二套图标库。若现有组件缺能力,先用向后兼容的 Input/slot 扩展,并给现有用例回归测试。
|
|
|
+
|
|
|
+## 10. 后端文件变更
|
|
|
+
|
|
|
+### 10.1 新增
|
|
|
+
|
|
|
+- 上文 `src/modules/listing-ai/**`;
|
|
|
+- `migrations/007_listing_ai.sql`;
|
|
|
+- `test/listing-ai.routes.test.ts`;
|
|
|
+- `test/listing-ai.rule-engine.test.ts`;
|
|
|
+- `test/jd-sp-client.signature.test.ts`;
|
|
|
+- `scripts/sync-jd-listings.ts`(全量/显式同步,支持 checkpoint/resume,不在启动时偷偷同步);
|
|
|
+- `scripts/score-listings.ts`、`scripts/verify-listing-rollout.ts`。
|
|
|
+
|
|
|
+worker 的冻结 scope、执行、重试、恢复和 stale 行为由 `listing-ai.routes.test.ts` 以真实 service/repository 流程覆盖,没有创建只重复同一流程的空壳 `listing-ai.worker.test.ts`。
|
|
|
+
|
|
|
+### 10.2 修改
|
|
|
+
|
|
|
+| 文件 | 修改内容 |
|
|
|
+|---|---|
|
|
|
+| `src/config/env.ts` | 增加 JD/目标 Parse/worker 并发/预算配置;按 storage/worker 条件校验 |
|
|
|
+| `src/app.ts` | 注入 listing repositories/service,挂载 `/api/listing-ai` |
|
|
|
+| `src/local-app.ts` | 注入 local demo repository,保证本地启动不因无 JD 凭据崩溃 |
|
|
|
+| `src/server.ts`、`src/local-server.ts` | 注入正确 storage repository;评分任务由 service 启动恢复,进程不执行隐式 JD 全量同步 |
|
|
|
+| `src/db/migrations.ts` | 只有 readiness 表清单为硬编码时才加入新表检查 |
|
|
|
+| `.env.example` | 只增加变量名和占位说明,不填真实值 |
|
|
|
+| `package.json` | 增加 `sync:jd-listings` / `score:jd-listings` / `verify:listing-rollout` 显式脚本 |
|
|
|
+
|
|
|
+### 10.3 删除
|
|
|
+
|
|
|
+第一阶段**不删除任何现有文件、route、表或 Parse class**。`tools/jd-import-products.mjs` 在服务端同步达到字段、幂等和故障恢复对等后才能标记 deprecated;再经过一个发布周期才讨论删除。
|
|
|
+
|
|
|
+## 11. 承重墙:禁止直接触碰
|
|
|
+
|
|
|
+1. **认证与权限**:不绕过 `/api` 认证中间件、WorkspaceAccessService 和权限名;生产不能启用 disabled auth。
|
|
|
+2. **身份键**:不把 JD productId 当 number,不改变 `jd:${shopId}:${productId}` 兼容业务键,不用 legacy asin 取代它。
|
|
|
+3. **数据语义**:不把 Listing 文本当用户 VOC;不把 missing/null 变成 0;不把库存推导为销量。
|
|
|
+4. **密钥边界**:AppSecret、MasterKey、access/refresh token 不进 Angular、assets、URL、localStorage、日志或 API response。
|
|
|
+5. **现有生命周期**:不扩大/修改现有 `analysis_run` enum 来承载 Listing 批任务;不破坏 insight -> decision -> action -> validation 链路。
|
|
|
+6. **UI 体系**:不引入新框架,不改 AI 视觉报告全局样式块,不用页面私有 CSS 复制已有组件。
|
|
|
+7. **源数据**:保留 raw/normalized 分层和 sourceHash;优化版本不覆盖源快照。
|
|
|
+8. **外部写操作**:MVP 禁止 JD 发布、上下架、改价、改库存。
|
|
|
+9. **路由兼容**:只增加 child routes,不改已有 URL 的含义或重定向。
|
|
|
+10. **上游判活**:不以 Parse base root 404 或异常 count 作为失败真值;必须探测 class 资源。
|
|
|
+
|
|
|
+## 12. 实施顺序与 PR 边界
|
|
|
+
|
|
|
+### PR-0:已完成的连接修复
|
|
|
+
|
|
|
+- `.gitignore`;
|
|
|
+- getProduct path 签名;
|
|
|
+- `itemStatus` schema 归一化;
|
|
|
+- JD/Parse 诊断脚本与复测文档。
|
|
|
+
|
|
|
+### PR-1:后端源目录与全量同步
|
|
|
+
|
|
|
+- 服务端 token/sign/client;
|
|
|
+- 全分页、详情并发、checkpoint、幂等;
|
|
|
+- source snapshot repository/API;
|
|
|
+- 20 商品 shadow run,再执行全量目录同步;
|
|
|
+- 不含 AI、不含页面。
|
|
|
+
|
|
|
+### PR-2:版本化规则引擎
|
|
|
+
|
|
|
+- v1 rubric、五维规则、coverage、证据;
|
|
|
+- 20 个样本由产品/运营人工复核并冻结阈值;
|
|
|
+- 单商品评分 API;
|
|
|
+- 不含批量 AI。
|
|
|
+
|
|
|
+### PR-3:批任务与 AI 建议
|
|
|
+
|
|
|
+- job/item/result/version 表;
|
|
|
+- worker、幂等、恢复、预算、Zod 输出校验;
|
|
|
+- 先跑 20,再跑 100,满足门禁后才跑当前全量;
|
|
|
+- 记录成本和耗时基线后再定义 SLA。
|
|
|
+
|
|
|
+### PR-4:Angular 工作台
|
|
|
+
|
|
|
+- 先接真实 API 合约和错误态;
|
|
|
+- 工作台 -> 商品页 -> 任务页 -> 版本页;
|
|
|
+- 不用 mock 数量覆盖真实 summary;
|
|
|
+- 做 keyboard、窄屏、空态、partial 状态回归。
|
|
|
+
|
|
|
+### PR-5:全量验收与灰度
|
|
|
+
|
|
|
+- 新建全量 job,冻结当时实际商品清单;
|
|
|
+- 运维监控、失败重试、重启恢复、安全扫描;
|
|
|
+- 只开放给指定 workspace/role;
|
|
|
+- 通过 go/no-go 后再扩大访问。
|
|
|
+
|
|
|
+## 13. 测试与成功判定
|
|
|
+
|
|
|
+### 13.1 连接与同步
|
|
|
+
|
|
|
+- [x] 签名单测固定验证:path `productId` 在签名串字段中但不在 query 中;
|
|
|
+- [x] 同一真实商品 getProduct HTTP 200,旧变体稳定复现 `99904000013`;
|
|
|
+- [x] 遍历所有页,页面汇总等于本次同步返回的动态 total;
|
|
|
+- [x] productId 去重后无重复、无空 ID;
|
|
|
+- [x] 每个冻结 productId 有 `available|empty|failed` 明确详情状态;
|
|
|
+- [x] 相同源数据重复同步,业务记录数不增长且 sourceHash 不变;
|
|
|
+- [x] 进程中断后从 checkpoint 恢复,不从头制造重复;
|
|
|
+- [x] 精确 403/code119 有限重试,永久 403 不被掩盖;
|
|
|
+- [x] readiness 请求 class 资源,不请求 base root。
|
|
|
+
|
|
|
+### 13.2 规则与 AI
|
|
|
+
|
|
|
+- [x] 相同 sourceHash + rubricVersion 重跑得到完全相同 numeric score/evidence;
|
|
|
+- [x] 必需字段缺失时 score=null/blocked,而不是 0;
|
|
|
+- [x] 每个扣分项能定位 `ruleId` 和 JD/DTO fieldPath;
|
|
|
+- [x] AI 超时、429、5xx、坏 JSON 时规则分仍保存,item 为 partial;
|
|
|
+- [x] AI 建议不修改规则分,不覆盖源快照;
|
|
|
+- [x] sourceHash 变化后旧版本明确标为 stale;
|
|
|
+- [ ] 20 商品人工复核通过后才能冻结 rubric v1;
|
|
|
+- [x] 20 -> 100 -> 全量规则评分逐级放量,每级统计错误、重试与耗时;AI 全量成本仍受预算门禁。
|
|
|
+
|
|
|
+### 13.3 API
|
|
|
+
|
|
|
+- [x] 所有 endpoint 有 Zod 输入校验和 workspace RBAC;
|
|
|
+- [x] 同一 Idempotency-Key + 同一 body 返回同一 job;同 key 不同 body 返回 409;
|
|
|
+- [x] cursor 不重复、不漏项,limit 最大 100;
|
|
|
+- [x] cancel/retry 只允许合法状态;
|
|
|
+- [x] 任务重启后总量、terminal 数与 item 明细一致;
|
|
|
+- [x] response 不含 raw token、MasterKey、AppSecret、完整 raw JD payload;
|
|
|
+- [x] audit log 包含发起、重试、取消、版本创建和采纳。
|
|
|
+
|
|
|
+### 13.4 前端
|
|
|
+
|
|
|
+- [x] UI 商品总数完全来自 API,代码与模板中不存在 614/821/824;
|
|
|
+- [x] 首屏只拉 summary + 当前页,不把全量商品载入浏览器;
|
|
|
+- [x] filter/sort/page 状态进入 URL,刷新可恢复;
|
|
|
+- [ ] loading、empty、error、partial、blocked、stale 均有不同展示;
|
|
|
+- [x] polling 在终态/离开页面时停止,无重复订阅;
|
|
|
+- [x] 单商品页五个 tab 的证据、建议、版本引用同一 sourceHash;
|
|
|
+- [x] “采用”只改变内部版本状态,浏览器网络中没有 JD 写请求;
|
|
|
+- [x] 京东以外平台不可发起任务;
|
|
|
+- [x] `ng build`、Listing API service 单测和后端关键路由集成测试通过。
|
|
|
+
|
|
|
+### 13.5 安全与发布门禁
|
|
|
+
|
|
|
+- [x] `git check-ignore docs/JD-CONNECTION-CONFIG.local.md` 命中;
|
|
|
+- [ ] Git 历史/工作树、`dist`、source map、浏览器请求和日志扫描无真实凭据;
|
|
|
+- [x] HTML 详情经过 allowlist sanitizer,无 script/event handler/javascript URL;
|
|
|
+- [x] 图片只通过安全 URL 策略展示,失败不执行任意协议;
|
|
|
+- [ ] 生产 `API_AUTH_MODE=parse`,跨 workspace 访问用例返回 403;
|
|
|
+- [x] worker concurrency、retry、timeout 均有上限;AI 通过显式开关且未获预算批准时不执行;
|
|
|
+- [x] 824(或当时动态总数)个冻结 item 最终全部进入 terminal 状态,无 orphan;
|
|
|
+- [x] `completed` 仅表示全部成功;混合结果必须是 `partial`;
|
|
|
+- [ ] 未完成性能基线前不承诺“几分钟完成”。在 20/100 商品实测后,将吞吐、P95 和成本阈值补入 rubric/release 文档。
|
|
|
+
|
|
|
+## 14. 风险、待确认项与决策责任
|
|
|
+
|
|
|
+| 风险/问题 | 当前处理 | 谁确认/何时阻塞 |
|
|
|
+|---|---|---|
|
|
|
+| Parse 间歇 403 | 精确错误有限重试 + 指标告警 | 连续失败超过重试预算时阻塞全量 |
|
|
|
+| Parse count 不可靠 | 使用同步摘要和冻结 ID 清单 | 后端实现时验证 |
|
|
|
+| JD 限流/分页边界未知 | 3 并发起步,20/100 shadow run | 全量前必须有实测 |
|
|
|
+| 核心卖点真实字段口径 | 先以 features/服务/规格归一化 | 产品与运营冻结 rubric 前确认 |
|
|
|
+| 类目禁限词/必填属性缺数据源 | v1 标 unknown,不擅自扣分 | 业务提供规则数据后进入 v2 |
|
|
|
+| 图片视觉质量 | MVP 不评分视觉语义 | 多模态网关接入后单独设计 |
|
|
|
+| AI 成本和速率 | 分级放量、预算门禁 | 全量任务前批准预算 |
|
|
|
+| HTML XSS/外链图片 | 服务端清洗 + URL 策略 | 安全测试不通过则阻塞上线 |
|
|
|
+| 商品同步期间变化 | job 冻结 productId+sourceHash | 变化商品标 stale,另建任务 |
|
|
|
+
|
|
|
+## 15. 回滚策略
|
|
|
+
|
|
|
+- 新路由可由 feature flag/导航权限隐藏;回滚前端不删除后端结果。
|
|
|
+- worker 可停止领取新任务;单实例重启会扫描并恢复持久化的 `queued/running` 任务。多实例 lease 是正式横向扩容前的门禁,见实施报告。
|
|
|
+- 新表/Parse class 为旁路新增,不修改旧 VOC 表;回滚应用版本无需 drop table。
|
|
|
+- rubric v1 不原地改;错误规则发布 v2 并重算,保留旧结果审计。
|
|
|
+- 外部 `ProductDetail` 兼容镜像在新 source repository 稳定一个发布周期前不移除。
|
|
|
+
|
|
|
+## 16. 开发开始前的 Go / No-Go 清单
|
|
|
+
|
|
|
+只有以下条件同时满足才开始 PR-1,而不是直接画页面:
|
|
|
+
|
|
|
+- [x] 本机凭据文件已忽略;
|
|
|
+- [x] getProduct 400 的真实错误码、错误消息和签名字段已记录;
|
|
|
+- [x] getProduct 修复后返回真实详情;
|
|
|
+- [x] 目标 Parse class 路由读写成功,root 404 语义澄清;
|
|
|
+- [x] `itemStatus` schema mismatch 已修复;
|
|
|
+- [x] 前后端边界、API DTO、表/类、路由和文件清单已写入本文;
|
|
|
+- [x] 承重墙、验收点、风险和回滚已写入本文;
|
|
|
+- [ ] 产品/运营确认 v1 五维权重、标题阈值、卖点口径;
|
|
|
+- [x] Postgres + parse_rest 两驱动均已实现;
|
|
|
+- [x] 目标 Parse 间歇 403 使用仅针对 119 的有限退避重试;
|
|
|
+- [ ] AI 负责人确认 20/100/全量预算与并发。
|
|
|
+
|
|
|
+前三个未决责任项可以与 PR-1 的纯同步工程并行,但在 PR-2 rubric 冻结和 PR-3 全量 AI 任务前必须关闭。
|