feat: AI改写功能集成角色数据 + UI优化

- 新增story_characters表和seed_characters.sql种子数据(27个角色)
- AI改写/续写功能注入角色信息(性别/年龄/外貌/性格)
- 首页UI下移避让微信退出按钮
- 个人中心页面布局重构
This commit is contained in:
2026-03-11 18:41:56 +08:00
parent 2470cea7e4
commit 4ac47c8474
8 changed files with 478 additions and 47 deletions

View File

@@ -65,7 +65,7 @@ export default class HomeScene extends BaseScene {
const stories = this.getFilteredStories();
const cardHeight = 130;
const gap = 12;
const startY = 175;
const startY = 190;
const tabHeight = 60;
const contentBottom = startY + stories.length * (cardHeight + gap);
const visibleBottom = this.screenHeight - tabHeight;
@@ -130,7 +130,7 @@ export default class HomeScene extends BaseScene {
}
renderContentTabs(ctx) {
const tabY = 60;
const tabY = 75;
const tabWidth = (this.screenWidth - 30) / 4;
const padding = 15;
@@ -160,7 +160,7 @@ export default class HomeScene extends BaseScene {
}
renderCategories(ctx) {
const startY = 95;
const startY = 110;
const tagHeight = 28;
let x = 15 - this.categoryScrollX;
@@ -207,7 +207,7 @@ export default class HomeScene extends BaseScene {
}
renderStoryList(ctx) {
const startY = 140;
const startY = 155;
const cardHeight = 130;
const cardMargin = 12;
const stories = this.getFilteredStories();
@@ -425,7 +425,7 @@ export default class HomeScene extends BaseScene {
this.touchStartY = touch.clientY;
this.hasMoved = false;
if (touch.clientY >= 90 && touch.clientY <= 130) {
if (touch.clientY >= 105 && touch.clientY <= 145) {
this.isCategoryDragging = true;
this.isDragging = false;
} else {
@@ -489,7 +489,7 @@ export default class HomeScene extends BaseScene {
}
// 分类点击
if (y >= 90 && y <= 130) {
if (y >= 105 && y <= 145) {
this.handleCategoryClick(x);
return;
}
@@ -530,7 +530,7 @@ export default class HomeScene extends BaseScene {
}
handleStoryClick(x, y) {
const startY = 140;
const startY = 155;
const cardHeight = 130;
const cardMargin = 12;
const stories = this.getFilteredStories();

View File

@@ -140,6 +140,7 @@ export default class ProfileScene extends BaseScene {
this.renderUserCard(ctx);
this.renderTabs(ctx);
this.renderList(ctx);
this.renderLogoutButton(ctx);
}
renderBackground(ctx) {
@@ -160,12 +161,6 @@ export default class ProfileScene extends BaseScene {
ctx.textAlign = 'center';
ctx.font = 'bold 17px sans-serif';
ctx.fillText('个人中心', this.screenWidth / 2, 35);
// 设置按钮
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '18px sans-serif';
ctx.textAlign = 'right';
ctx.fillText('⚙', this.screenWidth - 20, 35);
}
renderUserCard(ctx) {
@@ -223,10 +218,24 @@ export default class ProfileScene extends BaseScene {
ctx.textAlign = 'left';
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 16px sans-serif';
ctx.fillText(user.nickname || '游客用户', avatarX + avatarSize + 15, avatarY + 22);
const nickname = user.nickname || '游客用户';
ctx.fillText(nickname, avatarX + avatarSize + 15, avatarY + 22);
// 昵称旁边的编辑按钮
const nicknameWidth = ctx.measureText(nickname).width;
const editNicknameBtnX = avatarX + avatarSize + 15 + nicknameWidth + 8;
ctx.fillStyle = 'rgba(255,255,255,0.15)';
this.roundRect(ctx, editNicknameBtnX, avatarY + 8, 24, 20, 10);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('✎', editNicknameBtnX + 12, avatarY + 22);
this.editNicknameRect = { x: editNicknameBtnX, y: avatarY + 8, width: 24, height: 20 };
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '11px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`ID: ${user.userId || '未登录'}`, avatarX + avatarSize + 15, avatarY + 42);
// 创作者标签
@@ -367,6 +376,34 @@ export default class ProfileScene extends BaseScene {
ctx.restore();
}
// 渲染底部退出登录按钮
renderLogoutButton(ctx) {
const btnW = 120;
const btnH = 40;
const btnX = (this.screenWidth - btnW) / 2;
const btnY = this.screenHeight - 70;
// 按钮背景
ctx.fillStyle = 'rgba(239, 68, 68, 0.15)';
this.roundRect(ctx, btnX, btnY, btnW, btnH, 20);
ctx.fill();
// 按钮边框
ctx.strokeStyle = 'rgba(239, 68, 68, 0.4)';
ctx.lineWidth = 1;
this.roundRect(ctx, btnX, btnY, btnW, btnH, 20);
ctx.stroke();
// 按钮文字
ctx.fillStyle = '#ef4444';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('退出登录', this.screenWidth / 2, btnY + 26);
// 保存按钮区域
this.logoutBtnRect = { x: btnX, y: btnY, width: btnW, height: btnH };
}
// 渲染版本列表头部(返回按钮+故事标题)
renderVersionListHeader(ctx, startY) {
const headerY = startY - 5;
@@ -764,17 +801,29 @@ export default class ProfileScene extends BaseScene {
return;
}
// 设置按钮(右上角)
if (y < 50 && x > this.screenWidth - 50) {
this.showSettingsMenu();
return;
// 退出登录按钮
if (this.logoutBtnRect) {
const btn = this.logoutBtnRect;
if (x >= btn.x && x <= btn.x + btn.width && y >= btn.y && y <= btn.y + btn.height) {
this.confirmLogout();
return;
}
}
// 头像点击
// 头像点击(修改头像)
if (this.avatarRect) {
const rect = this.avatarRect;
if (x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height) {
this.showAvatarOptions();
this.chooseAndUploadAvatar();
return;
}
}
// 昵称编辑按钮点击
if (this.editNicknameRect) {
const rect = this.editNicknameRect;
if (x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height) {
this.showEditNicknameDialog();
return;
}
}

View File

@@ -26,6 +26,28 @@ class Story(Base):
updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
nodes = relationship("StoryNode", back_populates="story", cascade="all, delete-orphan")
characters = relationship("StoryCharacter", back_populates="story", cascade="all, delete-orphan")
class StoryCharacter(Base):
"""故事角色表"""
__tablename__ = "story_characters"
id = Column(Integer, primary_key=True, autoincrement=True)
story_id = Column(Integer, ForeignKey("stories.id", ondelete="CASCADE"), nullable=False)
name = Column(String(50), nullable=False)
role_type = Column(String(20), default="supporting") # protagonist/antagonist/supporting
gender = Column(String(10), default="")
age_range = Column(String(20), default="")
appearance = Column(Text) # 外貌描述
personality = Column(Text) # 性格描述
background = Column(Text) # 背景故事
avatar_prompt = Column(Text) # AI绘图提示词
avatar_url = Column(String(500), default="")
created_at = Column(TIMESTAMP, server_default=func.now())
updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
story = relationship("Story", back_populates="characters")
class StoryNode(Base):

View File

@@ -10,11 +10,32 @@ from typing import List, Optional
from datetime import datetime
from app.database import get_db
from app.models.story import Story, StoryDraft, DraftStatus
from app.models.story import Story, StoryDraft, DraftStatus, StoryCharacter
router = APIRouter(prefix="/drafts", tags=["草稿箱"])
# ============ 辅助函数 ============
async def get_story_characters(db: AsyncSession, story_id: int) -> List[dict]:
"""获取故事的所有角色并转为字典列表"""
result = await db.execute(
select(StoryCharacter).where(StoryCharacter.story_id == story_id)
)
characters = result.scalars().all()
return [
{
"name": c.name,
"role_type": c.role_type,
"gender": c.gender,
"age_range": c.age_range,
"appearance": c.appearance,
"personality": c.personality
}
for c in characters
]
# ============ 请求/响应模型 ============
class PathHistoryItem(BaseModel):
@@ -98,6 +119,9 @@ async def process_ai_rewrite(draft_id: int):
await db.commit()
return
# 获取故事角色
characters = await get_story_characters(db, story.id)
# 转换路径历史格式
path_history = draft.path_history or []
@@ -107,7 +131,8 @@ async def process_ai_rewrite(draft_id: int):
story_category=story.category or "未知",
path_history=path_history,
current_content=draft.current_content or "",
user_prompt=draft.user_prompt
user_prompt=draft.user_prompt,
characters=characters
)
if ai_result and ai_result.get("nodes"):
@@ -171,6 +196,9 @@ async def process_ai_rewrite_ending(draft_id: int):
await db.commit()
return
# 获取故事角色
characters = await get_story_characters(db, story.id)
# 从草稿字段获取结局信息
ending_name = draft.current_node_key or "未知结局"
ending_content = draft.current_content or ""
@@ -181,7 +209,8 @@ async def process_ai_rewrite_ending(draft_id: int):
story_category=story.category or "未知",
ending_name=ending_name,
ending_content=ending_content,
user_prompt=draft.user_prompt
user_prompt=draft.user_prompt,
characters=characters
)
if ai_result and ai_result.get("content"):
@@ -266,6 +295,9 @@ async def process_ai_continue_ending(draft_id: int):
await db.commit()
return
# 获取故事角色
characters = await get_story_characters(db, story.id)
# 从草稿字段获取结局信息
ending_name = draft.current_node_key or "未知结局"
ending_content = draft.current_content or ""
@@ -276,7 +308,8 @@ async def process_ai_continue_ending(draft_id: int):
story_category=story.category or "未知",
ending_name=ending_name,
ending_content=ending_content,
user_prompt=draft.user_prompt
user_prompt=draft.user_prompt,
characters=characters
)
if ai_result and ai_result.get("nodes"):

View File

@@ -9,7 +9,7 @@ from typing import Optional, List
from pydantic import BaseModel
from app.database import get_db
from app.models.story import Story, StoryNode, StoryChoice
from app.models.story import Story, StoryNode, StoryChoice, StoryCharacter
router = APIRouter()
@@ -214,6 +214,22 @@ async def ai_rewrite_ending(story_id: int, request: RewriteRequest, db: AsyncSes
if not story:
raise HTTPException(status_code=404, detail="故事不存在")
# 获取故事角色
char_result = await db.execute(
select(StoryCharacter).where(StoryCharacter.story_id == story_id)
)
characters = [
{
"name": c.name,
"role_type": c.role_type,
"gender": c.gender,
"age_range": c.age_range,
"appearance": c.appearance,
"personality": c.personality
}
for c in char_result.scalars().all()
]
# 调用 AI 服务
from app.services.ai import ai_service
@@ -222,7 +238,8 @@ async def ai_rewrite_ending(story_id: int, request: RewriteRequest, db: AsyncSes
story_category=story.category or "未知",
ending_name=request.ending_name or "未知结局",
ending_content=request.ending_content or "",
user_prompt=request.prompt
user_prompt=request.prompt,
characters=characters
)
if ai_result and ai_result.get("content"):
@@ -301,6 +318,22 @@ async def ai_rewrite_branch(
if not story:
raise HTTPException(status_code=404, detail="故事不存在")
# 获取故事角色
char_result = await db.execute(
select(StoryCharacter).where(StoryCharacter.story_id == story_id)
)
characters = [
{
"name": c.name,
"role_type": c.role_type,
"gender": c.gender,
"age_range": c.age_range,
"appearance": c.appearance,
"personality": c.personality
}
for c in char_result.scalars().all()
]
# 将 Pydantic 模型转换为字典列表
path_history = [
{"nodeKey": item.nodeKey, "content": item.content, "choice": item.choice}
@@ -315,7 +348,8 @@ async def ai_rewrite_branch(
story_category=story.category or "未知",
path_history=path_history,
current_content=request.currentContent,
user_prompt=request.prompt
user_prompt=request.prompt,
characters=characters
)
if ai_result and ai_result.get("nodes"):

View File

@@ -7,6 +7,28 @@ import httpx
import json
import re
def format_characters_prompt(characters: List[Dict]) -> str:
"""格式化角色信息为prompt文本"""
if not characters:
return ""
text = "\n【故事角色设定】\n"
for char in characters:
text += f"- {char.get('name', '未知')}{char.get('role_type', '配角')}"
if char.get('gender'):
text += f" {char.get('gender')}"
if char.get('age_range'):
text += f" {char.get('age_range')}"
if char.get('appearance'):
text += f" 外貌:{char.get('appearance')[:100]}"
if char.get('personality'):
text += f" 性格:{char.get('personality')[:100]}"
text += "\n"
return text
class AIService:
def __init__(self):
from app.config import get_settings
@@ -49,7 +71,8 @@ class AIService:
story_category: str,
ending_name: str,
ending_content: str,
user_prompt: str
user_prompt: str,
characters: List[Dict] = None
) -> Optional[Dict[str, Any]]:
"""
AI改写结局
@@ -58,14 +81,19 @@ class AIService:
if not self.enabled or not self.api_key:
return None
# 格式化角色信息
characters_text = format_characters_prompt(characters) if characters else ""
# 构建Prompt
system_prompt = """你是一个专业的互动故事创作专家。根据用户的改写指令,重新创作故事结局。
system_prompt = f"""你是一个专业的互动故事创作专家。根据用户的改写指令,重新创作故事结局。
{characters_text}
要求:
1. 保持原故事的世界观和人物性格
2. 结局要有张力和情感冲击
3. 结局内容字数控制在200-400字
4. 为新结局取一个4-8字的新名字体现改写后的剧情走向
5. 输出格式必须是JSON{"ending_name": "新结局名称", "content": "结局内容"}"""
5. 如果有角色设定,必须保持角色性格和外貌描述的一致性
6. 输出格式必须是JSON{{"ending_name": "新结局名称", "content": "结局内容"}}"""
user_prompt_text = f"""故事标题:{story_title}
故事分类:{story_category}
@@ -97,7 +125,8 @@ class AIService:
story_category: str,
path_history: List[Dict[str, str]],
current_content: str,
user_prompt: str
user_prompt: str,
characters: List[Dict] = None
) -> Optional[Dict[str, Any]]:
"""
AI改写中间章节生成新的剧情分支
@@ -107,12 +136,16 @@ class AIService:
print(f"[rewrite_branch] user_prompt={user_prompt}")
print(f"[rewrite_branch] path_history长度={len(path_history)}")
print(f"[rewrite_branch] current_content长度={len(current_content)}")
print(f"[rewrite_branch] characters数量={len(characters) if characters else 0}")
print(f"[rewrite_branch] enabled={self.enabled}, api_key存在={bool(self.api_key)}")
if not self.enabled or not self.api_key:
print(f"[rewrite_branch] 服务未启用或API Key为空返回None")
return None
# 格式化角色信息
characters_text = format_characters_prompt(characters) if characters else ""
# 构建路径历史文本
path_text = ""
for i, item in enumerate(path_history, 1):
@@ -121,19 +154,20 @@ class AIService:
path_text += f" → 用户选择:{item['choice']}\n"
# 构建系统提示词
system_prompt = """你是一个专业的互动故事续写专家。用户正在玩一个互动故事,想要在当前位置改变剧情走向。
system_prompt_header = f"""你是一个专业的互动故事续写专家。用户正在玩一个互动故事,想要在当前位置改变剧情走向。
{characters_text}
【任务】
请从当前节点开始,创作新的剧情分支。新剧情的第一句话必须紧接当前节点最后的情节,仿佛是同一段故事的自然延续,不能有任何跳跃感。
【写作要求】
1. 第一个节点必须紧密衔接当前剧情,像是同一段话的下一句
2. 生成 4-6 个新节点,形成有层次的剧情发展(起承转合)
3. 每个节点内容 150-300 字,要分 2-3 个自然段(用\n\n分隔),包含:场景描写、人物对话、心理活动
3. 每个节点内容 150-300 字,要分 2-3 个自然段(用\\n\\n分隔),包含:场景描写、人物对话、心理活动
4. 每个非结局节点有 2 个选项,选项要有明显的剧情差异和后果
5. 严格符合用户的改写意图,围绕用户指令展开剧情
6. 保持原故事的人物性格、语言风格和世界观
7. 对话要自然生动,描写要有画面感
7. 如果有角色设定,必须保持角色性格和外貌的一致性
8. 对话要自然生动,描写要有画面感
【关于结局 - 极其重要!】
★★★ 每一条分支路径的尽头必须是结局节点 ★★★
@@ -149,9 +183,10 @@ class AIService:
- special 特殊结局70-90分
【内容分段示例】
"content": "他的声音在耳边响起,像是一阵温柔的风。\n\n\"我喜欢你。\"他说,目光坚定地看着你。\n\n你的心跳漏了一拍,一时间不知该如何回应。"
【输出格式】严格JSON不要有任何额外文字
"content": "他的声音在耳边响起,像是一阵温柔的风。\\n\\n\\"我喜欢你。\\"他说,目光坚定地看着你。\\n\\n你的心跳漏了一拍,一时间不知该如何回应。"
"""
system_prompt_json = """【输出格式】严格JSON不要有任何额外文字
{
"nodes": {
"branch_1": {
@@ -179,7 +214,7 @@ class AIService:
]
},
"branch_ending_good": {
"content": "好结局内容200-400字...\n\n【达成结局xxx】",
"content": "好结局内容200-400字...\\n\\n【达成结局xxx】",
"speaker": "旁白",
"is_ending": true,
"ending_name": "结局名称",
@@ -187,7 +222,7 @@ class AIService:
"ending_score": 90
},
"branch_ending_bad": {
"content": "坏结局内容...\n\n【达成结局xxx】",
"content": "坏结局内容...\\n\\n【达成结局xxx】",
"speaker": "旁白",
"is_ending": true,
"ending_name": "结局名称",
@@ -195,7 +230,7 @@ class AIService:
"ending_score": 40
},
"branch_ending_neutral": {
"content": "中立结局...\n\n【达成结局xxx】",
"content": "中立结局...\\n\\n【达成结局xxx】",
"speaker": "旁白",
"is_ending": true,
"ending_name": "结局名称",
@@ -203,7 +238,7 @@ class AIService:
"ending_score": 60
},
"branch_ending_special": {
"content": "特殊结局...\n\n【达成结局xxx】",
"content": "特殊结局...\\n\\n【达成结局xxx】",
"speaker": "旁白",
"is_ending": true,
"ending_name": "结局名称",
@@ -213,6 +248,8 @@ class AIService:
},
"entryNodeKey": "branch_1"
}"""
system_prompt = system_prompt_header + system_prompt_json
# 构建用户提示词
user_prompt_text = f"""【原故事信息】
@@ -280,7 +317,8 @@ class AIService:
story_category: str,
ending_name: str,
ending_content: str,
user_prompt: str
user_prompt: str,
characters: List[Dict] = None
) -> Optional[Dict[str, Any]]:
"""
AI续写结局从结局开始续写新的剧情分支
@@ -289,15 +327,19 @@ class AIService:
print(f"[continue_ending] story_title={story_title}, category={story_category}")
print(f"[continue_ending] ending_name={ending_name}")
print(f"[continue_ending] user_prompt={user_prompt}")
print(f"[continue_ending] characters数量={len(characters) if characters else 0}")
print(f"[continue_ending] enabled={self.enabled}, api_key存在={bool(self.api_key)}")
if not self.enabled or not self.api_key:
print(f"[continue_ending] 服务未启用或API Key为空返回None")
return None
# 构建系统提示词
system_prompt = """你是一个专业的互动故事续写专家。用户已经到达故事的某个结局,现在想要从这个结局继续故事发展。
# 格式化角色信息
characters_text = format_characters_prompt(characters) if characters else ""
# 构建系统提示词
system_prompt_header = f"""你是一个专业的互动故事续写专家。用户已经到达故事的某个结局,现在想要从这个结局继续故事发展。
{characters_text}
【任务】
请从这个结局开始,创作故事的延续。新剧情必须紧接结局内容,仿佛故事并没有在此结束,而是有了新的发展。
@@ -308,7 +350,8 @@ class AIService:
4. 每个非结局节点有 2 个选项,选项要有明显的剧情差异和后果
5. 严格符合用户的续写意图,围绕用户指令展开剧情
6. 保持原故事的人物性格、语言风格和世界观
7. 对话要自然生动,描写要有画面感
7. 如果有角色设定,必须保持角色性格和外貌的一致性
8. 对话要自然生动,描写要有画面感
【关于新结局 - 极其重要!】
★★★ 每一条分支路径的尽头必须是新结局节点 ★★★
@@ -317,8 +360,9 @@ class AIService:
- 结局名称 4-8 字,体现剧情走向
- 如果有2个选项分支最终必须有2个不同的结局
- 每个结局必须有 "ending_score" 评分0-100
"""
【输出格式】严格JSON不要有任何额外文字
system_prompt_json = """【输出格式】严格JSON不要有任何额外文字
{
"nodes": {
"continue_1": {
@@ -357,6 +401,8 @@ class AIService:
"entryNodeKey": "continue_1"
}"""
system_prompt = system_prompt_header + system_prompt_json
# 构建用户提示词
user_prompt_text = f"""【原故事信息】
故事标题:{story_title}

View File

@@ -176,3 +176,41 @@ CREATE TABLE IF NOT EXISTS `play_records` (
CONSTRAINT `play_records_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
CONSTRAINT `play_records_ibfk_2` FOREIGN KEY (`story_id`) REFERENCES `stories` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='游玩记录表';
-- ============================================
-- 9. 故事角色表
-- ============================================
CREATE TABLE IF NOT EXISTS `story_characters` (
`id` INT NOT NULL AUTO_INCREMENT,
`story_id` INT NOT NULL COMMENT '所属故事ID',
`name` VARCHAR(50) NOT NULL COMMENT '角色名称',
`role_type` VARCHAR(20) DEFAULT 'supporting' COMMENT '角色类型: protagonist/antagonist/supporting',
`gender` VARCHAR(10) DEFAULT '' COMMENT '性别: male/female/unknown',
`age_range` VARCHAR(20) DEFAULT '' COMMENT '年龄段: child/teen/young/middle/old',
`appearance` TEXT COMMENT '外貌描述(用于生成图片)',
`personality` TEXT COMMENT '性格描述',
`background` TEXT COMMENT '背景故事',
`avatar_prompt` TEXT COMMENT 'AI绘图提示词',
`avatar_url` VARCHAR(500) DEFAULT '' COMMENT '角色头像URL',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_story_id` (`story_id`),
CONSTRAINT `story_characters_ibfk_1` FOREIGN KEY (`story_id`) REFERENCES `stories` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='故事角色表';
-- ============================================
-- 10. 节点角色关联表
-- ============================================
CREATE TABLE IF NOT EXISTS `node_characters` (
`id` INT NOT NULL AUTO_INCREMENT,
`story_id` INT NOT NULL COMMENT '故事ID',
`node_key` VARCHAR(50) NOT NULL COMMENT '节点key',
`character_id` INT NOT NULL COMMENT '角色ID',
`is_speaker` TINYINT(1) DEFAULT 0 COMMENT '是否为该节点说话者',
`emotion` VARCHAR(30) DEFAULT 'neutral' COMMENT '该节点情绪: happy/sad/angry/neutral等',
PRIMARY KEY (`id`),
KEY `idx_story_node` (`story_id`, `node_key`),
KEY `idx_character` (`character_id`),
CONSTRAINT `node_characters_ibfk_1` FOREIGN KEY (`character_id`) REFERENCES `story_characters` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='节点角色关联表';

View File

@@ -0,0 +1,209 @@
-- 角色种子数据
USE stardom_story;
-- ============================================
-- 故事1: 总裁的替身新娘
-- ============================================
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
(1, 1, '林诗语', 'protagonist', 'female', 'young', '清秀温婉的面容,一双明亮的杏眼,乌黑长发及腰,身材纤细,气质如兰', '善良隐忍,聪慧坚韧,温柔但有底线', 'young chinese woman, gentle beauty, long black hair, bright almond eyes, slender figure, elegant temperament, wedding dress'),
(2, 1, '陆景深', 'protagonist', 'male', 'young', '剑眉星目,薄唇紧抿,高大挺拔,气势凌厉,眼神冰冷如寒潭', '外冷内热,霸道专一,深情而克制', 'handsome chinese man, sharp eyebrows, thin lips, tall and imposing, cold piercing eyes, black suit, CEO style'),
(3, 1, '林诗韵', 'supporting', 'female', 'young', '容貌出众,浓妆艳抹,身姿妖娆', '任性自私,贪慕虚荣', 'beautiful chinese woman, heavy makeup, glamorous figure, designer clothes');
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
(1, 'start', 1, 0, 'nervous'),
(1, 'start', 2, 0, 'neutral'),
(1, 'choice1_a', 1, 0, 'anxious'),
(1, 'choice1_a', 2, 1, 'cold'),
(1, 'choice1_b', 1, 0, 'scared'),
(1, 'choice1_b', 2, 1, 'amused'),
(1, 'choice2_a', 1, 1, 'honest'),
(1, 'choice2_a', 2, 0, 'interested'),
(1, 'choice2_b', 1, 1, 'nervous'),
(1, 'choice2_b', 2, 0, 'smirking'),
(1, 'ending_good', 1, 0, 'happy'),
(1, 'ending_good', 2, 1, 'loving'),
(1, 'ending_normal', 1, 1, 'calm'),
(1, 'ending_normal', 2, 0, 'sad'),
(1, 'ending_bad', 1, 0, 'heartbroken'),
(1, 'ending_bad', 2, 1, 'cruel');
-- ============================================
-- 故事2: 密室中的第四个人
-- ============================================
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
(4, 2, '王教授', 'supporting', 'male', 'middle', '戴眼镜的中年男人,头发花白,穿着整洁的西装', '学者气质,表面儒雅,实际隐藏着秘密', 'middle-aged chinese man, glasses, graying hair, neat suit, scholarly appearance'),
(5, 2, '苏小姐', 'supporting', 'female', 'young', '穿着名牌,妆容精致,身上有淡淡香水味', '看似高傲,内心脆弱', 'young elegant chinese woman, designer clothes, delicate makeup, wealthy appearance'),
(6, 2, '李明', 'antagonist', 'male', 'young', '沉默寡言的青年,眼神阴沉,嘴角常带冷笑', '隐忍复仇,城府极深', 'young chinese man, gloomy eyes, cold expression, dark casual clothes');
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
(2, 'start', 4, 0, 'dead'),
(2, 'start', 5, 0, 'shocked'),
(2, 'start', 6, 0, 'calm'),
(2, 'investigate_room', 4, 0, 'dead'),
(2, 'question_su', 5, 1, 'nervous'),
(2, 'question_li', 6, 1, 'cold'),
(2, 'accuse_su', 5, 1, 'crying'),
(2, 'accuse_li', 6, 1, 'vengeful'),
(2, 'ending_truth', 6, 1, 'resigned'),
(2, 'ending_wrong', 5, 0, 'wronged');
-- ============================================
-- 故事3: 凤临天下
-- ============================================
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
(7, 3, '林清婉', 'protagonist', 'female', 'young', '眉目如画,肤若凝脂,身姿窈窕,举止端庄', '外柔内刚,隐忍聪慧,心思缜密', 'beautiful ancient chinese woman, delicate features, fair skin, graceful posture, palace costume, elegant hairpin'),
(8, 3, '皇帝', 'protagonist', 'male', 'young', '龙袍加身,眉宇威严,俊朗不凡,气势天成', '深沉多疑,霸道深情', 'handsome ancient chinese emperor, dragon robe, majestic presence, piercing eyes, imperial crown'),
(9, 3, '皇后', 'antagonist', 'female', 'middle', '珠翠环绕,气度雍容,凤冠霞帔,笑容不达眼底', '城府极深,手段狠辣', 'ancient chinese empress, phoenix crown, luxurious robes, dignified bearing, calculating smile');
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
(3, 'start', 7, 0, 'nervous'),
(3, 'meet_emperor', 7, 1, 'respectful'),
(3, 'meet_emperor', 8, 1, 'curious'),
(3, 'meet_consort', 7, 1, 'cautious'),
(3, 'meet_consort', 9, 1, 'scrutinizing'),
(3, 'choice_emperor', 7, 1, 'humble'),
(3, 'choice_emperor', 8, 1, 'pleased'),
(3, 'choice_queen', 7, 1, 'calculating'),
(3, 'choice_queen', 9, 1, 'satisfied'),
(3, 'ending_empress', 7, 0, 'triumphant'),
(3, 'ending_empress', 8, 1, 'loving'),
(3, 'ending_concubine', 7, 0, 'peaceful'),
(3, 'ending_tragic', 7, 0, 'despairing');
-- ============================================
-- 故事4: 暗恋那件小事
-- ============================================
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
(10, 4, '', 'protagonist', 'female', 'teen', '扎着马尾的高中女生,青涩可爱,眼神里藏着小秘密', '内向害羞,暗恋成痴,纯真善良', 'teenage chinese girl, ponytail, school uniform, shy expression, cute appearance'),
(11, 4, '沈昼', 'protagonist', 'male', 'teen', '校园男神,阳光帅气,篮球服下身材修长,笑起来眼睛弯弯', '温柔体贴,学霸气质,暖心细腻', 'handsome teenage chinese boy, school uniform, basketball jersey, warm smile, athletic build'),
(12, 4, '同桌', 'supporting', 'female', 'teen', '活泼开朗的女生,表情丰富,是个话痨', '热心肠,爱八卦,仗义', 'cheerful teenage chinese girl, expressive face, school uniform, lively personality');
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
(4, 'start', 10, 0, 'daydreaming'),
(4, 'start', 11, 0, 'neutral'),
(4, 'start', 12, 1, 'teasing'),
(4, 'library', 10, 1, 'flustered'),
(4, 'library', 11, 1, 'gentle'),
(4, 'basketball', 10, 0, 'excited'),
(4, 'basketball', 11, 1, 'playful'),
(4, 'rooftop', 10, 0, 'nervous'),
(4, 'rooftop', 11, 1, 'serious'),
(4, 'confess_yes', 10, 1, 'brave'),
(4, 'confess_yes', 11, 0, 'happy'),
(4, 'confess_no', 10, 0, 'tearful'),
(4, 'ending_friends', 10, 0, 'regretful');
-- ============================================
-- 故事5: 废柴逆袭录
-- ============================================
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
(13, 5, '陈风', 'protagonist', 'male', 'young', '相貌普通的少年,眼神坚定,穿着破旧的外门弟子服', '隐忍不屈,心性坚韧,大智若愚', 'young chinese man, ordinary appearance, determined eyes, worn-out cultivation robes, underdog aura'),
(14, 5, '天元大帝', 'supporting', 'male', 'old', '白发苍苍的虚影,仙风道骨,眼中闪烁着智慧的光芒', '睿智从容,亦师亦友', 'ancient chinese immortal, white long hair, ethereal figure, wise eyes, flowing robes, ghostly appearance'),
(15, 5, '赵天龙', 'antagonist', 'male', 'young', '内门弟子,华服锦衣,面带傲色,眼高于顶', '嚣张跋扈,欺软怕硬', 'arrogant young chinese man, luxurious cultivation robes, haughty expression, inner sect disciple');
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
(5, 'start', 13, 0, 'oppressed'),
(5, 'inheritance', 13, 0, 'shocked'),
(5, 'inheritance', 14, 1, 'wise'),
(5, 'challenge', 13, 0, 'calm'),
(5, 'challenge', 15, 1, 'mocking'),
(5, 'show_power', 13, 1, 'confident'),
(5, 'show_power', 15, 0, 'terrified'),
(5, 'hide_power', 13, 0, 'scheming'),
(5, 'hide_power', 14, 1, 'approving'),
(5, 'ending_immortal', 13, 0, 'transcendent'),
(5, 'ending_mortal', 13, 0, 'peaceful');
-- ============================================
-- 故事6: 回到高考前一天
-- ============================================
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
(16, 6, '', 'protagonist', 'male', 'teen', '穿着高中校服的少年,眼神带着超越年龄的沧桑', '成熟稳重,心怀遗憾,想要改变', 'teenage chinese boy, school uniform, eyes with wisdom beyond age, determined expression'),
(17, 6, '林小雨', 'supporting', 'female', 'teen', '瘦弱的女孩,眼眶微红,神情木然', '敏感脆弱,承受着巨大压力', 'fragile teenage chinese girl, red-rimmed eyes, blank expression, thin figure, school uniform'),
(18, 6, '爷爷', 'supporting', 'male', 'old', '慈祥的老人,满头银发,笑容温暖', '和蔼可亲,疼爱孙辈', 'kind elderly chinese man, silver hair, warm smile, traditional chinese clothes');
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
(6, 'start', 16, 0, 'shocked'),
(6, 'choice_study', 16, 0, 'focused'),
(6, 'choice_relax', 16, 0, 'thoughtful'),
(6, 'visit_girl', 16, 1, 'concerned'),
(6, 'visit_girl', 17, 1, 'vulnerable'),
(6, 'ending_perfect', 16, 0, 'fulfilled'),
(6, 'ending_changed', 16, 0, 'content'),
(6, 'ending_changed', 17, 1, 'grateful');
-- ============================================
-- 故事7: 逆风翻盘
-- ============================================
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
(19, 7, '陈墨', 'protagonist', 'male', 'young', '改头换面后的复仇者,西装革履,眼神锐利如鹰', '隐忍多年,城府极深,复仇心切', 'handsome chinese businessman, sharp suit, eagle-like eyes, confident posture, mysterious aura'),
(20, 7, '周明', 'antagonist', 'male', 'middle', '发福的中年男人,眼神世故贪婪,穿着名牌却掩不住小人嘴脸', '背信弃义,贪婪无耻', 'overweight chinese businessman, greedy eyes, expensive suit, treacherous appearance');
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
(7, 'start', 19, 0, 'vengeful'),
(7, 'meeting', 19, 1, 'calculating'),
(7, 'meeting', 20, 1, 'obsequious'),
(7, 'expose', 19, 1, 'revealing'),
(7, 'expose', 20, 1, 'terrified'),
(7, 'ending_revenge', 19, 0, 'empty'),
(7, 'ending_forgive', 19, 0, 'liberated');
-- ============================================
-- 故事8: 2099最后一班地铁
-- ============================================
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
(21, 8, '', 'protagonist', 'male', 'young', '末世的普通人,穿着简朴,眼神迷茫又带着一丝希望', '迷茫困惑,渴望改变', 'young man in futuristic simple clothes, contemplative expression, sci-fi subway setting'),
(22, 8, 'Zero', 'supporting', 'female', 'young', '穿白裙的神秘女孩,眼睛明亮,不属于这个时代', '超然世外,引导者', 'mysterious girl in white dress, bright luminous eyes, ethereal beauty, futuristic subway conductor');
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
(8, 'start', 21, 0, 'curious'),
(8, 'start', 22, 1, 'welcoming'),
(8, 'talk_girl', 21, 1, 'questioning'),
(8, 'talk_girl', 22, 1, 'mysterious'),
(8, 'choose_past', 21, 1, 'hopeful'),
(8, 'choose_past', 22, 1, 'guiding'),
(8, 'ending_past', 21, 0, 'determined'),
(8, 'ending_past', 22, 1, 'encouraging'),
(8, 'ending_future', 21, 0, 'amazed'),
(8, 'ending_future', 22, 1, 'serene');
-- ============================================
-- 故事9: 第七夜
-- ============================================
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
(23, 9, '', 'protagonist', 'male', 'young', '被困旅馆的旅客,神情紧张,黑眼圈明显', '胆小谨慎,求生欲强', 'nervous young man, dark circles under eyes, casual travel clothes, fearful expression'),
(24, 9, '老板娘', 'supporting', 'female', 'middle', '面容苍老,眼神奇怪,穿着老旧的旗袍', '神秘莫测,知道真相', 'mysterious middle-aged chinese woman, old-fashioned qipao, strange knowing eyes, creepy inn keeper'),
(25, 9, '白裙女孩', 'antagonist', 'female', 'young', '苍白的脸,穿着白裙,笑容诡异,若隐若现', '怨念深重,寻找陪伴', 'pale ghost girl in white dress, eerie smile, translucent figure, long black hair covering face, horror style');
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
(9, 'start', 23, 0, 'uneasy'),
(9, 'start', 24, 1, 'cryptic'),
(9, 'night_sound', 23, 0, 'terrified'),
(9, 'night_sound', 25, 1, 'pleading'),
(9, 'open_door', 23, 0, 'horrified'),
(9, 'open_door', 25, 1, 'sinister'),
(9, 'not_open', 23, 0, 'anxious'),
(9, 'ending_death', 23, 0, 'dying'),
(9, 'ending_death', 25, 1, 'satisfied'),
(9, 'ending_escape', 23, 0, 'relieved');
-- ============================================
-- 故事10: 我的室友是只猫
-- ============================================
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
(26, 10, '', 'protagonist', 'male', 'young', '普通大学生,一脸懵逼,被室友变猫吓到', '善良搞笑,容易慌张', 'confused chinese college student, dorm room setting, panicked expression, casual clothes'),
(27, 10, '室友/橘猫', 'protagonist', 'male', 'young', '胖胖的橘色大猫,会说话,表情丰富,原本是个学霸', '聪明话多,倒霉体质,吃货', 'fat orange tabby cat, expressive face, sitting like human, funny expression, college dorm background');
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
(10, 'start', 26, 0, 'shocked'),
(10, 'start', 27, 1, 'frustrated'),
(10, 'panic', 26, 1, 'panicking'),
(10, 'panic', 27, 1, 'embarrassed'),
(10, 'exam_plan', 26, 0, 'thinking'),
(10, 'exam_plan', 27, 1, 'excited'),
(10, 'bring_cat', 26, 0, 'nervous'),
(10, 'bring_cat', 27, 1, 'whispering'),
(10, 'ending_funny', 26, 0, 'relieved'),
(10, 'ending_funny', 27, 1, 'smug'),
(10, 'ending_caught', 26, 0, 'doomed'),
(10, 'ending_caught', 27, 1, 'defeated');