feat: CI/CD pipeline + Learning/Medicine/Travel services
Some checks failed
Build and Deploy GooSeek / build-backend (push) Failing after 1m4s
Build and Deploy GooSeek / build-webui (push) Failing after 1m2s
Build and Deploy GooSeek / deploy (push) Has been skipped

- Add Gitea Actions workflow for automated build & deploy
- Add K8s manifests: webui, travel-svc, medicine-svc, sandbox-svc
- Update kustomization for localhost:5000 registry
- Add ingress for gooseek.ru and api.gooseek.ru
- Learning cabinet with onboarding, courses, sandbox integration
- Medicine service with symptom analysis and doctor matching
- Travel service with itinerary planning
- Server setup scripts (NVIDIA/CUDA, K3s, Gitea runner)

Made-with: Cursor
This commit is contained in:
home
2026-03-02 20:25:44 +03:00
parent 08bd41e75c
commit ab48a0632b
92 changed files with 15562 additions and 2198 deletions

View File

@@ -0,0 +1,118 @@
package learning
import (
"context"
"encoding/json"
"fmt"
"github.com/gooseek/backend/internal/db"
"github.com/gooseek/backend/internal/llm"
)
type PersonalPlan struct {
Modules []PlanModule `json:"modules"`
TotalHours int `json:"total_hours"`
DifficultyAdjusted string `json:"difficulty_adjusted"`
PersonalizationNote string `json:"personalization_notes"`
MilestoneProject string `json:"milestone_project"`
}
type PlanModule struct {
Index int `json:"index"`
Title string `json:"title"`
Description string `json:"description"`
Skills []string `json:"skills"`
EstimatedHrs int `json:"estimated_hours"`
PracticeFocus string `json:"practice_focus"`
TaskCount int `json:"task_count"`
IsCheckpoint bool `json:"is_checkpoint"`
}
func BuildPersonalPlan(ctx context.Context, llmClient llm.Client, course *db.LearningCourse, profileJSON string) (json.RawMessage, error) {
profileInfo := "Профиль неизвестен — план по умолчанию."
if profileJSON != "" && profileJSON != "{}" {
profileInfo = "Профиль ученика:\n" + truncateStr(profileJSON, 2000)
}
outlineStr := string(course.BaseOutline)
if len(outlineStr) > 4000 {
outlineStr = outlineStr[:4000]
}
prompt := fmt.Sprintf(`Ты — ведущий методолог обучения в IT с 10-летним опытом. Адаптируй базовый план курса под конкретного ученика.
Курс: %s
Описание: %s
Сложность: %s
Длительность: %d часов
Базовый план:
%s
%s
ТРЕБОВАНИЯ:
1. Минимум теории, максимум боевой практики (как на реальных проектах в РФ)
2. Каждый модуль = конкретное практическое задание из реального проекта
3. Прогрессия: от простого к сложному, учитывая текущий уровень ученика
4. Каждый 3-й модуль — checkpoint (мини-проект для проверки навыков)
5. Финальный milestone project — полноценный проект для портфолио
6. Учитывай стек и опыт ученика — не повторяй то, что он уже знает
Ответь строго JSON:
{
"modules": [
{
"index": 0,
"title": "Название модуля",
"description": "Что изучаем и делаем",
"skills": ["навык1"],
"estimated_hours": 4,
"practice_focus": "Конкретная практическая задача из реального проекта",
"task_count": 3,
"is_checkpoint": false
}
],
"total_hours": 40,
"difficulty_adjusted": "intermediate",
"personalization_notes": "Как план адаптирован под ученика",
"milestone_project": "Описание финального проекта для портфолио"
}`, course.Title, course.ShortDescription, course.Difficulty, course.DurationHours, outlineStr, profileInfo)
var plan PersonalPlan
if err := generateAndParse(ctx, llmClient, prompt, &plan, 2); err != nil {
return course.BaseOutline, fmt.Errorf("plan generation failed, using base outline: %w", err)
}
if len(plan.Modules) == 0 {
return course.BaseOutline, nil
}
for i := range plan.Modules {
plan.Modules[i].Index = i
if plan.Modules[i].TaskCount == 0 {
plan.Modules[i].TaskCount = 2
}
}
if plan.TotalHours == 0 {
total := 0
for _, m := range plan.Modules {
total += m.EstimatedHrs
}
plan.TotalHours = total
}
result, err := json.Marshal(plan)
if err != nil {
return course.BaseOutline, err
}
return result, nil
}
func truncateStr(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}