feat: 配置中心-提示词与词表(四件套编辑/结构化词表/试金石面板/保存前验证)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-13 20:05:42 +08:00
parent cac91e9e3e
commit 39d72c53aa
8 changed files with 2229 additions and 10 deletions
+79 -1
View File
@@ -1,5 +1,5 @@
/**
* 配置中心 API 单元测试(Phase 3 Task 7a——品牌相关)
* 配置中心 API 单元测试(Phase 3 Task 7a——品牌相关;Task 7b——试金石)
*
* 钉住 API 层契约(事实源 app/platform/routes/config_routes.py 与
* upload_routes.py);HTTP 层行为已在 client.test.ts 覆盖,此处 mock http
@@ -7,6 +7,8 @@
* - GET config:快照 + version(原样组——缺键防御读取靠 normalizeBrandGroup)
* - PUT config/brand:请求体是**组值本身**(整组替换语义,非 {note,value}
* 包装),备注走 comment 查询参数,无备注时不带 params
* - POST wall/test(Task 7b):句子列表 + wall 覆盖(当前表单值预演,
* 不落库)+ has_contact/source 可选项——保存前自动验证复用本函数
* - 头像上传:multipart file 字段(FormData)
* - normalizeBrandGroup:缺键补空串 / 类型漂移防御 / topics 只留字符串项
*/
@@ -22,6 +24,7 @@ import {
fetchTenantConfig,
normalizeBrandGroup,
putConfigGroup,
testWall,
uploadBrandAvatar
} from '@/api/config'
@@ -116,6 +119,81 @@ describe('配置中心 API(src/api/config.ts)', () => {
})
})
describe('试金石 API(testWall · Phase 3 Task 7b)', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('POST wall/test:句子 + wall 覆盖(当前表单值预演)+ 判定选项', async () => {
const response = {
slug: 'jiancai',
wall_source: 'override',
results: [
{
sentence: 'The price is 500 USD per ton.',
action: 'blocked',
reason: 'boundary',
replacement: 'Prices vary a lot by config...'
},
{ sentence: 'PPR pipes in stock?', action: 'pass', reason: null, replacement: null }
]
}
vi.mocked(http.post).mockResolvedValue(response)
const wallOverride = {
price_patterns: ['\\$\\s?\\d'],
deflections: { en: ['...'], zh: ['...'] }
}
const res = await testWall(
'jiancai',
{
sentences: ['The price is 500 USD per ton.', 'PPR pipes in stock?'],
wall: wallOverride,
has_contact: true,
source: 'app'
},
{ showErrorMessage: false }
)
expect(res).toEqual(response)
expect(http.post).toHaveBeenCalledTimes(1)
expect(http.post).toHaveBeenCalledWith(
'/api/platform/tenants/jiancai/wall/test',
{
sentences: ['The price is 500 USD per ton.', 'PPR pipes in stock?'],
wall: wallOverride, // 覆盖墙原样直传——后端 compile_wall 校验后纯内存判定
has_contact: true,
source: 'app'
},
{ showErrorMessage: false } // 坏墙 400 字段路径由调用方内联展示
)
})
it('POST wall/test:最小 body(仅句子——缺省用当前生效墙)', async () => {
const response = {
slug: 'jiancai',
wall_source: 'current',
results: [{ sentence: 'Hello', action: 'pass', reason: null, replacement: null }]
}
vi.mocked(http.post).mockResolvedValue(response)
const res = await testWall('jiancai', { sentences: ['Hello'] })
expect(res).toEqual(response)
expect(http.post).toHaveBeenCalledWith(
'/api/platform/tenants/jiancai/wall/test',
{ sentences: ['Hello'] },
undefined
)
})
it('CONFIG_ENDPOINTS.wallTest:端点路径与后端 config_routes 一致', () => {
expect(CONFIG_ENDPOINTS.wallTest('jiancai')).toBe(
'/api/platform/tenants/jiancai/wall/test'
)
})
})
describe('normalizeBrandGroup(GET 原样组 → 表单完整值)', () => {
it('缺键补空串、topics 缺省为空数组(快照是原样组,可能缺键)', () => {
const out = normalizeBrandGroup({ title: 'HESUPIPE Support' })
+31 -3
View File
@@ -1,5 +1,6 @@
/**
* 租户配置中心 API(Phase 3 Task 3 骨架;Task 7a 实装品牌相关调用函数)
* 租户配置中心 API(Phase 3 Task 3 骨架;Task 7a 实装品牌相关调用函数;
* Task 7b 补词表试金石调用)
*
* 端点事实源:app/platform/routes/config_routes.py
* (配置快照 / 分组编辑 / 版本历史 / 回滚 / 重置 / 试金石)
@@ -16,6 +17,14 @@
* - 版本行字段是 updated_by / comment(非骨架期误写的 created_by / note);
* - 回滚请求体只认 version(note 不被后端读取);
* - 试金石响应逐句为 {sentence, action, reason, replacement}。
*
* ## Task 7b 试金石契约(config_routes.test_wall)
*
* - body:sentences 必填;wall 可选覆盖(编辑器当前表单值的**预演**——
* 后端先 compile_wall 校验后纯内存判定,不落库不 bump 版本),坏墙
* 400 带字段路径;has_contact / source 可选(默认 false / web);
* - 判定优先级与 conversation.py 执行侧一致:价格墙(blocked)→
* 零号码墙(replaced)→ 再索要墙(仅 has_contact,blocked 无替换)。
*/
import { http, type RequestOptions } from './client'
@@ -94,9 +103,12 @@ export interface ConfigRollbackResponse {
config: Record<string, Record<string, unknown>>
}
/** 词表试金石请求(POST .../wall/test) */
/** 词表试金石请求(POST .../wall/test)
* wall 覆盖 = 编辑器当前表单值的预演(后端 compile_wall 校验 + 纯内存
* 判定,不落库);缺省用当前生效墙。 */
export interface WallTestRequest {
sentences: string[]
wall?: Record<string, unknown>
has_contact?: boolean
source?: 'web' | 'app'
}
@@ -169,6 +181,20 @@ export function putConfigGroup(
)
}
/**
* 词表试金石(POST .../wall/test):逐句墙判定预览。传 wall 覆盖 =
* 编辑器当前表单值的预演(不落库不 bump 版本)——保存词表前的自动
* 验证复用本函数;坏墙(正则编译失败等)400 带字段路径,调用方需
* showErrorMessage: false 自行内联展示。
*/
export function testWall(
slug: string,
body: WallTestRequest,
config?: RequestOptions
): Promise<WallTestResponse> {
return http.post<WallTestResponse>(CONFIG_ENDPOINTS.wallTest(slug), body, config)
}
/**
* 头像上传(multipart file 字段):魔数校验 / 2MB 上限在后端
* (upload_routes.py),前端做扩展名与大小预检提前拦截。
@@ -190,7 +216,9 @@ export function uploadBrandAvatar(
/**
* GET 的 brand 组(原样快照,可能缺键/类型漂移)→ 表单友好的完整值。
* 缺键补空串(与 DEFAULT_CONFIG.brand 的空串出厂一致);topics 只留字符串项。
* 缺键补空串作表单中性值:提交空串 = 显式清空覆盖出厂(出厂
* title/color/avatar 本非空——"AI Support"/"#066fc0"/内置头像),快照
* 缺省键则由消费侧深合并回落出厂;UI 所见即所得。topics 只留字符串项。
*/
export function normalizeBrandGroup(
value: Record<string, unknown> | null | undefined