From b18bde8d2a637658f133cb54edc079d6b38d01c3 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Wed, 24 Jun 2026 00:37:19 -0600 Subject: [PATCH] update --- server/routes/generate.test.ts | 62 ++++++++++++++++++++++++++ server/routes/generate.ts | 9 +++- src/components/BatchGenerateDialog.vue | 8 +++- src/services/booksApi.ts | 7 ++- src/style.css | 14 ++++++ 5 files changed, 95 insertions(+), 5 deletions(-) diff --git a/server/routes/generate.test.ts b/server/routes/generate.test.ts index 4b63993..14a0bdb 100644 --- a/server/routes/generate.test.ts +++ b/server/routes/generate.test.ts @@ -86,6 +86,68 @@ describe('generate route', () => { expect(res.status).toBe(502) }) + it('returns 400 when theme is missing on outline', async () => { + const app = new Hono().route('/api/generate', createGenerateRouter('test-key')) + + const res = await app.request('/api/generate/outline', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({}), + }) + + expect(res.status).toBe(400) + }) + + it('uses the requested count in the outline prompt', async () => { + let sentBody = '' + globalThis.fetch = mock(async (_url: unknown, init: { body?: string } = {}) => { + sentBody = init.body ?? '' + return new Response( + JSON.stringify({ choices: [{ message: { content: 'A\nB\nC' } }] }), + { status: 200 }, + ) + }) as unknown as typeof fetch + + const app = new Hono().route('/api/generate', createGenerateRouter('test-key')) + const res = await app.request('/api/generate/outline', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ theme: 'Web 前端', count: 25 }), + }) + + expect(res.status).toBe(200) + expect(sentBody).toContain('共约25条') + const body = (await res.json()) as { titles: string[] } + expect(body.titles).toEqual(['A', 'B', 'C']) + }) + + it('defaults the outline count to 18 and clamps out-of-range values', async () => { + const captured: string[] = [] + globalThis.fetch = mock(async (_url: unknown, init: { body?: string } = {}) => { + captured.push(init.body ?? '') + return new Response( + JSON.stringify({ choices: [{ message: { content: 'A' } }] }), + { status: 200 }, + ) + }) as unknown as typeof fetch + + const app = new Hono().route('/api/generate', createGenerateRouter('test-key')) + + await app.request('/api/generate/outline', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ theme: 'Web 前端' }), + }) + await app.request('/api/generate/outline', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ theme: 'Web 前端', count: 999 }), + }) + + expect(captured[0]).toContain('共约18条') + expect(captured[1]).toContain('共约50条') + }) + it('returns 502 when Deepseek response has no content', async () => { globalThis.fetch = mock(async () => new Response(JSON.stringify({ choices: [] }), { status: 200 })) as unknown as typeof fetch diff --git a/server/routes/generate.ts b/server/routes/generate.ts index 3a46884..f515c5a 100644 --- a/server/routes/generate.ts +++ b/server/routes/generate.ts @@ -90,13 +90,18 @@ export function createGenerateRouter(apiKey: string | undefined): Hono { }) app.post('/outline', async (c) => { - const body = (await c.req.json().catch(() => null)) as { theme?: unknown } | null + const body = (await c.req.json().catch(() => null)) as + | { theme?: unknown; count?: unknown } + | null const theme = body?.theme if (typeof theme !== 'string' || theme.trim() === '') { return c.json({ error: '请提供课程主题。' }, 400) } + const rawCount = typeof body?.count === 'number' ? body.count : 18 + const count = Math.min(50, Math.max(1, Math.round(rawCount))) + if (!apiKey) { return c.json({ error: '未配置 DEEPSEEK_API_KEY。' }, 500) } @@ -115,7 +120,7 @@ export function createGenerateRouter(apiKey: string | undefined): Hono { { role: 'system', content: - '你是教学设计专家。根据用户提供的课程主题,生成一份完整的课时大纲标题列表,共约18条。' + + `你是教学设计专家。根据用户提供的课程主题,生成一份完整的课时大纲标题列表,共约${count}条。` + '每个标题格式为"项目名——课时任务",一行一个,不加序号、不加任何说明,直接输出标题列表。', }, { role: 'user', content: `课程主题:${theme.trim()}` }, diff --git a/src/components/BatchGenerateDialog.vue b/src/components/BatchGenerateDialog.vue index 5ce7ae2..f666d98 100644 --- a/src/components/BatchGenerateDialog.vue +++ b/src/components/BatchGenerateDialog.vue @@ -21,6 +21,7 @@ const emit = defineEmits<{ const phase = ref('theme') const theme = ref(props.defaultTheme ?? '') +const count = ref(18) const outlineText = ref('') const outlineError = ref(null) @@ -45,7 +46,7 @@ async function handleGenerateOutline(): Promise { phase.value = 'outline-loading' outlineError.value = null try { - const result = await booksApi.generateOutline(theme.value.trim()) + const result = await booksApi.generateOutline(theme.value.trim(), count.value) outlineText.value = result.titles.join('\n') phase.value = 'outline' } catch (error) { @@ -63,6 +64,7 @@ function handleStart(): void { function handleClose(): void { phase.value = 'theme' theme.value = props.defaultTheme ?? '' + count.value = 18 outlineText.value = '' outlineError.value = null emit('close') @@ -84,6 +86,10 @@ function handleClose(): void { placeholder="例如:Web 前端开发项目式教学" @keydown.enter="handleGenerateOutline" /> +
diff --git a/src/services/booksApi.ts b/src/services/booksApi.ts index c6f4478..5e2f51a 100644 --- a/src/services/booksApi.ts +++ b/src/services/booksApi.ts @@ -55,6 +55,9 @@ export function generateLesson(topic: string, signal?: AbortSignal): Promise { - return authedFetch('/api/generate/outline', { method: 'POST', body: JSON.stringify({ theme }) }) +export function generateOutline(theme: string, count = 18): Promise<{ titles: string[] }> { + return authedFetch('/api/generate/outline', { + method: 'POST', + body: JSON.stringify({ theme, count }), + }) } diff --git a/src/style.css b/src/style.css index 9a023b4..87f2dc6 100644 --- a/src/style.css +++ b/src/style.css @@ -849,6 +849,20 @@ table { border-color: var(--green-600); } +.batch-count-field { + display: flex; + align-items: center; + gap: 10px; + margin-top: 12px; + font-size: 14px; + color: var(--muted); +} + +.batch-count-field input { + flex: 0 0 auto; + width: 80px; +} + .batch-topics-count { font-size: 13px; color: var(--muted);