All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 46s
## 数据模型 - 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>
111 lines
3.7 KiB
TypeScript
111 lines
3.7 KiB
TypeScript
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);
|
||
}
|
||
}
|