状态:已实施并完成首轮全量验收(2026-08-21)
适用仓库:Saas-voc前端、Saas-voc-server后端
本文是实施契约;实际落地、全量数据结果和保留门禁见LISTING-AI-IMPLEMENTATION-REPORT-2026-08-21.md。任何字段、路由、表结构或验收口径变更,都应先修改本文并记录原因。
产品目标确认是真实 Listing AI 评分,不是“规则评分后由 AI 生成优化文案”。因此新增 listing-jd-ai-v1,并保留 listing-jd-v2 作为规则基线:
unknown/fail/weak/pass/strong、原文证据、理由和置信度;sourceHash + rubricVersion + model + promptVersion 稳定键;同键命中既有结果时不再次调用模型;模型、Prompt 或权重变化必须发布新版本,不覆盖旧结果;listing-jd-v2 规则分,并在 UI 标注“规则评分,尚未执行 AI 评分”;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 分即不允许扩大样本。
针对京东授权店铺当前 API 返回的全部商品,完成一个可恢复、可审计、可解释的 Listing 评分批任务,并提供:
页面上显示的商品总数必须来自同步摘要/API,不得写死 614、821 或本次实测的 824。
GET /sp-product/v0/products 返回动态总数,本次为 824。GET /sp-product/v0/products/{productId}?scene=pop 已成功;productId 必须参与 MD5 签名。{JD_TARGET_PARSE_URL}/classes/<ClassName>;挂载前缀根 GET 的 404 不能作为不可用证据。count=1 在当前托管环境不可靠。page=1,且 --limit 上限为 20,只能做联调,不能承担全量生产同步。DomesticDatasetService 加载的是 VOC 快照,不是本店实时 Listing 目录;不能用它替代 Listing 源数据。listing-api.service.ts 是停用的 Amazon 旧边界,不复用。复测原始结论见 JD-PRODUCT-DETAIL-PARSE-VERIFICATION-2026-08-21.md。
源 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 版本、评分类型和证据。
| 路由 | 页面类型 | 职责 |
|---|---|---|
/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。
page-header:标题、数据更新时间、京东状态、保存筛选、发起评分任务。page-toolbar:搜索、类目、上下架、评分状态、分数区间、数据完整度、更新时间、排序。data-table-shell:商品、标题、类目、SKU 数、数据完整度、总分、五维分、rubric 版本、最后评分时间、状态、操作。board-tabs 五个维度:标题、核心卖点、主图、详情、规格。ListingVersion.status=adopted 和审计事件,不调用京东发布。—,同时显示 not_returned、empty 或 parse_failed,不能显示为 0。queued -> running -> completed | partial | failed | cancelled。queued -> rules_scored -> ai_pending -> scored | partial | blocked | failed。jobId 恢复。后端先保留受控 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 | 必填 |
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 字符串。
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[];
}
overallScore 只在所有必需维度满足 rubric 的最低覆盖门槛时计算。coverage 与 quality score 是两条独立指标。没有详情不能得“详情 0 分”,而应是 score=null,status=blocked。listing-jd-v1 开始;发现 v1 将 JD 传输控制标记误判为重复卖点后,未原地修改,而是发布 listing-jd-v2 并完成全量重算,v1 继续保留审计。| 维度 | 可确定性评分 | 仅建议/待确认 |
|---|---|---|
| 标题 | 是否存在、长度、品牌/品类/型号覆盖、重复片段、异常符号、SKU 冲突 | 类目禁限词词库需业务确认 |
| 核心卖点 | features 数量、非空、重复、规格覆盖、服务信息覆盖 | 京东是否有独立卖点字段需更多样本确认 |
| 主图 | 图片数、主图标识、顺序、URL 结构/可访问、重复 URL | 构图、清晰度、文字占比需多模态模型,MVP 不评分 |
| 详情 | desktop/mobile 是否存在、清洗后文本长度、结构、两端一致性、危险标签 | 内容说服力由 AI 建议,不直接决定数值分 |
| 规格 | SPU/SKU 属性数量、必填规格覆盖、尺寸重量、属性一致性 | 类目必填属性清单需要规则数据源 |
在 20 商品 shadow benchmark 完成前,不把暂定阈值写死为产品真理。阈值由 rubric JSON 管理,并在样本评审后冻结 v1。
新增 src/modules/listing-ai/,不把评分任务塞进现有 analysis_run。现有 analysis_run 的类型和生命周期服务于 VOC 洞察,强行复用会污染语义和权限。
实际落地结构(小文件按职责合并,避免无意义拆分):
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 里写条件分支。
现有 tools/jd-import-products.mjs 仅保留为联调工具。生产同步必须满足:
(shopId, productId) 作为身份,现有兼容业务键 jd:${shopId}:${productId} 保留;sourceHash、sourceModifiedAt、syncedAt、detailStatus 和错误分类;ProductDetail、Product、AsinSkuMapping、JdStockSnapshot 兼容写入先保留;迁移完成前不删类、不改业务键;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 结果存储。
FmodeAiClient,前端不再逐商品请求 /api/ai/chat/completions;ai_invalid_output 并可重试;LISTING_AI_CONCURRENCY 与 LISTING_AI_MAX_ITEMS_PER_JOB 配置并发和单任务硬预算;超限在入队前返回 429,未配置模型时规则结果保留且任务为 partial,不无限重试。跨任务每日总预算仍是全量 AI 发布门禁。统一前缀:/api/listing-ai。沿用现有 AuthenticationMiddleware、WorkspaceAccessService、Zod、ApiError、audit,不创造第二套鉴权。
GET /api/listing-ai/products?workspaceId=&platform=jd&limit=25&cursor=&search=&categoryId=&itemStatus=&scoreStatus=&minScore=&maxScore=&coverageStatus=&sort=
{
"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。
GET /api/listing-ai/products/:productId?workspaceId=&platform=jd
返回 source、coverage、latestScore、versionsSummary。默认不返回完整 raw JD JSON。
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。
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
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,防止采纳过期建议。
| 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 的终态和脱敏错误码。
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。
| 文件 | 修改内容 |
|---|---|
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 代理不能覆盖时才改;优先不改 |
必须优先复用 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 扩展,并给现有用例回归测试。
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。
| 文件 | 修改内容 |
|---|---|
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 显式脚本 |
第一阶段不删除任何现有文件、route、表或 Parse class。tools/jd-import-products.mjs 在服务端同步达到字段、幂等和故障恢复对等后才能标记 deprecated;再经过一个发布周期才讨论删除。
/api 认证中间件、WorkspaceAccessService 和权限名;生产不能启用 disabled auth。jd:${shopId}:${productId} 兼容业务键,不用 legacy asin 取代它。analysis_run enum 来承载 Listing 批任务;不破坏 insight -> decision -> action -> validation 链路。.gitignore;itemStatus schema 归一化;productId 在签名串字段中但不在 query 中;99904000013;available|empty|failed 明确详情状态;ruleId 和 JD/DTO fieldPath;ng build、Listing API service 单测和后端关键路由集成测试通过。git check-ignore docs/JD-CONNECTION-CONFIG.local.md 命中;dist、source map、浏览器请求和日志扫描无真实凭据;API_AUTH_MODE=parse,跨 workspace 访问用例返回 403;completed 仅表示全部成功;混合结果必须是 partial;| 风险/问题 | 当前处理 | 谁确认/何时阻塞 |
|---|---|---|
| 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,另建任务 |
queued/running 任务。多实例 lease 是正式横向扩容前的门禁,见实施报告。ProductDetail 兼容镜像在新 source repository 稳定一个发布周期前不移除。只有以下条件同时满足才开始 PR-1,而不是直接画页面:
itemStatus schema mismatch 已修复;前三个未决责任项可以与 PR-1 的纯同步工程并行,但在 PR-2 rubric 冻结和 PR-3 全量 AI 任务前必须关闭。