Просмотр исходного кода

chore: keep local implementation notes out of git

Yi Jiarui 1 месяц назад
Родитель
Сommit
85c81521bf

+ 5 - 0
.gitignore

@@ -12,6 +12,11 @@ coverage/
 # Local credential-bearing connection notes must never be committed.
 docs/JD-CONNECTION-CONFIG.local.md
 
+# Local implementation/verification notes generated during development.
+docs/JD-PRODUCT-DETAIL-PARSE-VERIFICATION-*.md
+docs/LISTING-AI-IMPLEMENTATION-PLAN.md
+docs/LISTING-AI-IMPLEMENTATION-REPORT-*.md
+
 # Local source workbooks are not committed. Generated normalized data is.
 data/source/
 

+ 0 - 228
docs/JD-PRODUCT-DETAIL-PARSE-VERIFICATION-2026-08-21.md

@@ -1,228 +0,0 @@
-# JD 商品详情与目标 Parse 联调复测记录(2026-08-21)
-
-## 1. 结论
-
-本次先完成连接层修复,不进入 Listing AI 页面开发。
-
-1. `getProduct` 的 HTTP 400 已定位并修复。根因不是 `scene` 缺失,也不是 URL 路径错误,而是路径参数 `productId` 没有参与 JD MD5 签名。
-2. 目标 Parse 的 `/backend/vO3T8gAdwB/data` 是应用挂载前缀,不是可直接 GET 的健康检查资源;直接请求该前缀返回 404 是预期的路由表现。实际 REST 资源必须追加 `/classes/<ClassName>`。
-3. 已用 `ProductDetail` 资源完成读取与正式幂等写入。目标路径已可用:`/backend/vO3T8gAdwB/data/classes/ProductDetail`。
-4. 写入过程中发现并修复了 `ProductDetail.itemStatus` 的真实 schema 冲突:JD 详情中的状态是 Object,而目标 Parse 字段已是 String,现统一提取为字符串后再写入。
-5. 当前店铺商品总数由接口实时返回。本次 `paginationData.totalItems` 为 **824**;不得继续把 614、821 或 824 写死在页面或任务代码中。
-
-## 2. 安全边界
-
-- 凭据只从环境变量或本机忽略文件读取,本文不记录 AppSecret、MasterKey、access token 或 refresh token。
-- `docs/JD-CONNECTION-CONFIG.local.md` 已加入 `.gitignore`。
-- 诊断脚本只打印状态码、业务错误码、参与签名的**字段名**和响应字段路径,不打印签名原文、签名值或凭据值。
-- JD 和 Parse 调用仍必须留在 Node/服务端边界内,不能移动到 Angular 运行时。
-
-## 3. `getProduct` 400 复测矩阵
-
-测试资源:店铺中的真实商品 `10026651532867`。
-
-请求路径:
-
-```text
-GET https://api-cn.jd.com/rest/sp-product/v0/products/{productId}?scene=pop
-```
-
-| 变体 | 查询参数 | MD5 签名字段 | HTTP | JD 错误码 | 结果 |
-|---|---|---|---:|---|---|
-| 原实现 | `scene=pop` | `X-JOS-Access-Token`, `X-JOS-App-Key`, `X-JOS-Timestamp`, `scene` | 400 | `99904000013` | `请求缺少参数或参数无效,无法解析。` |
-| 修复实现 | `scene=pop` | 上述字段 + `productId` | 200 | — | `success=true`,返回商品详情 |
-| JSON request + path 签名 | `request={"scene":"pop"}` | 公共字段 + `productId`, `request` | 503 | `10110531002` | `ProductDetailRequest.scene is empty` |
-| JSON request,无 path 签名 | `request={"scene":"pop"}` | 公共字段 + `request` | 400 | `99904000013` | 参数无法解析 |
-
-### 正确签名规则
-
-该接口需要区分“发送位置”和“参与签名”两个概念:
-
-- `productId` 只在 URL path 中出现,但仍必须进入待签名字段集合;
-- `scene=pop` 作为 query 参数发送,同时进入待签名字段集合;
-- 公共字段 `X-JOS-App-Key`、`X-JOS-Access-Token`、`X-JOS-Timestamp` 进入签名;
-- `X-JOS-Sign`、`X-JOS-Sign-Method`、`X-JOS-Request-Identity` 不进入业务字段串;
-- 不能为了“让它参与签名”而把 `productId` 再重复放入 query。
-
-修复后的调用形态:
-
-```js
-jdGet(
-  `/sp-product/v0/products/${productId}`,
-  { scene: 'pop' },
-  accessToken,
-  { productId },
-);
-```
-
-## 4. 成功响应的真实字段
-
-以下仅记录本次 200 响应中已观察到的字段路径;字段是否为空仍需按每个商品单独建模。
-
-### 4.1 商品主信息
-
-- `productInfo.productId`
-- `productInfo.productName`
-- `productInfo.productTitle.title`
-- `productInfo.productTitle.titleBrandName`
-- `productInfo.brandInfo.brandId`
-- `productInfo.brandInfo.brandName`
-- `productInfo.categoryDetail.lastCategoryId`
-- `productInfo.categoryDetail.thirdCategoryId`
-- `productInfo.priceInfo.jdPrice`
-- `productInfo.priceInfo.costPrice`
-- `productInfo.itemNum`
-- `productInfo.upcCode`
-- `productInfo.createdTime`
-- `productInfo.modifiedTime`
-- `productInfo.productStatus.yn`
-- `productInfo.productStatus.productStatus`
-- `productInfo.productStatus.productStatusNew`
-- `productInfo.wareStatus`
-
-### 4.2 Listing 内容
-
-- `productInfo.productDetailDesc.desc`
-- `productInfo.productDetailDesc.mobileDesc`
-- `productInfo.features[].key`
-- `productInfo.features[].value`
-- `productInfo.goodsAttrInfos[].attrId`
-- `productInfo.goodsAttrInfos[].attrName`
-- `productInfo.goodsAttrInfos[].values[].attrValue`
-- `productInfo.goodsAttrInfos[].values[].attrValueAlias`
-- `material.mainImages[].imageInfoList[].imgUrl`
-- `material.mainImages[].imageInfoList[].orderSort`
-- `material.mainImages[].imageInfoList[].primaryFlag`
-- `material.mainImages[].imageInfoList[].gptFlag`
-
-### 4.3 SKU 与履约
-
-- `skuList[].skuId`
-- `skuList[].productId`
-- `skuList[].skuName`
-- `skuList[].outerId`
-- `skuList[].upcCode`
-- `skuList[].logo`
-- `skuList[].priceInfo.jdPrice`
-- `skuList[].stockNum`
-- `skuList[].skuEnableStatus`
-- `skuList[].skuStatus.onOffShelfStatus`
-- `skuList[].goodsAttrInfos[]`
-- `productInfo.length`, `width`, `height`, `weight`
-- `productInfo.logisticsInfo.delivery`
-- `productInfo.logisticsInfo.promiseId`
-- `productInfo.logisticsInfo.transportId`
-- `productInfo.logisticsInfo.packSpecification`
-- `productInfo.afterServiceInfo`
-- `productInfo.to7ReturnFlag`
-
-这些字段足以支持标题、主图元数据、详情、规格、SKU 与内容完整度的第一版评分;它们**不能**证明当前 AI 网关已具备图片视觉理解能力。
-
-## 5. 目标 Parse 404 诊断与修复
-
-### 5.1 路由矩阵
-
-| URL 形态 | 本次结果 | 判断 |
-|---|---:|---|
-| `/backend/vO3T8gAdwB/data` | 404 HTML | 仅是挂载前缀,不是健康资源 |
-| `/backend/vO3T8gAdwB/data/classes/ProductDetail?limit=1` | 200 JSON | 正确的目标 REST class 资源 |
-| `/backend/vO3T8gAdwB/classes/ProductDetail?limit=1` | 404 HTML | 缺少 `/data` |
-| `/backend/vO3T8gAdwB/data/parse/classes/ProductDetail?limit=1` | 404 HTML | 多拼了一层 `/parse` |
-| `/parse/classes/ProductDetail?limit=1` | 200 JSON | 全局 Parse 路由也可命中同一应用凭据,但不是本项目约定入口 |
-
-因此,代码中的目标 Parse 配置值保持为:
-
-```text
-https://server.fmode.cn/backend/vO3T8gAdwB/data
-```
-
-真正的读取/写入 URL 由客户端拼接为:
-
-```text
-{JD_TARGET_PARSE_URL}/classes/ProductDetail
-```
-
-“修复 404”不是把配置改成 `/parse`,而是停止对挂载前缀做根 GET,并改用真实资源 endpoint 做 readiness 验证。
-
-### 5.2 写入时发现的 schema 冲突
-
-修复签名后首次正式写入返回:
-
-```text
-HTTP 400: schema mismatch for ProductDetail.itemStatus;
-expected String but got Object
-```
-
-原因是 `getProduct.productInfo.productStatus` 为对象,而旧归一化逻辑可能原样写入 `itemStatus`。现已新增 `extractProductStatus()`,按以下优先级提取标量:
-
-```text
-code -> status -> value -> name -> label -> flattenText
-```
-
-目标 `ProductDetail.itemStatus` 继续保持 String,避免破坏既有 schema。
-
-### 5.3 上游不稳定性
-
-对正确 class 资源各连续采样 10 次,`/backend/.../data/classes/...` 与 `/parse/classes/...` 都出现间歇性:
-
-```json
-{ "code": 119, "error": "unauthorized" }
-```
-
-表现为 HTTP 403,随后重试可恢复为 200。这说明是上游节点/鉴权漂移,而不是 class 路由缺失。当前导入器保留了针对这一**精确错误**的有限重试;不能把所有 401/403 都无条件重试。
-
-另一个实测差异是:`limit=0&count=1` 可能返回 `count: 0`,而普通列表请求能返回真实 `results`。因此现阶段不能把 Parse `count` 当作导入完成度真值。
-
-## 6. 修复文件
-
-| 文件 | 变更 |
-|---|---|
-| `.gitignore` | 忽略本机凭据文档 |
-| `tools/jd-import-products.mjs` | `productId` 参与详情签名;状态对象归一化为 String |
-| `tools/jd-detail-diagnostic.mjs` | 新增安全的 JD 签名变体与字段路径诊断;源 Parse 精确 403 有限重试 |
-| `tools/parse-target-diagnostic.mjs` | 新增 Parse 路由、schema、class 查询与稳定性诊断 |
-| `docs/JD-FIRST-BATCH-IMPORT.md` | 更新真实状态和正确 readiness 方式 |
-| `docs/LISTING-AI-IMPLEMENTATION-PLAN.md` | 在开发前固化实际实施计划 |
-
-## 7. 最终复测结果
-
-### dry-run
-
-```json
-{
-  "mode": "dry-run",
-  "totalProducts": 824,
-  "products": 1,
-  "skuMappings": 1,
-  "stockSnapshots": 1,
-  "detailAvailable": 1
-}
-```
-
-### 正式幂等写入
-
-```json
-{
-  "mode": "imported",
-  "totalProducts": 824,
-  "products": 1,
-  "skuMappings": 5,
-  "stockSnapshots": 5,
-  "detailAvailable": 1
-}
-```
-
-商品/SKU 数量由被抽中的真实商品决定,不能断言每个商品只有一个 SKU。
-
-### 本阶段完成判定
-
-- [x] 本机凭据文档不再出现在 Git 未跟踪列表中;
-- [x] 复现旧签名的 HTTP 400 与 JD 错误码 `99904000013`;
-- [x] 加入 `productId` 签名字段后同一商品 HTTP 200;
-- [x] 记录实际响应字段路径;
-- [x] 正确 Parse class URL HTTP 200;
-- [x] 修复 `itemStatus` schema mismatch;
-- [x] 正式导入 `detailAvailable=1`;
-- [x] 明确上游间歇性 403 和 `count` 不可信风险;
-- [ ] 全量分页导入与 824 个商品评分尚未执行,必须按实施计划分阶段完成。
-

+ 0 - 613
docs/LISTING-AI-IMPLEMENTATION-PLAN.md

@@ -1,613 +0,0 @@
-# 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 任务前必须关闭。

+ 0 - 182
docs/LISTING-AI-IMPLEMENTATION-REPORT-2026-08-21.md

@@ -1,182 +0,0 @@
-# Listing AI 评分工作台实施与验收报告
-
-> 日期:2026-08-21  
-> 范围:`Saas-voc` Angular 前端、`Saas-voc-server` 后端、目标 Parse 首轮全量数据  
-> 结论:**规则评分基线已完成;固定 Rubric 的真实 AI 评分已初步落地并仅对受控样本验证,未对全量商品调用模型。**
-
-## 0. 当前初步发布状态(优先于下文历史说明)
-
-- `listing-jd-v2` 继续作为 824 个商品的确定性规则基线;当前最新结果分布为 **823 个规则评分 + 1 个真实 AI 评分**。
-- `listing-jd-ai-v1` 不是自由打分:模型只返回固定原子标准的等级、证据、理由和置信度,服务端按冻结权重合成分数。
-- 真实成功样本 `10020401158454`:规则基线 100,AI 综合分 62,模型 `deepseek-v4-flash`,Prompt `listing-ai-score-p1`。
-- 相同 `sourceHash + rubricVersion + model + promptVersion` 再次评分复用同一结果,不重复调用模型;实测缓存任务约 3.5 秒完成。
-- 前后端均限制单个 AI 任务最多 10 个商品;11 个商品请求在创建任务前返回 HTTP 429 `listing_ai_budget_exceeded`。
-- AI 失败不会覆盖既有规则分;源数据不足时结果为 `partial`,不会把 `unknown` 伪装成 0 分或通过。
-- 当前已知 UX 限制:创建任务后会进入任务详情;`partial` 页面的“重试失败项”文案可能造成误解,后续应改为“部分完成/缺少输入证据”。
-
-## 1. 最终结果
-
-本轮 JD 列表接口返回的动态商品总数为 **824**。全量同步与冻结评分任务结果如下:
-
-| 指标 | 结果 |
-|---|---:|
-| 源快照(去重 productId) | 824 |
-| 同步成功 / 失败 | 824 / 0 |
-| 任务冻结总数 / terminal | 824 / 824 |
-| 有效数值评分 | 765 |
-| 部分结果 | 59 |
-| blocked / failed | 0 / 0 |
-| 平均分(仅统计有数值结果) | 97.0 |
-| missing / orphan / stale result | 0 / 0 / 0 |
-| 五维结构异常 | 0 |
-
-最新规则任务:`985b4dd5-f4c2-4d0c-9764-56652e496ed3`,rubric 为 `listing-jd-v2`,状态为 `partial`。824 个 item 全部完成,其中 59 个商品因上游缺少必需字段而保留部分结果;维度不可评分数为卖点 4、详情 22、规格 40,各维度有重叠。系统没有把缺失字段伪装成 0 分。
-
-`listing-jd-v1` 曾错误地把 JD `features` 中大量值为 0/1 的传输控制标记当成营销卖点,导致多数商品误中“合并重复卖点”扣分。按“rubric 不原地修改”的承重墙发布 v2:内部标记不再参与卖点评分;上游没有独立营销卖点字段时,重复性规则为 unknown 且不扣分。v1 结果仍保留用于审计。
-
-任务时间为 2026-08-21 05:35:22.517Z 至 05:41:21.010Z,约 5 分 58 秒。该数据只作为当前环境基线,不承诺固定 SLA。
-
-## 2. 已落地调用链
-
-```text
-源 Parse EcomAuth(服务端取最新 token)
-  -> JdTokenProvider
-  -> JdSpClient(path/query/header 签名、超时、重试)
-  -> JdProductClient(多页列表 + 商品详情)
-  -> sync-jd-listings(去重、并发、checkpoint、幂等)
-  -> ListingSourceSnapshot(Parse 或 PostgreSQL)
-  -> ListingAiService + listing-jd-v2 rule engine
-  -> Job / Item / Result / Version(可恢复、可审计)
-  -> /api/listing-ai/*(认证、workspace RBAC、Zod、cursor)
-  -> Angular ListingAiApiService / Store
-  -> 工作台 / 商品详情 / 任务 / 版本页面
-```
-
-规则基线由版本化确定性规则产生;AI 评分只对固定语义标准作结构化判定,数值权重和总分仍由服务端计算。AI 不覆盖 JD 源快照,也不调用 JD 写接口。
-
-## 3. 后端落地
-
-### 3.1 核心能力
-
-- JD `productId` 作为 path 签名字段参与 MD5,但不重复出现在 query;`itemStatus` 兼容对象/字符串返回。
-- 列表全分页、多轮防抖去重、详情并发 3、超时/有限重试、checkpoint 原子串行写入和 `--resume`。
-- JD 图片相对路径归一化到受控 HTTPS CDN;HTML 服务端 allowlist 清洗。
-- `listing-jd-v2` 五维评分:标题、核心卖点、主图、详情、规格;每维保存 coverage、ruleId、fieldPath、证据和建议。
-- `sourceHash + rubricVersion` 保证确定性和结果幂等;源变化后旧分不作为当前分返回,旧版本标记 `stale`。
-- 批任务冻结 productId + sourceHash,支持幂等键、进度、取消、失败/部分重试,以及进程启动时恢复 `queued/running` 任务。
-- AI 输出通过严格 Zod schema;上游失败时不写入 AI 分并保留既有规则分。`LISTING_AI_CONCURRENCY` 和 `LISTING_AI_MAX_ITEMS_PER_JOB` 对并发与单任务费用上限做 fail-closed 控制。
-- 工作台真实 AI 入口只接受已选商品,前端和后端共同限制每任务最多 10 个商品。API 使用 `scoringMode=ai|rules` 区分 `listing-jd-ai-v1` 和 `listing-jd-v2`;后端拒绝超限请求时不会创建任务。
-- 版本只在内部创建/采纳;采纳前再次校验 sourceHash,并写 audit log。
-- Parse REST 与 PostgreSQL 两种持久化驱动均已实现;新增 PostgreSQL migration `007_listing_ai.sql` 和 Parse schema。
-
-### 3.2 API
-
-| Method | Path | 用途 |
-|---|---|---|
-| GET | `/api/listing-ai/products` | 当前页商品 + 动态 summary/filter/sort/cursor |
-| GET | `/api/listing-ai/products/:productId` | 清洗后的源详情、当前分、版本摘要 |
-| GET | `/api/listing-ai/products/:productId/scores/latest` | 当前 sourceHash 的最新评分 |
-| POST | `/api/listing-ai/score-jobs` | 以选中 ID 或筛选快照创建冻结任务 |
-| GET | `/api/listing-ai/score-jobs` | 任务列表 |
-| GET | `/api/listing-ai/score-jobs/:jobId` | 任务进度 |
-| GET | `/api/listing-ai/score-jobs/:jobId/items` | terminal/item/error 明细 |
-| POST | `/api/listing-ai/score-jobs/:jobId/retry` | 合法状态下重试 |
-| POST | `/api/listing-ai/score-jobs/:jobId/cancel` | 合法状态下取消 |
-| GET | `/api/listing-ai/versions` | workspace 版本列表 |
-| GET/POST | `/api/listing-ai/products/:productId/versions` | 商品版本列表/创建草稿 |
-| GET | `/api/listing-ai/versions/:versionId` | 版本详情 |
-| POST | `/api/listing-ai/versions/:versionId/adopt` | 内部采纳,不发布 JD |
-
-所有 endpoint 位于现有 `/api` 认证中间件之后,并使用 `WorkspaceAccessService`。写操作要求 `analysis:run` 或 `action:write`,且发起、重试、取消、创建版本、采纳都会写审计。
-
-### 3.3 运维命令
-
-真实凭据只通过服务端环境变量加载,不进入命令参数、Angular、日志或文档。
-
-```powershell
-# 建表/建 Parse class 后,全量同步;中断后使用同一 checkpoint 恢复
-npm run sync:jd-listings -- --workspace <workspace-id> --checkpoint logs/jd-listing-sync.json --resume
-
-# 默认只执行确定性规则评分,不产生 AI 费用
-npm run score:jd-listings -- --workspace <workspace-id>
-
-# 校验 sources/results/job/items/orphan/stale/维度完整性
-npm run verify:listing-rollout -- --workspace <workspace-id>
-```
-
-## 4. Angular 落地
-
-新增 lazy route `/listing-ai`,包含:
-
-- `/listing-ai/workbench`:动态汇总、筛选/排序 URL 持久化、cursor 分页、选中/筛选批量评分;
-- `/listing-ai/workbench/:productId`:移动端预览、五维规则/AI证据、模型、Prompt、置信度和改进建议;
-- `/listing-ai/tasks`、`/listing-ai/tasks/:jobId`:任务列表、2–5 秒 polling、进度、item 错误、重试/取消;
-- `/listing-ai/versions`:版本、源 hash、状态、内容差异和内部采纳。
-
-京东 tab 可用,天猫/抖音/拼多多明确禁用并显示“即将支持”。前端代码没有写死 614、821 或 824,首屏仅请求 summary 和当前页。证据展示合并在 `listing-dimension-panel`,版本 diff 合并在版本页面,没有增加无业务逻辑的包装组件。
-
-## 5. 主要文件变动
-
-### 后端新增
-
-- `src/modules/listing-ai/domain.ts`
-- `src/modules/listing-ai/listing-ai.service.ts`
-- `src/modules/listing-ai/routes.ts`、`schemas.ts`
-- `src/modules/listing-ai/clients/*`
-- `src/modules/listing-ai/normalization/*`
-- `src/modules/listing-ai/scoring/rule-engine.ts`
-- `src/modules/listing-ai/repositories/*`
-- `migrations/007_listing_ai.sql`
-- `scripts/sync-jd-listings.ts`
-- `scripts/score-listings.ts`
-- `scripts/verify-listing-rollout.ts`
-- `test/listing-ai.*.test.ts`、`test/jd-sp-client.signature.test.ts`
-
-后端同时修改 `src/app.ts`、`src/server.ts`、`src/local-app.ts`、`src/local-server.ts`、`src/config/env.ts`、`src/db/parse-rest.schema.ts`、`.env.example` 和 `package.json`,只做依赖注入、路由、配置、readiness 和脚本接线。
-
-### 前端新增
-
-- `src/modules/listing-ai/models/*`
-- `src/modules/listing-ai/services/*`
-- `src/modules/listing-ai/components/*`
-- `src/modules/listing-ai/pages/*`
-- `src/modules/listing-ai/listing-ai.routes.ts`
-- `src/modules/listing-ai/listing-ai.shared.scss`
-
-前端同时修改 `src/app/app.routes.ts`、`src/app/core/config/runtime-config.ts` 和现有导航配置。未删除旧 route、旧页面、旧表或旧 Parse class。
-
-## 6. 验证证据
-
-| 验证 | 结果 |
-|---|---|
-| 后端 `npm run typecheck` | 通过 |
-| 后端 `npm test` | 80/80 通过 |
-| Angular production build | 通过,hash `a89262792de71d81` |
-| Angular Listing AI 定向测试 | 2/2 ChromeHeadless 通过 |
-| 本地 API smoke | 目录分页、任务创建/执行、partial 语义通过 |
-| 真实 JD shadow | 20、100、824 逐级执行;最终同步 824/824 |
-| 真实规则评分 | 824/824 terminal;765 scored、59 partial、0 blocked、0 failed |
-| rollout verifier | missing/orphan/stale/invalidDimensions 均为 0 |
-
-## 7. 承重墙验证
-
-- 没有让 Angular 直连 JD 或 Parse;真实密钥只存在于被 `.gitignore` 命中的本地配置/服务端环境变量。
-- 没有修改现有 VOC 的 `analysis_run` 状态机,也没有改写 insight -> decision -> action -> validation 链路。
-- productId 全程按字符串处理;不从库存推导销量,不把 Listing 当 VOC。
-- 源快照、评分、建议版本分层;采纳不覆盖源,不调用 JD 写接口。
-- Parse 健康检查使用 class 资源;挂载根 404 不再被误判,HTTP 403/code119 只做有限重试。
-
-## 8. 尚未解除的发布门禁
-
-1. **AI 成本与质量批准**:当前硬限制为每任务最多 10 个商品,并且只完成受控样本验证;在人工 golden set、一致性回归、总预算和日预算冻结前,不开放 824 商品全量 AI 评分。
-2. **rubric 业务冻结**:工程规则已升级为 `listing-jd-v2` 并保留 v1 审计记录;标题阈值、核心卖点口径仍需产品/运营对 20 个样本人工复核。后续口径变化继续发布新版本,不覆盖既有结果。
-3. **多实例强租约**:当前启动恢复、幂等键和数据库唯一约束满足单 worker/受控部署恢复;若部署多个并发 worker 实例,需在领取任务前增加数据库级 lease/`SKIP LOCKED`。在此之前,同 workspace 只启一个 Listing score worker。
-
-以上门禁不影响已经完成的 824 商品确定性评分和工作台联调,但会阻止“全量 AI 优化建议”和多实例 worker 直接开放生产。
-
-### 8.1 任务状态语义
-
-- 创建成功后前端当前会导航到任务详情,用于观察异步执行进度;这是现有交互,不是服务端重定向。
-- `failed` 表示模型、网络、schema 或存储失败;此时不覆盖规则分。
-- `partial` 表示模型调用可能已经成功,但至少一个固定维度因源数据不足被判为 `unknown`,因此不生成伪造总分。
-- 例如商品 `10020422869649` 的真实 AI 请求已完成,但京东输入没有独立卖点文本,卖点维度为 `unknown`,任务因此为 `partial` 而不是调用失败。