471 lines
19 KiB
TypeScript
471 lines
19 KiB
TypeScript
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);
|
||
});
|