fix(admin): es 尾巴空=整键丢弃(spec §3.2 管道身份泄漏修复)
审核页 es 收尾句留空的原语义「不触 es 池(保持快照原值)」是误读—— spec §3.2 要求整键丢弃:出厂 es 池带行业身份(如建材档案的 tuberías),保留快照会把上一行业的收尾句继续投给本租户的西语客户。 - model.ts:新增 applyWallDispatch 纯函数(wall 保存步载荷落地—— 非 null 语言覆写、null 语言整键删除);wallPoolDispatch/WallDispatch 及三处旧「不触 es 池」注记全部改为整键丢弃语义 - index.vue:wall 步改用 applyWallDispatch;es 输入 placeholder 改 「留空 = 丢弃现有 es 池」;进审核页后台拉取现 es 池,留空且有句时 展示丢弃预览(破坏性操作不静默);skipAvatar 勾选保存中禁用 (防 for-of 期间改动 saveSteps 计算源) - 测试:原「es新句为null_整键删除」用例名实不符(断言的是保快照) 已正名并移交载荷断言;新增 applyWallDispatch 两用例——tuberías 泄漏链回归钉('es' in payload === false)+ 其余语言键透传 后端安全性已核实:boundary_filter 的 pools.get(lang) or pools["en"] 使缺 es 回退到新 EN 尾巴;wall_schema 仅必留 en/zh。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -14,10 +14,14 @@
|
|||||||
* 版本含 avatar 文件名,读-改-写才不丢;wall 带池形态分派注记;每步
|
* 版本含 avatar 文件名,读-改-写才不丢;wall 带池形态分派注记;每步
|
||||||
* 含 label 供进度展示);
|
* 含 label 供进度展示);
|
||||||
* - warningsLevel:规格判定=info / 剔除·数字移除=warning / 其他=info;
|
* - warningsLevel:规格判定=info / 剔除·数字移除=warning / 其他=info;
|
||||||
* - isTerminal404:ApiError status 404 → 终态(任务过期,停止轮询)。
|
* - isTerminal404:ApiError status 404 → 终态(任务过期,停止轮询);
|
||||||
|
* - wallPoolDispatch / applyWallDispatch:池形态分派(单句→替换、多句→
|
||||||
|
* 追加)与载荷落地——es 尾巴 null = **整键丢弃** es 池(spec §3.2 防
|
||||||
|
* 出厂 es 池行业身份泄漏,如建材档案「tuberías」泄漏给非管道租户)。
|
||||||
*/
|
*/
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import {
|
import {
|
||||||
|
applyWallDispatch,
|
||||||
buildSaveSequence,
|
buildSaveSequence,
|
||||||
dataUrlToBlob,
|
dataUrlToBlob,
|
||||||
draftToBrandForm,
|
draftToBrandForm,
|
||||||
@@ -233,9 +237,10 @@ describe('wallPoolDispatch', () => {
|
|||||||
expect(d.pools.es).toEqual(['uno', 'dos', 'tail-es'])
|
expect(d.pools.es).toEqual(['uno', 'dos', 'tail-es'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('es新句为null_整键删除', () => {
|
it('es新句为null_分派标记null(payload落地=整键删除,见 applyWallDispatch)', () => {
|
||||||
// es 尾巴缺失 = 合法降级(spec §4):pools.es 为 null——消费方载荷
|
// es 尾巴缺失 = 合法降级:分派结果 pools.es 为 null——**语义是整键
|
||||||
// 不触 es 键(保持 GET 快照原值),「整键删除」指分派结果该键为 null
|
// 丢弃**(spec §3.2:防出厂 es 池行业身份泄漏),不是「保持快照原值」。
|
||||||
|
// 载荷落地(键真的被删)由 applyWallDispatch 承担并在其用例断言。
|
||||||
const current = { en: ['a'], zh: ['中'], es: ['es-现句'] }
|
const current = { en: ['a'], zh: ['中'], es: ['es-现句'] }
|
||||||
const d = wallPoolDispatch(current, { en: 'e', zh: 'z', es: null })
|
const d = wallPoolDispatch(current, { en: 'e', zh: 'z', es: null })
|
||||||
expect(d.mode).toBe('replace')
|
expect(d.mode).toBe('replace')
|
||||||
@@ -263,3 +268,35 @@ describe('wallPoolDispatch', () => {
|
|||||||
expect(d.pools.es).toBeNull()
|
expect(d.pools.es).toBeNull()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---- applyWallDispatch(wall 保存步载荷落地) --------------------------------------------
|
||||||
|
|
||||||
|
describe('applyWallDispatch', () => {
|
||||||
|
it('es null = 整键删除(spec §3.2——出厂 es 池行业身份不外漏)', () => {
|
||||||
|
// 泄漏链回归钉:jiancai 出厂 es 池含「tuberías」(管道)——非管道
|
||||||
|
// 租户导入站点无西语内容时,es 键必须从载荷中消失(而非保留快照),
|
||||||
|
// 否则西语客户继续看到上一行业的收尾句。
|
||||||
|
const current = {
|
||||||
|
en: ['a'],
|
||||||
|
zh: ['中'],
|
||||||
|
es: ['Nuestro gerente te enviará el precio exacto a tu WhatsApp en breve. ¿Algo más sobre nuestras tuberías?']
|
||||||
|
}
|
||||||
|
const d = wallPoolDispatch(current, { en: 'e', zh: 'z', es: null })
|
||||||
|
const payload = applyWallDispatch(current, d)
|
||||||
|
// es 键不复存在(不是空数组、不是原句——键本身被删)
|
||||||
|
expect('es' in payload).toBe(false)
|
||||||
|
expect(payload).toEqual({ en: ['e'], zh: ['z'] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('非 null 语言覆写、其余语言键透传(快照 spread)', () => {
|
||||||
|
const current = { en: ['old-en'], zh: ['旧句一', '旧句二'], fr: ['bonjour'] }
|
||||||
|
const d = wallPoolDispatch(current, { en: 'new-en', zh: '新句', es: 'nuevo' })
|
||||||
|
const payload = applyWallDispatch(current, d)
|
||||||
|
expect(payload).toEqual({
|
||||||
|
en: ['new-en'],
|
||||||
|
zh: ['旧句一', '旧句二', '新句'],
|
||||||
|
es: ['nuevo'],
|
||||||
|
fr: ['bonjour'] // 快照其余语言键原样透传
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
- 顶部黄条「保存将覆盖当前配置(产生新版本,可随时回滚)」+ warnings
|
- 顶部黄条「保存将覆盖当前配置(产生新版本,可随时回滚)」+ warnings
|
||||||
分级区(warningsLevel:info 蓝 / warning 橙);
|
分级区(warningsLevel:info 蓝 / warning 橙);
|
||||||
- 左列品牌表单(draftToBrandForm 预填可改;头像 dataURL <img> 预览 +
|
- 左列品牌表单(draftToBrandForm 预填可改;头像 dataURL <img> 预览 +
|
||||||
「不上传头像」勾选)+ 墙尾巴 en/zh/es 三行(es 留空 = 不修改该语言)
|
「不上传头像」勾选)+ 墙尾巴 en/zh/es 三行(es 留空 = 整键丢弃现有
|
||||||
|
es 池——spec §3.2 防出厂行业身份泄漏;现池有句时展示丢弃预览)
|
||||||
+ 联系方式两开关;右列四件套 tabs(等宽 textarea + UTF-8 字节计数);
|
+ 联系方式两开关;右列四件套 tabs(等宽 textarea + UTF-8 字节计数);
|
||||||
- 「保存全部」按 buildSaveSequence 依次执行(复用 ConfigCard 视觉壳):
|
- 「保存全部」按 buildSaveSequence 依次执行(复用 ConfigCard 视觉壳):
|
||||||
头像(dataUrlToBlob→uploadBrandAvatar)→ brand → prompts → wall →
|
头像(dataUrlToBlob→uploadBrandAvatar)→ brand → prompts → wall →
|
||||||
@@ -197,7 +198,11 @@
|
|||||||
<span v-else class="avatar-none">无</span>
|
<span v-else class="avatar-none">无</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="avatar-side">
|
<div class="avatar-side">
|
||||||
<ElCheckbox v-if="brandForm.avatarDataUrl" v-model="skipAvatar">
|
<ElCheckbox
|
||||||
|
v-if="brandForm.avatarDataUrl"
|
||||||
|
v-model="skipAvatar"
|
||||||
|
:disabled="saveState === 'running'"
|
||||||
|
>
|
||||||
不上传头像(保留当前头像)
|
不上传头像(保留当前头像)
|
||||||
</ElCheckbox>
|
</ElCheckbox>
|
||||||
<p v-else class="avatar-tip">草稿未抓到可用头像——保存不涉及头像</p>
|
<p v-else class="avatar-tip">草稿未抓到可用头像——保存不涉及头像</p>
|
||||||
@@ -253,11 +258,25 @@
|
|||||||
<ElInput v-model="wallTails.zh" placeholder="中文收尾句(必填)" />
|
<ElInput v-model="wallTails.zh" placeholder="中文收尾句(必填)" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="es">
|
<ElFormItem label="es">
|
||||||
<ElInput v-model="wallTails.es" placeholder="西班牙语收尾句(留空 = 不修改现有 es 池)" />
|
<div class="es-tail-col">
|
||||||
|
<ElInput
|
||||||
|
v-model="wallTails.es"
|
||||||
|
placeholder="西班牙语收尾句(留空 = 丢弃现有 es 池,西语客户将看到英文收尾句)"
|
||||||
|
/>
|
||||||
|
<div v-if="esDropPreview" class="es-drop-note">
|
||||||
|
<p>
|
||||||
|
保存后将丢弃现有 es 池({{ currentEsPool.length }} 句,西语客户将看到上方英文收尾句):
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(s, i) in currentEsPool" :key="i">{{ s }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<p class="card-tip">
|
<p class="card-tip">
|
||||||
保存时按当前池形态分派:出厂单句池整句替换;已含人工编辑句(多句)的池追加到末项并提示。
|
保存时按当前池形态分派:出厂单句池整句替换;已含人工编辑句(多句)的池追加到末项并提示。es
|
||||||
|
留空 = 整键丢弃现有 es 池(西语会话回退英文收尾句)。
|
||||||
</p>
|
</p>
|
||||||
</ConfigCard>
|
</ConfigCard>
|
||||||
|
|
||||||
@@ -386,6 +405,7 @@
|
|||||||
import ConfigCard from '@/views/config/center/ConfigCard.vue'
|
import ConfigCard from '@/views/config/center/ConfigCard.vue'
|
||||||
import { PROMPTS_CAPS, formatKB, utf8Bytes } from '@/views/config/center/model'
|
import { PROMPTS_CAPS, formatKB, utf8Bytes } from '@/views/config/center/model'
|
||||||
import {
|
import {
|
||||||
|
applyWallDispatch,
|
||||||
buildSaveSequence,
|
buildSaveSequence,
|
||||||
dataUrlToBlob,
|
dataUrlToBlob,
|
||||||
draftToBrandForm,
|
draftToBrandForm,
|
||||||
@@ -632,6 +652,33 @@
|
|||||||
const wallTails = ref({ en: '', zh: '', es: '' })
|
const wallTails = ref({ en: '', zh: '', es: '' })
|
||||||
const contactForm = ref({ ccCompletion: false, emailLightAsk: false })
|
const contactForm = ref({ ccCompletion: false, emailLightAsk: false })
|
||||||
const skipAvatar = ref(false)
|
const skipAvatar = ref(false)
|
||||||
|
/** 现租户 es 池(进审核页时后台拉取——es 留空的丢弃预览数据源) */
|
||||||
|
const currentEsPool = ref<string[]>([])
|
||||||
|
|
||||||
|
/** es 留空且现池有句 → 展示将被丢弃的句子(spec §3.2 整键丢弃是破坏性
|
||||||
|
* 操作,对多句人工池不静默——预览让管理员看清丢的是什么) */
|
||||||
|
const esDropPreview = computed(
|
||||||
|
() => wallTails.value.es.trim() === '' && currentEsPool.value.length > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 后台拉取现租户 es 池(静默失败为空——预览缺失不阻塞审核);完成后
|
||||||
|
* 校验 slug 未变,防跨租户串台(关闭重开的竞态窗口) */
|
||||||
|
function loadCurrentEsPool(slug: string): void {
|
||||||
|
fetchTenantConfig(slug, { showErrorMessage: false })
|
||||||
|
.then((snap) => {
|
||||||
|
if (targetSlug.value !== slug) return
|
||||||
|
const group = snap.config.wall as Record<string, unknown> | undefined
|
||||||
|
const pools = group?.deflections_contact as Record<string, unknown> | undefined
|
||||||
|
const es = pools?.es
|
||||||
|
currentEsPool.value = Array.isArray(es)
|
||||||
|
? es.filter((s): s is string => typeof s === 'string')
|
||||||
|
: []
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (targetSlug.value !== slug) return
|
||||||
|
currentEsPool.value = []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function blankBrandForm(): SiteImportBrandForm {
|
function blankBrandForm(): SiteImportBrandForm {
|
||||||
return {
|
return {
|
||||||
@@ -656,6 +703,7 @@
|
|||||||
wallTails.value = { en: '', zh: '', es: '' }
|
wallTails.value = { en: '', zh: '', es: '' }
|
||||||
contactForm.value = { ccCompletion: false, emailLightAsk: false }
|
contactForm.value = { ccCompletion: false, emailLightAsk: false }
|
||||||
skipAvatar.value = false
|
skipAvatar.value = false
|
||||||
|
currentEsPool.value = []
|
||||||
saveState.value = 'idle'
|
saveState.value = 'idle'
|
||||||
completedKinds.value = []
|
completedKinds.value = []
|
||||||
saveErrorText.value = ''
|
saveErrorText.value = ''
|
||||||
@@ -675,6 +723,7 @@
|
|||||||
}
|
}
|
||||||
importVisible.value = false
|
importVisible.value = false
|
||||||
reviewVisible.value = true
|
reviewVisible.value = true
|
||||||
|
loadCurrentEsPool(targetSlug.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function byteInfo(tab: PromptsTab): { over: boolean; label: string } {
|
function byteInfo(tab: PromptsTab): { over: boolean; label: string } {
|
||||||
@@ -746,7 +795,7 @@
|
|||||||
return '主题色须为 3 或 6 位十六进制(如 #066fc0),或留空取挂件默认'
|
return '主题色须为 3 或 6 位十六进制(如 #066fc0),或留空取挂件默认'
|
||||||
}
|
}
|
||||||
if (!wallTails.value.en.trim() || !wallTails.value.zh.trim()) {
|
if (!wallTails.value.en.trim() || !wallTails.value.zh.trim()) {
|
||||||
return '墙尾巴 en / zh 不能为空(es 可留空 = 不修改该语言池)'
|
return '墙尾巴 en / zh 不能为空(es 可留空 = 丢弃现有 es 池)'
|
||||||
}
|
}
|
||||||
for (const tab of promptsTabs.value) {
|
for (const tab of promptsTabs.value) {
|
||||||
if (utf8Bytes(tab.text) > PROMPTS_CAPS[tab.key]) {
|
if (utf8Bytes(tab.text) > PROMPTS_CAPS[tab.key]) {
|
||||||
@@ -814,11 +863,7 @@
|
|||||||
zh: wallTails.value.zh.trim(),
|
zh: wallTails.value.zh.trim(),
|
||||||
es: wallTails.value.es.trim() === '' ? null : wallTails.value.es.trim()
|
es: wallTails.value.es.trim() === '' ? null : wallTails.value.es.trim()
|
||||||
})
|
})
|
||||||
const nextPool: Record<string, string[]> = { ...current }
|
const nextPool = applyWallDispatch(current, dispatch)
|
||||||
for (const lang of ['en', 'zh', 'es'] as const) {
|
|
||||||
const pool = dispatch.pools[lang]
|
|
||||||
if (pool !== null) nextPool[lang] = pool
|
|
||||||
}
|
|
||||||
await putConfigGroup(
|
await putConfigGroup(
|
||||||
slug,
|
slug,
|
||||||
'wall',
|
'wall',
|
||||||
@@ -1125,6 +1170,37 @@
|
|||||||
color: var(--art-gray-400);
|
color: var(--art-gray-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.es-tail-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.es-drop-note {
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--el-color-warning);
|
||||||
|
background: var(--el-color-warning-light-9);
|
||||||
|
border: 1px solid var(--el-color-warning-light-5);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 18px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 2px 0;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.switch-row {
|
.switch-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ export interface SiteImportDraft {
|
|||||||
evaluator: string
|
evaluator: string
|
||||||
}
|
}
|
||||||
/** 墙尾巴三语(deflections_contact 池的已留资态收尾句);es 可 null
|
/** 墙尾巴三语(deflections_contact 池的已留资态收尾句);es 可 null
|
||||||
* (合法降级——wall_schema 只必留 en/zh,保存时不触 es 池) */
|
* (合法降级——保存时整键丢弃 es 池,spec §3.2:防出厂 es 池行业身份
|
||||||
|
* 泄漏;西语会话由 boundary_filter 回退看到新 EN 尾巴) */
|
||||||
wall_tails: { en: string; zh: string; es: string | null }
|
wall_tails: { en: string; zh: string; es: string | null }
|
||||||
contact: { cc_completion: boolean; email_light_ask: boolean }
|
contact: { cc_completion: boolean; email_light_ask: boolean }
|
||||||
/** 白话注记清单(规格判定/剔除/移除/缺省兜底)——分级见 warningsLevel */
|
/** 白话注记清单(规格判定/剔除/移除/缺省兜底)——分级见 warningsLevel */
|
||||||
@@ -183,7 +184,7 @@ export interface SaveStep {
|
|||||||
* ——上传头像会写新版本(brand.avatar 落文件名),之后再读-改-写才不把
|
* ——上传头像会写新版本(brand.avatar 落文件名),之后再读-改-写才不把
|
||||||
* 文件名丢掉(spec §5)。wall 步的池形态分派:deflections_contact 出厂
|
* 文件名丢掉(spec §5)。wall 步的池形态分派:deflections_contact 出厂
|
||||||
* 单句 → 整句替换;已多句 → 追加(以保存时 GET 快照为准,es 尾巴 null
|
* 单句 → 整句替换;已多句 → 追加(以保存时 GET 快照为准,es 尾巴 null
|
||||||
* 则不触 es 池)。
|
* 则整键丢弃 es 池——spec §3.2)。
|
||||||
*/
|
*/
|
||||||
export function buildSaveSequence(draft: SiteImportDraft): SaveStep[] {
|
export function buildSaveSequence(draft: SiteImportDraft): SaveStep[] {
|
||||||
const steps: SaveStep[] = []
|
const steps: SaveStep[] = []
|
||||||
@@ -208,7 +209,7 @@ export function buildSaveSequence(draft: SiteImportDraft): SaveStep[] {
|
|||||||
{
|
{
|
||||||
kind: 'wall',
|
kind: 'wall',
|
||||||
label: '安全词表(墙尾巴)',
|
label: '安全词表(墙尾巴)',
|
||||||
note: 'GET config/wall 读-改-写:deflections_contact 按池形态分派——单句→替换、多句→追加(es 尾巴 null 不触 es 池),其余键透传'
|
note: 'GET config/wall 读-改-写:deflections_contact 按池形态分派——单句→替换、多句→追加(es 尾巴 null = 整键丢弃 es 池,spec §3.2),其余键透传'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: 'contact',
|
kind: 'contact',
|
||||||
@@ -222,8 +223,11 @@ export function buildSaveSequence(draft: SiteImportDraft): SaveStep[] {
|
|||||||
// ---- 墙池形态分派(Task 9——保存时以新鲜 GET 快照判断替换/追加) ------------------------
|
// ---- 墙池形态分派(Task 9——保存时以新鲜 GET 快照判断替换/追加) ------------------------
|
||||||
|
|
||||||
/** 分派结果:mode=append 时 UI 提示「原池含人工编辑句,已追加而非替换」;
|
/** 分派结果:mode=append 时 UI 提示「原池含人工编辑句,已追加而非替换」;
|
||||||
* pools 按语言给出目标池,null = 不触该语言(es 尾巴缺失的合法降级,
|
* pools 按语言给出目标池,null = 该语言**整键丢弃**(es 尾巴缺失的合法
|
||||||
* 载荷保持 GET 快照原值) */
|
* 降级——spec §3.2:防出厂 es 池的行业身份泄漏,如建材档案「tuberías」
|
||||||
|
* 泄漏给非管道租户的西语客户;丢弃后西语会话由 boundary_filter 的
|
||||||
|
* `pools.get(lang) or pools["en"]` 回退看到新 EN 尾巴,wall_schema 仅
|
||||||
|
* 必留 en/zh,缺 es 编译/运行双安全) */
|
||||||
export interface WallDispatch {
|
export interface WallDispatch {
|
||||||
mode: 'replace' | 'append'
|
mode: 'replace' | 'append'
|
||||||
pools: Record<'en' | 'zh' | 'es', string[] | null>
|
pools: Record<'en' | 'zh' | 'es', string[] | null>
|
||||||
@@ -241,7 +245,7 @@ function asStringList(value: unknown): string[] {
|
|||||||
* 保存时以**新鲜 GET** 的现池为判断基准,逐语言分派——
|
* 保存时以**新鲜 GET** 的现池为判断基准,逐语言分派——
|
||||||
* - 池内 >1 句 = 含人工编辑句 → 追加到末项(原句保留);
|
* - 池内 >1 句 = 含人工编辑句 → 追加到末项(原句保留);
|
||||||
* - ≤1 句(出厂单句/空/缺键)→ 整句替换为新尾巴;
|
* - ≤1 句(出厂单句/空/缺键)→ 整句替换为新尾巴;
|
||||||
* - es 新尾巴 null → pools.es 为 null(不触该语言池)。
|
* - es 新尾巴 null → pools.es 为 null(**整键丢弃**,见 WallDispatch 注)。
|
||||||
* 任一语言走了追加 → mode='append'(调用方展示追加提示文案)。
|
* 任一语言走了追加 → mode='append'(调用方展示追加提示文案)。
|
||||||
*/
|
*/
|
||||||
export function wallPoolDispatch(
|
export function wallPoolDispatch(
|
||||||
@@ -268,6 +272,24 @@ export function wallPoolDispatch(
|
|||||||
return { mode: appended ? 'append' : 'replace', pools }
|
return { mode: appended ? 'append' : 'replace', pools }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分派结果 → deflections_contact 目标池(wall 保存步载荷):非 null 语言
|
||||||
|
* 覆写、null 语言**整键删除**(实践上仅 es——en/zh 尾巴必填非空)。其
|
||||||
|
* 余语言键原样透传(快照 spread)。
|
||||||
|
*/
|
||||||
|
export function applyWallDispatch(
|
||||||
|
currentPools: Record<string, string[]>,
|
||||||
|
dispatch: WallDispatch
|
||||||
|
): Record<string, string[]> {
|
||||||
|
const nextPool: Record<string, string[]> = { ...(currentPools ?? {}) }
|
||||||
|
for (const lang of ['en', 'zh', 'es'] as const) {
|
||||||
|
const pool = dispatch.pools[lang]
|
||||||
|
if (pool === null) delete nextPool[lang]
|
||||||
|
else nextPool[lang] = pool
|
||||||
|
}
|
||||||
|
return nextPool
|
||||||
|
}
|
||||||
|
|
||||||
// ---- warnings 分级 -------------------------------------------------------------------
|
// ---- warnings 分级 -------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user