647 lines
23 KiB
Markdown
647 lines
23 KiB
Markdown
# AI 助手模型管理功能技术实施文档
|
||
|
||
| 文档版本 | 修订日期 | 修订人 | 修订内容 |
|
||
| :------- | :--------- | :----- | :------------------------------------------------- |
|
||
| V1.0 | 2026-05-23 | — | 基于《AI助手模型管理功能需求》V1.1 初稿 |
|
||
| V1.1 | 2026-05-23 | — | 补充与润色、标签推荐等功能的实施边界说明 |
|
||
|
||
**关联需求**:[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 联调检查清单
|
||
|
||
- [ ] Flyway V6 执行成功;旧 `ai_chat_session` 均有 `model_id`
|
||
- [ ] 管理端 CRUD、默认模型互斥、禁止删唯一启用
|
||
- [ ] 用户下拉仅启用模型;`localStorage` 恢复有效
|
||
- [ ] 新会话绑定所选模型;同会话中途改下拉 **不** 换模
|
||
- [ ] 禁用模型后旧会话只读、`POST /chat` 400
|
||
- [ ] 助手页 **无** 图片/语音上传入口
|
||
- [ ] 摘要/润色仍走 `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 | 后端 | 单元/集成测试 | ⬜ 未开始 |
|
||
| 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 | 联调 | 管理端上架第二模型 → 用户新会话切换 → 对话成功 | ⬜ 未开始 |
|
||
| 19 | 联调 | 禁用模型 → 旧会话只读 + chat 400 | ⬜ 未开始 |
|
||
| 20 | 验收 | 按需求 V1.1 验收标准逐项确认 | ⬜ 未开始 |
|
||
|
||
---
|
||
|
||
## 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
|