fix(bom-costing): classify flip shoe cabinets correctly
This commit is contained in:
parent
413debe66b
commit
7c53b42199
@ -1576,13 +1576,16 @@ describe('BomCostingWorkbenchClient', () => {
|
||||
expect(await screen.findByText('预计出厂成本')).toBeInTheDocument()
|
||||
const dynamicCostNotice = screen.getByTestId('bom-dynamic-cost-partial-notice')
|
||||
expect(dynamicCostNotice).toHaveTextContent(
|
||||
'1/3 个 SKU 已形成完整预估',
|
||||
'1/3 个 SKU 的原材质预估已完成',
|
||||
)
|
||||
expect(dynamicCostNotice).toHaveTextContent(
|
||||
'这是原材质批次结果,不代表当前材质替换方案已经完成',
|
||||
)
|
||||
expect(dynamicCostNotice).toHaveTextContent(
|
||||
'失败规格 · 事务启动超时',
|
||||
)
|
||||
expect(screen.getByRole('alert', {
|
||||
name: '本轮成本部分完成,包含成本生成失败',
|
||||
name: '原材质批次部分完成,包含成本生成失败',
|
||||
})).toBe(dynamicCostNotice)
|
||||
const failedSkuButton = screen.getByRole('button', { name: /失败规格/ })
|
||||
expect(within(failedSkuButton).getByText('生成失败')).toBeInTheDocument()
|
||||
|
||||
@ -2294,16 +2294,16 @@ function ResultCanvas({
|
||||
className="shrink-0 border-b border-[#b98743]/25 bg-[#fbf8f0]"
|
||||
role={dynamicCompletionNotice.failedSkuCount > 0 ? 'alert' : 'status'}
|
||||
aria-label={dynamicCompletionNotice.failedSkuCount > 0
|
||||
? '本轮成本部分完成,包含成本生成失败'
|
||||
: '本轮成本部分完成'}
|
||||
? '原材质批次部分完成,包含成本生成失败'
|
||||
: '原材质批次部分完成'}
|
||||
data-testid="bom-dynamic-cost-partial-notice"
|
||||
>
|
||||
<div className="mx-auto flex w-full max-w-[1500px] items-start gap-2 px-4 py-2.5 text-[10px] leading-4 text-[#715632] xl:px-5">
|
||||
<CircleAlert size={14} className="mt-0.5 shrink-0" aria-hidden="true" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-bold">本轮成本部分完成</p>
|
||||
<p className="font-bold">原材质批次部分完成</p>
|
||||
<p className="mt-0.5 text-[#806944]">
|
||||
{dynamicCompletionNotice.costedSkuCount}/{dynamicCompletionNotice.totalSkuCount} 个 SKU 已形成完整预估
|
||||
{dynamicCompletionNotice.costedSkuCount}/{dynamicCompletionNotice.totalSkuCount} 个 SKU 的原材质预估已完成
|
||||
{dynamicCompletionNotice.pendingSkuCount > 0
|
||||
? `,${dynamicCompletionNotice.pendingSkuCount} 个待补成本`
|
||||
: ''}
|
||||
@ -2312,7 +2312,7 @@ function ResultCanvas({
|
||||
: ''}
|
||||
{dynamicCompletionNotice.reviewSkuCount > 0
|
||||
? `,${dynamicCompletionNotice.reviewSkuCount} 个待复核`
|
||||
: ''}。成功 SKU 仍可正常使用,其余 SKU 可按现有事实重新核算。
|
||||
: ''}。这是原材质批次结果,不代表当前材质替换方案已经完成;其余 SKU 可按现有事实重新核算。
|
||||
</p>
|
||||
{dynamicCompletionNotice.failures.length > 0 ? (
|
||||
<ul className="mt-1 space-y-0.5 text-[#806944]">
|
||||
@ -3775,7 +3775,7 @@ export default function BomCostingWorkbenchClient({
|
||||
dynamicNotice && (
|
||||
dynamicNotice.failedSkuCount > 0 || dynamicNotice.pendingSkuCount > 0
|
||||
)
|
||||
? '本轮成本部分完成,待补 SKU 已列出'
|
||||
? '原材质批次部分完成,待补 SKU 已列出'
|
||||
: 'BOM 核算已完成',
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,205 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import type { BomCostingMultimodalProviderConfig } from './bom-costing-multimodal-gateway'
|
||||
import { buildStructuredBomCostingAnalysis } from './bom-costing-structure-blueprint'
|
||||
import {
|
||||
buildBomCostingCategoryGroupingKeys,
|
||||
buildBomCostingCategorySkillContext,
|
||||
selectBomCostingCategorySkill,
|
||||
} from './category-skills'
|
||||
import { buildDeterministicStructureBlueprint } from './bom-costing-structure-knowledge'
|
||||
import {
|
||||
PRODUCTION_FLIP_SHOE_CABINET_10_SKUS,
|
||||
buildProductionFlipShoeCabinet10FactSnapshot,
|
||||
buildProductionFlipShoeCabinet10Snapshot,
|
||||
} from './test-fixtures/production-flip-shoe-cabinet-10.fixture'
|
||||
|
||||
const config: BomCostingMultimodalProviderConfig = {
|
||||
baseUrl: 'https://ai.example.com/v1',
|
||||
apiKey: 'test-key',
|
||||
model: 'test-vision',
|
||||
}
|
||||
|
||||
describe('BOM v7 翻斗鞋柜生产形态回归', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('局部置物架和“桌面”事实不再把整件鞋柜排除或误判成桌类', () => {
|
||||
const snapshot = buildProductionFlipShoeCabinet10Snapshot()
|
||||
const factSnapshot = buildProductionFlipShoeCabinet10FactSnapshot()
|
||||
const selection = selectBomCostingCategorySkill(
|
||||
buildBomCostingCategorySkillContext({ snapshot, factSnapshot }),
|
||||
)
|
||||
|
||||
expect(selection.status).toBe('SUPPORTED')
|
||||
expect(selection.skill).toMatchObject({
|
||||
code: 'CABINET_BOOKSHELF',
|
||||
version: 'bom-category-skill/cabinet-bookshelf/v3',
|
||||
})
|
||||
const cabinet = selection.candidates.find((candidate) => (
|
||||
candidate.code === 'CABINET_BOOKSHELF'
|
||||
))!
|
||||
const table = selection.candidates.find((candidate) => candidate.code === 'TABLE')!
|
||||
expect(cabinet.exclusionSignals).toEqual([])
|
||||
expect(cabinet.matchedSignals.flatMap((signal) => signal.sources))
|
||||
.not.toContain('FACT')
|
||||
expect(table.matchedSignals).toEqual([])
|
||||
})
|
||||
|
||||
it('10 个真实 SKU 按翻斗数量、上架和背板形成稳定柜体结构组', () => {
|
||||
const snapshot = buildProductionFlipShoeCabinet10Snapshot()
|
||||
const factSnapshot = buildProductionFlipShoeCabinet10FactSnapshot()
|
||||
const selection = selectBomCostingCategorySkill(
|
||||
buildBomCostingCategorySkillContext({ snapshot, factSnapshot }),
|
||||
)
|
||||
const groupings = snapshot.skus.map((sku) => ({
|
||||
sku,
|
||||
grouping: buildBomCostingCategoryGroupingKeys({
|
||||
skill: selection.skill!,
|
||||
snapshot,
|
||||
sku,
|
||||
factSnapshot,
|
||||
}),
|
||||
}))
|
||||
|
||||
expect(groupings).toHaveLength(PRODUCTION_FLIP_SHOE_CABINET_10_SKUS.length)
|
||||
for (const { grouping } of groupings) {
|
||||
expect(grouping.structureDifferenceKeys).toEqual(expect.arrayContaining([
|
||||
'FLIP_SHOE_CABINET',
|
||||
'SHOE_FLIP_COUNT_3',
|
||||
]))
|
||||
expect(grouping.structureDifferenceKeys).not.toContain('DRAWER_PRESENT')
|
||||
}
|
||||
expect(groupings.filter(({ sku }) => sku.name.includes('上架'))).toHaveLength(5)
|
||||
expect(groupings.filter(({ grouping }) => (
|
||||
grouping.structureDifferenceKeys.includes('UPPER_OPEN_RACK')
|
||||
))).toHaveLength(5)
|
||||
expect(groupings.filter(({ grouping }) => (
|
||||
grouping.structureDifferenceKeys.includes('NO_BACK_PANEL')
|
||||
)).map(({ sku }) => sku.externalSkuId).sort()).toEqual([
|
||||
'5984281680356',
|
||||
'5984281680357',
|
||||
])
|
||||
expect(groupings.find(({ sku }) => sku.externalSkuId === '5984281680348')?.grouping)
|
||||
.toMatchObject({ materialFamily: '橡木' })
|
||||
expect(groupings.find(({ sku }) => sku.externalSkuId === '5984281680356')?.grouping)
|
||||
.toMatchObject({ materialFamily: '杨木' })
|
||||
|
||||
const blueprint = buildDeterministicStructureBlueprint({ snapshot, factSnapshot })
|
||||
expect(blueprint.supportStatus).toBe('SUPPORTED')
|
||||
expect(blueprint.categorySkill).toMatchObject({
|
||||
selectedSkillId: 'CABINET_BOOKSHELF',
|
||||
selectedSkillVersion: 'bom-category-skill/cabinet-bookshelf/v3',
|
||||
})
|
||||
const groupFor = (skuId: string) => blueprint.structureGroups.find((group) => (
|
||||
group.skuIds.includes(skuId)
|
||||
))!
|
||||
const requiredKeys = (skuId: string) => new Set(groupFor(skuId).componentSlots
|
||||
.filter((slot) => slot.required)
|
||||
.map((slot) => slot.componentKey))
|
||||
|
||||
const baseKeys = requiredKeys('5984281680348')
|
||||
expect([...baseKeys]).toEqual(expect.arrayContaining([
|
||||
'enclosure-side',
|
||||
'enclosure-top',
|
||||
'enclosure-bottom',
|
||||
'back-panel',
|
||||
'cover-shoe-flip-front',
|
||||
'shoe-flip-bin-assembly',
|
||||
'motion-shoe-flip',
|
||||
]))
|
||||
for (const unexpectedKey of [
|
||||
'partition-shelf',
|
||||
'cover-door',
|
||||
'motion-hinge',
|
||||
'drawer-box',
|
||||
'motion-drawer',
|
||||
'upper-rack-side',
|
||||
]) {
|
||||
expect(baseKeys.has(unexpectedKey)).toBe(false)
|
||||
}
|
||||
expect(groupFor('5984281680348').componentSlots.find(
|
||||
(slot) => slot.componentKey === 'cover-shoe-flip-front',
|
||||
)?.quantityPolicy.baseQuantity).toBe(3)
|
||||
expect(groupFor('5984281680348').componentSlots.find(
|
||||
(slot) => slot.componentKey === 'enclosure-side',
|
||||
)?.evidenceFactIds).not.toContain('shared-flip-bin-paulownia')
|
||||
expect(groupFor('5984281680348').componentSlots.find(
|
||||
(slot) => slot.componentKey === 'shoe-flip-bin-assembly',
|
||||
)?.evidenceFactIds).toContain('shared-flip-bin-paulownia')
|
||||
|
||||
const upperKeys = requiredKeys('5984281680349')
|
||||
expect([...upperKeys]).toEqual(expect.arrayContaining([
|
||||
'upper-rack-side',
|
||||
'upper-rack-horizontal',
|
||||
'upper-rack-hanging-kit',
|
||||
]))
|
||||
expect(requiredKeys('5984281680356').has('back-panel')).toBe(false)
|
||||
})
|
||||
|
||||
it('离线回退生成真实鞋柜 BOM,不再出现桌腿或桌面下框', async () => {
|
||||
const snapshot = buildProductionFlipShoeCabinet10Snapshot()
|
||||
const factSnapshot = buildProductionFlipShoeCabinet10FactSnapshot()
|
||||
jest.spyOn(global, 'fetch').mockRejectedValue(new Error('offline fixture'))
|
||||
|
||||
const { result, trace } = await buildStructuredBomCostingAnalysis({
|
||||
config,
|
||||
sourceUrl: snapshot.detailUrl,
|
||||
snapshot,
|
||||
factSnapshot,
|
||||
})
|
||||
|
||||
expect(result.skus).toHaveLength(PRODUCTION_FLIP_SHOE_CABINET_10_SKUS.length)
|
||||
expect(trace.fallbackSkuIds).toHaveLength(PRODUCTION_FLIP_SHOE_CABINET_10_SKUS.length)
|
||||
for (const fixtureSku of PRODUCTION_FLIP_SHOE_CABINET_10_SKUS) {
|
||||
const sku = result.skus.find((candidate) => (
|
||||
candidate.externalSkuId === fixtureSku.externalSkuId
|
||||
))!
|
||||
const linesByKey = new Map(sku.bomLines.map((line) => [
|
||||
line.externalLineId,
|
||||
line,
|
||||
] as const))
|
||||
for (const unexpectedKey of [
|
||||
'surface-main',
|
||||
'support-leg',
|
||||
'support-apron',
|
||||
'partition-shelf',
|
||||
'drawer-box',
|
||||
'motion-drawer',
|
||||
]) {
|
||||
expect(linesByKey.has(unexpectedKey)).toBe(false)
|
||||
}
|
||||
expect(linesByKey.get('cover-shoe-flip-front')).toMatchObject({
|
||||
quantity: 3,
|
||||
unit: '块',
|
||||
lengthMm: fixtureSku.name.includes('0.85米') ? 850 : 650,
|
||||
widthMm: 350,
|
||||
})
|
||||
expect(linesByKey.get('shoe-flip-bin-assembly')).toMatchObject({
|
||||
quantity: 3,
|
||||
unit: '套',
|
||||
materialName: '泡桐木',
|
||||
})
|
||||
expect(linesByKey.get('motion-shoe-flip')).toMatchObject({
|
||||
quantity: 3,
|
||||
unit: '套',
|
||||
})
|
||||
if (fixtureSku.upperRack) {
|
||||
expect([...linesByKey.keys()]).toEqual(expect.arrayContaining([
|
||||
'upper-rack-side',
|
||||
'upper-rack-horizontal',
|
||||
'upper-rack-hanging-kit',
|
||||
]))
|
||||
} else {
|
||||
expect(linesByKey.has('upper-rack-side')).toBe(false)
|
||||
}
|
||||
expect(linesByKey.has('back-panel')).toBe(!fixtureSku.noBackPanel)
|
||||
}
|
||||
|
||||
const oakBase = result.skus.find((sku) => sku.externalSkuId === '5984281680348')!
|
||||
const oakLines = new Map(oakBase.bomLines.map((line) => [line.externalLineId, line]))
|
||||
expect(oakLines.get('enclosure-side')).toMatchObject({ materialName: '橡木' })
|
||||
expect(oakLines.get('back-panel')).toMatchObject({ materialName: '泡桐木' })
|
||||
})
|
||||
})
|
||||
@ -54,6 +54,11 @@ describe('BOM v7 移动藤编双层置物架生产形态回归', () => {
|
||||
expect(selection.status).toBe('SUPPORTED')
|
||||
expect(selection.skill?.code).toBe('STORAGE_RACK')
|
||||
expect(selection.skill?.version).toBe('bom-category-skill/storage-rack/v2')
|
||||
expect(selection.candidates.find((candidate) => (
|
||||
candidate.code === 'STORAGE_RACK'
|
||||
))?.matchedSignals.find((signal) => (
|
||||
signal.signalKey === 'mobile-storage-trolley-topology'
|
||||
))?.sources).toContain('FACT')
|
||||
|
||||
const groupings = snapshot.skus.map((sku) => buildBomCostingCategoryGroupingKeys({
|
||||
skill: selection.skill!,
|
||||
|
||||
@ -218,7 +218,7 @@ const supportedV3AnalysisResult = {
|
||||
schemaVersion: 'competitor-analysis/v3' as const,
|
||||
structureBlueprint: {
|
||||
schemaVersion: 'structure-blueprint/v2' as const,
|
||||
knowledgeVersion: 'structure-priors/2026-08-13-v9',
|
||||
knowledgeVersion: 'structure-priors/2026-08-14-v10',
|
||||
supportStatus: 'SUPPORTED' as const,
|
||||
categorySkill: {
|
||||
selectedSkillId: 'TABLE',
|
||||
|
||||
@ -276,7 +276,7 @@ async function runStructurePipeline(orderedFixture: readonly ProductionWardrobe2
|
||||
}
|
||||
const checkpoint = readBomCostingStructureCheckpoint({
|
||||
kind: 'BOM_COSTING_STRUCTURE_CHECKPOINT',
|
||||
version: 13,
|
||||
version: 14,
|
||||
sourceSkuIds: snapshot.skus.map((sku) => sku.externalSkuId),
|
||||
blueprint,
|
||||
completedGroupKeys: [],
|
||||
|
||||
@ -199,7 +199,7 @@ describe('BOM v7 结构蓝图与 SKU 展开', () => {
|
||||
},
|
||||
)
|
||||
|
||||
it('当前 v11/v2 检查点可恢复,旧 v10 检查点失效', async () => {
|
||||
it('当前 v14/v2 检查点可恢复,旧 v13 检查点失效', async () => {
|
||||
const snapshot = snapshotWithSkus(1)
|
||||
const factSnapshot = {
|
||||
schemaVersion: BOM_COSTING_FACT_SCHEMA_VERSION,
|
||||
@ -222,7 +222,7 @@ describe('BOM v7 结构蓝图与 SKU 展开', () => {
|
||||
const group = blueprint.structureGroups[0]!
|
||||
const checkpointValue = {
|
||||
kind: 'BOM_COSTING_STRUCTURE_CHECKPOINT',
|
||||
version: 13,
|
||||
version: 14,
|
||||
sourceSkuIds: ['sku-1'],
|
||||
blueprint,
|
||||
completedGroupKeys: [group.structureGroupKey],
|
||||
@ -246,7 +246,7 @@ describe('BOM v7 结构蓝图与 SKU 展开', () => {
|
||||
}
|
||||
expect(readBomCostingStructureCheckpoint({
|
||||
...checkpointValue,
|
||||
version: 11,
|
||||
version: 13,
|
||||
})).toBeNull()
|
||||
expect(readBomCostingStructureCheckpoint({
|
||||
...checkpointValue,
|
||||
@ -307,7 +307,7 @@ describe('BOM v7 结构蓝图与 SKU 展开', () => {
|
||||
)
|
||||
const checkpoint = readBomCostingStructureCheckpoint({
|
||||
kind: 'BOM_COSTING_STRUCTURE_CHECKPOINT',
|
||||
version: 13,
|
||||
version: 14,
|
||||
sourceSkuIds: snapshot.skus.map((sku) => sku.externalSkuId),
|
||||
blueprint,
|
||||
completedGroupKeys: blueprint.structureGroups.map((group) => group.structureGroupKey),
|
||||
@ -889,7 +889,7 @@ describe('BOM v7 结构蓝图与 SKU 展开', () => {
|
||||
|
||||
expect(output.result.structureBlueprint.categorySkill).toMatchObject({
|
||||
selectedSkillId: 'CABINET_BOOKSHELF',
|
||||
selectedSkillVersion: 'bom-category-skill/cabinet-bookshelf/v2',
|
||||
selectedSkillVersion: 'bom-category-skill/cabinet-bookshelf/v3',
|
||||
})
|
||||
for (const sku of output.result.skus) {
|
||||
const byKey = new Map(sku.bomLines.map((line) => [line.externalLineId, line]))
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
import type { BomCostingSourceFactSnapshot } from '@/lib/bom-costing/fact-contracts'
|
||||
import type { MarketDetailSnapshot, MarketSkuSnapshot } from './market-detail-client'
|
||||
import {
|
||||
bomCostingMaterialSubjectIncludesTerm,
|
||||
buildDeterministicStructureBlueprint,
|
||||
} from './bom-costing-structure-knowledge'
|
||||
import { listBomCostingFactRecords } from './bom-costing-fact-evidence'
|
||||
@ -140,9 +141,10 @@ const instantiationPayloadSchema = z.object({
|
||||
|
||||
const BOM_COSTING_STRUCTURE_CHECKPOINT_KIND =
|
||||
'BOM_COSTING_STRUCTURE_CHECKPOINT' as const
|
||||
// Version 13 invalidates instantiated lines created before unrecognized
|
||||
// component material prose stopped shadowing a resolved primary cost family.
|
||||
const BOM_COSTING_STRUCTURE_CHECKPOINT_VERSION = 13 as const
|
||||
// Version 14 invalidates checkpoints created before whole-product category
|
||||
// signals were separated from local component facts and flip-shoe cabinet
|
||||
// topology became an explicit Skill contract.
|
||||
const BOM_COSTING_STRUCTURE_CHECKPOINT_VERSION = 14 as const
|
||||
|
||||
const bomCostingStructureCheckpointSchema = z.object({
|
||||
kind: z.literal(BOM_COSTING_STRUCTURE_CHECKPOINT_KIND),
|
||||
@ -806,6 +808,7 @@ const MATERIAL_DESCRIPTOR_PATTERNS = [
|
||||
{ pattern: /橡木(?!芯板)/i, descriptor: '橡木', family: 'OAK' },
|
||||
{ pattern: /碳化杨木/i, descriptor: '碳化杨木', family: 'POPLAR' },
|
||||
{ pattern: /杨木/i, descriptor: '杨木', family: 'POPLAR' },
|
||||
{ pattern: /泡桐木|泡桐/i, descriptor: '泡桐木', family: 'PAULOWNIA' },
|
||||
{ pattern: /松木/i, descriptor: '松木', family: 'PINE' },
|
||||
{ pattern: /桦木/i, descriptor: '桦木', family: 'BIRCH' },
|
||||
{ pattern: /多层板/i, descriptor: '多层板', family: 'PLYWOOD' },
|
||||
@ -907,6 +910,9 @@ const COMPONENT_MATERIAL_TERM_GROUPS = [
|
||||
['小挂钩'],
|
||||
['大挂钩'],
|
||||
['翻盖板', '后段板', '后侧盖板'],
|
||||
['翻斗门前板', '翻斗面', '翻斗门', '鞋柜翻门'],
|
||||
['翻斗盒', '鞋斗', '翻斗收纳格', '翻斗式鞋盒', '翻斗式鞋仓'],
|
||||
['上架', '上架竖板', '上架横板', '上架侧板'],
|
||||
['X形桌架', 'X型桌架', 'X形侧架', 'X型侧架'],
|
||||
] as const
|
||||
|
||||
@ -936,15 +942,14 @@ function materialFactExplicitlyBindsSlot(
|
||||
if (/木质部件|木质构件|木件材质/.test(subject) && slot.category === 'BOARD') return true
|
||||
if (/金属部件|金属构件|金属件材质/.test(subject) && slot.category === 'STEEL') return true
|
||||
return COMPONENT_MATERIAL_TERM_GROUPS.some((terms) => (
|
||||
terms.some((term) => subject.includes(normalizeMaterialScopeText(term))) &&
|
||||
terms.some((term) => bomCostingMaterialSubjectIncludesTerm(record.fact.subject, term)) &&
|
||||
terms.some((term) => slotText.includes(normalizeMaterialScopeText(term)))
|
||||
))
|
||||
}
|
||||
|
||||
function materialFactNamesConcreteComponent(record: MaterialFactRecord) {
|
||||
const subject = normalizeMaterialScopeText(record.fact.subject)
|
||||
return COMPONENT_MATERIAL_TERM_GROUPS.some((terms) => (
|
||||
terms.some((term) => subject.includes(normalizeMaterialScopeText(term)))
|
||||
terms.some((term) => bomCostingMaterialSubjectIncludesTerm(record.fact.subject, term))
|
||||
))
|
||||
}
|
||||
|
||||
@ -1224,6 +1229,30 @@ function resolveRearTopDepthMm(input: {
|
||||
return uniqueValues.length === 1 ? uniqueValues[0] : undefined
|
||||
}
|
||||
|
||||
function resolveShoeFlipFrontHeightMm(input: {
|
||||
slot: BomCostingStructureBlueprintV2['structureGroups'][number]['componentSlots'][number]
|
||||
sku: MarketSkuSnapshot
|
||||
factSnapshot: BomCostingSourceFactSnapshot
|
||||
}) {
|
||||
const allowedIds = new Set(input.slot.evidenceFactIds)
|
||||
const values = listBomCostingFactRecords(input.factSnapshot)
|
||||
.filter((record) => {
|
||||
if (
|
||||
!allowedIds.has(record.factId) ||
|
||||
record.fact.kind !== 'DIMENSION' ||
|
||||
record.fact.basis === 'VISUAL_ESTIMATE' ||
|
||||
record.fact.confidence < 0.75 ||
|
||||
!factAppliesToSku(record, input.sku.externalSkuId)
|
||||
) return false
|
||||
const context = `${record.fact.subject} ${record.fact.note ?? ''}`
|
||||
return /每层|单层|翻斗门|翻斗面|鞋斗前板/.test(context) && /高|高度/.test(context)
|
||||
})
|
||||
.map(factMillimeters)
|
||||
.filter((value): value is number => Boolean(value && value > 0))
|
||||
const uniqueValues = Array.from(new Set(values))
|
||||
return uniqueValues.length === 1 ? uniqueValues[0] : undefined
|
||||
}
|
||||
|
||||
function deterministicBoardGeometry(input: {
|
||||
slot: BomCostingStructureBlueprintV2['structureGroups'][number]['componentSlots'][number]
|
||||
structureDifferenceKeys: readonly string[]
|
||||
@ -1257,6 +1286,14 @@ function deterministicBoardGeometry(input: {
|
||||
lengthMm: height,
|
||||
widthMm: horizontal ? (horizontal * 1.05) / Math.max(1, input.quantity) : undefined,
|
||||
}
|
||||
case 'cover-shoe-flip-front': return {
|
||||
lengthMm: horizontal,
|
||||
widthMm: resolveShoeFlipFrontHeightMm({
|
||||
slot: input.slot,
|
||||
sku: input.sku,
|
||||
factSnapshot: input.factSnapshot,
|
||||
}),
|
||||
}
|
||||
case 'partition-vertical': return { lengthMm: height, widthMm: depth }
|
||||
case 'partition-horizontal': return {
|
||||
lengthMm: horizontal
|
||||
@ -1389,6 +1426,9 @@ function normalizeInstantiatedSku(input: {
|
||||
'module-horizontal-panel',
|
||||
'surface-upper-tray',
|
||||
'surface-rear-flip-panel',
|
||||
'cover-shoe-flip-front',
|
||||
'upper-rack-side',
|
||||
'upper-rack-horizontal',
|
||||
].includes(slot.componentKey) || (
|
||||
slot.componentKey === 'surface-main' &&
|
||||
input.group.structureDifferenceKeys.includes('SPLIT_REAR_FLIP_TOP')
|
||||
|
||||
@ -26,7 +26,7 @@ import {
|
||||
} from './category-skills'
|
||||
|
||||
export const BOM_COSTING_STRUCTURE_KNOWLEDGE_VERSION =
|
||||
'structure-priors/2026-08-13-v9' as const
|
||||
'structure-priors/2026-08-14-v10' as const
|
||||
|
||||
export type BomCostingStructureSlotPrior = BomCostingCategoryComponentDefinition
|
||||
|
||||
@ -47,6 +47,21 @@ function normalize(value: string) {
|
||||
return value.trim().toLocaleLowerCase('zh-CN').replace(/\s+/g, '')
|
||||
}
|
||||
|
||||
export function bomCostingMaterialSubjectIncludesTerm(
|
||||
subjectValue: string,
|
||||
termValue: string,
|
||||
) {
|
||||
const subject = normalize(subjectValue)
|
||||
const term = normalize(termValue)
|
||||
if (!subject || !term || !subject.includes(term)) return false
|
||||
const exclusionClauses = [
|
||||
...Array.from(subject.matchAll(/除([^;。()()]*?)(?:以外|之外|外)/g), (match) => match[1] ?? ''),
|
||||
...Array.from(subject.matchAll(/(?:不含|不包括|不包含|排除)([^;。()()]*)/g), (match) => match[1] ?? ''),
|
||||
]
|
||||
if (exclusionClauses.some((clause) => clause.includes(term))) return false
|
||||
return !subject.includes(`${term}除外`)
|
||||
}
|
||||
|
||||
function factTextForSlot(record: ReturnType<typeof listBomCostingFactRecords>[number]) {
|
||||
// MATERIAL.note is explanatory model prose. It can mention another part or
|
||||
// comparison SKU and must never bind that material to a component slot.
|
||||
@ -130,7 +145,12 @@ function findSlotFacts(
|
||||
const bindingKeywords = record.fact.kind === 'MATERIAL'
|
||||
? [...slot.keywords, ...(slot.materialKeywords ?? [])]
|
||||
: slot.keywords
|
||||
if (!bindingKeywords.some((keyword) => factTextForSlot(record).includes(normalize(keyword)))) return []
|
||||
const matchesSlot = record.fact.kind === 'MATERIAL'
|
||||
? bindingKeywords.some((keyword) => (
|
||||
bomCostingMaterialSubjectIncludesTerm(record.fact.subject, keyword)
|
||||
))
|
||||
: bindingKeywords.some((keyword) => factTextForSlot(record).includes(normalize(keyword)))
|
||||
if (!matchesSlot) return []
|
||||
const scopedSkuIds = getBomCostingFactScopedSkuIds(record)
|
||||
const directEvidence = record.fact.basis !== 'VISUAL_ESTIMATE'
|
||||
const isSkuSpecific = scopedSkuIds.length === 1 && skuIdSet.has(scopedSkuIds[0]!)
|
||||
|
||||
@ -805,6 +805,11 @@ describe('BomCostingService', () => {
|
||||
skillId: 'CABINET_BOOKSHELF',
|
||||
skillVersion: 'bom-category-skill/cabinet-bookshelf/v1',
|
||||
},
|
||||
{
|
||||
caseId: 'case-cabinet-skill-v2',
|
||||
skillId: 'CABINET_BOOKSHELF',
|
||||
skillVersion: 'bom-category-skill/cabinet-bookshelf/v2',
|
||||
},
|
||||
{
|
||||
caseId: 'case-storage-rack-skill-v1',
|
||||
skillId: 'STORAGE_RACK',
|
||||
@ -858,7 +863,7 @@ describe('BomCostingService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('最新 run 失败时仍按上一轮 READY 的柜体 Skill v1 判定为旧结果', async () => {
|
||||
it('最新 run 失败时仍按上一轮 READY 的柜体 Skill v2 判定为旧结果', async () => {
|
||||
jest.spyOn(bomCostingRepository, 'listCases').mockResolvedValue([
|
||||
buildReadyCaseSummaryRecord({
|
||||
caseId: 'case-cabinet-failed-after-v1',
|
||||
@ -866,13 +871,13 @@ describe('BomCostingService', () => {
|
||||
revision: 2,
|
||||
status: 'FAILED',
|
||||
resolvedCategorySkillId: 'CABINET_BOOKSHELF',
|
||||
resolvedCategorySkillVersion: 'bom-category-skill/cabinet-bookshelf/v2',
|
||||
resolvedCategorySkillVersion: 'bom-category-skill/cabinet-bookshelf/v3',
|
||||
resolvedStructureKnowledgeVersion: BOM_COSTING_STRUCTURE_KNOWLEDGE_VERSION,
|
||||
}, {
|
||||
revision: 1,
|
||||
status: 'READY',
|
||||
resolvedCategorySkillId: 'CABINET_BOOKSHELF',
|
||||
resolvedCategorySkillVersion: 'bom-category-skill/cabinet-bookshelf/v1',
|
||||
resolvedCategorySkillVersion: 'bom-category-skill/cabinet-bookshelf/v2',
|
||||
resolvedStructureKnowledgeVersion: BOM_COSTING_STRUCTURE_KNOWLEDGE_VERSION,
|
||||
}],
|
||||
}),
|
||||
@ -889,9 +894,9 @@ describe('BomCostingService', () => {
|
||||
|
||||
it.each([
|
||||
{
|
||||
caseId: 'case-cabinet-skill-v2',
|
||||
caseId: 'case-cabinet-skill-v3',
|
||||
skillId: 'CABINET_BOOKSHELF',
|
||||
skillVersion: 'bom-category-skill/cabinet-bookshelf/v2',
|
||||
skillVersion: 'bom-category-skill/cabinet-bookshelf/v3',
|
||||
},
|
||||
{
|
||||
caseId: 'case-wardrobe-skill-v1',
|
||||
@ -936,8 +941,8 @@ describe('BomCostingService', () => {
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: '紧邻旧 v7',
|
||||
knowledgeVersion: 'structure-priors/2026-08-13-v7',
|
||||
label: '紧邻旧 v9',
|
||||
knowledgeVersion: 'structure-priors/2026-08-13-v9',
|
||||
},
|
||||
{
|
||||
label: '旧 v6',
|
||||
@ -961,7 +966,7 @@ describe('BomCostingService', () => {
|
||||
revision: 1,
|
||||
status: 'READY',
|
||||
resolvedCategorySkillId: 'CABINET_BOOKSHELF',
|
||||
resolvedCategorySkillVersion: 'bom-category-skill/cabinet-bookshelf/v2',
|
||||
resolvedCategorySkillVersion: 'bom-category-skill/cabinet-bookshelf/v3',
|
||||
resolvedStructureKnowledgeVersion: knowledgeVersion,
|
||||
}],
|
||||
}),
|
||||
|
||||
@ -11,12 +11,14 @@ describe('柜体/书架品类 Skill', () => {
|
||||
'enclosure-side',
|
||||
'enclosure-top',
|
||||
'enclosure-bottom',
|
||||
'back-panel',
|
||||
'partition-shelf',
|
||||
]) {
|
||||
expect(requiredByKey.get(componentKey)?.excludedWhenStructureKeys)
|
||||
.toEqual(['MODULAR_POST_SHELF'])
|
||||
}
|
||||
expect(requiredByKey.get('back-panel')?.excludedWhenStructureKeys)
|
||||
.toEqual(['MODULAR_POST_SHELF', 'NO_BACK_PANEL'])
|
||||
expect(requiredByKey.get('partition-shelf')?.excludedWhenStructureKeys)
|
||||
.toEqual(['MODULAR_POST_SHELF', 'FLIP_SHOE_CABINET'])
|
||||
|
||||
const modularDifference = cabinetBookshelfCategorySkill.differences.find(
|
||||
(difference) => difference.differenceKey === 'MODULAR_POST_SHELF',
|
||||
@ -95,4 +97,69 @@ describe('柜体/书架品类 Skill', () => {
|
||||
/unitCost|unitPrice|estimateAmount|costAmount/,
|
||||
)
|
||||
})
|
||||
|
||||
it('翻斗鞋柜以门前板、鞋斗和开合机构表达,并与普通门抽互斥', () => {
|
||||
const optionalByKey = new Map(cabinetBookshelfCategorySkill.optionalComponents.map(
|
||||
(component) => [component.componentKey, component] as const,
|
||||
))
|
||||
for (const componentKey of [
|
||||
'cover-shoe-flip-front',
|
||||
'shoe-flip-bin-assembly',
|
||||
'motion-shoe-flip',
|
||||
]) {
|
||||
expect(optionalByKey.get(componentKey)?.requiredWhenStructureKeys)
|
||||
.toEqual(['FLIP_SHOE_CABINET'])
|
||||
expect(optionalByKey.get(componentKey)?.quantityByStructureKey).toEqual({
|
||||
SHOE_FLIP_COUNT_2: 2,
|
||||
SHOE_FLIP_COUNT_3: 3,
|
||||
SHOE_FLIP_COUNT_4: 4,
|
||||
SHOE_FLIP_COUNT_6: 6,
|
||||
})
|
||||
}
|
||||
expect(optionalByKey.get('cover-shoe-flip-front')).toMatchObject({
|
||||
category: 'BOARD',
|
||||
unit: '块',
|
||||
primaryMaterialSlot: true,
|
||||
})
|
||||
expect(optionalByKey.get('shoe-flip-bin-assembly')).toMatchObject({
|
||||
category: 'ACCESSORY',
|
||||
unit: '套',
|
||||
})
|
||||
for (const componentKey of [
|
||||
'cover-door',
|
||||
'motion-hinge',
|
||||
'motion-sliding-door',
|
||||
'drawer-box',
|
||||
'motion-drawer',
|
||||
]) {
|
||||
expect(optionalByKey.get(componentKey)?.excludedWhenStructureKeys)
|
||||
.toContain('FLIP_SHOE_CABINET')
|
||||
}
|
||||
expect(cabinetBookshelfCategorySkill.differences.find(
|
||||
(difference) => difference.differenceKey === 'FLIP_SHOE_CABINET',
|
||||
)).toMatchObject({
|
||||
exclusiveGroup: 'DOOR_MODE',
|
||||
allowProductWide: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('鞋柜上架和无背板均由当前 SKU 的明确差异控制', () => {
|
||||
const optionalByKey = new Map(cabinetBookshelfCategorySkill.optionalComponents.map(
|
||||
(component) => [component.componentKey, component] as const,
|
||||
))
|
||||
for (const componentKey of [
|
||||
'upper-rack-side',
|
||||
'upper-rack-horizontal',
|
||||
'upper-rack-hanging-kit',
|
||||
]) {
|
||||
expect(optionalByKey.get(componentKey)?.requiredWhenStructureKeys)
|
||||
.toEqual(['UPPER_OPEN_RACK'])
|
||||
}
|
||||
expect(cabinetBookshelfCategorySkill.differences.find(
|
||||
(difference) => difference.differenceKey === 'UPPER_OPEN_RACK',
|
||||
)?.allowProductWide).not.toBe(true)
|
||||
expect(cabinetBookshelfCategorySkill.differences.find(
|
||||
(difference) => difference.differenceKey === 'NO_BACK_PANEL',
|
||||
)?.allowProductWide).not.toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@ -16,10 +16,17 @@ const shelfCountDifferences = [2, 3, 4, 5, 6, 7, 8].map((count) => ({
|
||||
description: `明确 ${count} 层/格时形成独立结构组。`,
|
||||
}))
|
||||
|
||||
const shoeFlipCountQuantities = {
|
||||
SHOE_FLIP_COUNT_2: 2,
|
||||
SHOE_FLIP_COUNT_3: 3,
|
||||
SHOE_FLIP_COUNT_4: 4,
|
||||
SHOE_FLIP_COUNT_6: 6,
|
||||
} as const
|
||||
|
||||
export const cabinetBookshelfCategorySkill = {
|
||||
code: 'CABINET_BOOKSHELF',
|
||||
label: '柜体/书架',
|
||||
version: 'bom-category-skill/cabinet-bookshelf/v2',
|
||||
version: 'bom-category-skill/cabinet-bookshelf/v3',
|
||||
priority: 400,
|
||||
minimumScore: 12,
|
||||
minimumMargin: 3,
|
||||
@ -37,22 +44,28 @@ export const cabinetBookshelfCategorySkill = {
|
||||
{ componentKey: 'enclosure-side', role: 'ENCLOSURE', category: 'BOARD', partName: '左右侧板', stableQuantity: 2, unit: '块', primaryMaterialSlot: true, quantityRule: '同一连续柜体结构组固定 2 块。', dimensionRule: '高度按柜体高度,宽度按有效深度。', materialScope: '柜体主体材质。', keywords: ['侧板', '柜体侧板', '全高侧板'], excludedWhenStructureKeys: ['MODULAR_POST_SHELF'] },
|
||||
{ componentKey: 'enclosure-top', role: 'ENCLOSURE', category: 'BOARD', partName: '柜体顶板', stableQuantity: 1, unit: '块', primaryMaterialSlot: true, quantityRule: '每个连续柜体固定 1 块。', dimensionRule: '长度随柜宽,宽度随有效深度。', materialScope: '柜体主体材质。', keywords: ['顶板', '柜顶', '整幅顶板'], excludedWhenStructureKeys: ['MODULAR_POST_SHELF'] },
|
||||
{ componentKey: 'enclosure-bottom', role: 'ENCLOSURE', category: 'BOARD', partName: '柜体底板', stableQuantity: 1, unit: '块', primaryMaterialSlot: true, quantityRule: '每个连续柜体固定 1 块。', dimensionRule: '长度随柜宽,宽度随有效深度。', materialScope: '柜体主体材质。', keywords: ['底板', '柜底', '整幅底板'], excludedWhenStructureKeys: ['MODULAR_POST_SHELF'] },
|
||||
{ componentKey: 'back-panel', role: 'COVER', category: 'BOARD', partName: '背板', stableQuantity: 1, unit: '块', quantityRule: '连续柜体固定 1 组背板;拼接缝没有证据时不拆数量。', dimensionRule: '覆盖连续柜体背部开口,板厚必须单独取证。', materialScope: '背板可与主体不同。', keywords: ['背板', '后板', '整幅背板'], excludedWhenStructureKeys: ['MODULAR_POST_SHELF'] },
|
||||
{ componentKey: 'partition-shelf', role: 'PARTITION', category: 'BOARD', partName: '层板', stableQuantity: 3, unit: '块', primaryMaterialSlot: true, quantityRule: '基础连续柜体固定 3 块;明确层数形成独立结构组。', dimensionRule: '长度按所在净开口,宽度按有效深度。', materialScope: '层板材质。', keywords: ['层板', '隔板', '搁板'], excludedWhenStructureKeys: ['MODULAR_POST_SHELF'], quantityByStructureKey: Object.fromEntries([2, 3, 4, 5, 6, 7, 8].map((count) => [`SHELF_COUNT_${count}`, count])) },
|
||||
{ componentKey: 'back-panel', role: 'COVER', category: 'BOARD', partName: '背板', stableQuantity: 1, unit: '块', quantityRule: '连续柜体固定 1 组背板;拼接缝没有证据时不拆数量。', dimensionRule: '覆盖连续柜体背部开口,板厚必须单独取证。', materialScope: '背板可与主体不同。', keywords: ['背板', '后板', '整幅背板'], excludedWhenStructureKeys: ['MODULAR_POST_SHELF', 'NO_BACK_PANEL'] },
|
||||
{ componentKey: 'partition-shelf', role: 'PARTITION', category: 'BOARD', partName: '层板', stableQuantity: 3, unit: '块', primaryMaterialSlot: true, quantityRule: '基础连续柜体固定 3 块;明确层数形成独立结构组。翻斗鞋柜以独立鞋斗替代普通层板。', dimensionRule: '长度按所在净开口,宽度按有效深度。', materialScope: '层板材质。', keywords: ['层板', '隔板', '搁板'], excludedWhenStructureKeys: ['MODULAR_POST_SHELF', 'FLIP_SHOE_CABINET'], quantityByStructureKey: Object.fromEntries([2, 3, 4, 5, 6, 7, 8].map((count) => [`SHELF_COUNT_${count}`, count])) },
|
||||
CONNECTOR_COMPONENT,
|
||||
...COMMON_COST_COMPONENTS,
|
||||
],
|
||||
optionalComponents: [
|
||||
{ componentKey: 'cover-shoe-flip-front', role: 'COVER', category: 'BOARD', partName: '翻斗鞋柜门前板', stableQuantity: 1, unit: '块', primaryMaterialSlot: true, quantityRule: '每个独立翻斗配置 1 块门前板;只有当前 SKU 的明确层数/门数事实才能冻结为 2、3、4 或 6 块。', dimensionRule: '板长按当前 SKU 柜体宽度;单层门高只接受翻斗门直接尺寸事实,板厚独立取证。', materialScope: '翻斗面、翻斗门板或鞋柜门前板的明确材质;无专属事实时可继承单一主体材质。', keywords: ['翻斗门', '翻门', '翻斗面', '前部翻斗门板', '鞋柜正面翻斗柜门', '每层翻斗鞋柜'], requiredWhenStructureKeys: ['FLIP_SHOE_CABINET'], quantityByStructureKey: shoeFlipCountQuantities },
|
||||
{ componentKey: 'shoe-flip-bin-assembly', role: 'PARTITION', category: 'ACCESSORY', partName: '翻斗鞋斗组件', stableQuantity: 1, unit: '套', quantityRule: '每块翻斗门对应 1 套鞋斗组件;弧形侧板、挡板和内部横杆作为一套复合组件计量,不伪装成单块可裁切板。', dimensionRule: '鞋斗侧板、挡板和横杆尺寸必须有部件直接事实;不得复制整机尺寸。', materialScope: '翻斗盒、鞋斗、翻斗收纳格及其侧板/挡板的明确材质。', keywords: ['翻斗盒', '鞋斗', '翻斗收纳格', '翻斗式鞋盒', '翻斗式鞋仓', '鞋类收纳格'], requiredWhenStructureKeys: ['FLIP_SHOE_CABINET'], quantityByStructureKey: shoeFlipCountQuantities },
|
||||
{ componentKey: 'motion-shoe-flip', role: 'MOTION', category: 'ACCESSORY', partName: '翻斗鞋柜开合机构', stableQuantity: 1, unit: '套', quantityRule: '每个独立翻斗对应 1 套转轴、限位和连接机构。', dimensionRule: '机构型号、轴距和安装孔位必须直接取证;不得由柜体宽高猜测。', materialScope: '翻斗转轴、限位件、连接件和紧固件。', keywords: ['翻斗', '翻门', '鞋柜柜门开启机构', '鞋柜门体开启方式', '鞋柜开门方式'], requiredWhenStructureKeys: ['FLIP_SHOE_CABINET'], quantityByStructureKey: shoeFlipCountQuantities },
|
||||
{ componentKey: 'upper-rack-side', role: 'SUPPORT', category: 'BOARD', partName: '鞋柜上架竖向侧板', stableQuantity: 2, unit: '块', primaryMaterialSlot: true, quantityRule: '明确“上架+鞋柜”款固定 2 块竖向侧板;其他竖向构件数量必须有 SKU 专属事实。', dimensionRule: '上架侧板的长宽厚必须使用上架部件直接事实;不得把组合整机高度直接当单板长度。', materialScope: '上架竖板或侧板的明确材质;无专属事实时可继承单一主体材质。', keywords: ['上架', '上部开放式上架', '上部悬挂架', '开放式置物架'], requiredWhenStructureKeys: ['UPPER_OPEN_RACK'] },
|
||||
{ componentKey: 'upper-rack-horizontal', role: 'PARTITION', category: 'BOARD', partName: '鞋柜上架横向置物板', stableQuantity: 2, unit: '块', primaryMaterialSlot: true, quantityRule: '明确“上架+鞋柜”款按两道横向置物构件冻结为 2 块;更多层板必须有 SKU 专属事实。', dimensionRule: '长宽厚必须使用上架横板直接事实;不得复制下部鞋柜或组合整机尺寸。', materialScope: '上架横板、置物板或槽板的明确材质;无专属事实时可继承单一主体材质。', keywords: ['上架', '横向置物构件', '开放式多层置物架', '置物层板', '槽板'], requiredWhenStructureKeys: ['UPPER_OPEN_RACK'] },
|
||||
{ componentKey: 'upper-rack-hanging-kit', role: 'HARDWARE', category: 'ACCESSORY', partName: '鞋柜上架横杆与挂钩组件', stableQuantity: 1, unit: '套', quantityRule: '上架款固定 1 套横杆与挂钩组件;单件数量和规格必须直接取证。', dimensionRule: '横杆长度、截面和挂钩规格必须直接取证。', materialScope: '上架横杆、挂钩和连接五金,不继承木质主体材质。', keywords: ['上部悬挂架', '挂放横杆', '横杆', '挂钩'], requiredWhenStructureKeys: ['UPPER_OPEN_RACK'] },
|
||||
{ componentKey: 'module-side-panel', role: 'ENCLOSURE', category: 'BOARD', partName: '模块柜格侧板', stableQuantity: 6, unit: '块', primaryMaterialSlot: true, quantityRule: '基础模块支柱结构按 3 个独立开放方格冻结,每格左右侧板各 1 块,共 6 块;明确格数时为格数×2。', dimensionRule: '板宽按模块有效深度;板长必须有单格高度直接证据,不得用整机高度或整机高度÷格数猜测。', materialScope: '木质模块柜格主体材质。', keywords: ['木质部件', '模块侧板', '柜格侧板', '方形柜格', '方格柜格', '木质柜格'], requiredWhenStructureKeys: ['MODULAR_POST_SHELF'], quantityByStructureKey: Object.fromEntries([2, 3, 4, 5, 6, 7, 8].map((count) => [`SHELF_COUNT_${count}`, count * 2])) },
|
||||
{ componentKey: 'module-horizontal-panel', role: 'PARTITION', category: 'BOARD', partName: '模块柜格上下横板', stableQuantity: 6, unit: '块', primaryMaterialSlot: true, quantityRule: '基础模块支柱结构按 3 个独立开放方格冻结,每格顶板、底板各 1 块,共 6 块;明确格数时为格数×2,独立箱体之间不得合并共享横板。', dimensionRule: '板长按模块外宽,板宽按有效深度;不得用整机高度生成横板尺寸。', materialScope: '木质模块柜格主体材质。', keywords: ['木质部件', '模块横板', '柜格横板', '方形柜格', '方格柜格', '木质柜格'], requiredWhenStructureKeys: ['MODULAR_POST_SHELF'], quantityByStructureKey: Object.fromEntries([2, 3, 4, 5, 6, 7, 8].map((count) => [`SHELF_COUNT_${count}`, count * 2])) },
|
||||
{ componentKey: 'support-metal-post', role: 'SUPPORT', category: 'STEEL', partName: '模块金属支柱', stableQuantity: 4, unit: '根', quantityRule: '基础模块支柱结构按 4 个竖向支承点冻结;明确为其他数量时必须引用 SKU 专属事实。', dimensionRule: '必须按可见支柱的真实连续长度或明确分段长度计价;没有直接读数时不得把整机高度重复计到每一段支柱。', materialScope: '不锈钢、碳钢或其他明确标注的金属支柱。', keywords: ['金属部件', '金属支柱', '金属立杆', '竖直金属杆', '不锈钢杆', '支柱'], requiredWhenStructureKeys: ['MODULAR_POST_SHELF'] },
|
||||
{ componentKey: 'motion-caster', role: 'MOTION', category: 'ACCESSORY', partName: '移动脚轮组件', stableQuantity: 4, unit: '个', quantityRule: '只有明确带轮结构组才按 4 个落地支承点冻结;刹车轮分配需 SKU 专属事实。', dimensionRule: '轮径、安装高度和安装板规格无证据时待复核。', materialScope: '脚轮、安装板和紧固件。', keywords: ['脚轮', '万向轮', '刹车轮', '移动轮'], requiredWhenStructureKeys: ['CASTER_BASE'] },
|
||||
{ componentKey: 'partition-vertical', role: 'PARTITION', category: 'BOARD', partName: '竖向隔板', stableQuantity: 1, unit: '块', primaryMaterialSlot: true, quantityRule: '分栏结构组固定 1 块;更多分栏需专属事实。', dimensionRule: '按柜内净高和有效深度。', materialScope: '柜体主体材质。', keywords: ['竖隔板', '中竖板', '分栏'], requiredWhenStructureKeys: ['VERTICAL_DIVISION'] },
|
||||
{ componentKey: 'cover-door', role: 'COVER', category: 'BOARD', partName: '柜门', stableQuantity: 2, unit: '块', primaryMaterialSlot: true, quantityRule: '带门结构组固定 2 扇;明确门数需独立结构组或 SKU 专属事实。', dimensionRule: '按柜体开口范围推导。', materialScope: '门板材质和表面处理。', keywords: ['柜门', '门板'], requiredWhenStructureKeys: ['HINGED_DOOR', 'SLIDING_DOOR'] },
|
||||
{ componentKey: 'motion-hinge', role: 'MOTION', category: 'ACCESSORY', partName: '柜门铰链机构', stableQuantity: 1, unit: '套', quantityRule: '平开门结构组固定 1 套。', dimensionRule: '型号和杯孔无证据时待复核。', materialScope: '铰链和紧固件。', keywords: ['铰链', '合页'], requiredWhenStructureKeys: ['HINGED_DOOR'] },
|
||||
{ componentKey: 'motion-sliding-door', role: 'MOTION', category: 'ACCESSORY', partName: '移门轨道机构', stableQuantity: 1, unit: '套', quantityRule: '移门结构组固定 1 套。', dimensionRule: '轨道长度随开口宽度。', materialScope: '轨道、滑轮和限位件。', keywords: ['移门', '推拉门', '轨道'], requiredWhenStructureKeys: ['SLIDING_DOOR'] },
|
||||
{ componentKey: 'drawer-box', role: 'PARTITION', category: 'BOARD', partName: '抽屉盒板件', stableQuantity: 1, unit: '套', quantityRule: '带抽结构组固定 1 套;更多抽屉需专属事实。', dimensionRule: '按抽屉开口和柜内深度。', materialScope: '抽屉板件。', keywords: ['抽屉'], requiredWhenStructureKeys: ['DRAWER_PRESENT'] },
|
||||
{ componentKey: 'motion-drawer', role: 'MOTION', category: 'ACCESSORY', partName: '抽屉导轨机构', stableQuantity: 1, unit: '套', quantityRule: '每套抽屉盒配置成对导轨。', dimensionRule: '导轨长度不超过柜内有效深度。', materialScope: '导轨、拉手和紧固件。', keywords: ['抽屉', '导轨'], requiredWhenStructureKeys: ['DRAWER_PRESENT'] },
|
||||
{ componentKey: 'cover-door', role: 'COVER', category: 'BOARD', partName: '柜门', stableQuantity: 2, unit: '块', primaryMaterialSlot: true, quantityRule: '带门结构组固定 2 扇;明确门数需独立结构组或 SKU 专属事实。', dimensionRule: '按柜体开口范围推导。', materialScope: '门板材质和表面处理。', keywords: ['柜门', '门板'], requiredWhenStructureKeys: ['HINGED_DOOR', 'SLIDING_DOOR'], excludedWhenStructureKeys: ['FLIP_SHOE_CABINET'] },
|
||||
{ componentKey: 'motion-hinge', role: 'MOTION', category: 'ACCESSORY', partName: '柜门铰链机构', stableQuantity: 1, unit: '套', quantityRule: '平开门结构组固定 1 套。', dimensionRule: '型号和杯孔无证据时待复核。', materialScope: '铰链和紧固件。', keywords: ['铰链', '合页'], requiredWhenStructureKeys: ['HINGED_DOOR'], excludedWhenStructureKeys: ['FLIP_SHOE_CABINET'] },
|
||||
{ componentKey: 'motion-sliding-door', role: 'MOTION', category: 'ACCESSORY', partName: '移门轨道机构', stableQuantity: 1, unit: '套', quantityRule: '移门结构组固定 1 套。', dimensionRule: '轨道长度随开口宽度。', materialScope: '轨道、滑轮和限位件。', keywords: ['移门', '推拉门', '轨道'], requiredWhenStructureKeys: ['SLIDING_DOOR'], excludedWhenStructureKeys: ['FLIP_SHOE_CABINET'] },
|
||||
{ componentKey: 'drawer-box', role: 'PARTITION', category: 'BOARD', partName: '抽屉盒板件', stableQuantity: 1, unit: '套', quantityRule: '带抽结构组固定 1 套;更多抽屉需专属事实。', dimensionRule: '按抽屉开口和柜内深度。', materialScope: '抽屉板件。', keywords: ['抽屉'], requiredWhenStructureKeys: ['DRAWER_PRESENT'], excludedWhenStructureKeys: ['FLIP_SHOE_CABINET'] },
|
||||
{ componentKey: 'motion-drawer', role: 'MOTION', category: 'ACCESSORY', partName: '抽屉导轨机构', stableQuantity: 1, unit: '套', quantityRule: '每套抽屉盒配置成对导轨。', dimensionRule: '导轨长度不超过柜内有效深度。', materialScope: '导轨、拉手和紧固件。', keywords: ['抽屉', '导轨'], requiredWhenStructureKeys: ['DRAWER_PRESENT'], excludedWhenStructureKeys: ['FLIP_SHOE_CABINET'] },
|
||||
],
|
||||
relations: [
|
||||
{ relationType: 'CONNECTS', fromComponentKey: 'enclosure-side', toComponentKey: 'enclosure-top', rule: '侧板和顶板围合柜体。' },
|
||||
@ -62,6 +75,10 @@ export const cabinetBookshelfCategorySkill = {
|
||||
{ relationType: 'COVERS', fromComponentKey: 'cover-door', toComponentKey: 'enclosure-side', rule: '柜门覆盖正面开口。' },
|
||||
{ relationType: 'MOVES_WITH', fromComponentKey: 'motion-hinge', toComponentKey: 'cover-door', rule: '平开柜门必须关联铰链。' },
|
||||
{ relationType: 'MOVES_WITH', fromComponentKey: 'motion-drawer', toComponentKey: 'drawer-box', rule: '抽屉盒必须关联成对导轨。' },
|
||||
{ relationType: 'CONNECTS', fromComponentKey: 'cover-shoe-flip-front', toComponentKey: 'shoe-flip-bin-assembly', rule: '每块翻斗门前板必须连接对应的鞋斗组件。' },
|
||||
{ relationType: 'MOVES_WITH', fromComponentKey: 'motion-shoe-flip', toComponentKey: 'shoe-flip-bin-assembly', rule: '每套鞋斗组件必须关联翻转、限位和连接机构。' },
|
||||
{ relationType: 'SUPPORTS', fromComponentKey: 'upper-rack-side', toComponentKey: 'upper-rack-horizontal', rule: '上架横向置物板必须由竖向侧板形成稳定支承。' },
|
||||
{ relationType: 'CONNECTS', fromComponentKey: 'upper-rack-hanging-kit', toComponentKey: 'upper-rack-side', rule: '横杆和挂钩组件必须连接到上架支承结构。' },
|
||||
{ relationType: 'CONNECTS', fromComponentKey: 'module-side-panel', toComponentKey: 'module-horizontal-panel', rule: '每个独立开放方格由两块侧板和两块上下横板围合;无背板证据时不生成模块背板。' },
|
||||
{ relationType: 'SUPPORTS', fromComponentKey: 'support-metal-post', toComponentKey: 'module-horizontal-panel', rule: '每个独立木质方格必须由金属支柱形成稳定支承。' },
|
||||
{ relationType: 'CONNECTS', fromComponentKey: 'hardware-connectors', toComponentKey: 'support-metal-post', rule: '连接件必须把金属支柱与木质模块可靠连接。' },
|
||||
@ -69,6 +86,13 @@ export const cabinetBookshelfCategorySkill = {
|
||||
],
|
||||
differences: [
|
||||
...shelfCountDifferences,
|
||||
{ differenceKey: 'FLIP_SHOE_CABINET', token: 'flip-shoe', groupKind: 'STRUCTURE', terms: ['翻斗鞋柜', '翻斗式鞋柜', '翻门鞋柜', '翻斗式鞋仓', '鞋柜翻斗门', '鞋柜翻门'], excludedTerms: ['平开门鞋柜', '推拉门鞋柜'], exclusiveGroup: 'DOOR_MODE', priority: 220, allowProductWide: true, description: '翻斗鞋柜以独立门前板、鞋斗组件和翻转机构替代普通柜门、抽屉和层板。' },
|
||||
{ differenceKey: 'SHOE_FLIP_COUNT_2', token: 'flip-2', groupKind: 'STRUCTURE', terms: ['2个翻斗', '两个翻斗', '两层翻斗', '双层翻斗', '2块翻斗门', '两块翻斗门'], exclusiveGroup: 'SHOE_FLIP_COUNT', priority: 102, description: '当前 SKU 明确为 2 个翻斗。' },
|
||||
{ differenceKey: 'SHOE_FLIP_COUNT_3', token: 'flip-3', groupKind: 'STRUCTURE', terms: ['3个翻斗', '三个翻斗', '三层翻斗', '3层翻斗', '三层超薄翻斗鞋柜', '3块翻斗门', '三块翻斗门', '3块上下排列的正面翻斗门', '三个抽屉式柜门'], exclusiveGroup: 'SHOE_FLIP_COUNT', priority: 103, description: '当前 SKU 明确为 3 个翻斗。' },
|
||||
{ differenceKey: 'SHOE_FLIP_COUNT_4', token: 'flip-4', groupKind: 'STRUCTURE', terms: ['4个翻斗', '四个翻斗', '四层翻斗', '4层翻斗', '4块翻斗门', '四块翻斗门', '四扇翻斗门'], exclusiveGroup: 'SHOE_FLIP_COUNT', priority: 104, description: '当前 SKU 明确为 4 个翻斗。' },
|
||||
{ differenceKey: 'SHOE_FLIP_COUNT_6', token: 'flip-6', groupKind: 'STRUCTURE', terms: ['6个翻斗', '六个翻斗', '六层翻斗', '6层翻斗', '6块翻斗门', '六块翻斗门', '六扇翻斗门'], exclusiveGroup: 'SHOE_FLIP_COUNT', priority: 106, description: '当前 SKU 明确为 6 个翻斗。' },
|
||||
{ differenceKey: 'UPPER_OPEN_RACK', token: 'upper-rack', groupKind: 'STRUCTURE', terms: ['+上架', '上架+鞋柜', '鞋柜+上架', '鞋柜连上架', '上部开放式上架', '上部开放式置物架', '上部悬挂架', '上架与鞋柜'], excludedTerms: ['不含上架', '无上架'], exclusiveGroup: 'SHOE_UPPER_MODE', priority: 160, description: '上架款单列竖向侧板、横向置物板和横杆挂钩组件,不能把局部置物架误判成整件架类商品。' },
|
||||
{ differenceKey: 'NO_BACK_PANEL', token: 'no-back-panel', groupKind: 'STRUCTURE', terms: ['无后背板', '不带背板', '无背板'], exclusiveGroup: 'BACK_PANEL_MODE', priority: 160, description: '明确无背板的 SKU 不生成背板行。' },
|
||||
{ differenceKey: 'MODULAR_POST_SHELF', token: 'modular-post-shelf', groupKind: 'STRUCTURE', terms: ['模块支柱书架', '模块支柱层架', '模块金属支柱', '独立木质柜格', '方形木质柜格', '方格木质柜格', '彼此独立并由支柱连接', '支柱连接木质柜格'], excludedTerms: ['连续全高侧板', '整幅背板围合', '封闭连续柜体'], exclusiveGroup: 'CABINET_BODY_MODE', priority: 200, allowProductWide: true, description: '独立木质模块由金属支柱和连接件串联,移动款配置脚轮;不得套用连续全高侧板或整幅背板。' },
|
||||
{ differenceKey: 'FIXED_BASE', token: 'fixed-base', groupKind: 'STRUCTURE', terms: ['固定脚', '无脚轮', '不带脚轮'], exclusiveGroup: 'MOBILITY_BASE', priority: 220, description: '明确固定式底座时不得启用脚轮。' },
|
||||
{ differenceKey: 'CASTER_BASE', token: 'caster-base', groupKind: 'STRUCTURE', terms: ['带轮', '脚轮', '万向轮', '刹车轮', '移动展示柜', '移动展示架'], excludedTerms: ['无脚轮', '不带脚轮'], exclusiveGroup: 'MOBILITY_BASE', priority: 200, allowProductWide: true, description: '明确移动或脚轮事实才启用脚轮组件,模块支柱结构本身不隐含脚轮。' },
|
||||
@ -85,11 +109,15 @@ export const cabinetBookshelfCategorySkill = {
|
||||
{ validationKey: 'cabinet-stable-count', severity: 'ERROR', rule: '同结构组层板和围合板数量冻结;模型不能无 SKU 专属 factId 改数。', componentKeys: ['partition-shelf', 'enclosure-side', 'enclosure-top', 'enclosure-bottom'] },
|
||||
{ validationKey: 'modular-post-shelf-closure', severity: 'ERROR', rule: '模块支柱结构必须以逐块侧板、上下横板和金属支柱闭合,不得使用复合箱体行、连续柜壳或无证据背板。', componentKeys: ['module-side-panel', 'module-horizontal-panel', 'support-metal-post'] },
|
||||
{ validationKey: 'modular-caster-binding', severity: 'WARNING', rule: '脚轮只在明确 CASTER_BASE 结构下启用,并安装到模块支柱的落地支承点。', componentKeys: ['motion-caster', 'support-metal-post'] },
|
||||
{ validationKey: 'flip-shoe-closure', severity: 'ERROR', rule: '翻斗鞋柜必须以门前板、鞋斗组件和翻转机构逐斗闭合,不得套用普通抽屉或平开门。', componentKeys: ['cover-shoe-flip-front', 'shoe-flip-bin-assembly', 'motion-shoe-flip'] },
|
||||
{ validationKey: 'shoe-upper-rack-closure', severity: 'ERROR', rule: '上架款必须包含竖向侧板、横向置物板和横杆挂钩组件;基础鞋柜不得自动带上架。', componentKeys: ['upper-rack-side', 'upper-rack-horizontal', 'upper-rack-hanging-kit'] },
|
||||
],
|
||||
historicalFeatures: [
|
||||
{ featureKey: 'cabinet-openings', label: '门与开口布局', kind: 'STRUCTURE', terms: ['开放式', '平开门', '推拉门', '分栏'], description: '历史候选先匹配开口、门型和分栏布局。' },
|
||||
{ featureKey: 'cabinet-shelf-count', label: '层板层数', kind: 'STRUCTURE', terms: ['层', '格', '层板'], description: '层数是数量与成本的重要特征。' },
|
||||
{ featureKey: 'cabinet-drawer', label: '抽屉机构', kind: 'STRUCTURE', terms: ['抽屉', '导轨'], description: '带抽和无抽历史不能直接互用。' },
|
||||
{ featureKey: 'cabinet-shoe-flip', label: '翻斗鞋柜机构', kind: 'STRUCTURE', terms: ['翻斗鞋柜', '翻门鞋柜', '鞋斗', '翻斗门'], description: '翻斗鞋柜必须匹配门前板、鞋斗和翻转机构,不能复用桌或普通抽屉 BOM。' },
|
||||
{ featureKey: 'cabinet-shoe-upper-rack', label: '鞋柜上架', kind: 'STRUCTURE', terms: ['上架+鞋柜', '鞋柜连上架', '上部开放式上架'], description: '基础鞋柜与带上架款必须分组匹配历史结构。' },
|
||||
{ featureKey: 'cabinet-body-mode', label: '连续柜体/模块支柱', kind: 'STRUCTURE', terms: ['连续柜体', '模块箱体', '模块柜格', '金属支柱', '脚轮'], description: '模块支柱书架不能直接复用连续柜壳历史 BOM。' },
|
||||
...COMMON_HISTORICAL_FEATURES,
|
||||
],
|
||||
|
||||
@ -14,6 +14,7 @@ import type {
|
||||
BomCostingCategoryGroupingKeys,
|
||||
BomCostingCategorySignalDefinition,
|
||||
BomCostingCategorySignalMatch,
|
||||
BomCostingCategorySignalSource,
|
||||
BomCostingCategorySkill,
|
||||
BomCostingCategorySkillCandidate,
|
||||
BomCostingCategorySkillContext,
|
||||
@ -253,11 +254,14 @@ function matchSignal(
|
||||
context: BomCostingCategorySkillContext,
|
||||
): BomCostingCategorySignalMatch | null {
|
||||
const normalizedTerms = signal.terms.map(normalizeText).filter(Boolean)
|
||||
const texts = [
|
||||
const allowedSources = new Set<BomCostingCategorySignalSource>(
|
||||
signal.sources ?? ['PRODUCT', 'SKU'],
|
||||
)
|
||||
const texts = ([
|
||||
['PRODUCT', context.productText],
|
||||
['SKU', Array.from(context.skuTexts.values()).join(' ')],
|
||||
['FACT', context.factText],
|
||||
] as const
|
||||
] as const).filter(([source]) => allowedSources.has(source))
|
||||
const allText = texts.map(([, text]) => text).join(' ')
|
||||
const matchedTerms = normalizedTerms.filter((term) => allText.includes(term))
|
||||
const matched = signal.match === 'ALL'
|
||||
|
||||
@ -36,7 +36,7 @@ export const storageRackCategorySkill = {
|
||||
minimumMargin: 3,
|
||||
positiveSignals: [
|
||||
{ signalKey: 'storage-rack-explicit', terms: ['置物架', '储物架', '收纳架', '层架', '货架', '花架', '展示架'], weight: 22, reason: '商品明确属于开放式置物架。' },
|
||||
{ signalKey: 'mobile-storage-trolley-topology', terms: ['上层托盘', '下层置物篮', '脚轮'], weight: 34, match: 'ALL', reason: '上层托盘、下层置物篮和脚轮共同构成移动置物推车,不按普通桌类处理。' },
|
||||
{ signalKey: 'mobile-storage-trolley-topology', terms: ['上层托盘', '下层置物篮', '脚轮'], weight: 34, match: 'ALL', sources: ['PRODUCT', 'SKU', 'FACT'], reason: '上层托盘、下层置物篮和脚轮共同构成移动置物推车,不按普通桌类处理。' },
|
||||
{ signalKey: 'rack-context', terms: ['厨房架', '浴室架', '桌面架', '落地架', '墙上架'], weight: 12, reason: '出现置物架典型使用形态。' },
|
||||
{ signalKey: 'rack-structure', terms: ['立柱', '层板', '横撑', '斜撑'], weight: 6, reason: '出现开放层架结构。' },
|
||||
],
|
||||
|
||||
@ -38,6 +38,8 @@ export type BomCostingStructureRelationType =
|
||||
| 'MOVES_WITH'
|
||||
| 'ALIGNS_WITH'
|
||||
|
||||
export type BomCostingCategorySignalSource = 'PRODUCT' | 'SKU' | 'FACT'
|
||||
|
||||
export interface BomCostingCategorySignalDefinition {
|
||||
signalKey: string
|
||||
terms: readonly string[]
|
||||
@ -45,6 +47,12 @@ export interface BomCostingCategorySignalDefinition {
|
||||
reason: string
|
||||
match?: 'ANY' | 'ALL'
|
||||
hard?: boolean
|
||||
/**
|
||||
* Whole-product category signals default to seller product/SKU identity.
|
||||
* Raw image facts describe local parts and may participate only when a
|
||||
* Skill explicitly opts a signal into FACT evidence.
|
||||
*/
|
||||
sources?: readonly BomCostingCategorySignalSource[]
|
||||
}
|
||||
|
||||
export interface BomCostingCategorySignalMatch {
|
||||
@ -52,7 +60,7 @@ export interface BomCostingCategorySignalMatch {
|
||||
matchedTerms: string[]
|
||||
weight: number
|
||||
reason: string
|
||||
sources: Array<'PRODUCT' | 'SKU' | 'FACT'>
|
||||
sources: BomCostingCategorySignalSource[]
|
||||
hard: boolean
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,228 @@
|
||||
import {
|
||||
BOM_COSTING_FACT_SCHEMA_VERSION,
|
||||
type BomCostingSourceFactSnapshot,
|
||||
} from '@/lib/bom-costing/fact-contracts'
|
||||
import type { MarketDetailSnapshot } from '../market-detail-client'
|
||||
|
||||
export const PRODUCTION_FLIP_SHOE_CABINET_TITLE =
|
||||
'源氏木语实木超薄鞋柜 简约现代风格玄关鞋柜 上海配送橡木胡桃色鞋柜'
|
||||
|
||||
export const PRODUCTION_FLIP_SHOE_CABINET_10_SKUS = [
|
||||
{ externalSkuId: '5984281680348', name: '整装 / (1)(橡木原木色)0.65米超薄鞋柜(650*200*1160mm)Y00L08', dimensions: '650*200*1160mm', upperRack: false, noBackPanel: false, material: '橡木' },
|
||||
{ externalSkuId: '5984281680349', name: '整装 / (2)(橡木原木色)0.65米超薄鞋柜+上架(650*200*2110mm)Y00L08', dimensions: '650*200*2110mm', upperRack: true, noBackPanel: false, material: '橡木' },
|
||||
{ externalSkuId: '5984281680350', name: '整装 / (3)(橡木原木色)0.85米超薄鞋柜(850*200*1160mm)Y00L08', dimensions: '850*200*1160mm', upperRack: false, noBackPanel: false, material: '橡木' },
|
||||
{ externalSkuId: '5984281680351', name: '整装 / (4)(橡木原木色)0.85米超薄鞋柜 +上架(850*200*2110mm)Y00L08', dimensions: '850*200*2110mm', upperRack: true, noBackPanel: false, material: '橡木' },
|
||||
{ externalSkuId: '5984281680352', name: '整装 / (5)(橡木胡桃色)0.65米超薄鞋柜(650*200*1160mm)H00L08', dimensions: '650*200*1160mm', upperRack: false, noBackPanel: false, material: '橡木' },
|
||||
{ externalSkuId: '5984281680353', name: '整装 / (6)(橡木胡桃色)0.65米超薄鞋柜+上架(650*200*2110mm)H00L08', dimensions: '650*200*2110mm', upperRack: true, noBackPanel: false, material: '橡木' },
|
||||
{ externalSkuId: '5984281680354', name: '整装 / (7)(橡木胡桃色)0.85米超薄鞋柜(850*200*1160mm)H00L08', dimensions: '850*200*1160mm', upperRack: false, noBackPanel: false, material: '橡木' },
|
||||
{ externalSkuId: '5984281680355', name: '整装 / (8)(橡木胡桃色)0.85米超薄鞋柜+上架(850*200*2110mm)H00L08', dimensions: '850*200*2110mm', upperRack: true, noBackPanel: false, material: '橡木' },
|
||||
{ externalSkuId: '5984281680356', name: '整装 / (9)(杨木原木色)0.65米超薄鞋柜(无后背板)(650*200*1160mm)Y270L01', dimensions: '650*200*1160mm', upperRack: false, noBackPanel: true, material: '杨木' },
|
||||
{ externalSkuId: '5984281680357', name: '整装 / (10)(杨木原木色)0.65米超薄鞋柜+上架(无后背板)(650*200*2110mm)Y270L01', dimensions: '650*200*2110mm', upperRack: true, noBackPanel: true, material: '杨木' },
|
||||
] as const
|
||||
|
||||
export function buildProductionFlipShoeCabinet10Snapshot(): MarketDetailSnapshot {
|
||||
return {
|
||||
itemId: 'production-flip-shoe-cabinet-10',
|
||||
channel: 'taobao',
|
||||
title: PRODUCTION_FLIP_SHOE_CABINET_TITLE,
|
||||
shopName: '脱敏生产店铺',
|
||||
detailUrl: 'https://fixture.invalid/items/production-flip-shoe-cabinet-10',
|
||||
mainImages: [],
|
||||
detailImages: [],
|
||||
skuImages: [],
|
||||
requestId: 'request-production-flip-shoe-cabinet-10',
|
||||
skus: PRODUCTION_FLIP_SHOE_CABINET_10_SKUS.map((sku) => ({
|
||||
externalSkuId: sku.externalSkuId,
|
||||
name: sku.name,
|
||||
size: sku.dimensions,
|
||||
imageUrl: null,
|
||||
attributes: { 颜色分类: sku.name, 安装方式: '整装' },
|
||||
priceRaw: null,
|
||||
originalPriceRaw: null,
|
||||
quantity: null,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export function buildProductionFlipShoeCabinet10FactSnapshot(): BomCostingSourceFactSnapshot {
|
||||
return {
|
||||
schemaVersion: BOM_COSTING_FACT_SCHEMA_VERSION,
|
||||
imageFacts: [
|
||||
{
|
||||
imageId: 'IMG_SHARED_SHOE_STRUCTURE',
|
||||
sourceImageUrl: 'https://fixture.invalid/images/flip-shoe-shared.jpg',
|
||||
kind: 'DETAIL',
|
||||
label: '鞋柜结构与材质详情',
|
||||
externalSkuIds: [],
|
||||
observedTexts: ['三层翻斗鞋柜', '上部开放式置物架', 'FAS级橡木'],
|
||||
facts: [
|
||||
{
|
||||
factId: 'shared-flip-shoe-topology',
|
||||
kind: 'STRUCTURE',
|
||||
subject: '鞋柜柜体',
|
||||
value: '由顶板、左右侧板及三层上下叠置的正面翻斗门组成',
|
||||
relatedSkuIds: [],
|
||||
basis: 'IMAGE',
|
||||
confidence: 0.96,
|
||||
},
|
||||
{
|
||||
factId: 'shared-flip-shoe-mechanism',
|
||||
kind: 'MECHANISM',
|
||||
subject: '鞋柜翻斗门',
|
||||
value: '门板向前下方翻开,形成倾斜式鞋类收纳格',
|
||||
relatedSkuIds: [],
|
||||
basis: 'IMAGE',
|
||||
confidence: 0.95,
|
||||
},
|
||||
{
|
||||
factId: 'shared-flip-front-height',
|
||||
kind: 'DIMENSION',
|
||||
subject: '每层翻斗鞋柜高度',
|
||||
value: '35cm',
|
||||
relatedSkuIds: [],
|
||||
basis: 'SOURCE_TEXT',
|
||||
confidence: 0.93,
|
||||
},
|
||||
{
|
||||
factId: 'shared-upper-local-rack',
|
||||
kind: 'PART',
|
||||
subject: '上部开放式置物架',
|
||||
value: '部分上架款在鞋柜顶部配置两道横向置物构件',
|
||||
relatedSkuIds: [],
|
||||
basis: 'IMAGE',
|
||||
confidence: 0.91,
|
||||
},
|
||||
{
|
||||
factId: 'shared-misleading-table-material-label',
|
||||
kind: 'MATERIAL',
|
||||
subject: '桌面',
|
||||
value: '橡木',
|
||||
relatedSkuIds: [],
|
||||
basis: 'SOURCE_TEXT',
|
||||
confidence: 0.97,
|
||||
},
|
||||
{
|
||||
factId: 'shared-primary-oak',
|
||||
kind: 'MATERIAL',
|
||||
subject: '产品主材',
|
||||
value: '北美FAS级橡木',
|
||||
relatedSkuIds: [],
|
||||
basis: 'SOURCE_TEXT',
|
||||
confidence: 0.99,
|
||||
},
|
||||
{
|
||||
factId: 'shared-back-paulownia',
|
||||
kind: 'MATERIAL',
|
||||
subject: '背板',
|
||||
value: '泡桐木',
|
||||
relatedSkuIds: [],
|
||||
basis: 'SOURCE_TEXT',
|
||||
confidence: 0.96,
|
||||
},
|
||||
{
|
||||
factId: 'shared-flip-bin-paulownia',
|
||||
kind: 'MATERIAL',
|
||||
subject: '翻斗盒(除抽面、贴木、侧板外)',
|
||||
value: '泡桐木',
|
||||
relatedSkuIds: [],
|
||||
basis: 'SOURCE_TEXT',
|
||||
confidence: 0.93,
|
||||
},
|
||||
{
|
||||
factId: 'shared-flip-front-oak',
|
||||
kind: 'MATERIAL',
|
||||
subject: '翻斗面',
|
||||
value: '橡木',
|
||||
relatedSkuIds: [],
|
||||
basis: 'SOURCE_TEXT',
|
||||
confidence: 0.97,
|
||||
},
|
||||
{
|
||||
factId: 'shared-upper-rack-oak',
|
||||
kind: 'MATERIAL',
|
||||
subject: '上架',
|
||||
value: '橡木',
|
||||
relatedSkuIds: [],
|
||||
basis: 'SOURCE_TEXT',
|
||||
confidence: 0.97,
|
||||
},
|
||||
],
|
||||
},
|
||||
...PRODUCTION_FLIP_SHOE_CABINET_10_SKUS.map((sku, index) => ({
|
||||
imageId: `IMG_SHOE_SKU_${String(index + 1).padStart(2, '0')}`,
|
||||
sourceImageUrl: `https://fixture.invalid/images/flip-shoe-${sku.externalSkuId}.jpg`,
|
||||
kind: 'SKU' as const,
|
||||
label: `SKU图:${sku.externalSkuId}`,
|
||||
externalSkuIds: [sku.externalSkuId],
|
||||
observedTexts: [sku.name, sku.dimensions],
|
||||
facts: [
|
||||
{
|
||||
factId: `${sku.externalSkuId}-outer-size`,
|
||||
kind: 'DIMENSION' as const,
|
||||
subject: '鞋柜整体尺寸',
|
||||
value: sku.dimensions,
|
||||
relatedSkuIds: [sku.externalSkuId],
|
||||
basis: 'SOURCE_TEXT' as const,
|
||||
confidence: 0.99,
|
||||
},
|
||||
{
|
||||
factId: `${sku.externalSkuId}-configuration`,
|
||||
kind: 'SKU_DIFFERENCE' as const,
|
||||
subject: 'SKU配置',
|
||||
value: `${sku.material}${sku.upperRack ? '上架+' : ''}三层超薄翻斗鞋柜${sku.noBackPanel ? ',无后背板' : ''}`,
|
||||
relatedSkuIds: [sku.externalSkuId],
|
||||
basis: 'SOURCE_TEXT' as const,
|
||||
confidence: 0.99,
|
||||
},
|
||||
{
|
||||
factId: `${sku.externalSkuId}-flip-fronts`,
|
||||
kind: 'PART' as const,
|
||||
subject: '鞋柜正面翻斗柜门',
|
||||
value: '可见3块上下排列的正面翻斗门',
|
||||
relatedSkuIds: [sku.externalSkuId],
|
||||
basis: 'IMAGE' as const,
|
||||
confidence: 0.95,
|
||||
},
|
||||
{
|
||||
factId: `${sku.externalSkuId}-flip-mechanism`,
|
||||
kind: 'MECHANISM' as const,
|
||||
subject: '鞋柜柜门开启机构',
|
||||
value: '翻斗鞋柜',
|
||||
relatedSkuIds: [sku.externalSkuId],
|
||||
basis: 'SOURCE_TEXT' as const,
|
||||
confidence: 0.94,
|
||||
},
|
||||
...(sku.upperRack ? [{
|
||||
factId: `${sku.externalSkuId}-upper-rack`,
|
||||
kind: 'PART' as const,
|
||||
subject: '上架',
|
||||
value: '鞋柜上方开放式上架,含两道横向置物构件、横杆和挂钩',
|
||||
relatedSkuIds: [sku.externalSkuId],
|
||||
basis: 'IMAGE' as const,
|
||||
confidence: 0.96,
|
||||
}] : []),
|
||||
...(sku.noBackPanel ? [{
|
||||
factId: `${sku.externalSkuId}-no-back-panel`,
|
||||
kind: 'STRUCTURE' as const,
|
||||
subject: '背板',
|
||||
value: '无后背板',
|
||||
relatedSkuIds: [sku.externalSkuId],
|
||||
basis: 'SOURCE_TEXT' as const,
|
||||
confidence: 0.99,
|
||||
}] : []),
|
||||
...(sku.material === '杨木' ? [{
|
||||
factId: `${sku.externalSkuId}-poplar-material`,
|
||||
kind: 'MATERIAL' as const,
|
||||
subject: '柜体主材',
|
||||
value: '杨木',
|
||||
relatedSkuIds: [sku.externalSkuId],
|
||||
basis: 'SOURCE_TEXT' as const,
|
||||
confidence: 0.98,
|
||||
}] : []),
|
||||
],
|
||||
})),
|
||||
],
|
||||
failedImages: [],
|
||||
extractedAt: '2026-08-14T00:00:00.000Z',
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user