feat: add implementation plan and monorepo guide for DreamChat project

This commit is contained in:
GW_MC
2026-02-23 19:14:45 +08:00
parent fbd5c84eca
commit ab02758382
8 changed files with 4286 additions and 0 deletions

572
doc/api-spec.md Normal file
View File

@@ -0,0 +1,572 @@
# DreamChat API Specification
## Overview
This document defines the REST API and WebSocket specifications for DreamChat.
- **REST API**: For CRUD operations, file uploads, and synchronous requests
- **WebSocket**: For real-time chat streaming
- **OpenAPI**: Auto-generated from NestJS decorators for frontend client generation
## Base URL
```
Development: http://localhost:3000/api
Production: https://api.dreamchat.example/api
```
## Authentication
### JWT Token Flow
```http
POST /api/auth/login
Content-Type: application/json
{
"username": "user@example.com",
"password": "securepassword"
}
Response:
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 3600
}
```
### Keycloak Integration
```http
GET /api/auth/keycloak
Redirect to Keycloak login
Callback:
GET /api/auth/keycloak/callback?code=...
Response: { accessToken, refreshToken, expiresIn }
```
### WebSocket Auth
WebSocket connections authenticate via query parameter:
```
ws://localhost:3000/chat?token=eyJhbGciOiJIUzI1NiIs...
```
## REST API Endpoints
### Authentication Module
| Method | Endpoint | Description | Auth |
|--------|----------|-------------|------|
| POST | `/auth/login` | Local login | Public |
| POST | `/auth/refresh` | Refresh token | Public |
| POST | `/auth/logout` | Logout | Bearer |
| GET | `/auth/me` | Get current user | Bearer |
| GET | `/auth/keycloak` | Keycloak login URL | Public |
| GET | `/auth/keycloak/callback` | Keycloak callback | Public |
### Users Module
| Method | Endpoint | Description | Auth |
|--------|----------|-------------|------|
| POST | `/users` | Register new user | Public |
| GET | `/users/:id` | Get user profile | Bearer |
| PATCH | `/users/:id` | Update user | Bearer (own only) |
| DELETE | `/users/:id` | Delete user | Bearer (own/admin) |
### Characters Module
```typescript
// DTOs
class CreateCharacterDto {
name: string;
personalityPrompt: string;
backstory?: string;
attributes?: Record<string, any>;
avatarUrl?: string;
config?: Record<string, any>;
}
class CharacterResponseDto {
id: string;
name: string;
avatarUrl: string;
personalityPrompt: string;
backstory: string;
attributes: Record<string, any>;
createdAt: Date;
}
```
| Method | Endpoint | Description | Auth |
|--------|----------|-------------|------|
| GET | `/characters` | List user's characters | Bearer |
| POST | `/characters` | Create character | Bearer |
| GET | `/characters/:id` | Get character details | Bearer |
| PATCH | `/characters/:id` | Update character | Bearer (owner) |
| DELETE | `/characters/:id` | Delete character | Bearer (owner) |
**Example Requests:**
```http
POST /api/characters
Authorization: Bearer {token}
Content-Type: application/json
{
"name": "Alice",
"personalityPrompt": "You are Alice, a curious and adventurous explorer...",
"backstory": "Alice grew up in a small village...",
"attributes": {
"traits": ["curious", "brave", "witty"],
"age": 25,
"species": "human",
"skills": ["navigation", "survival"]
}
}
Response: 201 Created
{
"id": "uuid",
"name": "Alice",
"personalityPrompt": "...",
"attributes": { ... },
"createdAt": "2026-02-23T10:00:00Z"
}
```
### Conversations Module
```typescript
class CreateConversationDto {
characterId: string;
title?: string;
}
class ConversationResponseDto {
id: string;
title: string;
character: CharacterSummaryDto;
messageCount: number;
createdAt: Date;
}
```
| Method | Endpoint | Description | Auth |
|--------|----------|-------------|------|
| GET | `/conversations` | List conversations | Bearer |
| POST | `/conversations` | Create conversation | Bearer |
| GET | `/conversations/:id` | Get conversation | Bearer |
| PATCH | `/conversations/:id` | Update conversation | Bearer |
| DELETE | `/conversations/:id` | Delete conversation | Bearer |
### Messages Module
```typescript
class CreateMessageDto {
content: string;
}
class MessageResponseDto {
id: string;
role: 'user' | 'assistant';
content: string;
tokensUsed?: number;
model?: string;
createdAt: Date;
}
```
| Method | Endpoint | Description | Auth |
|--------|----------|-------------|------|
| GET | `/conversations/:id/messages` | Get messages (paginated) | Bearer |
| POST | `/conversations/:id/messages` | Send message (non-streaming) | Bearer |
| DELETE | `/messages/:id` | Delete message | Bearer |
### Import Module
```typescript
class FileImportDto {
// Multipart form data
file: File;
}
class UrlImportDto {
url: string;
}
class ImportResponseDto {
id: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
sourceName: string;
fileSize?: number;
content?: string;
errorMessage?: string;
createdAt: Date;
}
```
| Method | Endpoint | Description | Auth |
|--------|----------|-------------|------|
| POST | `/import/file` | Import file (txt, pdf, md) | Bearer |
| POST | `/import/url` | Import from URL | Bearer |
| GET | `/import/:id` | Get import status | Bearer |
| GET | `/import` | List imports | Bearer |
| DELETE | `/import/:id` | Delete import | Bearer |
**File Upload Request:**
```http
POST /api/import/file
Authorization: Bearer {token}
Content-Type: multipart/form-data
------Boundary
Content-Disposition: form-data; name="file"; filename="story.txt"
Content-Type: text/plain
(file content here)
------Boundary--
Response: 202 Accepted
{
"id": "import-uuid",
"status": "processing",
"sourceName": "story.txt",
"createdAt": "2026-02-23T10:00:00Z"
}
```
**URL Import Request:**
```http
POST /api/import/url
Authorization: Bearer {token}
Content-Type: application/json
{
"url": "https://archiveofourown.org/works/12345678"
}
Response: 202 Accepted
{
"id": "import-uuid",
"status": "processing",
"sourceName": "https://archiveofourown.org/works/12345678"
}
// If no scraper available:
Response: 400 Bad Request
{
"error": "UNSUPPORTED_URL",
"message": "No scraper available for this URL"
}
```
### Story Module (Phase 2)
```typescript
class CreateStoryBranchDto {
parentId?: string; // null for root
userDirection: string;
}
class StoryBranchResponseDto {
id: string;
conversationId: string;
parentId?: string;
title: string;
content: string;
userDirection: string;
depth: number;
children: StoryBranchResponseDto[];
createdAt: Date;
}
```
| Method | Endpoint | Description | Auth |
|--------|----------|-------------|------|
| GET | `/conversations/:id/story` | Get story tree | Bearer |
| POST | `/conversations/:id/story/branches` | Create new branch | Bearer |
| GET | `/story-branches/:id` | Get branch details | Bearer |
| PATCH | `/story-branches/:id` | Update branch | Bearer |
| DELETE | `/story-branches/:id` | Delete branch | Bearer |
**Get Story Tree:**
```http
GET /api/conversations/conv-id/story
Authorization: Bearer {token}
Response:
{
"root": {
"id": "root-uuid",
"title": "The Beginning",
"content": "Once upon a time...",
"depth": 0,
"children": [
{
"id": "branch-1",
"title": "The Left Path",
"content": "You chose the left path...",
"userDirection": "Go left into the dark forest",
"depth": 1,
"children": []
},
{
"id": "branch-2",
"title": "The Right Path",
"content": "You chose the right path...",
"userDirection": "Go right towards the castle",
"depth": 1,
"children": []
}
]
}
}
```
### Multi-Character Module (Phase 3)
```typescript
class AddParticipantDto {
characterId: string;
autoRespond: boolean;
}
class ParticipantResponseDto {
id: string;
character: CharacterSummaryDto;
isActive: boolean;
autoRespond: boolean;
}
```
| Method | Endpoint | Description | Auth |
|--------|----------|-------------|------|
| GET | `/conversations/:id/participants` | List participants | Bearer |
| POST | `/conversations/:id/participants` | Add participant | Bearer |
| DELETE | `/conversations/:id/participants/:charId` | Remove participant | Bearer |
## WebSocket Specification
### Connection
```javascript
const ws = new WebSocket('ws://localhost:3000/chat?token=JWT_TOKEN');
ws.onopen = () => {
console.log('Connected');
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
handleMessage(message);
};
```
### Message Protocol
All WebSocket messages use JSON format:
```typescript
interface WebSocketMessage {
type: string;
payload: any;
timestamp: string;
requestId?: string; // For correlating responses
}
```
### Client → Server Events
#### 1. Join Conversation
```typescript
// Join a conversation room
{
"type": "JOIN_CONVERSATION",
"payload": {
"conversationId": "conv-uuid"
},
"requestId": "req-123"
}
// Response
{
"type": "CONVERSATION_JOINED",
"payload": {
"conversationId": "conv-uuid",
"history": [ /* last N messages */ ]
},
"requestId": "req-123"
}
```
#### 2. Send Message (Streaming)
```typescript
{
"type": "SEND_MESSAGE",
"payload": {
"conversationId": "conv-uuid",
"content": "Hello, how are you?",
"streaming": true // Enable streaming response
},
"requestId": "msg-456"
}
```
#### 3. Stop Generation
```typescript
{
"type": "STOP_GENERATION",
"payload": {
"conversationId": "conv-uuid"
}
}
```
#### 4. Leave Conversation
```typescript
{
"type": "LEAVE_CONVERSATION",
"payload": {
"conversationId": "conv-uuid"
}
}
```
### Server → Client Events
#### 1. Message Acknowledged
```typescript
{
"type": "MESSAGE_ACK",
"payload": {
"messageId": "msg-uuid",
"status": "received"
},
"requestId": "msg-456"
}
```
#### 2. Stream Chunk
```typescript
{
"type": "STREAM_CHUNK",
"payload": {
"conversationId": "conv-uuid",
"chunk": " part of response",
"isComplete": false
},
"requestId": "msg-456"
}
```
#### 3. Stream Complete
```typescript
{
"type": "STREAM_COMPLETE",
"payload": {
"conversationId": "conv-uuid",
"messageId": "assistant-msg-uuid",
"content": "Full response text",
"tokensUsed": 150,
"model": "openai/gpt-4o"
},
"requestId": "msg-456"
}
```
#### 4. Error
```typescript
{
"type": "ERROR",
"payload": {
"code": "LLM_ERROR",
"message": "Failed to generate response",
"details": { ... }
},
"requestId": "msg-456"
}
```
### Error Codes
| Code | Description | HTTP Status |
|------|-------------|-------------|
| `UNAUTHORIZED` | Invalid or expired token | 401 |
| `FORBIDDEN` | Insufficient permissions | 403 |
| `NOT_FOUND` | Resource not found | 404 |
| `VALIDATION_ERROR` | Invalid input data | 400 |
| `LLM_ERROR` | LLM provider error | 502 |
| `RATE_LIMITED` | Too many requests | 429 |
| `FILE_TOO_LARGE` | File exceeds 50MB | 413 |
| `UNSUPPORTED_URL` | No scraper for URL | 400 |
## OpenAPI Configuration
### NestJS Setup
```typescript
// main.ts
const config = new DocumentBuilder()
.setTitle('DreamChat API')
.setDescription('Character simulation and chat API')
.setVersion('1.0')
.addBearerAuth()
.addTag('auth', 'Authentication endpoints')
.addTag('users', 'User management')
.addTag('characters', 'Character CRUD')
.addTag('conversations', 'Chat sessions')
.addTag('messages', 'Chat messages')
.addTag('import', 'Data import')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document);
// Also serve raw JSON for client generation
writeFileSync('./openapi-spec.json', JSON.stringify(document, null, 2));
```
### Frontend Client Generation
```bash
# Generate TypeScript client
npx openapi-generator-cli generate \
-i ./openapi-spec.json \
-g typescript-fetch \
-o ./apps/frontend/src/api/generated \
--additional-properties=supportsES6=true,npmName=dreamchat-api
# Or use Orval for React Query hooks
npx orval --config ./orval.config.js
```
## Rate Limiting
| Endpoint | Limit |
|----------|-------|
| Auth endpoints | 10 req/min |
| General API | 100 req/min |
| File upload | 5 req/min |
| WebSocket messages | 60 msg/min |
| LLM streaming | 20 requests/min |
```typescript
// Rate limit headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1645603200
```