api-server/src/modules/rag-chat/rag-chat.controller.ts
wangdl fe44dec567
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 46s
feat: M-CHAT ChatScope 会话系统完整实现
## 数据模型
- ChatSession +13 字段 (scopeType/scopeId/parentKnowledgeBaseId/createdFrom/isPinned/isArchived/isDeleted/modelMode/modelId/lastMessageAt)
- ChatMessage +scopeSnapshot (消息级 scope 快照)
- ChatCitation +lineStart/lineEnd +sourceId 索引
- 5 个新查询索引

## 核心能力
- open-or-create: 同 scope 继续会话 (200) / 新建 (201)
- scope 级检索: global/knowledge_base/material/knowledge_item/folder
- listSessions: scope 过滤 + isDeleted 排除 + isPinned 排序 + 分页元数据
- 自动标题: 首条消息截取 + 词边界处理
- 软删除 + 置顶/归档
- scope 字段创建后不可修改
- 全部端点 userId 鉴权

## 文档
- docs/chat-scope-design.md (设计文档 + 决策表)
- docs/chat-scope-api-contract.md (API 契约)
- docs/chat-scope-test-plan.md (33 条测试用例)
- prisma/migrations/backfill_chat_scope.sql (旧数据回填)

## Bug 修复
- #104: KnowledgeItem.sourceRef 填充 (material scope 检索修复)
- #102: sendMessageStream aiGateway null 保护
- listSessions isDeleted/isArchived 过滤 + 分页

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 17:27:40 +08:00

111 lines
3.7 KiB
TypeScript
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.

import { Controller, Get, Post, Patch, Delete, Body, Param, Res, Query, HttpCode } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import type { Response } from 'express';
import { RagChatService } from './rag-chat.service';
import type { CreateSessionResult } from './rag-chat.service';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import type { UserPayload } from '../../common/types';
@ApiTags('rag-chat')
@Controller('rag-chat')
@ApiBearerAuth()
export class RagChatController {
constructor(private readonly svc: RagChatService) {}
@Post('sessions')
@ApiOperation({ summary: '创建/打开对话 (open-or-create)' })
async createSession(
@CurrentUser() user: UserPayload,
@Res({ passthrough: true }) res: Response,
@Body() dto: {
scopeType: string;
scopeId?: string | null;
parentKnowledgeBaseId?: string | null;
createdFrom?: string;
title?: string;
},
) {
const result: CreateSessionResult = await this.svc.createSession(String(user.id), {
scopeType: dto.scopeType,
scopeId: dto.scopeId ?? null,
parentKnowledgeBaseId: dto.parentKnowledgeBaseId ?? null,
createdFrom: dto.createdFrom,
title: dto.title,
});
res.status(result.isNew ? 201 : 200);
return result.session;
}
@Get('sessions')
@ApiOperation({ summary: '对话列表' })
async listSessions(
@CurrentUser() user: UserPayload,
@Query('scopeType') scopeType?: string,
@Query('scopeId') scopeId?: string,
@Query('parentKnowledgeBaseId') parentKnowledgeBaseId?: string,
@Query('knowledgeBaseId') kbId?: string,
@Query('page') page?: string,
@Query('limit') limit?: string,
) {
return this.svc.listSessions(String(user.id), {
scopeType,
scopeId,
parentKnowledgeBaseId,
kbId,
page: page ? parseInt(page, 10) : undefined,
limit: limit ? parseInt(limit, 10) : undefined,
});
}
@Get('sessions/:id/messages')
@ApiOperation({ summary: '对话历史' })
async messages(@CurrentUser() user: UserPayload, @Param('id') id: string) {
return this.svc.getMessages(String(user.id), id);
}
@Post('sessions/:id/messages')
@ApiOperation({ summary: '发送消息(同步)' })
async sendMessage(@CurrentUser() user: UserPayload, @Param('id') id: string, @Body() dto: { content: string }) {
return this.svc.sendMessage(String(user.id), id, dto.content);
}
@Post('sessions/:id/stream')
@ApiOperation({ summary: '发送消息SSE 流式,支持思考过程)' })
async sendMessageStream(@CurrentUser() user: UserPayload, @Param('id') id: string, @Body() dto: { content: string }, @Res() res: Response) {
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
const userId = String(user.id);
for await (const chunk of this.svc.sendMessageStream(userId, id, dto.content)) {
if (res.destroyed) break;
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
}
res.end();
}
@Patch('sessions/:id')
@ApiOperation({ summary: '更新会话属性' })
async updateSession(
@CurrentUser() user: UserPayload,
@Param('id') id: string,
@Body() dto: {
title?: string;
isPinned?: boolean;
isArchived?: boolean;
modelMode?: string;
modelId?: string | null;
},
) {
return this.svc.updateSession(String(user.id), id, dto);
}
@Delete('sessions/:id')
@ApiOperation({ summary: '删除对话(软删除)' })
async deleteSession(@CurrentUser() user: UserPayload, @Param('id') id: string) {
return this.svc.deleteSession(String(user.id), id);
}
}