Files
Jog/doc/AI助手模型管理功能技术实施文档.md
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

24 KiB
Raw Permalink Blame History

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助手模型管理功能需求.mdV1.2
依赖能力AI助手功能技术实施文档.mdAI 助手 V1.1 已落地)

与其它 AI 功能(实施边界):本期 不修改 TextPolishServiceImplTagRecommendServiceImplAiSummaryServiceImplproperties.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-13allow_image / allow_voice 仅管理端配置 + API 返回;用户端 上传 UI

1. 功能概述

在现有 AI 助手基础上增加:

能力
管理后台 /admin/ai-models 模型 CRUD(逻辑删除)、默认模型、启用/禁用、多模态开关
用户端 助手页模型下拉;localStorage 记住选择;新会话绑定模型
后端 ai_assistant_modelai_chat_session.model_id;按 model_code 调 DashScope

与现网差异AiChatServiceImpl.buildRequestBody"model", properties.getModel() 改为会话绑定模型的 model_code


2. 核心实现思路

2.1 复用项

复用项 说明
AiSummaryServiceImpl.callModel 仍负责 HTTP 调用 DashScope,仅请求体 model 字段来源变化
DashScopeProperties api-keybase-urltimeoutmax-inputmodel 作种子与异常兜底
AiChatServiceImpl 在现有 chat / session 流程上扩展,不重写
管理后台模式 参考 AdminCategoryController + Categories.vue 表格 + 弹窗
@TableLogic ai_assistant_model.deletedai_chat_session.deleted 一致

2.2 新增组件关系

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。)

-- 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):

@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

// AiChatSessionVO 增加
private Long modelId;
private String modelDisplayName;
private Boolean modelEnabled;

// AiChatResultVO 增加(可选)
private String modelDisplayName;

// AiChatSendDTO 增加(可选,会话级绑定后 chat 内忽略)
private Long modelId;

枚举 enums/AiChatReadOnlyReason.java

public enum AiChatReadOnlyReason {
    ROUND_FULL,
    MODEL_DISABLED
}

3.4 后端 Service

3.4.1 AiAssistantModelService / AiAssistantModelServiceImpl

方法 说明
List<AiAssistantModelVO> listAllForAdmin() deleted=0ORDER 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=1deleted=0
AiAssistantModel getById(Long id) 含已删(@TableLogic 需用自定义查询或 selectById 带逻辑)
AiAssistantModel requireEnabledById(Long id) 创建会话用
AiAssistantModel getDefaultModel() is_default=1 AND deleted=0;无则 warn + 合成 fallbackdashscope.model

默认模型切换(事务内):

@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
}

编码唯一

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 联查或批量查模型,填充 modelDisplayNamemodelEnabled
listMessages 返回类型改为 AiChatSessionMessagesVO;计算 readOnlyReason
chat 开头校验绑定模型 enabledbuildRequestBody(history, modelCode)
日志 log.info("AI 助手成功 sessionId={}, modelId={}, modelCode={}, ...")

禁用校验chat 内,在轮次校验之后):

AiAssistantModel bound = aiAssistantModelService.getById(session.getModelId());
if (bound == null || Boolean.FALSE.equals(bound.getEnabled())) {
    throw new BusinessException(ErrorCode.AI_MODEL_DISABLED);
}

buildRequestBody 签名

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

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 = falsereadOnlyReason = MODEL_DISABLED,且 chat 拒绝。


3.5 后端 Controller

3.5.1 AdminAiModelController.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

@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

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

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.vueel-table + el-dialog 表单
  • 表单字段:展示名称、模型编码、描述、启用、允许上传图片允许上传语音、默认、排序
  • 列表列:ID、展示名称、编码、状态、图片、语音、默认、排序、操作
  • 删除:ElMessageBox.confirmdeleteAdminAiModel
  • 不实现 任何上传控件(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

状态新增

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

await loadAppModels()
restoreModelIdFromStorage()
// 原有 loadSessions + selectSession

readOnly 计算

const readOnly = computed(() =>
  roundCount.value >= maxRounds || modelEnabled.value === false
)

selectSession:解析 listAiChatMessages 新结构:

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

const res = await createAiChatSession({ modelId: selectedModelId.value })

AiChatPanel 扩展

  • PropsselectedModelIdavailableModelsreadOnlyReason
  • 标题栏:el-select 绑定 selectedModelId@changelocalStorage 改当前 session
  • 提示条:readOnlyReason === 'MODEL_DISABLED' 时显示停用文案;ROUND_FULL 沿用现有满 5 轮条(可合并为两个 v-if

不实现:图片/语音上传按钮(即使 allowImageUpload=true 也不展示)。


4. 核心逻辑说明

4.1 创建会话时模型绑定

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 禁用后只读

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 后端单元测试

新建 AiAssistantModelServiceImplTestMockito):

用例 预期
创建重复 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 回填(线上库已 V6
  • 管理端 CRUD、默认互斥、禁止删唯一启用(单测 + API IT + E2E
  • 用户下拉仅启用模型;localStorage 恢复(代码 + E2E 选模)
  • 新会话绑定所选模型;同会话不改绑(单测 + 设计)
  • 禁用模型后旧会话只读、POST /chat 5009IT + E2E-19
  • 助手页 图片/语音上传入口
  • 摘要/润色仍走 dashscope.model

6. 待办事项

状态说明 未开始 🔄 进行中  已完成

序号 阶段 待办项 状态
1 数据库 新增 V6__ai_assistant_model.sql(建表、种子、session.model_id 回填) 已完成
2 后端 实体 AiAssistantModelAiChatSession.modelId 已完成
3 后端 AiAssistantModelMapper 已完成
4 后端 DTO/VOAiAssistantModelSaveDTOAiAssistantModelVOAiAssistantModelAppVOAiChatSessionMessagesVOCreateAiChatSessionDTO 已完成
5 后端 AiChatReadOnlyReason 枚举 已完成
6 后端 ErrorCode 新增 5007~5011 已完成
7 后端 AiAssistantModelService + ImplCRUD、默认、校验) 已完成
8 后端 AdminAiModelController 已完成
9 后端 AiChatController 增加 GET /modelssessions 支持 body 已完成
10 后端 AiChatServiceImpl 改造(绑定、禁用校验、modelCode、消息包装) 已完成
11 后端 单元/集成测试(AiAssistantModelServiceImplTestAiChatServiceImplTest / 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

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)。