import { z } from "zod"; const exerciseSchema = z.object({ day: z.number().int().min(1), name: z.string().min(1), category: z.string().min(1), duration: z.number().positive(), description: z.string().min(1), tips: z.string().min(1), sets: z.number().int().positive(), reps: z.number().int().positive(), }); const normalizedPlanSchema = z.object({ title: z.string().min(1), exercises: z.array(exerciseSchema).min(1), }); const normalizedAdjustedPlanSchema = normalizedPlanSchema.extend({ adjustmentNotes: z.string().min(1), }); type NormalizedExercise = z.infer; type NormalizedPlan = z.infer; type NormalizedAdjustedPlan = z.infer; const dayKeyPattern = /^day[_\s-]?(\d+)$/i; function extractTextContent(content: unknown) { if (typeof content === "string") { return content; } if (Array.isArray(content)) { const text = content .map(item => (item && typeof item === "object" && "text" in item ? String((item as { text?: unknown }).text ?? "") : "")) .join("") .trim(); return text.length > 0 ? text : null; } return null; } function parseJsonContent(content: unknown) { const text = extractTextContent(content); if (!text) { throw new Error("LLM did not return text content"); } try { return JSON.parse(text) as Record; } catch (error) { throw new Error(`LLM returned invalid JSON: ${error instanceof Error ? error.message : "unknown error"}`); } } function toPositiveNumber(value: unknown, fallback: number) { const parsed = typeof value === "number" ? value : Number(value); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } function toPositiveInteger(value: unknown, fallback: number) { const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } function inferCategory(...values: Array) { const text = values .filter((value): value is string => typeof value === "string") .join(" "); if (/(墙|wall)/i.test(text)) return "墙壁练习"; if (/(步|移动|footwork|shuffle|split step)/i.test(text)) return "脚步移动"; if (/(挥拍|shadow|正手|反手|发球|截击)/i.test(text)) return "影子挥拍"; return "体能训练"; } function normalizeExercise( day: number, exercise: Record, section?: Record ): NormalizedExercise { const name = typeof exercise.name === "string" && exercise.name.trim().length > 0 ? exercise.name.trim() : typeof section?.focus === "string" && section.focus.trim().length > 0 ? section.focus.trim() : `第${day}天训练项目`; const description = typeof exercise.description === "string" && exercise.description.trim().length > 0 ? exercise.description.trim() : typeof section?.focus === "string" && section.focus.trim().length > 0 ? section.focus.trim() : `${name}训练`; const tips = typeof exercise.tips === "string" && exercise.tips.trim().length > 0 ? exercise.tips.trim() : typeof section?.focus === "string" && section.focus.trim().length > 0 ? `重点关注:${section.focus.trim()}` : "保持动作稳定,注意训练节奏。"; return { day, name, category: inferCategory(exercise.category, name, description, section?.focus), duration: toPositiveNumber( exercise.duration ?? exercise.duration_minutes, toPositiveNumber(section?.duration_minutes, 10) ), description, tips, sets: toPositiveInteger(exercise.sets, 3), reps: toPositiveInteger(exercise.reps, 10), }; } function normalizeDayMapPlan( raw: Record, fallbackTitle: string ): NormalizedPlan { const exercises: NormalizedExercise[] = []; for (const [key, value] of Object.entries(raw)) { const match = key.match(dayKeyPattern); if (!match || !value || typeof value !== "object" || Array.isArray(value)) { continue; } const day = Number.parseInt(match[1] ?? "", 10); if (!Number.isFinite(day) || day <= 0) { continue; } const section = value as Record; const sectionExercises = Array.isArray(section.exercises) ? section.exercises.filter( (item): item is Record => Boolean(item) && typeof item === "object" && !Array.isArray(item) ) : []; for (const exercise of sectionExercises) { exercises.push(normalizeExercise(day, exercise, section)); } } return normalizedPlanSchema.parse({ title: typeof raw.title === "string" && raw.title.trim().length > 0 ? raw.title.trim() : fallbackTitle, exercises, }); } export function normalizeTrainingPlanResponse(params: { content: unknown; fallbackTitle: string; }): NormalizedPlan { const raw = parseJsonContent(params.content); if (Array.isArray(raw.exercises)) { return normalizedPlanSchema.parse(raw); } return normalizeDayMapPlan(raw, params.fallbackTitle); } export function normalizeAdjustedPlanResponse(params: { content: unknown; fallbackTitle: string; }): NormalizedAdjustedPlan { const raw = parseJsonContent(params.content); if (Array.isArray(raw.exercises)) { return normalizedAdjustedPlanSchema.parse({ ...raw, adjustmentNotes: typeof raw.adjustmentNotes === "string" && raw.adjustmentNotes.trim().length > 0 ? raw.adjustmentNotes.trim() : "已根据最近分析结果调整训练内容。", }); } const normalized = normalizeDayMapPlan(raw, params.fallbackTitle); return normalizedAdjustedPlanSchema.parse({ ...normalized, adjustmentNotes: typeof raw.adjustmentNotes === "string" && raw.adjustmentNotes.trim().length > 0 ? raw.adjustmentNotes.trim() : typeof raw.adjustment_notes === "string" && raw.adjustment_notes.trim().length > 0 ? raw.adjustment_notes.trim() : "已根据最近分析结果调整训练内容。", }); }