From 36fe534029cab5bbf68c2998d4c3342b94cdf399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Wed, 14 Oct 2026 19:03:36 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20=E5=8F=98=E9=87=8F=E9=9B=86?= =?UTF-8?q?=E5=B7=AE=E6=9B=BF=E4=BB=A3=E9=85=8D=E5=AF=B9=E9=80=92=E5=BD=92?= =?UTF-8?q?=EF=BC=88=E5=88=A0=E5=B0=BE=E8=A1=8C/=E6=95=B4=E9=94=AE?= =?UTF-8?q?=E6=A3=80=E5=87=BA=EF=BC=89+=20=E6=A0=87=E7=AD=BE=E5=A5=91?= =?UTF-8?q?=E7=BA=A6=E9=92=89=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- .../config/center/__tests__/model.test.ts | 16 +++++-- admin/src/views/config/center/model.ts | 31 ++++++------ .../2026-10-14-config-center-ux-visual.md | 48 +++++++++++-------- 3 files changed, 56 insertions(+), 39 deletions(-) diff --git a/admin/src/views/config/center/__tests__/model.test.ts b/admin/src/views/config/center/__tests__/model.test.ts index 50affbd..aa365e4 100644 --- a/admin/src/views/config/center/__tests__/model.test.ts +++ b/admin/src/views/config/center/__tests__/model.test.ts @@ -55,6 +55,7 @@ import { rowToEntry, rowsToEntries, rowToSingleEntry, + SCENARIO_WHEN_LABELS, scopedConfigJson, secretFieldState, snapshotGroupKeys, @@ -795,7 +796,7 @@ describe('模板变量保护', () => { expect(removedTemplateVariables('发到 {intl} 联系', '发到手机联系')).toEqual(['intl']) expect(removedTemplateVariables('发到 {intl}', '发到 {intl}')).toEqual([]) }) - it('wall 整组递归检出(池+模板+提醒),同变量去重', () => { + it('wall 整组递归检出(池+模板+提醒),同变量去重(全域消失才报)', () => { const before = { deflections: { en: ['call {intl}'] }, cc_templates: { echo: { en: 'at {intl}' } }, @@ -803,14 +804,20 @@ describe('模板变量保护', () => { } const after = { deflections: { en: ['call me'] }, - cc_templates: { echo: { en: 'at {intl}' } }, - reminders: { cc_guide: { echo: '补全为 {intl}' } } + cc_templates: { echo: { en: 'at email' } }, + reminders: { cc_guide: { echo: '补全为手机号' } } } expect(collectRemovedWallVariables(before, after)).toEqual(['intl']) }) it('快照缺键不误报', () => { expect(collectRemovedWallVariables({}, { deflections: { en: ['x'] } })).toEqual([]) }) + it('删除含变量的尾行/整语言键同样检出(位置无关)', () => { + const before = { deflections: { en: ['plain', 'call {intl}'] } } + expect(collectRemovedWallVariables(before, { deflections: { en: ['plain'] } })).toEqual(['intl']) + const before2 = { cc_templates: { echo: { en: 'at {intl}' } } } + expect(collectRemovedWallVariables(before2, { cc_templates: { echo: {} } })).toEqual(['intl']) + }) }) describe('WALL_SCENARIOS(场景卡定义)', () => { @@ -819,4 +826,7 @@ describe('WALL_SCENARIOS(场景卡定义)', () => { expect(price?.pools.map((p) => p.field)).toEqual(['deflections', 'deflections_contact']) expect(price?.pools.map((p) => p.when)).toEqual(['not_given', 'given']) }) + it('状态标签契约(ScenarioCard 消费)', () => { + expect(SCENARIO_WHEN_LABELS).toEqual({ not_given: '客户还没留联系方式时', given: '客户已经留过之后' }) + }) }) diff --git a/admin/src/views/config/center/model.ts b/admin/src/views/config/center/model.ts index 51ed961..7b4d9cc 100644 --- a/admin/src/views/config/center/model.ts +++ b/admin/src/views/config/center/model.ts @@ -489,31 +489,30 @@ export function removedTemplateVariables(before: string, after: string): string[ return templateVariables(before).filter((v) => !afterSet.has(v)) } -function collectRemovedStrings(before: unknown, after: unknown, out: string[]): void { - if (typeof before === 'string' && typeof after === 'string') { - out.push(...removedTemplateVariables(before, after)) - } else if ( - before !== null && after !== null && - typeof before === 'object' && typeof after === 'object' - ) { - const b = before as Record - const a = after as Record - for (const key of Object.keys(b)) collectRemovedStrings(b[key], a[key], out) +function collectVariables(value: unknown, out: string[]): void { + if (typeof value === 'string') { + out.push(...templateVariables(value)) + } else if (value !== null && typeof value === 'object') { + // typeof 收窄只到 object(无索引签名)——与上方 asRecord 同款断言收形 + const record = value as Record + for (const key of Object.keys(record)) collectVariables(record[key], out) } } -/** wall 整组文本区(池/no_contact/cc_templates/reminders)递归比对——被删变量去重清单 */ +/** wall 整组文本区(池/no_contact/cc_templates/reminders)变量集差—— + * before 出现过且 after 全域不再出现的变量(位置无关:删除尾行/整键同样检出) */ export function collectRemovedWallVariables( before: Record | null | undefined, after: Record | null | undefined ): string[] { - const out: string[] = [] - const b = before ?? {} - const a = after ?? {} + const beforeVars: string[] = [] + const afterVars: string[] = [] for (const key of [...POOL_FIELDS, 'no_contact', 'cc_templates', 'reminders']) { - collectRemovedStrings(b[key], a[key], out) + collectVariables((before ?? {})[key], beforeVars) + collectVariables((after ?? {})[key], afterVars) } - return [...new Set(out)] + const afterSet = new Set(afterVars) + return [...new Set(beforeVars.filter((v) => !afterSet.has(v)))] } // ---- 试金石 ------------------------------------------------------------------------ diff --git a/docs/superpowers/plans/2026-10-14-config-center-ux-visual.md b/docs/superpowers/plans/2026-10-14-config-center-ux-visual.md index afa8673..ab05443 100644 --- a/docs/superpowers/plans/2026-10-14-config-center-ux-visual.md +++ b/docs/superpowers/plans/2026-10-14-config-center-ux-visual.md @@ -507,7 +507,7 @@ emits 声明追加 `(e: 'navigate', group: string): void`。 - [ ] **Step 1: 写失败测试**: ```ts -import { poolLanguages, templateVariables, removedTemplateVariables, collectRemovedWallVariables, WALL_SCENARIOS } from '../model' +import { poolLanguages, templateVariables, removedTemplateVariables, collectRemovedWallVariables, WALL_SCENARIOS, SCENARIO_WHEN_LABELS } from '../model' describe('poolLanguages(场景卡语言 tabs)', () => { it('多池语言并集,必留语言在前、附加语言按序', () => { @@ -529,7 +529,7 @@ describe('模板变量保护', () => { expect(removedTemplateVariables('发到 {intl} 联系', '发到手机联系')).toEqual(['intl']) expect(removedTemplateVariables('发到 {intl}', '发到 {intl}')).toEqual([]) }) - it('wall 整组递归检出(池+模板+提醒),同变量去重', () => { + it('wall 整组递归检出(池+模板+提醒),同变量去重(全域消失才报)', () => { const before = { deflections: { en: ['call {intl}'] }, cc_templates: { echo: { en: 'at {intl}' } }, @@ -537,14 +537,20 @@ describe('模板变量保护', () => { } const after = { deflections: { en: ['call me'] }, - cc_templates: { echo: { en: 'at {intl}' } }, - reminders: { cc_guide: { echo: '补全为 {intl}' } } + cc_templates: { echo: { en: 'at email' } }, + reminders: { cc_guide: { echo: '补全为手机号' } } } expect(collectRemovedWallVariables(before, after)).toEqual(['intl']) }) it('快照缺键不误报', () => { expect(collectRemovedWallVariables({}, { deflections: { en: ['x'] } })).toEqual([]) }) + it('删除含变量的尾行/整语言键同样检出(位置无关)', () => { + const before = { deflections: { en: ['plain', 'call {intl}'] } } + expect(collectRemovedWallVariables(before, { deflections: { en: ['plain'] } })).toEqual(['intl']) + const before2 = { cc_templates: { echo: { en: 'at {intl}' } } } + expect(collectRemovedWallVariables(before2, { cc_templates: { echo: {} } })).toEqual(['intl']) + }) }) describe('WALL_SCENARIOS(场景卡定义)', () => { @@ -553,6 +559,9 @@ describe('WALL_SCENARIOS(场景卡定义)', () => { expect(price?.pools.map((p) => p.field)).toEqual(['deflections', 'deflections_contact']) expect(price?.pools.map((p) => p.when)).toEqual(['not_given', 'given']) }) + it('状态标签契约(ScenarioCard 消费)', () => { + expect(SCENARIO_WHEN_LABELS).toEqual({ not_given: '客户还没留联系方式时', given: '客户已经留过之后' }) + }) }) ``` @@ -631,31 +640,30 @@ export function removedTemplateVariables(before: string, after: string): string[ return templateVariables(before).filter((v) => !afterSet.has(v)) } -function collectRemovedStrings(before: unknown, after: unknown, out: string[]): void { - if (typeof before === 'string' && typeof after === 'string') { - out.push(...removedTemplateVariables(before, after)) - } else if ( - before !== null && after !== null && - typeof before === 'object' && typeof after === 'object' - ) { - const b = before as Record - const a = after as Record - for (const key of Object.keys(b)) collectRemovedStrings(b[key], a[key], out) +function collectVariables(value: unknown, out: string[]): void { + if (typeof value === 'string') { + out.push(...templateVariables(value)) + } else if (value !== null && typeof value === 'object') { + // typeof 收窄只到 object(无索引签名)——断言收形 + const record = value as Record + for (const key of Object.keys(record)) collectVariables(record[key], out) } } -/** wall 整组文本区(池/no_contact/cc_templates/reminders)递归比对——被删变量去重清单 */ +/** wall 整组文本区(池/no_contact/cc_templates/reminders)变量集差—— + * before 出现过且 after 全域不再出现的变量(位置无关:删除尾行/整键同样检出) */ export function collectRemovedWallVariables( before: Record | null | undefined, after: Record | null | undefined ): string[] { - const out: string[] = [] - const b = before ?? {} - const a = after ?? {} + const beforeVars: string[] = [] + const afterVars: string[] = [] for (const key of [...POOL_FIELDS, 'no_contact', 'cc_templates', 'reminders']) { - collectRemovedStrings(b[key], a[key], out) + collectVariables((before ?? {})[key], beforeVars) + collectVariables((after ?? {})[key], afterVars) } - return [...new Set(out)] + const afterSet = new Set(afterVars) + return [...new Set(beforeVars.filter((v) => !afterSet.has(v)))] } ```