فهرست منبع

fix: 修复后端 TypeScript 编译错误

- 修复 nodes 中 llm 导入路径错误 (../../services/llm -> ../../../services/llm)
- 修复动态导入缺少 .js 扩展名 (./index -> ./index.js)
- 修复 bookStore.update 支持 title/bookScale 字段
- 修复 fillAiResultsAsync 参数类型 (number -> string)
- 修复 plan.node.ts ChatMessage 类型注解
- 修复 outline.parser.ts recommendSubsections 字段
- 修复 tts.service.ts splitByEnglishSentence 未定义问题
- 修复 llm/index.ts listAll -> listEnabled
- 修复 mock.provider.ts 缺少 name/baseUrl 属性

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
MyFramework User 3 ماه پیش
والد
کامیت
60035a4230

+ 2 - 0
server/src/modules/book-generator/book-generator.store.ts

@@ -553,6 +553,8 @@ export class BookStore {
     errorMsg: string;
     totalChapters: number;
     bookAnalysis: string;
+    title: string;
+    bookScale: string;
   }>): Promise<Book | null> {
     // 如果传入 outline 对象,转换为 outlineJson 字符串
     const updateData: any = { ...data, updatedAt: new Date() };

+ 1 - 1
server/src/modules/book-generator/book-queue.processor.ts

@@ -58,7 +58,7 @@ export async function resumeInterruptedTasks() {
 
     console.log(`[DBQueue] 恢复 ${booksToResume.length} 个真正中断的任务...`);
 
-    const { langGraphGenerator } = await import('./index');
+    const { langGraphGenerator } = await import('./index.js');
     for (const book of booksToResume) {
       console.log(`[DBQueue] 恢复: 《${book.title}》 (bookId=${book.id})`);
       langGraphGenerator.generate(

+ 1 - 1
server/src/modules/book-generator/fault-tolerance.ts

@@ -299,7 +299,7 @@ export async function attemptAutoRecovery(bookId: string) {
     await sleep(config.recoveryDelayMs);
     
     // 直接重新生成(跳过队列,数据库驱动更可靠)
-    const { langGraphGenerator } = await import('./index');
+    const { langGraphGenerator } = await import('./index.js');
     langGraphGenerator.generate(
       bookId,
       book.description || book.title,

+ 4 - 4
server/src/modules/book-generator/langgraph-controller.ts

@@ -503,7 +503,7 @@ router.post('/books', optionalAuth, async (ctx: Context) => {
     if (body.immediateGenerate === false) {
       // 交互模式:异步填充 AI 结果,但不需要启动生成
       if (aiPreparing) {
-        fillAiResultsAsync(book.id, body.description, providedTitle, providedBookScale);
+        fillAiResultsAsync(book.id.toString(), body.description, providedTitle, providedBookScale);
       }
       ctx.body = { code: 0, message: '书籍创建成功(交互模式)', data: { book, genStage: 'draft', mode: 'interactive', aiPreparing } };
       return;
@@ -588,7 +588,7 @@ router.post('/books', optionalAuth, async (ctx: Context) => {
  * 异步填充 AI 结果(交互模式使用,不启动生成)
  */
 async function fillAiResultsAsync(
-  bookId: number,
+  bookId: string,
   description: string,
   providedTitle?: string,
   providedBookScale?: string,
@@ -1163,7 +1163,7 @@ router.post('/books/:id/resume', async (ctx: Context) => {
     }
 
     // 直接重新生成(跳过队列)
-    const { langGraphGenerator } = await import('./index');
+    const { langGraphGenerator } = await import('./index.js');
     langGraphGenerator.generate(
       bookId,
       book.description || book.title,
@@ -2883,7 +2883,7 @@ router.post('/books/:id/interactive/generate', optionalAuth, async (ctx: Context
     console.log(`[Interactive] 开始并行生成内容: bookId=${bookId}`);
 
     // ===== 直接异步生成(跳过队列) =====
-    const { langGraphGenerator } = await import('./index');
+    const { langGraphGenerator } = await import('./index.js');
     langGraphGenerator.generate(
       bookId,
       book.description,

+ 1 - 1
server/src/modules/book-generator/nodes/full-outline.node.ts

@@ -6,7 +6,7 @@
 
 import { GraphState } from '../graph';
 import { bookStore } from '../book-generator.store';
-import { callLLMWithMessages, ChatMessage } from '../../services/llm';
+import { callLLMWithMessages, ChatMessage } from '../../../services/llm';
 import { callLLMWithRetry, executeNodeWithTimeout, FAULT_TOLERANCE_CONFIG } from '../fault-tolerance';
 import { PROGRESS } from '../utils';
 import { getScaleConfig } from '../book-type-config';

+ 1 - 1
server/src/modules/book-generator/nodes/per-chapter.node.ts

@@ -12,7 +12,7 @@
 
 import { GraphState } from '../graph';
 import { bookStore } from '../book-generator.store';
-import { callLLMWithMessages, ChatMessage } from '../../services/llm';
+import { callLLMWithMessages, ChatMessage } from '../../../services/llm';
 import { callLLMWithRetry, executeNodeWithTimeout, FAULT_TOLERANCE_CONFIG } from '../fault-tolerance';
 import { PROGRESS } from '../utils';
 import { parseOutline } from '../parsers/outline.parser';

+ 2 - 2
server/src/modules/book-generator/nodes/plan.node.ts

@@ -11,7 +11,7 @@
 
 import { GraphState } from '../graph';
 import { bookStore } from '../book-generator.store';
-import { callLLMWithMessages } from '../../../services/llm';
+import { callLLMWithMessages, ChatMessage } from '../../../services/llm';
 import { PROGRESS } from '../utils';
 import { getScaleConfig } from '../book-type-config';
 
@@ -156,7 +156,7 @@ export async function planBookNode(state: typeof GraphState.State): Promise<Part
 
     const prompt = buildPlanPrompt(title, description, bookScale);
 
-    const messages = [
+    const messages: ChatMessage[] = [
       { role: 'system', content: '你是一位资深的图书策划编辑。请严格按照要求的 JSON 格式输出规划结果,不要添加任何额外文字。' },
       { role: 'user', content: prompt },
     ];

+ 1 - 0
server/src/modules/book-generator/parsers/outline.parser.ts

@@ -6,6 +6,7 @@
 export interface OutlineData {
   mainTheme: string;
   structureLogic: string;
+  recommendSubsections?: boolean;
   chapters: Array<{
     number: number;
     title: string;

+ 84 - 28
server/src/modules/tts/tts.service.ts

@@ -22,37 +22,93 @@ function logToFile(msg: string) {
   fs.appendFileSync(LOG_FILE, `[${timestamp}] ${msg}\n`);
 }
 
-// 可用音色列表(使用阿里云官方音色)
+// 可用音色列表(CosyVoice v3-flash - 67个音色)
+// 数据来源:阿里云百炼官方文档 2026-05
 export const VOICES: Voice[] = [
-  { id: 'cherry', name: '芊悦', gender: 'female', description: '阳光积极、亲切自然' },
-  { id: 'serena', name: '苏瑶', gender: 'female', description: '温柔女声' },
-  { id: 'ethan', name: '晨煦', gender: 'male', description: '阳光温暖、活力男声' },
-  { id: 'chelsie', name: '千雪', gender: 'female', description: '二次元虚拟女友' },
-  { id: 'momo', name: '茉兔', gender: 'female', description: '撒娇搞怪' },
-  { id: 'vivian', name: '十三', gender: 'female', description: '可爱小暴躁' },
-  { id: 'moon', name: '月白', gender: 'male', description: '率性帅气' },
-  { id: 'maia', name: '四月', gender: 'female', description: '知性温柔' },
-  { id: 'kai', name: '凯', gender: 'male', description: '舒缓放松' },
-  { id: 'nofish', name: '不吃鱼', gender: 'male', description: '不会翘舌音' },
+  // 标杆音色
+  { id: 'longanyang',    name: '龙安洋',   gender: 'male',   description: '阳光大男孩 20~30岁' },
+  { id: 'longanhuan',    name: '龙安欢',   gender: 'female', description: '欢脱元气女 20~30岁' },
+  { id: 'longhuhu_v3',   name: '龙呼呼',   gender: 'female', description: '天真烂漫女童 6~10岁' },
+  // 有声书 - 男声
+  { id: 'longsanshu_v3', name: '龙三叔',   gender: 'male',   description: '沉稳质感男 25~45岁' },
+  { id: 'longxiu_v3',    name: '龙修',     gender: 'male',   description: '博才说书男 25~35岁' },
+  { id: 'longnan_v3',    name: '龙楠',     gender: 'male',   description: '睿智青年男 25~30岁' },
+  { id: 'longyichen_v3', name: '龙逸尘',   gender: 'male',   description: '洒脱活力男 20~30岁' },
+  { id: 'longlaobo_v3',  name: '龙老伯',   gender: 'male',   description: '沧桑岁月爷 60岁以上' },
+  // 有声书 - 女声
+  { id: 'longmiao_v3',   name: '龙妙',     gender: 'female', description: '抑扬顿挫女 25~30岁' },
+  { id: 'longyuan_v3',   name: '龙媛',     gender: 'female', description: '温暖治愈女 35~40岁' },
+  { id: 'longyue_v3',    name: '龙悦',     gender: 'female', description: '温暖磁性女 30~35岁' },
+  { id: 'longwanjun_v3', name: '龙婉君',   gender: 'female', description: '细腻柔声女 20~30岁' },
+  { id: 'longlaoyi_v3',  name: '龙老姨',   gender: 'female', description: '烟火从容阿姨 60岁以上' },
+  // 客服/电话
+  { id: 'longyingxun_v3',  name: '龙应询',  gender: 'male',   description: '年轻青涩男 20~25岁' },
+  { id: 'longyingjing_v3', name: '龙应静',  gender: 'female', description: '低调冷静女 25~35岁' },
+  { id: 'longyingling_v3', name: '龙应聆',  gender: 'female', description: '温和共情女 25~30岁' },
+  { id: 'longyingtao_v3',  name: '龙应桃',  gender: 'female', description: '温柔淡定女 25~30岁' },
+  { id: 'longyingmu_v3',   name: '龙应沐',  gender: 'female', description: '优雅知性女 25~30岁' },
+  { id: 'longyingxiao_v3', name: '龙应笑',  gender: 'female', description: '清甜推销女 20~25岁' },
+  // 语音助手
+  { id: 'longxiaochun_v3', name: '龙小淳',  gender: 'female', description: '知性积极女 25~30岁' },
+  { id: 'longxiaoxia_v3',  name: '龙小夏',  gender: 'female', description: '沉稳权威女 25~30岁' },
+  { id: 'longanyun_v3',    name: '龙安昀',  gender: 'male',   description: '居家暖男 30~35岁' },
+  { id: 'longanwen_v3',    name: '龙安温',  gender: 'female', description: '优雅知性女 25~35岁' },
+  { id: 'longanli_v3',     name: '龙安莉',  gender: 'female', description: '利落从容女 25~35岁' },
+  { id: 'longanlang_v3',   name: '龙安朗',  gender: 'male',   description: '清爽利落男 20~25岁' },
+  { id: 'longyumi_v3',     name: 'YUMI',    gender: 'female', description: '正经青年女 20~25岁' },
+  // 社交陪伴
+  { id: 'longanqin_v3',  name: '龙安亲',  gender: 'female', description: '亲和活泼女 20~25岁' },
+  { id: 'longanya_v3',   name: '龙安雅',  gender: 'female', description: '高雅气质女 25~35岁' },
+  { id: 'longanzhi_v3',  name: '龙安智',  gender: 'male',   description: '睿智轻熟男 25~35岁' },
+  { id: 'longanling_v3', name: '龙安灵',  gender: 'female', description: '思维灵动女 20~30岁' },
+  { id: 'longantai_v3',  name: '龙安台',  gender: 'female', description: '嗲甜台湾女 20~25岁' },
+  { id: 'longhua_v3',    name: '龙华',    gender: 'female', description: '元气甜美女 20~25岁' },
+  { id: 'longcheng_v3',  name: '龙橙',    gender: 'male',   description: '智慧青年男 20~25岁' },
+  { id: 'longze_v3',     name: '龙泽',    gender: 'male',   description: '温暖元气男 25~30岁' },
+  { id: 'longzhe_v3',    name: '龙哲',    gender: 'male',   description: '呆板大暖男 25~30岁' },
+  { id: 'longyan_v3',    name: '龙颜',    gender: 'female', description: '温暖春风女 30~35岁' },
+  { id: 'longxing_v3',   name: '龙星',    gender: 'female', description: '温婉邻家女 20~25岁' },
+  { id: 'longtian_v3',   name: '龙天',    gender: 'male',   description: '磁性理智男 30~35岁' },
+  { id: 'longwan_v3',    name: '龙婉',    gender: 'female', description: '细腻柔声女 20~30岁' },
+  { id: 'longqiang_v3',  name: '龙嫱',    gender: 'female', description: '浪漫风情女 30~35岁' },
+  { id: 'longfeifei_v3', name: '龙菲菲',  gender: 'female', description: '甜美娇气女 20~25岁' },
+  { id: 'longhao_v3',    name: '龙浩',    gender: 'male',   description: '多情忧郁男 30~35岁' },
+  { id: 'longanrou_v3',  name: '龙安柔',  gender: 'female', description: '温柔闺蜜女 20~35岁' },
+  { id: 'longhan_v3',    name: '龙寒',    gender: 'male',   description: '温暖痴情男 30~35岁' },
+  // 童声
+  { id: 'longpaopao_v3',    name: '龙泡泡',   gender: 'female', description: '飞天泡泡音 6~15岁' },
+  { id: 'longjielidou_v3',  name: '龙杰力豆', gender: 'male',   description: '阳光顽皮男 10岁' },
+  { id: 'longxian_v3',      name: '龙仙',     gender: 'female', description: '豪放可爱女 12岁' },
+  { id: 'longling_v3',      name: '龙铃',     gender: 'female', description: '稚气呆板女 10岁' },
+  { id: 'longshanshan_v3',  name: '龙闪闪',   gender: 'female', description: '戏剧化童声 6~15岁' },
+  { id: 'longniuniu_v3',    name: '龙牛牛',   gender: 'male',   description: '阳光男童声 6~15岁' },
+  // 方言
+  { id: 'longjiaxin_v3',  name: '龙嘉欣',  gender: 'female', description: '优雅粤语女 30~35岁' },
+  { id: 'longjiayi_v3',   name: '龙嘉怡',  gender: 'female', description: '知性粤语女 25~30岁' },
+  { id: 'longanyue_v3',   name: '龙安粤',  gender: 'male',   description: '欢脱粤语男 25~35岁' },
+  { id: 'longlaotie_v3',  name: '龙老铁',  gender: 'male',   description: '东北直率男 25~30岁' },
+  { id: 'longshange_v3',  name: '龙陕哥',  gender: 'male',   description: '陕北原味男 25~35岁' },
+  // 新闻/直播
+  { id: 'longshuo_v3',    name: '龙硕',    gender: 'male',   description: '博才干练男 25~30岁' },
+  { id: 'longshu_v3',     name: '龙书',    gender: 'male',   description: '沉稳青年男 20~25岁' },
+  { id: 'loongbella_v3',  name: 'Bella3.0',gender: 'female', description: '精准干练女 25~30岁' },
+  { id: 'longanran_v3',   name: '龙安燃',  gender: 'female', description: '活泼质感女 30~40岁' },
+  { id: 'longanxuan_v3',  name: '龙安宣',  gender: 'female', description: '经典直播女 30~40岁' },
+  // 特色
+  { id: 'longjiqi_v3',   name: '龙机器',  gender: 'male',   description: '呆萌机器人 20~30岁' },
+  { id: 'longhouge_v3',  name: '龙猴哥',  gender: 'male',   description: '经典猴哥 20~25岁' },
+  { id: 'longdaiyu_v3',  name: '龙黛玉',  gender: 'female', description: '娇率才女音 15~25岁' },
+  { id: 'longfei_v3',    name: '龙飞',    gender: 'male',   description: '热血磁性男 30~35岁' },
+  // 外语
+  { id: 'loongkyong_v3', name: '韩语女',   gender: 'female', description: '韩语女声' },
+  { id: 'loongriko_v3',  name: 'Riko霓虹', gender: 'female', description: '二次元日语女' },
+  { id: 'loongabby_v3',  name: '美语女',   gender: 'female', description: '美式英文女' },
+  { id: 'loongandy_v3',  name: '美语男',   gender: 'male',   description: '美式英文男' },
 ];
 
-// 前端音色 ID 到阿里云音色的映射
-const VOICE_MAPPING: Record<string, string> = {
-  cherry: 'Cherry',
-  serena: 'Serena',
-  ethan: 'Ethan',
-  chelsie: 'Chelsie',
-  momo: 'Momo',
-  vivian: 'Vivian',
-  moon: 'Moon',
-  maia: 'Maia',
-  kai: 'Kai',
-  nofish: 'Nofish',
-};
-
-// 获取阿里云音色名称
+// CosyVoice v3-flash 音色ID直接用作API参数名,无需映射
 export function getAliyunVoice(voiceId: string): string {
-  return VOICE_MAPPING[voiceId] || 'Cherry';
+  return voiceId;
 }
 
 // 获取或创建默认"我的音频"书籍
@@ -887,7 +943,7 @@ function buildSimpleLrc(text: string, duration: number): string {
   }
   if (sentences.length <= 1 && text.length > 20) {
     // 英文按句号切分后再按固定长度兜底
-    const englishSentences = splitByEnglishSentence(text, 50);
+    const englishSentences = text.split(/(?<=[.!?])\s+/).filter(s => s.trim().length > 0);
     if (englishSentences.length > 1) {
       sentences = englishSentences;
     } else {

+ 1 - 1
server/src/services/llm/index.ts

@@ -204,7 +204,7 @@ function resolveProviderKey(modelId: string): string {
   if (cfg?.vendor) return cfg.vendor;
   // fallback: 从 registry 查找
   const registry = getLlmRegistry();
-  for (const node of registry.listAll()) {
+  for (const node of registry.listEnabled()) {
     if (node.provider.hasModel(modelId)) return node.provider.vendor;
   }
   return modelId;

+ 2 - 0
server/src/services/llm/mock.provider.ts

@@ -6,8 +6,10 @@ import { ILlmProvider, LlmModelConfig } from './provider.interface';
 import { logAiCall } from '../ai-call-logger';
 
 export class MockLlmProvider implements ILlmProvider {
+  readonly name = 'mock-llm';
   readonly vendor = 'mock';
   readonly displayName = 'Mock LLM';
+  readonly baseUrl = 'mock://localhost';
   readonly textModels = ['mock-text'];
   readonly modelConfigs = new Map<string, LlmModelConfig>([
     ['mock-text', {