# AI 助手模型管理功能技术实施文档 | 文档版本 | 修订日期 | 修订人 | 修订内容 | | :------- | :--------- | :----- | :------------------------------------------------- | | V1.0 | 2026-05-23 | — | 基于《AI助手模型管理功能需求》V1.1 初稿 | | V1.1 | 2026-05-23 | — | 补充与润色、标签推荐等功能的实施边界说明 | | V1.2 | 2026-05-24 | — | 同步 §6 待办进度;§5.3 联调清单标注自动化已覆盖项 | | V1.3 | 2026-05-24 | — | §6 待办 18~19 标为已完成(API IT + Playwright E2E) | | V1.4 | 2026-05-24 | — | §6 待办 20 完成;新增《AI助手模型管理功能验收报告》 | **关联需求**:[AI助手模型管理功能需求.md](./AI助手模型管理功能需求.md)(V1.2) **依赖能力**:[AI助手功能技术实施文档.md](./AI助手功能技术实施文档.md)(AI 助手 V1.1 已落地) **与其它 AI 功能(实施边界)**:本期 **不修改** `TextPolishServiceImpl`、`TagRecommendServiceImpl`、`AiSummaryServiceImpl` 的 `properties.getModel()` 逻辑;润色、标签推荐、摘要仍走 `dashscope.model`。仅改造 `AiChatServiceImpl` 及新增 `AiAssistantModel*` 相关类。 --- ## 前提条件 **数据安全规则**(与项目一致): - 联调产生的测试模型、会话,完成后可清理;勿动名为 **「hello」** 的文章 **环境依赖**: - AI 助手相关表已存在(Flyway `V5__ai_chat_tables.sql`) - `DASHSCOPE_API_KEY` 已配置 - MySQL、Redis 可用 **产品约定(实施须遵守)**: - **D-08**:会话创建时绑定 `model_id`,同会话不换模 - **D-09**:模型 `enabled=0` 后,绑定会话 **仅可查看**,`POST /chat` 拒绝 - **D-10**:摘要 / 润色 / 标签推荐 **不改**,仍用 `DashScopeProperties.model` - **D-13**:`allow_image` / `allow_voice` 仅管理端配置 + API 返回;用户端 **无** 上传 UI --- ## 1. 功能概述 在现有 AI 助手基础上增加: | 端 | 能力 | | :----- | :------------------------------------------------------------------- | | 管理后台 | `/admin/ai-models` 模型 CRUD(逻辑删除)、默认模型、启用/禁用、多模态开关 | | 用户端 | 助手页模型下拉;`localStorage` 记住选择;新会话绑定模型 | | 后端 | 表 `ai_assistant_model`;`ai_chat_session.model_id`;按 `model_code` 调 DashScope | **与现网差异**:`AiChatServiceImpl.buildRequestBody` 中 `"model", properties.getModel()` 改为会话绑定模型的 `model_code`。 --- ## 2. 核心实现思路 ### 2.1 复用项 | 复用项 | 说明 | | :----- | :--- | | `AiSummaryServiceImpl.callModel` | 仍负责 HTTP 调用 DashScope,仅请求体 `model` 字段来源变化 | | `DashScopeProperties` | `api-key`、`base-url`、`timeout`、`max-input`;`model` 作种子与异常兜底 | | `AiChatServiceImpl` | 在现有 chat / session 流程上扩展,不重写 | | 管理后台模式 | 参考 `AdminCategoryController` + `Categories.vue` 表格 + 弹窗 | | `@TableLogic` | `ai_assistant_model.deleted` 与 `ai_chat_session.deleted` 一致 | ### 2.2 新增组件关系 ```mermaid flowchart LR subgraph admin [管理端] AiModelsVue --> aiModelApi aiModelApi --> AdminAiModelController end subgraph app [用户端] AiAssistantVue --> aiModelApi AiAssistantVue --> aiChatApi aiChatApi --> AiChatController end AdminAiModelController --> AiAssistantModelService AiChatController --> AiChatService AiChatService --> AiAssistantModelService AiAssistantModelService --> AiAssistantModelMapper AiChatService --> AiChatSessionMapper ``` ### 2.3 模型解析与只读(核心) ``` createSession(userId, modelId?) → resolveModelForNewSession(modelId) // 启用且未删;否则默认模型 → insert session.model_id chat(userId, dto) → session = getSessionForUser(...) → model = getModelById(session.model_id) // 含已禁用、已删记录(历史需要) → if model == null || model.deleted → 400 → if !model.enabled → throw AI_MODEL_DISABLED → buildRequestBody(..., model.modelCode) ``` **只读原因**(`readOnlyReason`): | 值 | 条件 | | :--- | :--- | | `null` | 可继续对话 | | `ROUND_FULL` | `round_count >= 5` | | `MODEL_DISABLED` | 绑定模型 `enabled = 0`(优先于 ROUND_FULL 展示文案时,前端 `modelEnabled === false` 时用停用提示) | --- ## 3. 改动项清单 ### 3.1 数据库(Flyway) **文件**:`jog-admin/src/main/resources/db/migration/V6__ai_assistant_model.sql` (当前仓库最大版本为 V5,故使用 V6。) ```sql -- V6__ai_assistant_model.sql -- AI 助手:可配置模型 + 会话绑定 CREATE TABLE ai_assistant_model ( id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, model_code VARCHAR(64) NOT NULL COMMENT 'DashScope model 参数', display_name VARCHAR(32) NOT NULL, description VARCHAR(200) NULL, enabled TINYINT NOT NULL DEFAULT 1, allow_image TINYINT NOT NULL DEFAULT 0, allow_voice TINYINT NOT NULL DEFAULT 0, is_default TINYINT NOT NULL DEFAULT 0, sort INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted TINYINT NOT NULL DEFAULT 0, INDEX idx_enabled_sort (enabled, sort, id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 种子:与 application.yml 默认 qwen-turbo 对齐;部署若 DASHSCOPE_MODEL 不同,上线后可在后台改 model_code INSERT INTO ai_assistant_model ( model_code, display_name, description, enabled, allow_image, allow_voice, is_default, sort ) VALUES ( 'qwen-turbo', '默认模型', '系统迁移种子模型', 1, 0, 0, 1, 0 ); ALTER TABLE ai_chat_session ADD COLUMN model_id BIGINT NULL COMMENT '绑定 ai_assistant_model.id' AFTER user_id; UPDATE ai_chat_session SET model_id = (SELECT id FROM ai_assistant_model WHERE is_default = 1 AND deleted = 0 LIMIT 1) WHERE model_id IS NULL; ``` > **注意**:`model_code` 唯一性由应用层在 `deleted=0` 范围内校验,不在 DB 建唯一索引(避免软删后无法复用编码)。 --- ### 3.2 后端实体与 Mapper | 文件 | 说明 | | :--- | :--- | | `entity/AiAssistantModel.java` | `@TableName("ai_assistant_model")`,`@TableLogic` on `deleted` | | `mapper/AiAssistantModelMapper.java` | `BaseMapper` | **实体字段映射**(Java 侧建议布尔包装,MyBatis 映射 TINYINT): ```java @Data @TableName("ai_assistant_model") public class AiAssistantModel { @TableId(type = IdType.AUTO) private Long id; private String modelCode; private String displayName; private String description; private Boolean enabled; private Boolean allowImage; private Boolean allowVoice; private Boolean isDefault; private Integer sort; @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt; @TableLogic private Integer deleted; } ``` **修改** `entity/AiChatSession.java`:增加 `private Long modelId;` --- ### 3.3 后端 DTO / VO | 文件 | 说明 | | :--- | :--- | | `dto/AiAssistantModelSaveDTO.java` | 管理端新增/修改:`modelCode, displayName, description, enabled, allowImageUpload, allowVoiceUpload, isDefault, sort` + Jakarta 校验 | | `dto/AiAssistantModelVO.java` | 管理端列表/详情(含 `modelCode`) | | `dto/AiAssistantModelAppVO.java` | 用户端列表(**无** `modelCode`) | | `dto/CreateAiChatSessionDTO.java` | 可选 `modelId` | | `dto/AiChatSessionMessagesVO.java` | 消息列表包装:`messages, modelEnabled, readOnlyReason, roundCount, maxRounds` | | `dto/CreateAiChatSessionDTO.java` | `@RequestBody(required = false)` 用于 POST sessions | **扩展已有 VO**: ```java // AiChatSessionVO 增加 private Long modelId; private String modelDisplayName; private Boolean modelEnabled; // AiChatResultVO 增加(可选) private String modelDisplayName; // AiChatSendDTO 增加(可选,会话级绑定后 chat 内忽略) private Long modelId; ``` **枚举** `enums/AiChatReadOnlyReason.java`: ```java public enum AiChatReadOnlyReason { ROUND_FULL, MODEL_DISABLED } ``` --- ### 3.4 后端 Service #### 3.4.1 `AiAssistantModelService` / `AiAssistantModelServiceImpl` | 方法 | 说明 | | :--- | :--- | | `List listAllForAdmin()` | `deleted=0`,`ORDER BY sort ASC, id ASC` | | `AiAssistantModelVO getByIdForAdmin(Long id)` | 不存在 → `NOT_FOUND` | | `AiAssistantModelVO create(AiAssistantModelSaveDTO dto)` | 校验编码唯一、默认模型互斥 | | `void update(Long id, AiAssistantModelSaveDTO dto)` | 禁用前 `assertAtLeastOneEnabled`;设默认时清除其它 `is_default` | | `void deleteLogical(Long id)` | 禁止删当前默认;删后仍须至少一条 `enabled`(若删的是启用项) | | `List listEnabledForApp()` | `enabled=1` 且 `deleted=0` | | `AiAssistantModel getById(Long id)` | 含已删(`@TableLogic` 需用自定义查询或 `selectById` 带逻辑) | | `AiAssistantModel requireEnabledById(Long id)` | 创建会话用 | | `AiAssistantModel getDefaultModel()` | `is_default=1 AND deleted=0`;无则 warn + 合成 fallback(`dashscope.model`) | **默认模型切换**(事务内): ```java @Transactional public void applyDefaultFlag(Long modelId, boolean isDefault) { if (isDefault) { modelMapper.update(null, Wrappers.lambdaUpdate() .set(AiAssistantModel::getIsDefault, false) .eq(AiAssistantModel::getDeleted, 0)); } // 再 update 目标行 is_default } ``` **编码唯一**: ```java long count = modelMapper.selectCount(Wrappers.lambdaQuery() .eq(AiAssistantModel::getModelCode, code) .ne(excludeId != null, AiAssistantModel::getId, excludeId)); if (count > 0) throw new BusinessException(ErrorCode.AI_MODEL_CODE_DUPLICATE); ``` #### 3.4.2 修改 `AiChatService` / `AiChatServiceImpl` | 变更点 | 说明 | | :----- | :--- | | `createSession(Long userId, Long modelId)` | 解析模型 → `session.setModelId(...)` | | `listSessions` | `toSessionVO` 联查或批量查模型,填充 `modelDisplayName`、`modelEnabled` | | `listMessages` | 返回类型改为 `AiChatSessionMessagesVO`;计算 `readOnlyReason` | | `chat` | 开头校验绑定模型 `enabled`;`buildRequestBody(history, modelCode)` | | 日志 | `log.info("AI 助手成功 sessionId={}, modelId={}, modelCode={}, ...")` | **禁用校验**(`chat` 内,在轮次校验之后): ```java AiAssistantModel bound = aiAssistantModelService.getById(session.getModelId()); if (bound == null || Boolean.FALSE.equals(bound.getEnabled())) { throw new BusinessException(ErrorCode.AI_MODEL_DISABLED); } ``` **`buildRequestBody` 签名**: ```java private Map buildRequestBody(List history, String modelCode) { // ... return Map.of( "model", modelCode, "messages", messages, "temperature", 0.7, "top_p", 0.9 ); } ``` **`resolveReadOnlyReason`**: ```java private AiChatReadOnlyReason resolveReadOnlyReason(AiChatSession session, AiAssistantModel model) { if (model != null && Boolean.FALSE.equals(model.getEnabled())) { return AiChatReadOnlyReason.MODEL_DISABLED; } if (session.getRoundCount() != null && session.getRoundCount() >= MAX_ROUNDS) { return AiChatReadOnlyReason.ROUND_FULL; } return null; } ``` > 已删除模型:`getById` 若被 `@TableLogic` 过滤为 null,展示 `modelDisplayName = "已下架模型"`,`modelEnabled = false`,`readOnlyReason = MODEL_DISABLED`,且 `chat` 拒绝。 --- ### 3.5 后端 Controller #### 3.5.1 `AdminAiModelController.java` ```java @Tag(name = "管理员-AI 助手模型") @RestController @RequestMapping("/api/admin/ai/models") @RequiredArgsConstructor public class AdminAiModelController { private final AiAssistantModelService modelService; @GetMapping public ApiResult> list() { ... } @GetMapping("/{id}") public ApiResult get(@PathVariable Long id) { ... } @PostMapping public ApiResult create(@Valid @RequestBody AiAssistantModelSaveDTO dto) { ... } @PutMapping("/{id}") public ApiResult update(@PathVariable Long id, @Valid @RequestBody AiAssistantModelSaveDTO dto) { ... } @DeleteMapping("/{id}") public ApiResult delete(@PathVariable Long id) { ... } } ``` 权限:沿用 Spring Security 对 `/api/admin/**` 的 ADMIN 规则(与 `CategoryController` 一致)。 #### 3.5.2 扩展 `AiChatController.java` ```java @GetMapping("/models") public ApiResult> listModels(@AuthenticationPrincipal Long userId) { return ApiResult.success(aiAssistantModelService.listEnabledForApp()); } @PostMapping("/sessions") public ApiResult createSession( @AuthenticationPrincipal Long userId, @RequestBody(required = false) CreateAiChatSessionDTO body) { Long modelId = body == null ? null : body.getModelId(); return ApiResult.success(aiChatService.createSession(userId, modelId)); } @GetMapping("/sessions/{sessionId}/messages") public ApiResult listMessages(...) { ... } ``` --- ### 3.6 错误码 在 `jog-common/.../ErrorCode.java` 新增(code 段延续 500x): | 枚举 | code | message(默认) | | :--- | :--- | :--- | | `AI_MODEL_CODE_DUPLICATE` | 5007 | 模型编码已存在 | | `AI_MODEL_NOT_AVAILABLE` | 5008 | 模型不可用 | | `AI_MODEL_DISABLED` | 5009 | 该会话所用模型已停用,仅可查看历史 | | `AI_MODEL_MUST_KEEP_ENABLED` | 5010 | 须至少保留一个启用的模型 | | `AI_MODEL_DEFAULT_REQUIRED` | 5011 | 请先指定其它模型为默认后再删除 | `BusinessException` 可覆盖 message 以贴合 FR-AI-MODEL-ADM-07/08 文案。 --- ### 3.7 前端改动 #### 3.7.1 API **新建** `jog-frontend/src/api/aiModel.js`: ```js import request from '@/utils/request' // 管理端 export function listAdminAiModels() { return request.get('/api/admin/ai/models') } export function createAdminAiModel(data) { return request.post('/api/admin/ai/models', data) } export function updateAdminAiModel(id, data) { return request.put(`/api/admin/ai/models/${id}`, data) } export function deleteAdminAiModel(id) { return request.delete(`/api/admin/ai/models/${id}`) } // 用户端 export function listAppAiModels() { return request.get('/api/app/ai/models') } ``` **修改** `jog-frontend/src/api/aiChat.js`: ```js export function createAiChatSession(data) { return request.post('/api/app/ai/sessions', data ?? {}) } export function listAiChatMessages(sessionId) { // 响应 data 结构变为 { messages, modelEnabled, readOnlyReason, roundCount, maxRounds } return request.get(`/api/app/ai/sessions/${sessionId}/messages`) } ``` #### 3.7.2 管理后台 `views/admin/AiModels.vue` - 参考 `Categories.vue`:`el-table` + `el-dialog` 表单 - 表单字段:展示名称、模型编码、描述、启用、**允许上传图片**、**允许上传语音**、默认、排序 - 列表列:ID、展示名称、编码、状态、图片、语音、默认、排序、操作 - 删除:`ElMessageBox.confirm` → `deleteAdminAiModel` - **不实现** 任何上传控件(D-13) #### 3.7.3 路由与菜单 | 文件 | 变更 | | :--- | :--- | | `router/index.js` | `children` 增加 `{ path: 'ai-models', name: 'AdminAiModels', component: ..., meta: { title: 'AI 模型', requiresAdmin: true } }` | | `layouts/AdminLayout.vue` | `el-menu-item index="/admin/ai-models"` 文案「AI 模型」 | #### 3.7.4 用户端 `AiAssistant.vue` + `AiChatPanel.vue` **状态新增**: ```js const MODEL_STORAGE_KEY = 'jog_ai_assistant_model_id' const availableModels = ref([]) const selectedModelId = ref(null) const modelEnabled = ref(true) const readOnlyReason = ref(null) // 'ROUND_FULL' | 'MODEL_DISABLED' | null ``` **`onMounted`**: ```js await loadAppModels() restoreModelIdFromStorage() // 原有 loadSessions + selectSession ``` **`readOnly` 计算**: ```js const readOnly = computed(() => roundCount.value >= maxRounds || modelEnabled.value === false ) ``` **`selectSession`**:解析 `listAiChatMessages` 新结构: ```js const res = await listAiChatMessages(item.id) const payload = res.data messages.value = payload.messages ?? [] modelEnabled.value = payload.modelEnabled !== false readOnlyReason.value = payload.readOnlyReason ?? null roundCount.value = payload.roundCount ?? item.roundCount ?? 0 ``` **`handleNewSession`**: ```js const res = await createAiChatSession({ modelId: selectedModelId.value }) ``` **`AiChatPanel` 扩展**: - Props:`selectedModelId`、`availableModels`、`readOnlyReason` - 标题栏:`el-select` 绑定 `selectedModelId`,`@change` 写 `localStorage`(**不** 改当前 session) - 提示条:`readOnlyReason === 'MODEL_DISABLED'` 时显示停用文案;`ROUND_FULL` 沿用现有满 5 轮条(可合并为两个 `v-if`) **不实现**:图片/语音上传按钮(即使 `allowImageUpload=true` 也不展示)。 --- ## 4. 核心逻辑说明 ### 4.1 创建会话时模型绑定 ```mermaid sequenceDiagram participant FE as 前端 participant BE as AiChatService participant MS as AiAssistantModelService FE->>BE: POST /sessions { modelId: 2 } BE->>MS: requireEnabledById(2) 或 getDefaultModel() MS-->>BE: model(id=2, modelCode=qwen-plus) BE->>BE: insert session(model_id=2) BE-->>FE: { sessionId } ``` ### 4.2 禁用后只读 ```mermaid sequenceDiagram participant U as 用户 participant FE as 前端 participant BE as 后端 U->>FE: 点击绑定已禁用模型的会话 FE->>BE: GET /sessions/{id}/messages BE-->>FE: modelEnabled=false, readOnlyReason=MODEL_DISABLED FE->>FE: 禁用输入框 + 提示条 U->>FE: 尝试发送 FE->>FE: readOnly 拦截(可选) FE->>BE: POST /chat BE-->>FE: 400 AI_MODEL_DISABLED ``` ### 4.3 与多模态字段的关系(本期) - 管理端保存 `allowImage` / `allowVoice` 至 DB - `GET /api/app/ai/models` 返回两字段,前端 **不消费** - `POST /chat` **无** `attachments` 字段;后续迭代在 Service 内校验 `allowImage` 等 --- ## 5. 测试要点 ### 5.1 后端单元测试 **新建** `AiAssistantModelServiceImplTest`(Mockito): | 用例 | 预期 | | :--- | :--- | | 创建重复 modelCode | `AI_MODEL_CODE_DUPLICATE` | | 设 B 为默认 | 仅 B `isDefault=true` | | 禁用最后一个启用模型 | `AI_MODEL_MUST_KEEP_ENABLED` | | 删除默认模型 | `AI_MODEL_DEFAULT_REQUIRED` | **扩展** `AiChatServiceImplTest`: | 用例 | 预期 | | :--- | :--- | | createSession 带 modelId | `session.modelId` 正确 | | chat 使用绑定 modelCode | `callModel` 参数含对应 model | | 绑定模型 enabled=false | `AI_MODEL_DISABLED` | | listMessages | `readOnlyReason=MODEL_DISABLED` | **扩展** `AiChatServiceIntegrationTest`:覆盖消息包装 VO。 ### 5.2 前端手工验收 对照需求 AC-AI-MODEL-ADM-*、AC-AI-MODEL-FE-*、AC-AI-MODEL-BE-*。 ### 5.3 联调检查清单 - [x] Flyway V6 执行成功;`ai_chat_session.model_id` 回填(线上库已 V6) - [x] 管理端 CRUD、默认互斥、禁止删唯一启用(单测 + API IT + E2E) - [x] 用户下拉仅启用模型;`localStorage` 恢复(代码 + E2E 选模) - [x] 新会话绑定所选模型;同会话不改绑(单测 + 设计) - [x] 禁用模型后旧会话只读、`POST /chat` 5009(IT + E2E-19) - [x] 助手页 **无** 图片/语音上传入口 - [x] 摘要/润色仍走 `dashscope.model` --- ## 6. 待办事项 > **状态说明**:⬜ 未开始 🔄 进行中 ✅ 已完成 | 序号 | 阶段 | 待办项 | 状态 | | :--- | :--- | :----- | :--- | | 1 | 数据库 | 新增 `V6__ai_assistant_model.sql`(建表、种子、session.model_id 回填) | ✅ 已完成 | | 2 | 后端 | 实体 `AiAssistantModel`、`AiChatSession.modelId` | ✅ 已完成 | | 3 | 后端 | `AiAssistantModelMapper` | ✅ 已完成 | | 4 | 后端 | DTO/VO:`AiAssistantModelSaveDTO`、`AiAssistantModelVO`、`AiAssistantModelAppVO`、`AiChatSessionMessagesVO`、`CreateAiChatSessionDTO` | ✅ 已完成 | | 5 | 后端 | `AiChatReadOnlyReason` 枚举 | ✅ 已完成 | | 6 | 后端 | `ErrorCode` 新增 5007~5011 | ✅ 已完成 | | 7 | 后端 | `AiAssistantModelService` + `Impl`(CRUD、默认、校验) | ✅ 已完成 | | 8 | 后端 | `AdminAiModelController` | ✅ 已完成 | | 9 | 后端 | `AiChatController` 增加 `GET /models`;sessions 支持 body | ✅ 已完成 | | 10 | 后端 | `AiChatServiceImpl` 改造(绑定、禁用校验、modelCode、消息包装) | ✅ 已完成 | | 11 | 后端 | 单元/集成测试(`AiAssistantModelServiceImplTest`;`AiChatServiceImplTest` / `AiChatServiceIntegrationTest` 已适配并通过) | ✅ 已完成 | | 12 | 前端 | `api/aiModel.js` | ✅ 已完成 | | 13 | 前端 | `views/admin/AiModels.vue` | ✅ 已完成 | | 14 | 前端 | `AdminLayout` + `router` 注册 `/admin/ai-models` | ✅ 已完成 | | 15 | 前端 | `api/aiChat.js` 适配新 sessions/messages 结构 | ✅ 已完成 | | 16 | 前端 | `AiAssistant.vue` 模型列表、localStorage、readOnly 逻辑 | ✅ 已完成 | | 17 | 前端 | `AiChatPanel.vue` 模型选择器 + 停用提示条 | ✅ 已完成 | | 18 | 联调 | 管理端上架第二模型 → 用户新会话切换 → 对话成功 | ✅ 已完成(`AiModelManagementApiIT` + `e2e/tests/ai-model-management.spec.js` TC-E2E-18) | | 19 | 联调 | 禁用模型 → 旧会话只读 + chat 5009 | ✅ 已完成(`AiModelManagementApiIT` + `e2e/tests/ai-model-management.spec.js` TC-E2E-19) | | 20 | 验收 | 按需求 V1.1 验收标准逐项确认 | ✅ 已完成(见 [AI助手模型管理功能验收报告.md](./AI助手模型管理功能验收报告.md)) | --- ## 7. 实施顺序建议 1. **Flyway V6** → 本地启动验证表与种子、session 回填 2. **模型 Service + Admin Controller** → Postman 测通 CRUD 3. **AiChatService 改造** → 单元测试 → 与前端联调 sessions/chat/messages 4. **管理后台页面** → 运营可配模型 5. **助手页选择器 + 只读** → 禁用场景联调 6. **回归** 原 AI 助手 5 轮、重试、限流不受影响 --- ## 8. 风险与注意事项 | 风险 | 说明 | 缓解 | | :--- | :--- | :--- | | 种子 model_code 与环境不一致 | SQL 写死 `qwen-turbo` | 上线后管理端改编码或重建默认行 | | `listMessages` 破坏性变更 | 由 `List` 改为包装对象 | 前后端同 PR 发布;仅 AI 助手页消费 | | 逻辑删模型后 session.model_id 悬空 | 展示「已下架」 | `getById` 自定义查询含 deleted 或冗余 displayName 快照(首版联查+兜底文案) | | 禁用与满 5 轮双重只读 | 文案冲突 | 前端优先展示 `MODEL_DISABLED` | | 误开上传 UI | 违背 D-13 | Code Review 确认 Panel 无 upload 组件 | --- ## 9. 需求追溯矩阵(摘要) | 需求编号 | 实施落点 | | :------- | :------- | | FR-AI-MODEL-ADM-* | 待办 7、8、13、14 | | FR-AI-MODEL-FE-* | 待办 15~17 | | FR-AI-MODEL-BE-* | 待办 1~11 | | D-08 ~ D-13 | 待办 10、16~19 | | NFR-02 ~ NFR-05 | 待办 6、8、10、11 | --- **编写人:** AI 助手 **评审人:** 待评审 **生效日期:** 2026-05-23 **进度摘要(V1.4):** 待办 1~20 已完成;核心 AC 自动化通过,FE-09 列为非阻塞缺口(见验收报告 §4)。