人生轨迹功能完善

This commit is contained in:
2025-12-23 22:10:25 +08:00
parent 97abbefaa3
commit 56cacb7163
12 changed files with 310 additions and 38 deletions
+44 -10
View File
@@ -71,20 +71,47 @@ def run_command(cmd, cwd=None, shell=True, capture=True):
return False, "", str(e)
def exec_ssh_cmd(cmd):
def exec_ssh_cmd(cmd, timeout=30):
"""通过SSH执行远程命令"""
ssh_cmd = f'ssh {USERNAME}@{SERVER_IP} "{cmd}"'
return run_command(ssh_cmd)
ssh_cmd = f'ssh -o ConnectTimeout=10 -o BatchMode=yes {USERNAME}@{SERVER_IP} "{cmd}"'
try:
result = subprocess.run(
ssh_cmd,
shell=True,
capture_output=True,
text=True,
timeout=timeout
)
return result.returncode == 0, result.stdout.strip(), result.stderr.strip()
except subprocess.TimeoutExpired:
log_error(f"SSH命令超时: {cmd}")
return False, "", "命令执行超时"
except Exception as e:
return False, "", str(e)
def scp_upload(local_path, remote_path, recursive=False):
"""通过SCP上传文件或目录"""
r_flag = "-r" if recursive else ""
scp_cmd = f'scp {r_flag} "{local_path}" {USERNAME}@{SERVER_IP}:{remote_path}'
success, stdout, stderr = run_command(scp_cmd)
if not success:
log_error(f"SCP上传失败: {stderr}")
return success
scp_cmd = f'scp -o ConnectTimeout=10 -o BatchMode=yes {r_flag} "{local_path}" {USERNAME}@{SERVER_IP}:{remote_path}'
try:
result = subprocess.run(
scp_cmd,
shell=True,
capture_output=True,
text=True,
timeout=120 # 上传文件给更长时间
)
if result.returncode != 0:
log_error(f"SCP上传失败: {result.stderr}")
return False
return True
except subprocess.TimeoutExpired:
log_error("SCP上传超时")
return False
except Exception as e:
log_error(f"SCP上传异常: {e}")
return False
def check_env():
@@ -136,11 +163,15 @@ def deploy():
# 1. 创建远程目录
log_info("创建远程目录...")
exec_ssh_cmd(f"mkdir -p {release_path}")
success, _, err = exec_ssh_cmd(f"mkdir -p {release_path}")
if not success:
log_error(f"创建远程目录失败: {err}")
sys.exit(1)
# 2. 上传文件
log_info("上传文件到服务器...")
for item in DIST_DIR.iterdir():
log_info(f" 上传: {item.name}")
if item.is_file():
if not scp_upload(item, f"{release_path}/"):
log_error("文件上传失败")
@@ -160,7 +191,10 @@ def deploy():
# 检查目标路径是否为普通目录
exec_ssh_cmd(f"if [ -d '{LINK_PATH}' ] && [ ! -L '{LINK_PATH}' ]; then mv '{LINK_PATH}' '{LINK_PATH}_backup_$(date +%s)'; fi")
# 创建软链接
exec_ssh_cmd(f"ln -snf '{release_path}' '{LINK_PATH}'")
success, _, err = exec_ssh_cmd(f"ln -snf '{release_path}' '{LINK_PATH}'")
if not success:
log_error(f"切换版本失败: {err}")
sys.exit(1)
log_info(f"部署完成!当前版本指向: {release_path}")
+55 -2
View File
@@ -98,7 +98,9 @@ const transformToBackendFormat = (frontendData) => {
style,
length,
content,
isSelected
isSelected,
character,
events
} = frontendData;
// 解析内容生成标题和各部分
@@ -127,6 +129,55 @@ const transformToBackendFormat = (frontendData) => {
});
}
// 格式化角色信息
let characterInfo = '';
if (character) {
const parts = [
`姓名:${character.nickname || '未设置'}`,
`性别:${character.gender || '未设置'}`,
`MBTI${character.mbti || '未设置'}`,
`星座:${character.zodiac || '未设置'}`,
`职业:${character.profession || '未设置'}`,
`兴趣爱好:${character.hobbies?.join(',') || '无'}`
];
if (character.future) {
if (character.future.vision) parts.push(`未来愿景:${character.future.vision}`);
if (character.future.ideal) parts.push(`理想生活:${character.future.ideal}`);
}
characterInfo = parts.join('\n');
}
// 格式化过往经历
let lifeEventsSummary = '';
const eventParts = [];
// 1. 核心记忆 (Childhood, Joy, Low from character data)
if (character) {
if (character.childhood?.text) {
eventParts.push(`【童年记忆】(${character.childhood.date || '未知时间'}): ${character.childhood.text}`);
}
if (character.joy?.text) {
eventParts.push(`【高光时刻】(${character.joy.date || '未知时间'}): ${character.joy.text}`);
}
if (character.low?.text) {
eventParts.push(`【至暗时刻】(${character.low.date || '未知时间'}): ${character.low.text}`);
}
}
// 2. 详细人生事件
if (events && Array.isArray(events)) {
events.forEach(e => {
const dateStr = e.time || e.eventDate || '未知时间';
const titleStr = e.title || '无标题';
const contentStr = e.content || '';
eventParts.push(`【人生事件】(${dateStr}) ${titleStr}${contentStr ? ': ' + contentStr : ''}`);
});
}
lifeEventsSummary = eventParts.join('\n');
return {
id,
title,
@@ -138,7 +189,9 @@ const transformToBackendFormat = (frontendData) => {
plotClimax,
plotEnding,
plotJson: content ? { fullContent: content } : null,
isSelected
isSelected,
characterInfo,
lifeEventsSummary
};
};
+8 -6
View File
@@ -2,7 +2,6 @@ import { useState, useEffect } from 'react';
import { UserCog, PenTool, Sparkles, BookOpen, Loader2 } from 'lucide-react';
import { GlassCard, GlassButton, GlassInput, GlassSelect } from '../components/ui';
import useStore from '../store/useStore';
import { generateEpicScript } from '../services/ai';
import { scriptStyles, scriptLengths } from '../utils/constants';
/**
@@ -48,12 +47,15 @@ const ScriptView = ({ onOpenProfile }) => {
setIsLoading(true);
try {
const content = await generateEpicScript(
{ theme, style, length, character: registrationData },
lifeEvents
);
// 直接调用后端创建接口,由后端调用AI生成
await addScript({
theme,
style,
length,
character: registrationData,
events: lifeEvents
});
addScript({ theme, style, length, content });
setTheme('');
} catch (error) {
console.error('Failed to generate script:', error);
+76 -13
View File
@@ -3,7 +3,76 @@ import { Plus, Wind, Sparkles } from 'lucide-react';
import { GlassCard, GlassButton, GlassInput, GlassTextarea } from '../components/ui';
import Modal from '../components/Modal';
import useStore from '../store/useStore';
import { analyzeLifeEvent } from '../services/ai';
/**
* 格式化 AI 反馈内容的组件
*/
const FeedbackContent = ({ content }) => {
if (!content) return null;
// 检查是否为结构化格式 (包含分隔符 --- 和标题标识 ####)
const isStructured = content.includes('---') && content.includes('####');
if (!isStructured) {
return (
<p className="text-xs italic text-white/50 leading-loose whitespace-pre-wrap">
{content}
</p>
);
}
// 解析结构化内容
const sections = content.split('---')
.map(s => s.trim())
.filter(s => s && s.length > 0);
return (
<div className="space-y-5 mt-2">
{sections.map((section, index) => {
// 移除 #### 前缀
const cleanSection = section.replace(/^####\s*/, '');
// 提取标题 (通常在 【】 中)
const titleMatch = cleanSection.match(/【(.*?)】/);
const title = titleMatch ? titleMatch[1] : '';
// 提取正文
let body = cleanSection;
if (titleMatch) {
body = cleanSection.replace(titleMatch[0], '').trim();
}
if (!title && !body) return null;
return (
<div key={index} className="text-xs leading-relaxed">
{title && (
<h5 className="text-orange-100 font-bold mb-2 flex items-center gap-2 text-[11px] tracking-wide">
{title}
</h5>
)}
<div className="text-white/60 pl-3 border-l-2 border-orange-200/10 space-y-1">
{body.split('\n').map((line, i) => {
const trimmedLine = line.trim();
if (!trimmedLine) return null;
// 简单的列表项处理
if (trimmedLine.startsWith('*') || trimmedLine.startsWith('-')) {
return (
<div key={i} className="flex gap-2 pl-1">
<span className="text-orange-200/40"></span>
<span>{trimmedLine.substring(1).trim()}</span>
</div>
);
}
return <p key={i}>{trimmedLine}</p>;
})}
</div>
</div>
);
})}
</div>
);
};
/**
* TimelineView 组件
@@ -42,20 +111,16 @@ const TimelineView = () => {
setIsLoading(true);
try {
// 调用 AI 分析
const aiFeedback = await analyzeLifeEvent(eventForm);
// 添加事件
addLifeEvent({
...eventForm,
aiFeedback
// 直接调用后端添加事件,由后端调用AI进行疗愈分析
await addLifeEvent({
...eventForm
});
// 重置表单并关闭模态框
setEventForm({ title: '', time: '', content: '' });
setIsModalOpen(false);
} catch (error) {
console.error('Failed to analyze event:', error);
console.error('Failed to add event:', error);
} finally {
setIsLoading(false);
}
@@ -124,15 +189,13 @@ const TimelineView = () => {
{/* AI 反馈区域 - 仅在有反馈时显示 */}
{event.aiFeedback && (
<div className="ai-glow-card p-5 rounded-2xl bg-orange-200/[0.02] border border-orange-200/5">
<div className="flex items-center gap-2 mb-2">
<div className="flex items-center gap-2 mb-4">
<Sparkles className="w-3 h-3 text-orange-200" />
<span className="text-[9px] uppercase tracking-[0.2em] text-orange-200/60 font-bold">
引路人洞察
</span>
</div>
<p className="text-xs italic text-white/50 leading-loose">
{event.aiFeedback}
</p>
<FeedbackContent content={event.aiFeedback} />
</div>
)}
</GlassCard>