feat: add concurrent batch lesson generation

This commit is contained in:
2026-06-16 07:33:29 -06:00
parent 2321b5f68e
commit 8b0e4df43d
3 changed files with 179 additions and 9 deletions

View File

@@ -19,6 +19,17 @@ export type LoadStatus = 'loading' | 'loaded' | 'error'
export type GenerateLessonResult = { ok: true } | { ok: false; message: string }
export type BatchGenerateLessonResult =
| { ok: true; completed: number }
| { ok: false; completed: number; message: string }
export interface BatchGenerateLessonOptions {
concurrency?: number
isCancelled?: () => boolean
onTopicStart?: (topic: string) => void
onLessonComplete?: (count: number) => void
}
export interface ImportResult {
imported: number
failed: Array<{ filename: string; message: string }>
@@ -43,6 +54,10 @@ export interface TeachingBookStore {
updateDesign: (id: DesignId, updater: (design: TeachingDesign) => void) => void
clearBook: () => void
generateLesson: (topic: string) => Promise<GenerateLessonResult>
generateLessons: (
topics: readonly string[],
options?: BatchGenerateLessonOptions,
) => Promise<BatchGenerateLessonResult>
regenerateLesson: (id: DesignId) => Promise<GenerateLessonResult>
}
@@ -242,6 +257,64 @@ export function useTeachingBook(bookId: string): TeachingBookStore {
}
}
async function generateLessons(
topics: readonly string[],
options: BatchGenerateLessonOptions = {},
): Promise<BatchGenerateLessonResult> {
const concurrency = Math.max(1, options.concurrency ?? 3)
const workerCount = Math.min(concurrency, topics.length)
const results = new Array<TeachingDesign | undefined>(topics.length)
let nextStartIndex = 0
let nextAppendIndex = 0
let appendedCount = 0
let firstError: string | null = null
function appendReadyLessons(): void {
let readyCount = 0
while (nextAppendIndex < results.length) {
const design = results[nextAppendIndex]
if (!design) break
book.value.designs.push(design)
book.value.selectedId = design.id
nextAppendIndex++
readyCount++
}
if (readyCount > 0) {
appendedCount += readyCount
touch()
options.onLessonComplete?.(readyCount)
}
}
async function runWorker(): Promise<void> {
while (!firstError && !options.isCancelled?.()) {
const index = nextStartIndex
if (index >= topics.length) return
nextStartIndex++
const topic = topics[index]!
options.onTopicStart?.(topic)
try {
const result = await booksApi.generateLesson(topic)
results[index] = parseTeachingDesign(result.filename, result.markdown)
appendReadyLessons()
} catch (error) {
firstError = error instanceof Error ? error.message : '生成失败。'
}
}
}
await Promise.all(Array.from({ length: workerCount }, () => runWorker()))
appendReadyLessons()
return firstError
? { ok: false, completed: appendedCount, message: firstError }
: { ok: true, completed: appendedCount }
}
async function regenerateLesson(id: DesignId): Promise<GenerateLessonResult> {
const existing = book.value.designs.find((d) => d.id === id)
if (!existing) return { ok: false, message: '找不到该教案。' }
@@ -282,6 +355,7 @@ export function useTeachingBook(bookId: string): TeachingBookStore {
updateDesign,
clearBook,
generateLesson,
generateLessons,
regenerateLesson,
}
}