Files
Jog/doc/AI助手模型管理功能技术实施文档.md
T
nojava 6c8b31e38a feat: AI 助手模型管理(CRUD、会话绑定、自动化验收)
- Flyway V6:模型表、默认种子、ai_chat_session.model_id 回填
- 管理端 /api/admin/ai/models;用户端选模与新会话绑定
- 禁用模型后会话只读(5009);chat 按会话 modelCode 调 DashScope
- 前端 AiModels 页、助手模型选择器与 localStorage
- 单测、AiModelManagementApiIT、Playwright E2E;e2e Profile 关闭注解限流
- 技术实施文档与验收报告;移除过期冲突分析文档

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 03:36:31 +08:00

651 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<AiAssistantModel>` |
**实体字段映射**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<AiAssistantModelVO> 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<AiAssistantModelAppVO> 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.<AiAssistantModel>lambdaUpdate()
.set(AiAssistantModel::getIsDefault, false)
.eq(AiAssistantModel::getDeleted, 0));
}
// 再 update 目标行 is_default
}
```
**编码唯一**
```java
long count = modelMapper.selectCount(Wrappers.<AiAssistantModel>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<String, Object> buildRequestBody(List<AiChatMessage> 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<AiAssistantModelVO>> list() { ... }
@GetMapping("/{id}")
public ApiResult<AiAssistantModelVO> get(@PathVariable Long id) { ... }
@PostMapping
public ApiResult<AiAssistantModelVO> create(@Valid @RequestBody AiAssistantModelSaveDTO dto) { ... }
@PutMapping("/{id}")
public ApiResult<Void> update(@PathVariable Long id, @Valid @RequestBody AiAssistantModelSaveDTO dto) { ... }
@DeleteMapping("/{id}")
public ApiResult<Void> delete(@PathVariable Long id) { ... }
}
```
权限:沿用 Spring Security 对 `/api/admin/**` 的 ADMIN 规则(与 `CategoryController` 一致)。
#### 3.5.2 扩展 `AiChatController.java`
```java
@GetMapping("/models")
public ApiResult<List<AiAssistantModelAppVO>> listModels(@AuthenticationPrincipal Long userId) {
return ApiResult.success(aiAssistantModelService.listEnabledForApp());
}
@PostMapping("/sessions")
public ApiResult<CreateAiChatSessionVO> 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<AiChatSessionMessagesVO> 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` 5009IT + 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)。