{"repo": "2150489918xf-a11y/ai-edu", "files": {"server/services/aiOutlineService.js": {"32ce596ff32068c7fa6561400b4f02851ea37a24": {"t": 1785746677.6746664, "n": 3895, "h": "a7837b258c0511be", "c": "function createHttpError(statusCode, code, message, details = {}) {\n const error = new Error(message);\n error.statusCode = statusCode;\n error.code = code;\n error.details = details;\n return error;\n}\n\nfunction stripMarkdownFences(value) {\n return String(value || '')\n .replace(/^```(?:json|markdown|md)?\\s*/i, '')\n .replace(/```\\s*$/i, '')\n .trim();\n}\n\nfunction normalizeMessages(messages = []) {\n return Array.isArray(messages)\n ? messages\n .filter((message) => ['user', 'assistant', 'teacher', 'ai'].includes(message.role) && (message.content || message.text))\n .map((message) => ({\n role: ['assistant', 'ai'].includes(message.role) ? 'assistant' : 'user',\n content: String(message.content || message.text)\n }))\n : [];\n}\n\nfunction buildSystemPrompt() {\n return [\n 'You are an AI assistant for high-school teachers creating and revising lesson outlines.',\n 'Respond in Simplified Chinese.',\n 'You may answer naturally, explain your reasoning, or ask for missing information.',\n 'When the teacher asks you to generate, rewrite, supplement, or revise the outline, append exactly one structured block after the natural-language reply.',\n 'The block format is:',\n ':::outline-json',\n '{ ...complete JSON... }',\n ':::',\n 'Do not wrap the JSON block in Markdown fences.',\n 'The JSON root must contain version, tags, and sections.',\n 'tags may be strings or objects like { \"text\": \"力与加速度\", \"tone\": \"success\" }.',\n 'sections must contain 3 to 6 teaching sections unless the teacher explicitly asks otherwise.',\n 'Each section must contain id, phase, time, title, status, active, and cards.',\n 'Each cards item must be { \"label\": \"教师动作\", \"content\": \"具体内容\", \"tone\": \"default\" }.',\n 'tone must be one of default, focus, warning, success, muted.',\n 'At most one section may have active=true.',\n 'Keep the outline consistent with grade, subject, duration, and teaching goal.'\n ].join('\\n');\n}\n\nfunction buildUserPrompt({ course = {}, currentOutline = null }) {\n return [\n `课程标题: ${course.title || course.shortTitle || '当前课程'}`,\n `学段: ${course.grade || '未提供'}`,\n `学科: ${course.subject || '未提供'}`,\n `课时时长: ${course.duration || '未提供'}`,\n `教学目标: ${course.goal || '未提供'}`,\n '当前结构化大纲 JSON:',\n JSON.stringify(currentOutline || {}, null, 2),\n '请结合后续完整会话处理老师请求。'\n ].join('\\n');\n}\n\nexport function createAiOutlineService({ env = process.env, fetchImpl = globalThis.fetch } = {}) {\n if (!fetchImpl) {\n throw new Error('fetch implementation is required');\n }\n\n return {\n async generateOutlineReply(request = {}) {\n if (!env.DEEPSEEK_API_KEY) {\n throw createHttpError(500, 'AI_CREDENTIALS_MISSING', 'Missing DEEPSEEK_API_KEY');\n }\n\n const model = env.DEEPSEEK_MODEL || env.AI_MODEL || 'deepseek-chat';\n const baseUrl = env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com';\n const response = await fetchImpl(`${baseUrl.replace(/\\/$/, '')}/chat/completions`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${env.DEEPSEEK_API_KEY}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n model,\n messages: [\n { role: 'system', content: buildSystemPrompt() },\n { role: 'user', content: buildUserPrompt(request) },\n ...normalizeMessages(request.messages)\n ],\n temperature: 0.2\n })\n });\n\n const payload = await response.json().catch(() => ({}));\n if (!response.ok) {\n throw createHttpError(\n response.status,\n 'AI_PROVIDER_ERROR',\n payload.error?.message || 'DeepSeek request failed'\n );\n }\n\n return {\n content: stripMarkdownFences(payload.choices?.[0]?.message?.content || ''),\n provider: 'deepseek',\n model\n };\n }\n };\n}\n"}, "a06b53439145542bf46d451391237f8146b87252": {"t": 1785766760.159095, "n": 3895, "h": "a7837b258c0511be", "c": "function createHttpError(statusCode, code, message, details = {}) {\n const error = new Error(message);\n error.statusCode = statusCode;\n error.code = code;\n error.details = details;\n return error;\n}\n\nfunction stripMarkdownFences(value) {\n return String(value || '')\n .replace(/^```(?:json|markdown|md)?\\s*/i, '')\n .replace(/```\\s*$/i, '')\n .trim();\n}\n\nfunction normalizeMessages(messages = []) {\n return Array.isArray(messages)\n ? messages\n .filter((message) => ['user', 'assistant', 'teacher', 'ai'].includes(message.role) && (message.content || message.text))\n .map((message) => ({\n role: ['assistant', 'ai'].includes(message.role) ? 'assistant' : 'user',\n content: String(message.content || message.text)\n }))\n : [];\n}\n\nfunction buildSystemPrompt() {\n return [\n 'You are an AI assistant for high-school teachers creating and revising lesson outlines.',\n 'Respond in Simplified Chinese.',\n 'You may answer naturally, explain your reasoning, or ask for missing information.',\n 'When the teacher asks you to generate, rewrite, supplement, or revise the outline, append exactly one structured block after the natural-language reply.',\n 'The block format is:',\n ':::outline-json',\n '{ ...complete JSON... }',\n ':::',\n 'Do not wrap the JSON block in Markdown fences.',\n 'The JSON root must contain version, tags, and sections.',\n 'tags may be strings or objects like { \"text\": \"力与加速度\", \"tone\": \"success\" }.',\n 'sections must contain 3 to 6 teaching sections unless the teacher explicitly asks otherwise.',\n 'Each section must contain id, phase, time, title, status, active, and cards.',\n 'Each cards item must be { \"label\": \"教师动作\", \"content\": \"具体内容\", \"tone\": \"default\" }.',\n 'tone must be one of default, focus, warning, success, muted.',\n 'At most one section may have active=true.',\n 'Keep the outline consistent with grade, subject, duration, and teaching goal.'\n ].join('\\n');\n}\n\nfunction buildUserPrompt({ course = {}, currentOutline = null }) {\n return [\n `课程标题: ${course.title || course.shortTitle || '当前课程'}`,\n `学段: ${course.grade || '未提供'}`,\n `学科: ${course.subject || '未提供'}`,\n `课时时长: ${course.duration || '未提供'}`,\n `教学目标: ${course.goal || '未提供'}`,\n '当前结构化大纲 JSON:',\n JSON.stringify(currentOutline || {}, null, 2),\n '请结合后续完整会话处理老师请求。'\n ].join('\\n');\n}\n\nexport function createAiOutlineService({ env = process.env, fetchImpl = globalThis.fetch } = {}) {\n if (!fetchImpl) {\n throw new Error('fetch implementation is required');\n }\n\n return {\n async generateOutlineReply(request = {}) {\n if (!env.DEEPSEEK_API_KEY) {\n throw createHttpError(500, 'AI_CREDENTIALS_MISSING', 'Missing DEEPSEEK_API_KEY');\n }\n\n const model = env.DEEPSEEK_MODEL || env.AI_MODEL || 'deepseek-chat';\n const baseUrl = env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com';\n const response = await fetchImpl(`${baseUrl.replace(/\\/$/, '')}/chat/completions`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${env.DEEPSEEK_API_KEY}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n model,\n messages: [\n { role: 'system', content: buildSystemPrompt() },\n { role: 'user', content: buildUserPrompt(request) },\n ...normalizeMessages(request.messages)\n ],\n temperature: 0.2\n })\n });\n\n const payload = await response.json().catch(() => ({}));\n if (!response.ok) {\n throw createHttpError(\n response.status,\n 'AI_PROVIDER_ERROR',\n payload.error?.message || 'DeepSeek request failed'\n );\n }\n\n return {\n content: stripMarkdownFences(payload.choices?.[0]?.message?.content || ''),\n provider: 'deepseek',\n model\n };\n }\n };\n}\n"}}}}