docs: 计划评审修复(SecretField 接线/引擎测试咬合/退役时序/导入约定)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-14 16:36:05 +08:00
parent faf981ec9b
commit bae51b00e3
@@ -11,7 +11,9 @@
**执行环境注意(Windows):**
- 所有命令在仓库根 `D:\refactor\all-kefu` 下执行;前端命令先 `cd admin`
- 单测:`cd admin && pnpm exec vitest run src/views/config/center/__tests__/model.test.ts`(全量:`pnpm test`)
- 构建:`cd admin && pnpm build`(vue-tsc --noEmit && vite build);build 后若 `components.d.ts`/`auto-imports.d.ts` 出现行尾漂移:`git diff --ignore-cr-at-eol` 为空则 `git checkout -- admin/src/types/import/components.d.ts admin/types/auto-imports.d.ts` 还原(入库版优先纪律)
- 构建:`cd admin && pnpm build`(vue-tsc --noEmit && vite build);build 后若 `components.d.ts`/`auto-imports.d.ts` 出现行尾漂移:`git diff --ignore-cr-at-eol` 为空则 `git checkout -- admin/src/types/import/components.d.ts admin/src/types/import/auto-imports.d.ts` 还原(入库版优先纪律)
- **新组件须显式 import**:view 内新组件(ConfigCard/SecretField/KeyValueEditor/ContactFormPane/EngineFormPane/MiscFormPane/wall/ScenarioCard/wall/WallAdvanced)不被 unplugin-vue-components 自动注册(它只扫 src/components)——消费方 SFC 必须 `import ConfigCard from './ConfigCard.vue'` 等(与 index.vue 显式 import 既有 panes 同款);漏 import 构建不报错但渲染空白,dev 走查必看
- **ElMessageBox 非 auto-import**(auto-imports 只有 ElMessage):用到 `ElMessageBox.confirm` 的文件顶部 `import { ElMessageBox } from 'element-plus'`(reset.ts 同款)
- 本地验收环境:Docker 容器 all-kefu-app-1(8018,`/admin/`)跑的是**构建产物**——前端改动本地验收用 dev server:`cd admin && pnpm dev`(默认 3006,代理已配好);超管 admin / 密码 12345678(本机测试值);登录滑块为纯前端校验可直接拖
- 每个任务收尾 `git status --short` 应只剩本任务相关文件;截图等临时产物放 `.superpowers/`(已 gitignore),收尾删净
@@ -409,7 +411,7 @@ export function secretFieldState(
]
```
`activeGroupLabel` 改为 `ALL_NAV_ITEMS.find((i) => i.key === activeGroup.value)?.label ?? ''`;删除 `IMPLEMENTED_GROUPS`。
`activeGroupLabel` 改为 `ALL_NAV_ITEMS.find((i) => i.key === activeGroup.value)?.label ?? ''`;删除 `IMPLEMENTED_GROUPS`。**GroupKey 类型重定义**(原 `typeof GROUP_NAV` 已删):`type GroupKey = (typeof ALL_NAV_ITEMS)[number]['key']`——须放在 ALL_NAV_ITEMS 之后;`activeGroup`/`onGroupSelect` 等既有引用不变。
- [ ] **Step 2: 重写导航模板**——`<ElCard class="nav-card">` 内的 ElMenu 区改为:
@@ -450,7 +452,7 @@ export function secretFieldState(
**Files:**
- Modify: `admin/src/views/config/center/CredsPane.vue`(三卡模板重排,脚本层函数全部保留)
- [ ] **Step 1: 邮件卡改造**——`<ElCard>` 换 `<ConfigCard icon="solar:letter-linear" title="邮件通知" subtitle="客户留下线索后,系统用这组配置发通知邮件" info="整组保存 · 立即生效 · 产生新版本可回滚;带 • 的字段留空 = 沿用平台默认" danger-label="恢复出厂:把邮件配置重置为初始档案值">`;表单区改双列 grid(`<div class="form-grid">` 包 ElFormItem:服务器/端口同排、账号/发件人同排、密码独占、通知邮箱独占——样式 `.form-grid { display:grid; grid-template-columns:1fr 1fr; gap:0 16px; .span-2 { grid-column:span 2 } }`);SMTP 密码整块换 `<SecretField v-model="emailForm.smtpPassword" :masked="emailPasswordCleared ? '' : emailPasswordMasked" :cleared="emailPasswordCleared" @update:cleared="emailPasswordCleared = $event" />`(原 secret-row/清除按钮/field-warn 模板删除);底部操作条:
- [ ] **Step 1: 邮件卡改造**——`<ElCard>` 换 `<ConfigCard icon="solar:letter-linear" title="邮件通知" subtitle="客户留下线索后,系统用这组配置发通知邮件" info="整组保存 · 立即生效 · 产生新版本可回滚;带 • 的字段留空 = 沿用平台默认" danger-label="恢复出厂:把邮件配置重置为初始档案值">`;表单区改双列 grid(`<div class="form-grid">` 包 ElFormItem:服务器/端口同排、账号/发件人同排、密码独占、通知邮箱独占——样式 `.form-grid { display:grid; grid-template-columns:1fr 1fr; gap:0 16px; .span-2 { grid-column:span 2 } }`);SMTP 密码整块换 `<SecretField v-model="emailForm.smtpPassword" :masked="emailPasswordMasked" :cleared="emailPasswordCleared" @update:cleared="emailPasswordCleared = $event" />`(**masked 传原始掩码值**——SecretField 内部 v-if 按 masked===哨兵 渲染清除按钮,传 `cleared ? '' : masked` 会让按钮在清除态消失、恢复保持不可达;徽章优先级 secretFieldState 已处理 cleared>masked)。原 secret-row/清除按钮/field-warn 模板删除。**提交路径不动**:onSaveEmail 里 buildEmailCredsPayload(form, emailPasswordCleared ? '' : emailPasswordMasked) 维持现状(脚本层三元,与模板 prop 无关);底部操作条:
```html
<template #footer-left>
@@ -837,7 +839,7 @@ props `{ model: WallEditModel }`;迁移块内的 section-note 保留但首行
- Modify: `admin/src/views/config/center/index.vue`(wall pane 挂载处加 `@navigate`)
- [ ] **Step 1: 主体重排**——外层换 `ConfigCard icon="solar:chat-round-line-duotone" title="AI 话术" subtitle="AI 回复客户时用的标准句与引擎规则——改动保存后立即对线上对话生效" info="保存前自动跑试金石预演;正则与 AI 指令在下方「高级」区"`;主面顺序:`<WallScenarioCard v-for="s in WALL_SCENARIOS" :key="s.key" :def="s" :model="model" />` → no_contact 卡(三键行编辑,NO_CONTACT_KEYS 白话标签:web=网页会话(有按钮)/ app=App 会话(无按钮)/ already=客户已留过)→ cc_templates 卡(四组 × CC_TEMPLATE_VARIANTS 封闭语言直接渲染,标注 `{intl}` 为运行期回填变量、不可删除)→ 试金石面板(现状保留)→ `<WallAdvanced :model="model" />`(默认折叠)。旧模板的池区/正则区/提醒区删除(已迁移)
- [ ] **Step 2: 保存路径加变量保护**——onSave 组装 payload 后:
- [ ] **Step 2: 保存路径加变量保护**——onSave 组装 payload 后(ElMessageBox 须在 WallPane 顶部 `import { ElMessageBox } from 'element-plus'`——非 auto-import):
```ts
const removed = collectRemovedWallVariables(props.group, payload)
@@ -853,7 +855,7 @@ if (removed.length) {
(后续保持既有试金石预演 → PUT 流程不变。)
- [ ] **Step 3: 验证**——`pnpm build` 零错误;`pnpm dev` 走查:两场景卡语言切换/加删语言(删 en/zh 被拦)、no_contact/cc 卡可编辑、高级区默认折叠、试金石可用、保存一个改动走通试金石流程
- [ ] **Step 3: 验证**——`pnpm build` 零错误;`pnpm dev` 走查:两场景卡语言切换/添加语言、必留语言 en/zh **无删除按钮**(其行可被逐条清空——此时卡头「缺必留语言」警示标签出现、后端 400 兜底,这是设计而非硬拦截)、no_contact/cc 卡可编辑、高级区默认折叠、试金石可用、保存一个改动走通试金石流程。注:spec §5.2「编辑器内变量高亮」有意裁剪为「保存时 collectRemovedWallVariables 确认 + cc 卡静态标注」——记为本计划终态,非遗漏
- [ ] **Step 4: 既有 wall 相关 vitest 全量**——`pnpm exec vitest run src/views/config/center/__tests__/model.test.ts` 全绿(initWallEdit/buildWallPayload 契约不回归)
- [ ] **Step 5: Commit**——`git commit -m "feat(admin): WallPane 重组为场景卡+高级折叠(Batch 2 收口)"`
@@ -870,13 +872,13 @@ if (removed.length) {
- [ ] **Step 2: 验证**——`pnpm build`;dev 走查:折叠区未展开时保存,payload 仍含三件原值(读-改-写不丢——normalizePromptsGroup 从快照取全四键,保存必填备注逻辑不变)
- [ ] **Step 3: Commit**——`git commit -m "feat(admin): PromptsPane 重组(company_facts 主面+引擎级折叠)"`
### Task 12: model.ts 三组表单映射(TDD,JsonGroup 函数退役)
### Task 12: model.ts 三组表单映射纯函数(TDD)
**Files:**
- Modify: `admin/src/views/config/center/model.ts`(通用 JSON 组区块整体替换)
- Test: `admin/src/views/config/center/__tests__/model.test.ts`(JSON 组旧测试删除,新测试替换)
- Modify: `admin/src/views/config/center/model.ts`(文件末尾追加新区块;旧 JSON 组区块**暂留**——Task 13 接线后才退役,保证本任务收尾时仓库可构建)
- Test: `admin/src/views/config/center/__tests__/model.test.ts`(追加新用例;旧 JSON 组用例不动)
- [ ] **Step 1: 写失败测试**(jsonGroupDraft/parseJsonGroupDraft 的旧 describe 块先删除):
- [ ] **Step 1: 写失败测试**(model.test.ts 追加,import 行并入既有 from '../model' 名单):
```ts
import {
@@ -904,12 +906,12 @@ describe('engine 表单化', () => {
.toEqual({ rateLimit: '120', wsConnLimit: '5', seedRows: [{ key: 'a', value: 'b' }] })
expect(engineToForm(null)).toEqual({ rateLimit: '', wsConnLimit: '', seedRows: [] })
})
it('payload:空数值槽保快照原值(快照也无 → 键省略);未知键透传', () => {
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 })
.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 })
.toEqual({ rate_limit_per_hour: 60, context_seed_keys: {} })
})
it('payload:seed 行空键值剔除、重建映射', () => {
expect(engineToPayload(
@@ -944,7 +946,7 @@ describe('KeyValue 行校验(engine seed / misc extras 共用)', () => {
```
- [ ] **Step 2: 跑测试确认失败**——新函数未定义 FAIL
- [ ] **Step 3: 实现**——「通用 JSON 组编辑」区块(`JsonGroupKey` 到 `JSON_GROUP_FIELDS` 整段)替换为:
- [ ] **Step 3: 实现**——model.ts 文件末尾(`JSON_GROUP_FIELDS` 区块之后)追加新区块:
```ts
// ---- contact / engine / misc 表单化(spec §5.4-5.7;JsonGroupPane 退役) --------------
@@ -1097,39 +1099,41 @@ export function miscToPayload(
}
```
同时:`JsonGroupKey`/`jsonGroupDraft`/`parseJsonGroupDraft`/`JSON_GROUP_FIELDS` 全部删除;`RESET_GROUP_LABELS` 保留不动(确认框用,含 email/push/ai 三凭据组)。model.test.ts 中引用退役函数的旧用例删除。
同时**不删**任何既有导出(`JsonGroupKey`/`jsonGroupDraft`/`parseJsonGroupDraft`/`JSON_GROUP_FIELDS` 与其测试原样保留,Task 13 统一退役——避免「model 删了函数但 index.vue 还在 import」的中间不可构建态)。
- [ ] **Step 4: 跑测试确认通过**(新 describe 全绿 + 无退役函数残留引用导致的全量失败——`pnpm test` 全量跑)
- [ ] **Step 5: Commit**——`git commit -m "feat(admin): contact/engine/misc 表单映射纯函数(JsonGroup 函数退役)"`
- [ ] **Step 4: 跑测试确认通过**——`pnpm exec vitest run src/views/config/center/__tests__/model.test.ts` 新 describe 全绿(旧用例不动仍绿)
- [ ] **Step 5: Commit**——`git commit -m "feat(admin): contact/engine/misc 表单映射纯函数"`
### Task 13: 三份表单 pane + KeyValueEditor
### Task 13: 三份表单 pane + KeyValueEditor + JsonGroup 退役
**Files:**
- Create: `admin/src/views/config/center/KeyValueEditor.vue`
- Create: `admin/src/views/config/center/ContactFormPane.vue`
- Create: `admin/src/views/config/center/EngineFormPane.vue`
- Create: `admin/src/views/config/center/MiscFormPane.vue`
- Modify: `admin/src/views/config/center/index.vue`(JSON_GROUPS 分发处换三组件 + navigate 事件接线)
- Delete: `admin/src/views/config/center/JsonGroupPane.vue`
- Modify: `admin/src/views/config/center/index.vue`(JSON_GROUPS 分发处换三组件 + 退役清理)
- Modify: `admin/src/views/config/center/model.ts`(删旧 JSON 组区块)
- Test: `admin/src/views/config/center/__tests__/model.test.ts`(删旧 JSON 组用例与 import 名单中的退役函数)
- [ ] **Step 1: KeyValueEditor.vue**——props `{ rows: KeyValueRow[]; keyPlaceholder: string; valuePlaceholder: string }`;模板 = 行循环(两 ElInput mono + 删除钮)+底部「+ 添加一行」;emit 无(直接改 reactive rows,与项目模式一致)
- [ ] **Step 2: ContactFormPane.vue**——ConfigCard `icon="solar:phone-line-duotone" title="联系方式" subtitle="AI 什么时候向客户要 WhatsApp / 邮箱"`;两个 ElSwitch(label 用 model 注释白话:cc_completion=「号码缺国家代码时自动补全并确认」/ email_light_ask=「留了号码没留邮箱时补要一次」);template_languages 用 ElSelect multiple allow-create(placeholder「语言代码如 en / zh」)+`<p class="field-hint">邮件通知使用的语言(暂未生效——引擎接线排期中)</p>`;保存走 contactToPayload、错误内联;footer/danger 同 Task 5 模式;emit saved/navigate
- [ ] **Step 3: EngineFormPane.vue**——ConfigCard `icon="solar:widget-5-line-duotone" title="聊天限流" subtitle="防刷与并发的引擎级参数(一般不用改)" info="留空 = 保持当前已存值;出厂档案缺省时省略该键回退平台默认"`;rate/ws 两 ElInput(placeholder「如 120(正整数)」/「如 5」)+推荐值提示;ElCollapse「会话上下文映射(context_seed_keys)」内 KeyValueEditor;保存 = validateEngineForm → engineToPayload → PUT engine
- [ ] **Step 4: MiscFormPane.vue**——ConfigCard `icon="solar:earth-line-duotone" title="国家区号映射" subtitle="客户用国名留言时,系统补全电话区号的额外规则"`;KeyValueEditor(key placeholder「国名(如 saudi arabia)」value「区号(如 966)」);validateKeyValueRows → miscToPayload → PUT misc
- [ ] **Step 5: index.vue 接线**——`JSON_GROUPS` 三元的 `<JsonGroupPane>` 分发(index.vue:543 附近的 v-if JSON_GROUPS.has(activeGroup) 块)替换为三组件各挂 `<ContactFormPane v-else-if="activeGroup==='contact'" :slug="activeSlug" :group="snapshot?.contact" @saved="onGroupSaved" @navigate="onGroupSelect" />` 同款三份;onGroupSaved 现有回写逻辑复用
- [ ] **Step 6: 验证**——`pnpm build`;`pnpm test` 全绿;dev 走查三组表单(含未知键透传:在 platform.db 里给某租户 contact 组塞 `_custom: 1` 再 GET → 表单保存 → `_custom` 仍在——可用后台 API 或直接 sqlite 验证)
- [ ] **Step 7: Commit**——`git commit -m "feat(admin): contact/engine/misc 表单 pane(KeyValueEditor 共享)"`
- [ ] **Step 5: index.vue 接线 + 退役清理**——`<JsonGroupPane>` 分发(index.vue:543 附近的 `v-if JSON_GROUPS.has(activeGroup)` 块)替换为三组件:`<ContactFormPane v-if="activeGroup==='contact'" :slug="activeSlug" :group="snapshot?.contact" @saved="onGroupSaved" @navigate="onGroupSelect" />` + EngineFormPane/MiscFormPane 同款三份(新组件显式 import);同步删除 `JSON_GROUPS` 常量、`activeJsonGroup`、`JsonGroupPane` import 与 `JsonGroupKey` 相关 import;onGroupSaved 现有回写逻辑复用
- [ ] **Step 6: 退役删除**——`git rm admin/src/views/config/center/JsonGroupPane.vue`;model.ts 删「通用 JSON 组编辑」整段(`JsonGroupKey`/`jsonGroupDraft`/`parseJsonGroupDraft`/`JSON_GROUP_FIELDS`;`RESET_GROUP_LABELS` 保留不动);model.test.ts 删旧 JSON 组 describe 块与 import 名单中三个退役函数;`grep -rn "JsonGroupPane\|jsonGroupDraft\|parseJsonGroupDraft\|JSON_GROUP_FIELDS" admin/src` 零残留
- [ ] **Step 7: 验证**——`pnpm build`;`pnpm test` 全绿(总数 = 基线 − 旧 JSON 组用例数 + Task 12 新增数);dev 走查三组表单(含未知键透传:后台给某租户 contact 组塞 `_custom: 1` 再 GET → 表单保存 → `_custom` 仍在——可用平台 API 或直接 sqlite 验证)
- [ ] **Step 8: Commit**——`git commit -m "feat(admin): contact/engine/misc 表单 pane + JsonGroup 退役"`
### Task 14: JsonGroupPane 删除 + 未保存离开保护
### Task 14: 未保存离开保护(脏检测 + 三处拦截)
**Files:**
- Delete: `admin/src/views/config/center/JsonGroupPane.vue`
- Modify: `admin/src/views/config/center/index.vue`(dirty 汇聚 + 三处拦截)
- Modify: `admin/src/views/config/center/WallPane.vue` / `PromptsPane.vue` / `CredsPane.vue`(emit dirty)
- Modify: `admin/src/views/config/center/ContactFormPane.vue` / `EngineFormPane.vue` / `MiscFormPane.vue`(emit dirty)
- [ ] **Step 1: 删除 JsonGroupPane.vue**——`git rm admin/src/views/config/center/JsonGroupPane.vue`;grep `JsonGroupPane` 确认零残留(含 reset.ts 与测试)
- [ ] **Step 2: 各 pane 上报 dirty**——七个编辑面(brand 在 index.vue 内联)各加 `const dirty = computed(() => ...)`(判定 = 当前编辑态 JSON.stringify ≠ 初始归一态 JSON.stringify:brand 对比表单初值、prompts 对比 normalizePromptsGroup、wall 对比 initWallEdit 克隆、creds 对比三组 normalize 态、三表单对比 ToForm 初值),`watch(dirty, (v) => emit('dirty', v), { immediate: true })`;emits 增加 `(e: 'dirty', value: boolean): void`;index.vue 记录 `const paneDirty = ref(false)`,各 pane `@dirty="paneDirty = $event"`
- [ ] **Step 3: 三处拦截**——index.vue:
- [ ] **Step 1: 各 pane 上报 dirty**——七个编辑面(brand 在 index.vue 内联)各加 `const dirty = computed(() => ...)`(判定 = 当前编辑态 JSON.stringify ≠ 初始归一态 JSON.stringify:brand 对比表单初值、prompts 对比 normalizePromptsGroup、wall 对比 initWallEdit 克隆、creds 对比三组 normalize 态、三表单对比 ToForm 初值),`watch(dirty, (v) => emit('dirty', v), { immediate: true })`;emits 增加 `(e: 'dirty', value: boolean): void`;index.vue 记录 `const paneDirty = ref(false)`,各 pane `@dirty="paneDirty = $event"`
- [ ] **Step 2: 三处拦截**——index.vue(ElMessageBox 顶部显式 import):
```ts
async function confirmDiscard(): Promise<boolean> {
@@ -1157,10 +1161,10 @@ watch(activeSlug, async () => {
})
```
租户切换的还原实现:`const lastConfirmedSlug = ref('')`——watch(activeSlug) 先比对 `pendingSlug` 模式(实现时以 ElSelect change 事件前置确认代替 watch 更稳:改用 `@change` 处理器承载确认+回滚,确认失败则 `activeSlug.value = 旧值`)。路由离开:`onBeforeRouteLeave(async () => await confirmDiscard())`
租户切换的还原实现:watch(activeSlug) 先比对模式(实现时以 ElSelect change 事件前置确认代替 watch 更稳:改用 `@change` 处理器承载确认+回滚,确认失败则 `activeSlug.value = 旧值`)。路由离开:`onBeforeRouteLeave(async () => await confirmDiscard())`
- [ ] **Step 4: 验证**——`pnpm build`;dev 走查:brand 改标题不保存 → 切导航组弹确认;wall 改句子 → 切租户弹确认、取消后租户选择还原、草稿还在;保存后切换不弹
- [ ] **Step 5: Commit**——`git commit -m "feat(admin): 未保存离开保护 + JsonGroupPane 退役(Batch 3 收口)"`
- [ ] **Step 3: 验证**——`pnpm build`;dev 走查:brand 改标题不保存 → 切导航组弹确认;wall 改句子 → 切租户弹确认、取消后租户选择还原、草稿还在;保存后切换不弹
- [ ] **Step 4: Commit**——`git commit -m "feat(admin): 未保存离开保护(Batch 3 收口)"`
---