From 324e644ef68443f58194dd697dcc8e40c8e45d4b Mon Sep 17 00:00:00 2001 From: chyoo Date: Tue, 11 Aug 2026 14:21:21 +0900 Subject: [PATCH] fix: restrict NAV2 prebuilt artifacts to amd64 --- .../migration.sql | 13 +++++ .../migration.sql | 18 +++++++ scripts/reseed-base-nav2-curriculum.sql | 18 +++---- src/chat/chat.controller.ts | 4 +- src/chat/chat.service.ts | 50 +++++++++++++++---- src/chat/dto/cancel-active-ai-response.dto.ts | 12 +++++ src/chat/dto/create-ai-response.dto.ts | 5 +- src/chat/dto/create-chat-message.dto.ts | 5 +- src/chat/dto/list-chat-threads.dto.ts | 6 ++- 9 files changed, 109 insertions(+), 22 deletions(-) create mode 100644 prisma/migrations/20260810173000_fix_mkmini_prebuilt_artifact/migration.sql create mode 100644 prisma/migrations/20260811103000_limit_nav2_prebuilt_artifacts_to_amd64/migration.sql create mode 100644 src/chat/dto/cancel-active-ai-response.dto.ts diff --git a/prisma/migrations/20260810173000_fix_mkmini_prebuilt_artifact/migration.sql b/prisma/migrations/20260810173000_fix_mkmini_prebuilt_artifact/migration.sql new file mode 100644 index 0000000..f707316 --- /dev/null +++ b/prisma/migrations/20260810173000_fix_mkmini_prebuilt_artifact/migration.sql @@ -0,0 +1,13 @@ +UPDATE "course_week_modules" +SET "prebuilt_artifacts" = jsonb_build_array( + jsonb_build_object( + 'type', 'build-install', + 'fileName', 'lms-nav2-mkmini-build-install-amd64.tar.zst', + 'url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-mkmini/1.0.0/prebuilt/amd64/lms-nav2-mkmini-build-install-amd64.tar.zst', + 'checksumSha256', 'a11ac9e920fcbe5c5a54e086b72d453932ece32e0c7976858435c0a0ba73a8f9', + 'architecture', 'amd64', + 'required', false, + 'extractPath', '.' + ) +) +WHERE "folder_name" = '[lms-nav2-mkmini]nav2-mkmini_v1.0.0_ws'; diff --git a/prisma/migrations/20260811103000_limit_nav2_prebuilt_artifacts_to_amd64/migration.sql b/prisma/migrations/20260811103000_limit_nav2_prebuilt_artifacts_to_amd64/migration.sql new file mode 100644 index 0000000..cae9a0a --- /dev/null +++ b/prisma/migrations/20260811103000_limit_nav2_prebuilt_artifacts_to_amd64/migration.sql @@ -0,0 +1,18 @@ +UPDATE course_week_modules AS module +SET prebuilt_artifacts = ( + SELECT jsonb_agg( + CASE + WHEN artifact->>'url' LIKE '%/studio-artifacts/modules/lms-nav2-%/1.0.0/prebuilt/%' + AND COALESCE(NULLIF(artifact->>'architecture', ''), '') = '' + THEN jsonb_set(artifact, '{architecture}', '"amd64"'::jsonb, true) + ELSE artifact + END + ) + FROM jsonb_array_elements(module.prebuilt_artifacts) AS artifact +) +WHERE EXISTS ( + SELECT 1 + FROM jsonb_array_elements(module.prebuilt_artifacts) AS artifact + WHERE artifact->>'url' LIKE '%/studio-artifacts/modules/lms-nav2-%/1.0.0/prebuilt/%' + AND COALESCE(NULLIF(artifact->>'architecture', ''), '') = '' +); diff --git a/scripts/reseed-base-nav2-curriculum.sql b/scripts/reseed-base-nav2-curriculum.sql index a841815..2f899e7 100644 --- a/scripts/reseed-base-nav2-curriculum.sql +++ b/scripts/reseed-base-nav2-curriculum.sql @@ -153,23 +153,23 @@ WHERE w.course_id IN (SELECT id FROM target_course) AND w.week = 2; WITH module_definitions(category, name, folder_name, template_repository_url, prebuilt_artifacts, default_parameters, docker_image) AS ( VALUES ('Simulator', 'NAV2-Gazebo', '[lms-nav2-gazebo-simulator]nav2-simulator_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-simulator.git', - jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-simulator/1.0.0/prebuilt/latest/lms-nav2-gazebo-simulator-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-simulator-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('gui:=false'), 'future-nav2-gazebo:humble'), + jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-simulator/1.0.0/prebuilt/latest/lms-nav2-gazebo-simulator-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-simulator-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('gui:=false'), 'future-nav2-gazebo:humble'), ('Vehicle', 'NAV2-TurtleBot3Burger', '[lms-nav2-gazebo-vehicle]nav2-vehicle_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-vehicle.git', - jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-vehicle/1.0.0/prebuilt/latest/lms-nav2-gazebo-vehicle-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-vehicle-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'platform:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'), + jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-vehicle/1.0.0/prebuilt/latest/lms-nav2-gazebo-vehicle-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-vehicle-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'platform:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'), ('Map', 'NAV2-Map', '[lms-nav2-gazebo-map]nav2-map_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-map.git', - jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-map/1.0.0/prebuilt/latest/lms-nav2-gazebo-map-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-map-build-install.tar.zst', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'), + jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-map/1.0.0/prebuilt/latest/lms-nav2-gazebo-map-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-map-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'), ('Localization', 'NAV2-SLAM', '[lms-nav2-gazebo-slam]nav2-slam_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-slam.git', - jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-slam/1.0.0/prebuilt/latest/lms-nav2-gazebo-slam-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-slam-build-install.tar.zst', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'), + jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-slam/1.0.0/prebuilt/latest/lms-nav2-gazebo-slam-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-slam-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'), ('Localization', 'NAV2-Localization', '[lms-nav2-gazebo-localization]nav2-localization_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-localization.git', - jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-localization/1.0.0/prebuilt/latest/lms-nav2-gazebo-localization-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-localization-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('localization_mode:=slam'), 'future-nav2-gazebo:humble'), + jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-localization/1.0.0/prebuilt/latest/lms-nav2-gazebo-localization-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-localization-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('localization_mode:=slam'), 'future-nav2-gazebo:humble'), ('Perception', 'NAV2-Perception', '[lms-nav2-gazebo-perception]nav2-perception_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-perception.git', - jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-perception/1.0.0/prebuilt/latest/lms-nav2-gazebo-perception-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-perception-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true'), 'future-nav2-gazebo:humble'), + jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-perception/1.0.0/prebuilt/latest/lms-nav2-gazebo-perception-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-perception-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true'), 'future-nav2-gazebo:humble'), ('Planning', 'NAV2-Planning', '[lms-nav2-gazebo-planning]nav2-planning_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-planning.git', - jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-planning/1.0.0/prebuilt/latest/lms-nav2-gazebo-planning-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-planning-build-install.tar.zst', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'), + jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-planning/1.0.0/prebuilt/latest/lms-nav2-gazebo-planning-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-planning-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'), ('Control', 'NAV2-Control', '[lms-nav2-gazebo-control]nav2-control_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-control.git', - jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-control/1.0.0/prebuilt/latest/lms-nav2-gazebo-control-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-control-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'robot:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'), + jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-control/1.0.0/prebuilt/latest/lms-nav2-gazebo-control-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-control-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'robot:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'), ('Vehicle', 'NAV2-MKMini', '[lms-nav2-mkmini]nav2-mkmini_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-mkmini.git', - jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-mkmini/1.0.0/prebuilt/amd64/lms-nav2-mkmini-build-install-amd64.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-mkmini-build-install-amd64.tar.zst', 'checksumSha256', '8b36d3fda6a81a49da140529d9874e99b734d05e01e12a49e719e2b26f8803d9', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=false', 'platform:=mkmini', 'robot:=mkmini', 'level:=2d'), 'future-nav2:humble-jetson_orin') + jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-mkmini/1.0.0/prebuilt/amd64/lms-nav2-mkmini-build-install-amd64.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-mkmini-build-install-amd64.tar.zst', 'checksumSha256', 'a11ac9e920fcbe5c5a54e086b72d453932ece32e0c7976858435c0a0ba73a8f9', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=false', 'platform:=mkmini', 'robot:=mkmini', 'level:=2d'), 'future-nav2:humble-jetson_orin') ) INSERT INTO course_week_modules (week_id, sort_order, category, name, folder_name, template_repository_url, source_type, prebuilt_artifacts, default_parameters, docker_image) SELECT w.id, row_number() OVER (PARTITION BY w.id ORDER BY module_definitions.folder_name), category, name, folder_name, template_repository_url, 'GIT', prebuilt_artifacts, default_parameters, docker_image diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts index eddc7bf..cac823a 100644 --- a/src/chat/chat.controller.ts +++ b/src/chat/chat.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Delete, Get, Param, Post, Query, Req } from '@nestjs/ import { Request } from 'express'; import { ChatService } from './chat.service'; +import { CancelActiveAiResponseDto } from './dto/cancel-active-ai-response.dto'; import { CreateAiResponseDto } from './dto/create-ai-response.dto'; import { CreateChatMessageDto } from './dto/create-chat-message.dto'; import { CreateInstructorChatMessageDto } from './dto/create-instructor-chat-message.dto'; @@ -64,10 +65,11 @@ export class ChatController { @Post('ai/cancel-active') cancelActiveAiResponse( @Req() request: Request, - @Body() body: { week?: number }, + @Body() body: CancelActiveAiResponseDto, ) { return this.chatService.cancelActiveAiResponse( this.getBearerToken(request), + body.courseId, body.week, ); } diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index eaf4322..9eafe0f 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -57,6 +57,7 @@ type FutureDriveLlmChatResponse = { const STUDENT_CHAT_VISIBLE_MESSAGE_LIMIT = 20; const AI_CHAT_CONTEXT_MESSAGE_LIMIT = 6; +const PUBLIC_GUIDE_COURSE_SLUG = 'autonomy-studio-lms-guide'; @Injectable() export class ChatService { @@ -79,7 +80,7 @@ export class ChatService { async listThreads(accessToken: string | undefined, query: ListChatThreadsDto) { const student = await this.getLmsUser(accessToken); - const course = await this.getActiveCourse(); + const course = await this.getStudentCourse(student.id, query.courseId); const week = query.week ? await this.getCourseWeek(course.id, query.week) : null; @@ -122,7 +123,7 @@ export class ChatService { async clearStudentThreads(accessToken: string | undefined, query: ListChatThreadsDto) { const student = await this.getLmsUser(accessToken); - const course = await this.getActiveCourse(); + const course = await this.getStudentCourse(student.id, query.courseId); const week = query.week ? await this.getCourseWeek(course.id, query.week) : null; @@ -167,7 +168,7 @@ export class ChatService { dto: CreateChatMessageDto, ) { const student = await this.getLmsUser(accessToken); - const course = await this.getActiveCourse(); + const course = await this.getStudentCourse(student.id, dto.courseId); const week = dto.week ? await this.getCourseWeek(course.id, dto.week) : null; const thread = await this.getOrCreateThread({ courseId: course.id, @@ -220,7 +221,7 @@ export class ChatService { dto: CreateAiResponseDto, ) { const student = await this.getLmsUser(accessToken); - const course = await this.getActiveCourse(); + const course = await this.getStudentCourse(student.id, dto.courseId); const week = dto.week ? await this.getCourseWeek(course.id, dto.week) : null; const thread = await this.getOrCreateThread({ courseId: course.id, @@ -425,10 +426,11 @@ export class ChatService { async cancelActiveAiResponse( accessToken: string | undefined, + courseId: string, weekNumber?: number, ) { const student = await this.getLmsUser(accessToken); - const course = await this.getActiveCourse(); + const course = await this.getStudentCourse(student.id, courseId); const week = weekNumber ? await this.getCourseWeek(course.id, weekNumber) : null; const message = await this.prisma.chatMessage.findFirst({ where: { @@ -487,7 +489,7 @@ export class ChatService { query: ListChatThreadsDto, ) { const instructor = await this.getInstructorLmsUser(accessToken); - const course = await this.getActiveCourse(); + const course = await this.getInstructorCourse(instructor.id, query.courseId); const week = query.week ? await this.getCourseWeek(course.id, query.week) : null; @@ -680,11 +682,41 @@ export class ChatService { }; } - private async getActiveCourse() { + private async getStudentCourse(studentId: string, courseId?: string) { + if (!courseId) throw new NotFoundException('Course not found'); + const course = await this.prisma.course.findFirst({ - where: { slug: 'futuredrive-autonomy-basic', isActive: true }, + where: { + id: courseId, + isActive: true, + OR: [ + { slug: PUBLIC_GUIDE_COURSE_SLUG }, + { + enrollments: { + some: { + userId: studentId, + status: 'ACTIVE', + }, + }, + }, + ], + }, }); - if (!course) throw new NotFoundException('Curriculum course not found'); + if (!course) throw new NotFoundException('Course not found'); + return course; + } + + private async getInstructorCourse(instructorId: string, courseId?: string) { + if (!courseId) throw new NotFoundException('Course not found'); + + const course = await this.prisma.course.findFirst({ + where: { + id: courseId, + isActive: true, + teacherId: instructorId, + }, + }); + if (!course) throw new NotFoundException('Course not found'); return course; } diff --git a/src/chat/dto/cancel-active-ai-response.dto.ts b/src/chat/dto/cancel-active-ai-response.dto.ts new file mode 100644 index 0000000..b97885f --- /dev/null +++ b/src/chat/dto/cancel-active-ai-response.dto.ts @@ -0,0 +1,12 @@ +import { IsInt, IsOptional, IsUUID, Max, Min } from 'class-validator'; + +export class CancelActiveAiResponseDto { + @IsUUID() + courseId: string; + + @IsOptional() + @IsInt() + @Min(1) + @Max(100) + week?: number; +} diff --git a/src/chat/dto/create-ai-response.dto.ts b/src/chat/dto/create-ai-response.dto.ts index 3b57ec6..2f8dae8 100644 --- a/src/chat/dto/create-ai-response.dto.ts +++ b/src/chat/dto/create-ai-response.dto.ts @@ -1,6 +1,9 @@ -import { IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; +import { IsInt, IsOptional, IsString, IsUUID, Max, Min } from 'class-validator'; export class CreateAiResponseDto { + @IsUUID() + courseId: string; + @IsString() content: string; diff --git a/src/chat/dto/create-chat-message.dto.ts b/src/chat/dto/create-chat-message.dto.ts index 05c2906..f97c9e1 100644 --- a/src/chat/dto/create-chat-message.dto.ts +++ b/src/chat/dto/create-chat-message.dto.ts @@ -1,6 +1,9 @@ -import { IsIn, IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; +import { IsIn, IsInt, IsOptional, IsString, IsUUID, Max, Min } from 'class-validator'; export class CreateChatMessageDto { + @IsUUID() + courseId: string; + @IsIn(['ai', 'teacher']) mode: 'ai' | 'teacher'; diff --git a/src/chat/dto/list-chat-threads.dto.ts b/src/chat/dto/list-chat-threads.dto.ts index 4b61dae..8789966 100644 --- a/src/chat/dto/list-chat-threads.dto.ts +++ b/src/chat/dto/list-chat-threads.dto.ts @@ -1,7 +1,11 @@ import { Transform } from 'class-transformer'; -import { IsBoolean, IsIn, IsInt, IsOptional, Max, Min } from 'class-validator'; +import { IsBoolean, IsIn, IsInt, IsOptional, IsUUID, Max, Min } from 'class-validator'; export class ListChatThreadsDto { + @IsOptional() + @IsUUID() + courseId?: string; + @IsOptional() @IsIn(['ai', 'teacher']) mode?: 'ai' | 'teacher';