Files
ai/backend/prisma/seed.ts
T

471 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});