import { Prisma, PrismaClient } from '@prisma/client'; import { hash } from 'bcryptjs'; import { DEFAULT_AI_ROUTER_CONFIG } from '../src/ai-router/ai-router.types'; const prisma = new PrismaClient(); const mockProviders = [ ['TextProvider', 'mock-text', 'Mock Text Provider', 'mock-text-v1'], ['NovelProvider', 'mock-novel', 'Mock Novel Provider', 'mock-novel-v1'], ['ImageProvider', 'mock-image', 'Mock Image Provider', 'mock-image-v1'], ['VideoProvider', 'mock-video', 'Mock Video Provider', 'mock-video-v1'], ['VoiceProvider', 'mock-voice', 'Mock Voice Provider', 'mock-voice-v1'], ['LipSyncProvider', 'mock-lipsync', 'Mock Lip Sync Provider', 'mock-lipsync-v1'], ['ModerationProvider', 'mock-moderation', 'Mock Moderation Provider', 'mock-moderation-v1'], ['QualityCheckProvider', 'mock-qc', 'Mock Quality Check Provider', 'mock-qc-v1'], ['FileParseProvider', 'mock-file-parse', 'Mock File Parse Provider', 'mock-file-parse-v1'], ['EmbeddingProvider', 'mock-embedding', 'Mock Embedding Provider', 'mock-embedding-v1'] ] as const; const defaultAgentPrompts = [ { agent_name: 'NovelIdeaCoachAgent', provider_type: 'TextProvider', default_provider_code: 'volcengine-doubao-seed20-mini-text', system_prompt: '你是网文平台金牌主编、商业化小说策划、听书编导和短剧改编策划。你的用户是不会写小说的运营,你必须把他的白话需求变成可选择的高质量小说方案。', user_prompt_template: [ '请根据运营输入生成 3 个高商业化小说方向卡。必须严格输出紧凑 JSON,不要 Markdown,不要解释。', '运营不是专业作家,所以每个方案要短、准、好判断:为什么能火、主角是谁、开篇怎么抓人。', '本步骤只做方向筛选,不写卷纲和章节蓝图;后续 IP 圣经 Agent 会补完整结构。', '必须兼顾后续听书和短剧改编:人物关系清楚、场景可视化、每章能有钩子。', '输出 JSON 字段:proposals。proposals 是数组,每个元素包含:', 'id, title, genre, novel_scale, target_audience, target_words, target_chapters, style_code, logline, opening_hook, main_character, core_conflict, selling_points, risk_notes, adaptation_targets, writing_rules, forbidden_rules, score, operator_reason。', '要求:', '1. title 要像真实网文书名,不要泛泛而谈。', '2. logline 不超过 35 字,opening_hook 不超过 55 字。', '3. main_character 和 core_conflict 各不超过 35 字。', '4. selling_points 只给 3 条,每条不超过 10 字;risk_notes 只给 2 条,每条不超过 12 字。', '5. writing_rules 只给 2 条,forbidden_rules 只给 2 条,每条不超过 12 字。', '6. target_words 和 target_chapters 必须是数字或 null。', '7. adaptation_targets 只输出 ["听书","短剧"],operator_reason 不超过 35 字,score 是 0-100。', '小说长度:{{novel_scale}}', '目标读者:{{target_audience}}', '题材:{{genre}}', '风格代码:{{style_code}}', '目标字数:{{target_words}}', '目标章节数:{{target_chapters}}', '后续用途:{{adaptation_targets}}', '运营灵感:{{inspiration_text}}', '必须包含:{{must_have_text}}', '禁止内容:{{avoid_text}}', '参考风格/作品:{{reference_titles}}' ].join('\n\n'), output_schema_json: { type: 'object', required: ['proposals'], properties: { proposals: { type: 'array', items: { type: 'object', required: ['title', 'logline', 'opening_hook', 'main_character', 'core_conflict', 'selling_points', 'score'], properties: { id: { type: 'string' }, title: { type: 'string' }, genre: { type: 'string' }, novel_scale: { type: 'string' }, target_audience: { type: 'string' }, target_words: { type: 'number' }, target_chapters: { type: 'number' }, style_code: { type: 'string' }, logline: { type: 'string' }, opening_hook: { type: 'string' }, main_character: { type: 'string' }, core_conflict: { type: 'string' }, selling_points: { type: 'array' }, risk_notes: { type: 'array' }, adaptation_targets: { type: 'array' }, writing_rules: { type: 'array' }, forbidden_rules: { type: 'array' }, score: { type: 'number' }, operator_reason: { type: 'string' } } } } } }, temperature: 0.72, max_output_tokens: 1200 }, { agent_name: 'IPBibleAgent', provider_type: 'NovelProvider', default_provider_code: 'openai-responses-novel', system_prompt: '你是专业长篇小说总策划、网文主编、文学作家、剧作结构师和影视导演。你的任务是建立稳定、可长期创作、可听书、可短剧改编的 IP 圣经。', user_prompt_template: [ '请根据用户 Brief 生成 IP 圣经。必须严格输出 JSON,不要 Markdown。', 'JSON 字段:core_logline, theme, worldbuilding, main_characters, supporting_characters, antagonist_system, volume_structure, foreshadow_plan, writing_rules, forbidden_rules, adaptation_rules。', '要求:主线清晰,人设稳定,有禁止改动项,有伏笔规划,考虑听书和 AI 短剧改编。', '用户 Brief:{{brief}}', '小说类型:{{novel_scale}}', '题材:{{genre}}', '风格:{{style}}', '禁止内容:{{avoid}}' ].join('\n\n'), output_schema_json: { type: 'object', required: ['core_logline', 'theme', 'worldbuilding', 'main_characters', 'volume_structure', 'writing_rules', 'forbidden_rules'], properties: { core_logline: { type: 'string' }, theme: { type: 'string' }, worldbuilding: { type: 'object' }, main_characters: { type: 'array' }, supporting_characters: { type: 'array' }, antagonist_system: { type: 'object' }, volume_structure: { type: 'array' }, foreshadow_plan: { type: 'array' }, writing_rules: { type: 'array' }, forbidden_rules: { type: 'array' }, adaptation_rules: { type: 'object' } } }, temperature: 0.6, max_output_tokens: 6000 }, { agent_name: 'ChapterCardAgent', provider_type: 'NovelProvider', default_provider_code: 'openai-responses-novel', system_prompt: '你是长篇小说章节导演。你的任务是设计下一章章节卡,不写正文,只输出可执行的章节方案。', user_prompt_template: [ '请生成第 {{chapter_no}} 章章节卡。必须严格输出 JSON,不要 Markdown。', '本章必须承接上一章,推动主线、人物变化或伏笔进展,不得水剧情。', 'IP 圣经摘要:{{ip_bible_summary}}', '当前卷纲:{{volume_outline}}', '最近章节摘要:{{recent_summaries}}', '人物当前状态:{{character_states}}', '当前伏笔:{{active_foreshadows}}', '禁止事项:{{forbidden_rules}}', '请输出:chapter_no, title, chapter_goal, scenes, must_happen, must_not_happen, foreshadows_to_add, foreshadows_to_advance, foreshadows_to_resolve, ending_hook, adaptation_notes。' ].join('\n\n'), output_schema_json: { type: 'object', required: ['chapter_no', 'title', 'chapter_goal', 'scenes', 'must_happen', 'must_not_happen', 'ending_hook'], properties: { chapter_no: { type: 'number' }, title: { type: 'string' }, chapter_goal: { type: 'string' }, scenes: { type: 'array' }, must_happen: { type: 'array' }, must_not_happen: { type: 'array' }, foreshadows_to_add: { type: 'array' }, foreshadows_to_advance: { type: 'array' }, foreshadows_to_resolve: { type: 'array' }, ending_hook: { type: 'string' }, adaptation_notes: { type: 'object' } } }, temperature: 0.45, max_output_tokens: 4000 }, { agent_name: 'NovelWriterAgent', provider_type: 'NovelProvider', default_provider_code: 'anthropic-claude-novel', system_prompt: '你是专业小说家。你必须严格按章节卡写正文,不改变 IP 圣经,不改变人物状态,不提前揭露秘密。', user_prompt_template: [ '请创作第 {{chapter_no}} 章正文,只输出正文,不要解释。', '目标字数:{{target_words}}', '文风规则:{{style_rules}}', 'IP 圣经摘要:{{ip_bible_summary}}', '人物档案:{{character_profiles}}', '人物当前状态:{{character_states}}', '最近章节摘要:{{recent_summaries}}', '章节卡:{{chapter_card}}', '禁止规则:{{forbidden_rules}}', '要求:画面感强、对白符合身份、每场戏推动剧情/人物/伏笔,不能流水账,不能狗血。' ].join('\n\n'), output_schema_json: null, temperature: 0.75, max_output_tokens: 9000 }, { agent_name: 'NovelPolishAgent', provider_type: 'NovelProvider', default_provider_code: 'anthropic-claude-novel', system_prompt: '你是小说修辞和节奏编辑。你只负责润色文风、节奏、对白和画面感,不得改变剧情事实。', user_prompt_template: [ '请润色下面章节,输出完整润色稿,不要解释。', '目标文风:{{style_rules}}', '章节卡:{{chapter_card}}', '禁止改变:{{forbidden_rules}}', '原稿:{{draft_text}}' ].join('\n\n'), output_schema_json: null, temperature: 0.65, max_output_tokens: 9000 }, { agent_name: 'ContinuityCheckAgent', provider_type: 'NovelProvider', default_provider_code: 'openai-responses-novel', system_prompt: '你是严苛的小说连续性审稿人,只检查冲突,不负责夸奖。', user_prompt_template: [ '请检查本章是否违背设定、人设、时间线、地点、伏笔。必须严格输出 JSON,不要 Markdown。', 'IP 圣经:{{ip_bible}}', '人物状态:{{character_states}}', '伏笔库:{{active_foreshadows}}', '章节卡:{{chapter_card}}', '本章正文:{{chapter_text}}', '请输出:has_conflict, conflicts, character_drift, timeline_errors, setting_errors, foreshadow_errors, fix_suggestions。' ].join('\n\n'), output_schema_json: { type: 'object', required: ['has_conflict', 'conflicts', 'fix_suggestions'], properties: { has_conflict: { type: 'boolean' }, conflicts: { type: 'array' }, character_drift: { type: 'array' }, timeline_errors: { type: 'array' }, setting_errors: { type: 'array' }, foreshadow_errors: { type: 'array' }, fix_suggestions: { type: 'array' } } }, temperature: 0.2, max_output_tokens: 3000 }, { agent_name: 'QualityCheckAgent', provider_type: 'NovelProvider', default_provider_code: 'openai-responses-novel', system_prompt: '你是严苛的小说主编和质量评分官。你只输出评分、问题和修复策略。', user_prompt_template: [ '请为本章评分。必须严格输出 JSON,不要 Markdown。', '评分维度:剧情推进、人物一致性、上下文连续性、文风质量、情绪张力、伏笔管理、场景画面感、听书适配度、短剧适配度。', 'IP 圣经:{{ip_bible}}', '章节卡:{{chapter_card}}', '连续性检查:{{continuity_check}}', '本章正文:{{chapter_text}}', '请输出:pass, total_score, scores, problems, must_fix, optional_suggestions, rewrite_required, rewrite_strategy。' ].join('\n\n'), output_schema_json: { type: 'object', required: ['pass', 'total_score', 'scores', 'problems', 'rewrite_required'], properties: { pass: { type: 'boolean' }, total_score: { type: 'number' }, scores: { type: 'object' }, problems: { type: 'array' }, must_fix: { type: 'array' }, optional_suggestions: { type: 'array' }, rewrite_required: { type: 'boolean' }, rewrite_strategy: { type: 'array' } } }, temperature: 0.2, max_output_tokens: 3000 }, { agent_name: 'RepairAgent', provider_type: 'NovelProvider', default_provider_code: 'anthropic-claude-novel', system_prompt: '你是小说修稿编辑。你只修复质检指出的问题,不新增无关设定,不破坏上下文。', user_prompt_template: [ '请根据质检报告修复本章,输出完整修复稿,不要解释。', 'IP 圣经摘要:{{ip_bible_summary}}', '章节卡:{{chapter_card}}', '原正文:{{chapter_text}}', '质检报告:{{quality_report}}', '连续性检查:{{continuity_check}}' ].join('\n\n'), output_schema_json: null, temperature: 0.55, max_output_tokens: 9000 }, { agent_name: 'MemoryUpdateAgent', provider_type: 'NovelProvider', default_provider_code: 'volcengine-doubao-seed20-pro-novel', system_prompt: '你是小说连续性档案管理员。你必须准确、简洁,不得加入正文没有发生的内容,不得猜测未来。', user_prompt_template: [ '请根据本章正文更新记忆库。必须严格输出 JSON,不要 Markdown。', '已有角色状态:{{character_states}}', '已有伏笔库:{{active_foreshadows}}', '本章正文:{{chapter_text}}', '请输出:chapter_summary, character_updates, relationship_updates, world_updates, timeline_update, location_update, new_foreshadows, updated_foreshadows, resolved_foreshadows, next_chapter_must_continue, forbidden_to_forget。' ].join('\n\n'), output_schema_json: { type: 'object', required: ['chapter_summary', 'character_updates', 'timeline_update', 'next_chapter_must_continue'], properties: { chapter_summary: { type: 'string' }, character_updates: { type: 'array' }, relationship_updates: { type: 'array' }, world_updates: { type: 'array' }, timeline_update: { type: 'string' }, location_update: { type: 'string' }, new_foreshadows: { type: 'array' }, updated_foreshadows: { type: 'array' }, resolved_foreshadows: { type: 'array' }, next_chapter_must_continue: { type: 'array' }, forbidden_to_forget: { type: 'array' } } }, temperature: 0.25, max_output_tokens: 3500 } ] as const; async function main() { const adminPassword = process.env.SEED_ADMIN_PASSWORD || 'Admin123!'; const adminPasswordHash = await hash(adminPassword, 12); const admin = await prisma.user.upsert({ where: { email: 'admin@example.com' }, update: { password_hash: adminPasswordHash, role: 'admin', status: 'active' }, create: { email: 'admin@example.com', password_hash: adminPasswordHash, nickname: 'System Admin', role: 'admin', status: 'active' } }); for (const [providerType, providerCode, displayName, modelName] of mockProviders) { const isEnabled = providerCode !== 'mock-lipsync'; await prisma.providerConfig.upsert({ where: { provider_type_provider_code: { provider_type: providerType, provider_code: providerCode } }, update: { display_name: displayName, mode: 'mock', model_name: modelName, is_enabled: isEnabled, priority: 100, rate_limit_json: { rpm: 120, concurrency: 8 }, cost_rule_json: { flat_cost: 0, unit: 'mock' } }, create: { provider_type: providerType, provider_code: providerCode, display_name: displayName, mode: 'mock', model_name: modelName, is_enabled: isEnabled, priority: 100, config_json: { note: 'Used until the MVP flow is complete.' }, rate_limit_json: { rpm: 120, concurrency: 8 }, cost_rule_json: { flat_cost: 0, unit: 'mock' } } }); } for (const prompt of defaultAgentPrompts) { await prisma.agentPrompt.upsert({ where: { agent_name_version: { agent_name: prompt.agent_name, version: 1 } }, update: { provider_type: prompt.provider_type, default_provider_code: prompt.default_provider_code ?? null, system_prompt: prompt.system_prompt, user_prompt_template: prompt.user_prompt_template, output_schema_json: prompt.output_schema_json ?? Prisma.JsonNull, temperature: prompt.temperature, max_output_tokens: prompt.max_output_tokens, is_active: true }, create: { agent_name: prompt.agent_name, version: 1, provider_type: prompt.provider_type, default_provider_code: prompt.default_provider_code ?? null, system_prompt: prompt.system_prompt, user_prompt_template: prompt.user_prompt_template, output_schema_json: prompt.output_schema_json ?? Prisma.JsonNull, temperature: prompt.temperature, max_output_tokens: prompt.max_output_tokens, is_active: true } }); } await prisma.systemConfig.upsert({ where: { config_key: 'system_a.current_stage' }, update: { config_value: { stage: 'stage-02-database-schema' } }, create: { config_key: 'system_a.current_stage', config_value: { stage: 'stage-02-database-schema' }, description: 'Tracks current System A development stage.' } }); await prisma.systemConfig.upsert({ where: { config_key: 'security.api_crypto_enabled' }, update: {}, create: { config_key: 'security.api_crypto_enabled', config_value: { enabled: false }, description: 'Controls frontend/backend API payload encryption. Default off for testing; enable manually in production.', is_public: true } }); await prisma.systemConfig.upsert({ where: { config_key: 'ai.router.v1' }, update: {}, create: { config_key: 'ai.router.v1', config_value: DEFAULT_AI_ROUTER_CONFIG, description: 'AI Router V1 route config for automatic provider selection by language, shot score and budget.', is_public: false } }); await prisma.quotaAccount.upsert({ where: { user_id: admin.id }, update: {}, create: { user_id: admin.id, total_quota: 100, available_quota: 100, status: 'active' } }); } main() .then(async () => { await prisma.$disconnect(); }) .catch(async (error) => { console.error(error); await prisma.$disconnect(); process.exit(1); });