Forráskód Böngészése

添加Ollama集成功能,包括聊天、文本生成、模型列表获取和服务状态检查的API端点,同时更新相关服务和DTO,新增axios依赖。

max 7 órája
szülő
commit
4cdbad5d52

+ 1 - 0
LMS-NodeJs/package.json

@@ -29,6 +29,7 @@
     "@nestjs/typeorm": "^11.0.0",
     "@types/multer": "^2.0.0",
     "@types/uuid": "^10.0.0",
+    "axios": "^1.11.0",
     "class-transformer": "^0.5.1",
     "class-validator": "^0.14.2",
     "multer": "^2.0.2",

+ 30 - 0
LMS-NodeJs/pnpm-lock.yaml

@@ -35,6 +35,9 @@ importers:
       '@types/uuid':
         specifier: ^10.0.0
         version: 10.0.0
+      axios:
+        specifier: ^1.11.0
+        version: 1.11.0
       class-transformer:
         specifier: ^0.5.1
         version: 0.5.1
@@ -1402,6 +1405,9 @@ packages:
     resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==}
     engines: {node: '>= 6.0.0'}
 
+  axios@1.11.0:
+    resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
+
   b4a@1.6.7:
     resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==}
 
@@ -2053,6 +2059,15 @@ packages:
   flatted@3.3.3:
     resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
 
+  follow-redirects@1.15.9:
+    resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+
   for-each@0.3.5:
     resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
     engines: {node: '>= 0.4'}
@@ -2986,6 +3001,9 @@ packages:
     resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
     engines: {node: '>= 0.10'}
 
+  proxy-from-env@1.1.0:
+    resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
   punycode@2.3.1:
     resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
     engines: {node: '>=6'}
@@ -5187,6 +5205,14 @@ snapshots:
 
   aws-ssl-profiles@1.1.2: {}
 
+  axios@1.11.0:
+    dependencies:
+      follow-redirects: 1.15.9
+      form-data: 4.0.4
+      proxy-from-env: 1.1.0
+    transitivePeerDependencies:
+      - debug
+
   b4a@1.6.7: {}
 
   babel-jest@29.7.0(@babel/core@7.28.0):
@@ -5905,6 +5931,8 @@ snapshots:
 
   flatted@3.3.3: {}
 
+  follow-redirects@1.15.9: {}
+
   for-each@0.3.5:
     dependencies:
       is-callable: 1.2.7
@@ -6945,6 +6973,8 @@ snapshots:
       forwarded: 0.2.0
       ipaddr.js: 1.9.1
 
+  proxy-from-env@1.1.0: {}
+
   punycode@2.3.1: {}
 
   pure-rand@6.1.0: {}

+ 46 - 0
LMS-NodeJs/src/aide/aide.controller.ts

@@ -10,6 +10,7 @@ import {
 import { AideService } from './aide.service';
 import { CreateAideDto } from './dto/create-aide.dto';
 import { UpdateAideDto } from './dto/update-aide.dto';
+import { OllamaChatDto, OllamaGenerateDto } from './dto/ollama-chat.dto';
 import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
 
 @ApiTags('aide')
@@ -61,4 +62,49 @@ export class AideController {
   remove(@Param('id') id: string) {
     return this.aideService.remove(+id);
   }
+
+  // Ollama 相关端点
+
+  @Post('ollama/chat')
+  @ApiOperation({
+    summary: '与 Ollama 聊天',
+    description: '与 Ollama 模型进行对话聊天',
+  })
+  @ApiResponse({ status: 200, description: '聊天成功' })
+  @ApiResponse({ status: 503, description: 'Ollama 服务不可用' })
+  async chatWithOllama(@Body() chatDto: OllamaChatDto) {
+    return await this.aideService.chatWithOllama(chatDto);
+  }
+
+  @Post('ollama/generate')
+  @ApiOperation({
+    summary: '使用 Ollama 生成文本',
+    description: '使用 Ollama 模型生成文本内容',
+  })
+  @ApiResponse({ status: 200, description: '生成成功' })
+  @ApiResponse({ status: 503, description: 'Ollama 服务不可用' })
+  async generateWithOllama(@Body() generateDto: OllamaGenerateDto) {
+    return await this.aideService.generateWithOllama(generateDto);
+  }
+
+  @Get('ollama/models')
+  @ApiOperation({
+    summary: '获取 Ollama 模型列表',
+    description: '获取可用的 Ollama 模型列表',
+  })
+  @ApiResponse({ status: 200, description: '获取成功' })
+  @ApiResponse({ status: 503, description: 'Ollama 服务不可用' })
+  async getOllamaModels(): Promise<any> {
+    return await this.aideService.getOllamaModels();
+  }
+
+  @Get('ollama/health')
+  @ApiOperation({
+    summary: '检查 Ollama 服务状态',
+    description: '检查 Ollama 服务是否正常运行',
+  })
+  @ApiResponse({ status: 200, description: '检查完成' })
+  async checkOllamaHealth() {
+    return await this.aideService.checkOllamaHealth();
+  }
 }

+ 2 - 1
LMS-NodeJs/src/aide/aide.module.ts

@@ -1,9 +1,10 @@
 import { Module } from '@nestjs/common';
 import { AideService } from './aide.service';
 import { AideController } from './aide.controller';
+import { OllamaService } from './ollama.service';
 
 @Module({
   controllers: [AideController],
-  providers: [AideService],
+  providers: [AideService, OllamaService],
 })
 export class AideModule {}

+ 39 - 2
LMS-NodeJs/src/aide/aide.service.ts

@@ -1,10 +1,14 @@
 import { Injectable } from '@nestjs/common';
 import { CreateAideDto } from './dto/create-aide.dto';
 import { UpdateAideDto } from './dto/update-aide.dto';
+import { OllamaService } from './ollama.service';
+import { OllamaChatDto, OllamaGenerateDto } from './dto/ollama-chat.dto';
 
 @Injectable()
 export class AideService {
-  create(createAideDto: CreateAideDto) {
+  constructor(private readonly ollamaService: OllamaService) {}
+
+  create(_createAideDto: CreateAideDto) {
     return 'This action adds a new aide';
   }
 
@@ -16,11 +20,44 @@ export class AideService {
     return `This action returns a #${id} aide`;
   }
 
-  update(id: number, updateAideDto: UpdateAideDto) {
+  update(id: number, _updateAideDto: UpdateAideDto) {
     return `This action updates a #${id} aide`;
   }
 
   remove(id: number) {
     return `This action removes a #${id} aide`;
   }
+
+  /**
+   * 与 Ollama 进行聊天对话
+   */
+  async chatWithOllama(chatDto: OllamaChatDto) {
+    return await this.ollamaService.chat(chatDto);
+  }
+
+  /**
+   * 使用 Ollama 生成文本
+   */
+  async generateWithOllama(generateDto: OllamaGenerateDto) {
+    return await this.ollamaService.generate(generateDto);
+  }
+
+  /**
+   * 获取可用的 Ollama 模型列表
+   */
+  async getOllamaModels() {
+    return await this.ollamaService.listModels();
+  }
+
+  /**
+   * 检查 Ollama 服务状态
+   */
+  async checkOllamaHealth() {
+    const isHealthy = await this.ollamaService.healthCheck();
+    return {
+      status: isHealthy ? 'healthy' : 'unhealthy',
+      message: isHealthy ? 'Ollama 服务运行正常' : 'Ollama 服务不可用',
+      timestamp: new Date().toISOString(),
+    };
+  }
 }

+ 105 - 0
LMS-NodeJs/src/aide/dto/ollama-chat.dto.ts

@@ -0,0 +1,105 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsOptional, IsArray, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+
+export class OllamaMessageDto {
+  @ApiProperty({
+    description: '消息角色',
+    example: 'user',
+    enum: ['user', 'assistant', 'system'],
+  })
+  @IsString()
+  role: 'user' | 'assistant' | 'system';
+
+  @ApiProperty({
+    description: '消息内容',
+    example: '你好,请介绍一下你自己',
+  })
+  @IsString()
+  content: string;
+}
+
+export class OllamaChatDto {
+  @ApiProperty({
+    description: '模型名称',
+    example: 'llama2',
+    required: false,
+  })
+  @IsOptional()
+  @IsString()
+  model?: string = 'llama2';
+
+  @ApiProperty({
+    description: '消息列表',
+    type: [OllamaMessageDto],
+    example: [{ role: 'user', content: '你好,请介绍一下你自己' }],
+  })
+  @IsArray()
+  @ValidateNested({ each: true })
+  @Type(() => OllamaMessageDto)
+  messages: OllamaMessageDto[];
+
+  @ApiProperty({
+    description: '是否流式响应',
+    example: false,
+    required: false,
+  })
+  @IsOptional()
+  stream?: boolean = false;
+
+  @ApiProperty({
+    description: '温度参数',
+    example: 0.7,
+    required: false,
+  })
+  @IsOptional()
+  temperature?: number = 0.7;
+
+  @ApiProperty({
+    description: '最大令牌数',
+    example: 2048,
+    required: false,
+  })
+  @IsOptional()
+  max_tokens?: number = 2048;
+}
+
+export class OllamaGenerateDto {
+  @ApiProperty({
+    description: '模型名称',
+    example: 'llama2',
+  })
+  @IsString()
+  model: string;
+
+  @ApiProperty({
+    description: '提示词',
+    example: '请写一首关于春天的诗',
+  })
+  @IsString()
+  prompt: string;
+
+  @ApiProperty({
+    description: '是否流式响应',
+    example: false,
+    required: false,
+  })
+  @IsOptional()
+  stream?: boolean = false;
+
+  @ApiProperty({
+    description: '温度参数',
+    example: 0.7,
+    required: false,
+  })
+  @IsOptional()
+  temperature?: number = 0.7;
+
+  @ApiProperty({
+    description: '最大令牌数',
+    example: 2048,
+    required: false,
+  })
+  @IsOptional()
+  max_tokens?: number = 2048;
+}

+ 176 - 0
LMS-NodeJs/src/aide/ollama.service.ts

@@ -0,0 +1,176 @@
+import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
+import axios, { AxiosResponse } from 'axios';
+import { OllamaChatDto, OllamaGenerateDto } from './dto/ollama-chat.dto';
+
+export interface OllamaResponse {
+  model: string;
+  created_at: string;
+  message: {
+    role: string;
+    content: string;
+  };
+  done: boolean;
+  total_duration: number;
+  load_duration: number;
+  prompt_eval_count: number;
+  prompt_eval_duration: number;
+  eval_count: number;
+  eval_duration: number;
+}
+
+export interface OllamaGenerateResponse {
+  model: string;
+  created_at: string;
+  response: string;
+  done: boolean;
+  context: number[];
+  total_duration: number;
+  load_duration: number;
+  prompt_eval_count: number;
+  prompt_eval_duration: number;
+  eval_count: number;
+  eval_duration: number;
+}
+
+@Injectable()
+export class OllamaService {
+  private readonly baseUrl: string;
+
+  constructor() {
+    // 默认 Ollama 服务地址,可以通过环境变量配置
+    this.baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
+  }
+
+  /**
+   * 与 Ollama 进行聊天对话
+   */
+  async chat(chatDto: OllamaChatDto): Promise<OllamaResponse> {
+    try {
+      const response: AxiosResponse<OllamaResponse> = await axios.post(
+        `${this.baseUrl}/api/chat`,
+        {
+          model: chatDto.model || 'llama2',
+          messages: chatDto.messages,
+          stream: chatDto.stream || false,
+          options: {
+            temperature: chatDto.temperature || 0.7,
+            num_predict: chatDto.max_tokens || 2048,
+          },
+        },
+        {
+          headers: {
+            'Content-Type': 'application/json',
+          },
+          timeout: 30000, // 30秒超时
+        },
+      );
+
+      return response.data;
+    } catch (error) {
+      if (axios.isAxiosError(error)) {
+        if (error.code === 'ECONNREFUSED') {
+          throw new HttpException(
+            '无法连接到 Ollama 服务,请确保 Ollama 正在运行',
+            HttpStatus.SERVICE_UNAVAILABLE,
+          );
+        }
+        if (error.response) {
+          throw new HttpException(
+            `Ollama 服务错误: ${error.response.data?.error || error.message}`,
+            error.response.status,
+          );
+        }
+      }
+      throw new HttpException(
+        `调用 Ollama 服务失败: ${error instanceof Error ? error.message : '未知错误'}`,
+        HttpStatus.INTERNAL_SERVER_ERROR,
+      );
+    }
+  }
+
+  /**
+   * 使用 Ollama 生成文本
+   */
+  async generate(
+    generateDto: OllamaGenerateDto,
+  ): Promise<OllamaGenerateResponse> {
+    try {
+      const response: AxiosResponse<OllamaGenerateResponse> = await axios.post(
+        `${this.baseUrl}/api/generate`,
+        {
+          model: generateDto.model,
+          prompt: generateDto.prompt,
+          stream: generateDto.stream || false,
+          options: {
+            temperature: generateDto.temperature || 0.7,
+            num_predict: generateDto.max_tokens || 2048,
+          },
+        },
+        {
+          headers: {
+            'Content-Type': 'application/json',
+          },
+          timeout: 30000, // 30秒超时
+        },
+      );
+
+      return response.data;
+    } catch (error) {
+      if (axios.isAxiosError(error)) {
+        if (error.code === 'ECONNREFUSED') {
+          throw new HttpException(
+            '无法连接到 Ollama 服务,请确保 Ollama 正在运行',
+            HttpStatus.SERVICE_UNAVAILABLE,
+          );
+        }
+        if (error.response) {
+          throw new HttpException(
+            `Ollama 服务错误: ${error.response.data?.error || error.message}`,
+            error.response.status,
+          );
+        }
+      }
+      throw new HttpException(
+        `调用 Ollama 服务失败: ${error instanceof Error ? error.message : '未知错误'}`,
+        HttpStatus.INTERNAL_SERVER_ERROR,
+      );
+    }
+  }
+
+  /**
+   * 获取可用的模型列表
+   */
+  async listModels(): Promise<any> {
+    try {
+      const response: AxiosResponse = await axios.get(
+        `${this.baseUrl}/api/tags`,
+      );
+      return response.data;
+    } catch (error) {
+      if (axios.isAxiosError(error)) {
+        if (error.code === 'ECONNREFUSED') {
+          throw new HttpException(
+            '无法连接到 Ollama 服务,请确保 Ollama 正在运行',
+            HttpStatus.SERVICE_UNAVAILABLE,
+          );
+        }
+      }
+      throw new HttpException(
+        `获取模型列表失败: ${error instanceof Error ? error.message : '未知错误'}`,
+        HttpStatus.INTERNAL_SERVER_ERROR,
+      );
+    }
+  }
+
+  /**
+   * 检查 Ollama 服务状态
+   */
+  async healthCheck(): Promise<boolean> {
+    try {
+      await axios.get(`${this.baseUrl}/api/tags`, { timeout: 5000 });
+      return true;
+    } catch (_error) {
+      return false;
+    }
+  }
+}

+ 12 - 6
LMS-NodeJs/src/author/author.service.ts

@@ -1,4 +1,8 @@
-import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
+import {
+  Injectable,
+  NotFoundException,
+  BadRequestException,
+} from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
 import { Repository } from 'typeorm';
 import { CreateAuthorDto } from './dto/create-author.dto';
@@ -28,11 +32,11 @@ export class AuthorService {
       where: { id },
       relations: ['books'],
     });
-    
+
     if (!author) {
       throw new NotFoundException(`作者ID ${id} 不存在`);
     }
-    
+
     return author;
   }
 
@@ -44,12 +48,14 @@ export class AuthorService {
 
   async remove(id: number): Promise<void> {
     const author = await this.findOne(id);
-    
+
     // 检查作者是否有关联的书籍
     if (author.books && author.books.length > 0) {
-      throw new BadRequestException(`无法删除作者,该作者还有 ${author.books.length} 本关联的书籍`);
+      throw new BadRequestException(
+        `无法删除作者,该作者还有 ${author.books.length} 本关联的书籍`,
+      );
     }
-    
+
     await this.authorRepository.remove(author);
   }
 }

+ 24 - 17
LMS-NodeJs/src/chapter/chapter.controller.ts

@@ -34,9 +34,9 @@ export class ChapterController {
   }
 
   @Get('book/:bookId')
-  @ApiOperation({ 
-    summary: '根据书籍获取章节', 
-    description: '根据书籍ID获取所有相关章节' 
+  @ApiOperation({
+    summary: '根据书籍获取章节',
+    description: '根据书籍ID获取所有相关章节',
   })
   @ApiParam({ name: 'bookId', description: '书籍ID', example: 1 })
   @ApiResponse({ status: 200, description: '获取成功' })
@@ -45,9 +45,9 @@ export class ChapterController {
   }
 
   @Get('parent/:parentId')
-  @ApiOperation({ 
-    summary: '根据父章节获取子章节', 
-    description: '根据父章节ID获取所有子章节' 
+  @ApiOperation({
+    summary: '根据父章节获取子章节',
+    description: '根据父章节ID获取所有子章节',
   })
   @ApiParam({ name: 'parentId', description: '父章节ID', example: 1 })
   @ApiResponse({ status: 200, description: '获取成功' })
@@ -56,11 +56,15 @@ export class ChapterController {
   }
 
   @Get('chapterId/:chapterId')
-  @ApiOperation({ 
-    summary: '根据章节ID获取章节', 
-    description: '根据章节的唯一ID获取章节信息' 
+  @ApiOperation({
+    summary: '根据章节ID获取章节',
+    description: '根据章节的唯一ID获取章节信息',
+  })
+  @ApiParam({
+    name: 'chapterId',
+    description: '章节ID',
+    example: 'book_1_20240101120000_abc123',
   })
-  @ApiParam({ name: 'chapterId', description: '章节ID', example: 'book_1_20240101120000_abc123' })
   @ApiResponse({ status: 200, description: '获取成功' })
   @ApiResponse({ status: 404, description: '章节不存在' })
   findByChapterId(@Param('chapterId') chapterId: string) {
@@ -68,9 +72,9 @@ export class ChapterController {
   }
 
   @Patch(':id/sort/:sort')
-  @ApiOperation({ 
-    summary: '更新章节排序', 
-    description: '更新章节的排序位置' 
+  @ApiOperation({
+    summary: '更新章节排序',
+    description: '更新章节的排序位置',
   })
   @ApiParam({ name: 'id', description: '章节ID', example: 1 })
   @ApiParam({ name: 'sort', description: '新的排序值', example: 2 })
@@ -81,15 +85,18 @@ export class ChapterController {
   }
 
   @Patch(':id/status/:status')
-  @ApiOperation({ 
-    summary: '更新章节状态', 
-    description: '更新章节的发布状态' 
+  @ApiOperation({
+    summary: '更新章节状态',
+    description: '更新章节的发布状态',
   })
   @ApiParam({ name: 'id', description: '章节ID', example: 1 })
   @ApiParam({ name: 'status', description: '章节状态', example: 'published' })
   @ApiResponse({ status: 200, description: '状态更新成功' })
   @ApiResponse({ status: 404, description: '章节不存在' })
-  updateStatus(@Param('id') id: string, @Param('status') status: ChapterStatus) {
+  updateStatus(
+    @Param('id') id: string,
+    @Param('status') status: ChapterStatus,
+  ) {
     return this.chapterService.updateStatus(+id, status);
   }
 

+ 4 - 3
LMS-NodeJs/src/chapter/chapter.service.ts

@@ -17,16 +17,17 @@ export class ChapterService {
 
   private generateChapterId(bookId: number): string {
     const now = new Date();
-    const timestamp = now.getFullYear().toString() +
+    const timestamp =
+      now.getFullYear().toString() +
       (now.getMonth() + 1).toString().padStart(2, '0') +
       now.getDate().toString().padStart(2, '0') +
       now.getHours().toString().padStart(2, '0') +
       now.getMinutes().toString().padStart(2, '0') +
       now.getSeconds().toString().padStart(2, '0');
-    
+
     // 生成6位随机字符串
     const randomStr = Math.random().toString(36).substring(2, 8);
-    
+
     return `book_${bookId}_${timestamp}_${randomStr}`;
   }
 

+ 12 - 8
LMS-NodeJs/src/chapter/dto/create-chapter.dto.ts

@@ -6,8 +6,10 @@ import {
   IsDateString,
   Min,
   Max,
+  IsEnum,
 } from 'class-validator';
 import { ApiProperty } from '@nestjs/swagger';
+import { ChapterStatus } from '../entities/chapter.entity';
 
 export class CreateChapterDto {
   @ApiProperty({
@@ -58,30 +60,32 @@ export class CreateChapterDto {
   @ApiProperty({
     description: '章节状态',
     example: 'published',
-    enum: ['draft', 'published', 'archived'],
-    default: 'draft',
+    enum: ChapterStatus,
+    default: ChapterStatus.DRAFT,
   })
   @IsOptional()
-  @IsString({ message: '章节状态必须是字符串' })
-  status?: string = 'draft';
+  @IsEnum(ChapterStatus, { message: '章节状态必须是有效的枚举值' })
+  status?: ChapterStatus = ChapterStatus.DRAFT;
 
   @ApiProperty({
     description: '创建时间',
     example: '2024-01-01T00:00:00.000Z',
     format: 'date-time',
+    required: false,
   })
-  @IsNotEmpty({ message: '创建时间不能为空' })
+  @IsOptional()
   @IsDateString({}, { message: '创建时间格式不正确' })
-  createdAt: string;
+  createdAt?: string;
 
   @ApiProperty({
     description: '修改时间',
     example: '2024-01-01T00:00:00.000Z',
     format: 'date-time',
+    required: false,
   })
-  @IsNotEmpty({ message: '修改时间不能为空' })
+  @IsOptional()
   @IsDateString({}, { message: '修改时间格式不正确' })
-  updatedAt: string;
+  updatedAt?: string;
 
   @ApiProperty({
     description: '修改人',

+ 42 - 21
LMS-NodeJs/src/content/content.controller.ts

@@ -19,7 +19,10 @@ export class ContentController {
   constructor(private readonly contentService: ContentService) {}
 
   @Post()
-  @ApiOperation({ summary: '创建内容', description: '创建新的内容记录,自动生成新版本' })
+  @ApiOperation({
+    summary: '创建内容',
+    description: '创建新的内容记录,自动生成新版本',
+  })
   @ApiResponse({ status: 201, description: '内容创建成功' })
   @ApiResponse({ status: 400, description: '请求参数错误' })
   create(@Body() createContentDto: CreateContentDto) {
@@ -34,11 +37,15 @@ export class ContentController {
   }
 
   @Get('chapter/:chapterId')
-  @ApiOperation({ 
-    summary: '获取章节最新内容', 
-    description: '根据章节ID获取最新版本的内容' 
+  @ApiOperation({
+    summary: '获取章节最新内容',
+    description: '根据章节ID获取最新版本的内容',
+  })
+  @ApiParam({
+    name: 'chapterId',
+    description: '章节ID',
+    example: 'book_1_20240101120000_abc123',
   })
-  @ApiParam({ name: 'chapterId', description: '章节ID', example: 'book_1_20240101120000_abc123' })
   @ApiResponse({ status: 200, description: '获取成功' })
   @ApiResponse({ status: 404, description: '内容不存在' })
   findByChapterId(@Param('chapterId') chapterId: string) {
@@ -46,39 +53,53 @@ export class ContentController {
   }
 
   @Get('chapter/:chapterId/versions')
-  @ApiOperation({ 
-    summary: '获取章节所有版本', 
-    description: '根据章节ID获取所有版本的内容' 
+  @ApiOperation({
+    summary: '获取章节所有版本',
+    description: '根据章节ID获取所有版本的内容',
+  })
+  @ApiParam({
+    name: 'chapterId',
+    description: '章节ID',
+    example: 'book_1_20240101120000_abc123',
   })
-  @ApiParam({ name: 'chapterId', description: '章节ID', example: 'book_1_20240101120000_abc123' })
   @ApiResponse({ status: 200, description: '获取成功' })
   findVersionsByChapterId(@Param('chapterId') chapterId: string) {
     return this.contentService.findVersionsByChapterId(chapterId);
   }
 
   @Get('chapter/:chapterId/version/:version')
-  @ApiOperation({ 
-    summary: '获取指定版本内容', 
-    description: '根据章节ID和版本号获取指定版本的内容' 
+  @ApiOperation({
+    summary: '获取指定版本内容',
+    description: '根据章节ID和版本号获取指定版本的内容',
+  })
+  @ApiParam({
+    name: 'chapterId',
+    description: '章节ID',
+    example: 'book_1_20240101120000_abc123',
   })
-  @ApiParam({ name: 'chapterId', description: '章节ID', example: 'book_1_20240101120000_abc123' })
   @ApiParam({ name: 'version', description: '版本号', example: 1 })
   @ApiResponse({ status: 200, description: '获取成功' })
   @ApiResponse({ status: 404, description: '内容不存在' })
-  findByChapterIdAndVersion(@Param('chapterId') chapterId: string, @Param('version') version: string) {
+  findByChapterIdAndVersion(
+    @Param('chapterId') chapterId: string,
+    @Param('version') version: string,
+  ) {
     return this.contentService.findByChapterIdAndVersion(chapterId, +version);
   }
 
   @Patch(':id/status/:status')
-  @ApiOperation({ 
-    summary: '更新内容状态', 
-    description: '更新内容的发布状态' 
+  @ApiOperation({
+    summary: '更新内容状态',
+    description: '更新内容的发布状态',
   })
   @ApiParam({ name: 'id', description: '内容ID', example: 1 })
   @ApiParam({ name: 'status', description: '内容状态', example: 'published' })
   @ApiResponse({ status: 200, description: '状态更新成功' })
   @ApiResponse({ status: 404, description: '内容不存在' })
-  updateStatus(@Param('id') id: string, @Param('status') status: ContentStatus) {
+  updateStatus(
+    @Param('id') id: string,
+    @Param('status') status: ContentStatus,
+  ) {
     return this.contentService.updateStatus(+id, status);
   }
 
@@ -95,9 +116,9 @@ export class ContentController {
   }
 
   @Patch(':id')
-  @ApiOperation({ 
-    summary: '更新内容', 
-    description: '根据ID更新内容,会创建新版本而不是修改现有版本' 
+  @ApiOperation({
+    summary: '更新内容',
+    description: '根据ID更新内容,会创建新版本而不是修改现有版本',
   })
   @ApiParam({ name: 'id', description: '内容ID', example: 1 })
   @ApiResponse({ status: 200, description: '更新成功' })

+ 9 - 3
LMS-NodeJs/src/content/content.service.ts

@@ -21,11 +21,15 @@ export class ContentService {
       where: { chapterId: createContentDto.chapterId },
     });
     if (!chapter) {
-      throw new NotFoundException(`章节ID为${createContentDto.chapterId}的章节不存在`);
+      throw new NotFoundException(
+        `章节ID为${createContentDto.chapterId}的章节不存在`,
+      );
     }
 
     // 获取当前章节的最新版本号
-    const latestVersion = await this.getLatestVersion(createContentDto.chapterId);
+    const latestVersion = await this.getLatestVersion(
+      createContentDto.chapterId,
+    );
     const newVersion = latestVersion + 1;
 
     const content = this.contentRepository.create(createContentDto);
@@ -108,7 +112,9 @@ export class ContentService {
     });
 
     if (!content) {
-      throw new NotFoundException(`章节ID为${chapterId}版本${version}的内容不存在`);
+      throw new NotFoundException(
+        `章节ID为${chapterId}版本${version}的内容不存在`,
+      );
     }
 
     return content;

+ 119 - 0
LMS-NodeJs/test-ollama.md

@@ -0,0 +1,119 @@
+# Ollama 集成测试指南
+
+## 概述
+在 `src/aide` 模块中已成功集成了 Ollama 调用功能,提供了以下 API 端点:
+
+## API 端点
+
+### 1. 与 Ollama 聊天
+**POST** `/aide/ollama/chat`
+
+请求体示例:
+```json
+{
+  "model": "llama2",
+  "messages": [
+    {
+      "role": "user",
+      "content": "你好,请介绍一下你自己"
+    }
+  ],
+  "stream": false,
+  "temperature": 0.7,
+  "max_tokens": 2048
+}
+```
+
+### 2. 使用 Ollama 生成文本
+**POST** `/aide/ollama/generate`
+
+请求体示例:
+```json
+{
+  "model": "llama2",
+  "prompt": "请写一首关于春天的诗",
+  "stream": false,
+  "temperature": 0.7,
+  "max_tokens": 2048
+}
+```
+
+### 3. 获取可用模型列表
+**GET** `/aide/ollama/models`
+
+### 4. 检查 Ollama 服务状态
+**GET** `/aide/ollama/health`
+
+## 环境配置
+
+可以通过环境变量配置 Ollama 服务地址:
+```bash
+export OLLAMA_BASE_URL=http://localhost:11434
+```
+
+默认地址为:`http://localhost:11434`
+
+## 使用前提
+
+1. 确保 Ollama 服务正在运行
+2. 确保已安装所需的模型(如 llama2)
+3. 启动 NestJS 应用
+
+## 测试步骤
+
+1. 启动应用:
+```bash
+pnpm run start:dev
+```
+
+2. 检查 Ollama 服务状态:
+```bash
+curl http://localhost:3000/aide/ollama/health
+```
+
+3. 获取可用模型:
+```bash
+curl http://localhost:3000/aide/ollama/models
+```
+
+4. 测试聊天功能:
+```bash
+curl -X POST http://localhost:3000/aide/ollama/chat \
+  -H "Content-Type: application/json" \
+  -d '{
+    "model": "llama2",
+    "messages": [
+      {
+        "role": "user",
+        "content": "你好,请介绍一下你自己"
+      }
+    ]
+  }'
+```
+
+5. 测试文本生成:
+```bash
+curl -X POST http://localhost:3000/aide/ollama/generate \
+  -H "Content-Type: application/json" \
+  -d '{
+    "model": "llama2",
+    "prompt": "请写一首关于春天的诗"
+  }'
+```
+
+## 错误处理
+
+- 如果 Ollama 服务未运行,会返回 503 错误
+- 如果模型不存在,会返回相应的错误信息
+- 所有错误都有详细的中文错误信息
+
+## 功能特性
+
+- ✅ 支持聊天对话
+- ✅ 支持文本生成
+- ✅ 支持获取模型列表
+- ✅ 支持健康检查
+- ✅ 完整的错误处理
+- ✅ Swagger API 文档
+- ✅ 类型安全
+- ✅ 可配置的服务地址