feat(admin): contact/engine/misc 表单映射纯函数
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -36,12 +36,18 @@ import {
|
|||||||
buildWallPayload,
|
buildWallPayload,
|
||||||
COMPARE_SCOPE_ALL,
|
COMPARE_SCOPE_ALL,
|
||||||
collectRemovedWallVariables,
|
collectRemovedWallVariables,
|
||||||
|
contactToForm,
|
||||||
|
contactToPayload,
|
||||||
descendingVersions,
|
descendingVersions,
|
||||||
|
engineToForm,
|
||||||
|
engineToPayload,
|
||||||
formatKB,
|
formatKB,
|
||||||
formatVersionTime,
|
formatVersionTime,
|
||||||
initWallEdit,
|
initWallEdit,
|
||||||
JSON_GROUP_FIELDS,
|
JSON_GROUP_FIELDS,
|
||||||
jsonGroupDraft,
|
jsonGroupDraft,
|
||||||
|
miscToForm,
|
||||||
|
miscToPayload,
|
||||||
normalizeAiCredsGroup,
|
normalizeAiCredsGroup,
|
||||||
normalizeEmailCredsGroup,
|
normalizeEmailCredsGroup,
|
||||||
normalizePromptsGroup,
|
normalizePromptsGroup,
|
||||||
@@ -63,6 +69,8 @@ import {
|
|||||||
touchstoneSentences,
|
touchstoneSentences,
|
||||||
utf8Bytes,
|
utf8Bytes,
|
||||||
validateEmailCredsForm,
|
validateEmailCredsForm,
|
||||||
|
validateEngineForm,
|
||||||
|
validateKeyValueRows,
|
||||||
validatePushCredsForm,
|
validatePushCredsForm,
|
||||||
type EmailCredsForm,
|
type EmailCredsForm,
|
||||||
type PromptsDraft,
|
type PromptsDraft,
|
||||||
@@ -830,3 +838,62 @@ describe('WALL_SCENARIOS(场景卡定义)', () => {
|
|||||||
expect(SCENARIO_WHEN_LABELS).toEqual({ not_given: '客户还没留联系方式时', given: '客户已经留过之后' })
|
expect(SCENARIO_WHEN_LABELS).toEqual({ not_given: '客户还没留联系方式时', given: '客户已经留过之后' })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---- contact / engine / misc 表单化(Task 12;JsonGroup 退役配套) ---------------------
|
||||||
|
|
||||||
|
describe('contact 表单化', () => {
|
||||||
|
it('快照 → 表单(bool 缺省 false / 语言数组)', () => {
|
||||||
|
expect(contactToForm({ cc_completion: true, email_light_ask: false, template_languages: ['en', 'zh'] }))
|
||||||
|
.toEqual({ ccCompletion: true, emailLightAsk: false, templateLanguages: ['en', 'zh'] })
|
||||||
|
expect(contactToForm(null)).toEqual({ ccCompletion: false, emailLightAsk: false, templateLanguages: [] })
|
||||||
|
})
|
||||||
|
it('表单 → payload:接管键覆写 + 未知键透传', () => {
|
||||||
|
const snapshot = { cc_completion: false, email_light_ask: false, template_languages: [], _meta: 'keep' }
|
||||||
|
expect(contactToPayload({ ccCompletion: true, emailLightAsk: true, templateLanguages: ['en', ' '] }, snapshot))
|
||||||
|
.toEqual({ _meta: 'keep', cc_completion: true, email_light_ask: true, template_languages: ['en'] })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('engine 表单化', () => {
|
||||||
|
it('快照 → 表单(数值转串 / seed 映射转行)', () => {
|
||||||
|
expect(engineToForm({ rate_limit_per_hour: 120, ws_conn_limit: 5, context_seed_keys: { a: 'b' } }))
|
||||||
|
.toEqual({ rateLimit: '120', wsConnLimit: '5', seedRows: [{ key: 'a', value: 'b' }] })
|
||||||
|
expect(engineToForm(null)).toEqual({ rateLimit: '', wsConnLimit: '', seedRows: [] })
|
||||||
|
})
|
||||||
|
it('payload:空数值槽保快照原值(快照也无 → 键省略);未知键透传;seed 恒重建', () => {
|
||||||
|
const snapshot = { rate_limit_per_hour: 120, ws_conn_limit: 5, other: 1 }
|
||||||
|
expect(engineToPayload({ rateLimit: '', wsConnLimit: '9', seedRows: [] }, snapshot))
|
||||||
|
.toEqual({ other: 1, rate_limit_per_hour: 120, ws_conn_limit: 9, context_seed_keys: {} })
|
||||||
|
expect(engineToPayload({ rateLimit: '60', wsConnLimit: '', seedRows: [] }, null))
|
||||||
|
.toEqual({ rate_limit_per_hour: 60, context_seed_keys: {} })
|
||||||
|
})
|
||||||
|
it('payload:seed 行空键值剔除、重建映射', () => {
|
||||||
|
expect(engineToPayload(
|
||||||
|
{ rateLimit: '1', wsConnLimit: '1', seedRows: [{ key: 'product_name', value: 'app_product_name' }, { key: '', value: 'x' }] },
|
||||||
|
{}
|
||||||
|
).context_seed_keys).toEqual({ product_name: 'app_product_name' })
|
||||||
|
})
|
||||||
|
it('校验:非正整数报错', () => {
|
||||||
|
expect(validateEngineForm({ rateLimit: '0', wsConnLimit: '', seedRows: [] }))
|
||||||
|
.toEqual(['每 IP 每小时发言上限:须为正整数'])
|
||||||
|
expect(validateEngineForm({ rateLimit: '30', wsConnLimit: '2', seedRows: [] })).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('misc 表单化', () => {
|
||||||
|
it('往返:快照 → 表单 → payload 保未知键', () => {
|
||||||
|
const snapshot = { name_to_region_extras: { 'saudi arabia': '966' }, keep: true }
|
||||||
|
const form = miscToForm(snapshot)
|
||||||
|
expect(form.extrasRows).toEqual([{ key: 'saudi arabia', value: '966' }])
|
||||||
|
expect(miscToPayload({ extrasRows: [{ key: 'saudi arabia', value: '966' }, { key: 'uae', value: '971' }] }, snapshot))
|
||||||
|
.toEqual({ keep: true, name_to_region_extras: { 'saudi arabia': '966', uae: '971' } })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('KeyValue 行校验(engine seed / misc extras 共用)', () => {
|
||||||
|
it('重复键与空键报错', () => {
|
||||||
|
expect(validateKeyValueRows([{ key: 'a', value: '1' }, { key: 'a', value: '2' }], '映射'))
|
||||||
|
.toEqual(['映射:键 a 重复'])
|
||||||
|
expect(validateKeyValueRows([{ key: '', value: '1' }], '映射')).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -934,3 +934,148 @@ export const JSON_GROUP_FIELDS: Record<
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- contact / engine / misc 表单化(spec §5.4-5.7;JsonGroup 退役配套) ----------------
|
||||||
|
|
||||||
|
export interface KeyValueRow {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 键值行校验(空行整行忽略;非空键重复报错;半行序列化时剔除不报错) */
|
||||||
|
export function validateKeyValueRows(rows: KeyValueRow[], label: string): string[] {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const errors: string[] = []
|
||||||
|
for (const row of rows) {
|
||||||
|
if (row.key.trim() === '' && row.value.trim() === '') continue
|
||||||
|
const key = row.key.trim()
|
||||||
|
if (key === '') continue // 半行(有 value 无 key)序列化时整行剔除,不报错
|
||||||
|
if (seen.has(key)) errors.push(`${label}:键 ${key} 重复`)
|
||||||
|
seen.add(key)
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowsToRecord(rows: KeyValueRow[]): Record<string, string> {
|
||||||
|
const out: Record<string, string> = {}
|
||||||
|
for (const row of rows) {
|
||||||
|
const key = row.key.trim()
|
||||||
|
if (key !== '') out[key] = row.value
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordToRows(value: unknown): KeyValueRow[] {
|
||||||
|
const src = asRecord(value) ?? {}
|
||||||
|
return Object.entries(src)
|
||||||
|
.filter(([, v]) => typeof v === 'string')
|
||||||
|
.map(([key, v]) => ({ key, value: v as string }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- contact ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface ContactForm {
|
||||||
|
ccCompletion: boolean
|
||||||
|
emailLightAsk: boolean
|
||||||
|
templateLanguages: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contactToForm(
|
||||||
|
group: Record<string, unknown> | null | undefined
|
||||||
|
): ContactForm {
|
||||||
|
const src = group ?? {}
|
||||||
|
return {
|
||||||
|
ccCompletion: src.cc_completion === true,
|
||||||
|
emailLightAsk: src.email_light_ask === true,
|
||||||
|
templateLanguages: strList(src.template_languages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contactToPayload(
|
||||||
|
form: ContactForm,
|
||||||
|
group: Record<string, unknown> | null | undefined
|
||||||
|
): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
...(group ?? {}),
|
||||||
|
cc_completion: form.ccCompletion,
|
||||||
|
email_light_ask: form.emailLightAsk,
|
||||||
|
template_languages: form.templateLanguages.map((s) => s.trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- engine -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface EngineForm {
|
||||||
|
/** 正整数串;'' = 未填(payload 保快照原值/省略键——绝不提交非法值) */
|
||||||
|
rateLimit: string
|
||||||
|
wsConnLimit: string
|
||||||
|
seedRows: KeyValueRow[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function intToForm(value: unknown): string {
|
||||||
|
return typeof value === 'number' && Number.isInteger(value) ? String(value) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function engineToForm(
|
||||||
|
group: Record<string, unknown> | null | undefined
|
||||||
|
): EngineForm {
|
||||||
|
const src = group ?? {}
|
||||||
|
return {
|
||||||
|
rateLimit: intToForm(src.rate_limit_per_hour),
|
||||||
|
wsConnLimit: intToForm(src.ws_conn_limit),
|
||||||
|
seedRows: recordToRows(src.context_seed_keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单 → payload:空数值槽 = 保快照原值(spread 已带上;快照无值则不带键
|
||||||
|
* ——resolver 出厂回退),非空槽覆写;context_seed_keys 行编辑恒重建(空行
|
||||||
|
* 剔除——用户显式管理该映射,清空即提交空映射);其余未知键透传。
|
||||||
|
*/
|
||||||
|
export function engineToPayload(
|
||||||
|
form: EngineForm,
|
||||||
|
group: Record<string, unknown> | null | undefined
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const out: Record<string, unknown> = { ...(group ?? {}) }
|
||||||
|
const rate = form.rateLimit.trim()
|
||||||
|
if (rate !== '') out.rate_limit_per_hour = Number(rate)
|
||||||
|
const ws = form.wsConnLimit.trim()
|
||||||
|
if (ws !== '') out.ws_conn_limit = Number(ws)
|
||||||
|
out.context_seed_keys = rowsToRecord(form.seedRows)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateEngineForm(form: EngineForm): string[] {
|
||||||
|
const errors: string[] = []
|
||||||
|
const rate = form.rateLimit.trim()
|
||||||
|
if (rate !== '' && (!/^\d+$/.test(rate) || Number(rate) < 1)) {
|
||||||
|
errors.push('每 IP 每小时发言上限:须为正整数')
|
||||||
|
}
|
||||||
|
const ws = form.wsConnLimit.trim()
|
||||||
|
if (ws !== '' && (!/^\d+$/.test(ws) || Number(ws) < 1)) {
|
||||||
|
errors.push('每 IP WebSocket 连接上限:须为正整数')
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- misc ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface MiscForm {
|
||||||
|
extrasRows: KeyValueRow[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function miscToForm(
|
||||||
|
group: Record<string, unknown> | null | undefined
|
||||||
|
): MiscForm {
|
||||||
|
return { extrasRows: recordToRows((group ?? {}).name_to_region_extras) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function miscToPayload(
|
||||||
|
form: MiscForm,
|
||||||
|
group: Record<string, unknown> | null | undefined
|
||||||
|
): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
...(group ?? {}),
|
||||||
|
name_to_region_extras: rowsToRecord(form.extrasRows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user