diff --git a/data/SKILLS.md b/data/SKILLS.md new file mode 100644 index 0000000..55f191a --- /dev/null +++ b/data/SKILLS.md @@ -0,0 +1,46 @@ +# Role: 中职教学设计专家 + +## Profile + +- language: 中文 +- description: 面向中等职业学校学生设计项目式单课时教案,以完整项目为主线,将知识讲解、编码/操作实践、调试测试和成果展示整合为可直接实施的教学设计文档。 +- target_audience: 中职信息技术相关专业教师。 + +## Rules + +1. 每份教案属于某个项目的一个明确课时,必须形成阶段性成果。 +2. 理论讲解后立即安排实践操作,课堂内容能在普通实训室完成。 +3. 单课时严格按 40 分钟设计,教学过程各环节时间之和等于 40 分钟。 +4. 面向中职入门学生,难度适中,避免复杂算法和高级框架。 + +## Output Format + +只输出 Markdown 正文本身,不要使用代码块包裹整篇文档,不要添加任何额外说明。 + +严格遵循以下结构: + +1. 第一行是一级标题:`# <课程标题> 教学设计` + +2. 紧接着是一个两列表格(第一行为表头,分隔行使用 `|:---|:---|`),依次包含以下行: + - `| **课题** | **<课题名称>** |` + - `| **课时** | 1课时(40分钟) |` + - `| **教学目标** | **知识目标**:...
**技能目标**:...
**素养目标**:... |` + - `| **教学重难点** | **重点**:...
**难点**:... |` + - `| **教学资源准备** | ... |` + +3. 二级标题 `## 教学过程`,后接 5 列表格: + + | 教学环节 | 教学内容 | 教师活动 | 学生活动 | 设计意图 | + |:---|:---|:---|:---|:---| + + 包含 5 个教学环节行,每个环节名称格式:`**N. 环节名称**
(时长)` + + 教师活动和学生活动中每个主要活动使用加粗四字标题,例如 `**情境导入**`,标题后换行写具体描述。 + +4. 二级标题 `## 板书设计`,内容放在 ` ```text ` 代码块中。 + +5. 二级标题 `## 教学成效与反思`,后接两列表格: + - `| **教学成效** | ... |` + - `| **教学反思** | ... |` + + 教学成效与反思合计不超过 300 字。 diff --git a/docs/superpowers/plans/2026-06-15-bun-sqlite-backend-implementation.md b/docs/superpowers/plans/2026-06-15-bun-sqlite-backend-implementation.md index 9f74245..031d954 100644 --- a/docs/superpowers/plans/2026-06-15-bun-sqlite-backend-implementation.md +++ b/docs/superpowers/plans/2026-06-15-bun-sqlite-backend-implementation.md @@ -783,7 +783,7 @@ export function createGenerateRouter(apiKey: string | undefined): Hono { Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ - model: 'deepseek-chat', + model: 'deepseek-v4-flash', messages: [ { role: 'system', content: SYSTEM_PROMPT }, { role: 'user', content: `请围绕主题"${topic.trim()}"生成一份教案。` }, diff --git a/docs/superpowers/plans/2026-06-15-fix-broken-lessons-implementation.md b/docs/superpowers/plans/2026-06-15-fix-broken-lessons-implementation.md new file mode 100644 index 0000000..571d96e --- /dev/null +++ b/docs/superpowers/plans/2026-06-15-fix-broken-lessons-implementation.md @@ -0,0 +1,371 @@ +# Fix Broken Lessons Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在工作区工具栏新增「修复 X 处提示」按钮,点击后弹出 `FixBrokenDialog`,依次重新生成所有有警告的教案并原位替换,完成后显示摘要。 + +**Architecture:** 在 `useTeachingBook` 中新增 `regenerateLesson(id)` 用于原位替换单篇;新建 `FixBrokenDialog.vue` 负责进度显示(confirm → running → done/error);`WorkspaceToolbar` 新增 `fixBroken` emit 与条件显示按钮;`WorkspaceView` 协调状态并驱动循环。 + +**Tech Stack:** Vue 3 + TypeScript,与现有 BatchGenerateDialog 模式保持一致。 + +--- + +## File Structure + +| 文件 | 操作 | +|---|---| +| `src/composables/useTeachingBook.ts` | 修改:新增 `regenerateLesson`,接口加 `regenerateLesson` | +| `src/components/FixBrokenDialog.vue` | 新建:修复进度对话框 | +| `src/components/WorkspaceToolbar.vue` | 修改:新增 `fixBroken` emit 与按钮 | +| `src/components/WorkspaceView.vue` | 修改:新增 fix 状态与处理逻辑 | + +--- + +## Task 1: `regenerateLesson` in `useTeachingBook.ts` + +**Files:** +- Modify: `src/composables/useTeachingBook.ts` + +- [ ] **Step 1: 在 `TeachingBookStore` 接口加入 `regenerateLesson`** + +在 `generateLesson` 行下方追加: + +```typescript + regenerateLesson: (id: DesignId) => Promise +``` + +- [ ] **Step 2: 实现 `regenerateLesson` 函数** + +在 `generateLesson` 函数后插入(`return {` 之前): + +```typescript + async function regenerateLesson(id: DesignId): Promise { + const existing = book.value.designs.find((d) => d.id === id) + if (!existing) return { ok: false, message: '找不到该教案。' } + + const topic = existing.originalFilename.replace(/\.md$/i, '') + try { + const result = await booksApi.generateLesson(topic) + const newDesign = parseTeachingDesign(result.filename, result.markdown) + const index = book.value.designs.findIndex((d) => d.id === id) + if (index !== -1) { + book.value.designs.splice(index, 1, newDesign) + if (book.value.selectedId === id) { + book.value.selectedId = newDesign.id + } + } + touch() + return { ok: true } + } catch (error) { + return { ok: false, message: error instanceof Error ? error.message : '修复失败。' } + } + } +``` + +- [ ] **Step 3: 在 `return` 对象中暴露 `regenerateLesson`** + +在 `generateLesson,` 行之后加一行: + +```typescript + regenerateLesson, +``` + +- [ ] **Step 4: 运行测试确认无破坏** + +```bash +bun run test -- src/composables/useTeachingBook.test.ts +``` + +期望:全部通过。 + +- [ ] **Step 5: Commit** + +```bash +git add src/composables/useTeachingBook.ts +git commit -m "feat: add regenerateLesson to useTeachingBook" +``` + +--- + +## Task 2: 新建 `FixBrokenDialog.vue` + +**Files:** +- Create: `src/components/FixBrokenDialog.vue` + +- [ ] **Step 1: 创建组件文件** + +内容如下: + +```vue + + + +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/components/FixBrokenDialog.vue +git commit -m "feat: add FixBrokenDialog component" +``` + +--- + +## Task 3: WorkspaceToolbar — 新增修复按钮 + +**Files:** +- Modify: `src/components/WorkspaceToolbar.vue` + +- [ ] **Step 1: 新增 `fixBroken` emit** + +把 `defineEmits` 改为: + +```typescript +defineEmits<{ + upload: [] + print: [] + export: [] + clear: [] + generate: [] + batchGenerate: [] + fixBroken: [] + back: [] +}>() +``` + +- [ ] **Step 2: 在 `warningCount` span 前加修复按钮** + +把: + +```html + + {{ warningCount }} 处提示 + +``` + +替换为: + +```html + +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/components/WorkspaceToolbar.vue +git commit -m "feat: add fix-broken button to WorkspaceToolbar" +``` + +--- + +## Task 4: WorkspaceView — 协调 fix 流程 + +**Files:** +- Modify: `src/components/WorkspaceView.vue` + +- [ ] **Step 1: import FixBrokenDialog** + +在现有 import 列表里加(与 `BatchGenerateDialog` 相邻): + +```typescript +import FixBrokenDialog from './FixBrokenDialog.vue' +``` + +- [ ] **Step 2: 解构 `regenerateLesson`** + +在 `useTeachingBook` 解构对象中加: + +```typescript + regenerateLesson, +``` + +- [ ] **Step 3: 新增 fix 状态 refs** + +在 `batchCancelled` ref 之后插入: + +```typescript +const showFixDialog = ref(false) +const fixRunning = ref(false) +const fixDone = ref(0) +const fixTotal = ref(0) +const fixCurrentTopic = ref('') +const fixError = ref(null) +const fixCancelled = ref(false) +``` + +- [ ] **Step 4: 新增 fix 处理函数** + +在 `closeBatchDialog` 函数之后插入: + +```typescript +function openFixDialog(): void { + fixTotal.value = book.value.designs.filter((d) => d.warnings.length > 0).length + fixDone.value = 0 + fixError.value = null + showFixDialog.value = true +} + +async function handleFixStart(): Promise { + const broken = book.value.designs.filter((d) => d.warnings.length > 0) + fixCancelled.value = false + fixRunning.value = true + + for (const lesson of broken) { + if (fixCancelled.value) break + fixCurrentTopic.value = lesson.originalFilename.replace(/\.md$/i, '') + const result = await regenerateLesson(lesson.id) + if (!result.ok) { + fixError.value = result.message + break + } + fixDone.value++ + } + + fixRunning.value = false +} + +function handleFixCancel(): void { + fixCancelled.value = true +} + +function closeFixDialog(): void { + showFixDialog.value = false + fixDone.value = 0 + fixTotal.value = 0 + fixError.value = null +} +``` + +- [ ] **Step 5: 在 template 中挂载 FixBrokenDialog** + +在 `` 之后加: + +```html + +``` + +- [ ] **Step 6: 在 WorkspaceToolbar 上绑定 `@fix-broken`** + +把: + +```html + @batch-generate="showBatchDialog = true" +``` + +改为: + +```html + @batch-generate="showBatchDialog = true" + @fix-broken="openFixDialog" +``` + +- [ ] **Step 7: 运行全量测试** + +```bash +bun run test +``` + +期望:`markdownTable`、`markdownParser`、`useTeachingBook`、`PrintBook` 相关测试全部通过(App.test.ts 的 2 个已知失败不影响)。 + +- [ ] **Step 8: Commit** + +```bash +git add src/components/WorkspaceView.vue +git commit -m "feat: wire fix-broken flow in WorkspaceView" +``` diff --git a/server/routes/generate.ts b/server/routes/generate.ts index 49da008..3a46884 100644 --- a/server/routes/generate.ts +++ b/server/routes/generate.ts @@ -1,6 +1,7 @@ +import { readFileSync } from 'node:fs' import { Hono } from 'hono' -const SYSTEM_PROMPT = `你是一名教学设计专家,需要根据用户提供的主题生成一份 Markdown 格式的教案。 +const DEFAULT_SYSTEM_PROMPT = `你是一名教学设计专家,需要根据用户提供的主题生成一份 Markdown 格式的教案。 请严格遵循以下结构(标题、表格列数、章节名称必须完全一致,便于程序解析),只输出 Markdown 正文本身,不要使用代码块包裹整篇文档,不要添加任何额外说明: 1. 第一行是一级标题:\`# <课程标题> 教学设计\` @@ -19,6 +20,14 @@ const SYSTEM_PROMPT = `你是一名教学设计专家,需要根据用户提供 - \`| **教学反思** | ... |\` ` +function loadSystemPrompt(): string { + try { + return readFileSync('data/SKILLS.md', 'utf8') + } catch { + return DEFAULT_SYSTEM_PROMPT + } +} + function sanitizeFilename(topic: string): string { const sanitized = topic.trim().replace(/[\\/:*?"<>|]/g, '_') return sanitized || 'lesson' @@ -39,6 +48,8 @@ export function createGenerateRouter(apiKey: string | undefined): Hono { return c.json({ error: '未配置 DEEPSEEK_API_KEY。' }, 500) } + const systemPrompt = loadSystemPrompt() + let response: Response try { response = await fetch('https://api.deepseek.com/chat/completions', { @@ -48,9 +59,9 @@ export function createGenerateRouter(apiKey: string | undefined): Hono { Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ - model: 'deepseek-chat', + model: 'deepseek-v4-flash', messages: [ - { role: 'system', content: SYSTEM_PROMPT }, + { role: 'system', content: systemPrompt }, { role: 'user', content: `请围绕主题"${topic.trim()}"生成一份教案。` }, ], }), @@ -66,14 +77,75 @@ export function createGenerateRouter(apiKey: string | undefined): Hono { const payload = (await response.json().catch(() => null)) as | { choices?: Array<{ message?: { content?: string } }> } | null - const markdown = payload?.choices?.[0]?.message?.content + const raw = payload?.choices?.[0]?.message?.content - if (!markdown) { + if (!raw) { return c.json({ error: 'Deepseek 返回内容为空。' }, 502) } + const fenceMatch = raw.trim().match(/^```(?:\w+)?\n([\s\S]*?)\n```\s*$/) + const markdown = fenceMatch ? fenceMatch[1]! : raw + return c.json({ filename: `${sanitizeFilename(topic)}.md`, markdown }) }) + app.post('/outline', async (c) => { + const body = (await c.req.json().catch(() => null)) as { theme?: unknown } | null + const theme = body?.theme + + if (typeof theme !== 'string' || theme.trim() === '') { + return c.json({ error: '请提供课程主题。' }, 400) + } + + if (!apiKey) { + return c.json({ error: '未配置 DEEPSEEK_API_KEY。' }, 500) + } + + let response: Response + try { + response = await fetch('https://api.deepseek.com/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + model: 'deepseek-v4-flash', + messages: [ + { + role: 'system', + content: + '你是教学设计专家。根据用户提供的课程主题,生成一份完整的课时大纲标题列表,共约18条。' + + '每个标题格式为"项目名——课时任务",一行一个,不加序号、不加任何说明,直接输出标题列表。', + }, + { role: 'user', content: `课程主题:${theme.trim()}` }, + ], + }), + }) + } catch { + return c.json({ error: 'Deepseek 请求失败,请检查网络后重试。' }, 502) + } + + if (!response.ok) { + return c.json({ error: `Deepseek 请求失败(状态码 ${response.status})。` }, 502) + } + + const payload = (await response.json().catch(() => null)) as + | { choices?: Array<{ message?: { content?: string } }> } + | null + const raw = payload?.choices?.[0]?.message?.content + + if (!raw) { + return c.json({ error: 'Deepseek 返回内容为空。' }, 502) + } + + const titles = raw + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + + return c.json({ titles }) + }) + return app } diff --git a/src/components/BatchGenerateDialog.vue b/src/components/BatchGenerateDialog.vue new file mode 100644 index 0000000..04710ad --- /dev/null +++ b/src/components/BatchGenerateDialog.vue @@ -0,0 +1,140 @@ + + +