feat: 实装AI改写结局功能 - 接入DeepSeek API - AI动态生成新结局名称 - 新增rewrite类型结局样式 - 修复请求超时问题
This commit is contained in:
@@ -137,10 +137,10 @@ export default class StoryManager {
|
|||||||
ending_name: ending?.name,
|
ending_name: ending?.name,
|
||||||
ending_content: ending?.content,
|
ending_content: ending?.content,
|
||||||
prompt: prompt
|
prompt: prompt
|
||||||
});
|
}, { timeout: 60000 });
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('AI改写失败:', error);
|
console.error('AI改写失败:', error?.errMsg || error?.message || JSON.stringify(error));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export default class EndingScene extends BaseScene {
|
|||||||
super(main, params);
|
super(main, params);
|
||||||
this.storyId = params.storyId;
|
this.storyId = params.storyId;
|
||||||
this.ending = params.ending;
|
this.ending = params.ending;
|
||||||
|
console.log('EndingScene 接收到的结局:', JSON.stringify(this.ending));
|
||||||
this.showButtons = false;
|
this.showButtons = false;
|
||||||
this.fadeIn = 0;
|
this.fadeIn = 0;
|
||||||
this.particles = [];
|
this.particles = [];
|
||||||
@@ -103,6 +104,11 @@ export default class EndingScene extends BaseScene {
|
|||||||
gradient.addColorStop(0.5, '#3a3515');
|
gradient.addColorStop(0.5, '#3a3515');
|
||||||
gradient.addColorStop(1, '#2d2d1f');
|
gradient.addColorStop(1, '#2d2d1f');
|
||||||
break;
|
break;
|
||||||
|
case 'rewrite':
|
||||||
|
gradient.addColorStop(0, '#1a0a2e');
|
||||||
|
gradient.addColorStop(0.5, '#2d1b4e');
|
||||||
|
gradient.addColorStop(1, '#4a1942');
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
gradient.addColorStop(0, '#0f0c29');
|
gradient.addColorStop(0, '#0f0c29');
|
||||||
gradient.addColorStop(0.5, '#302b63');
|
gradient.addColorStop(0.5, '#302b63');
|
||||||
@@ -195,6 +201,7 @@ export default class EndingScene extends BaseScene {
|
|||||||
case 'good': return '✨ 完美结局';
|
case 'good': return '✨ 完美结局';
|
||||||
case 'bad': return '💔 悲伤结局';
|
case 'bad': return '💔 悲伤结局';
|
||||||
case 'hidden': return '🔮 隐藏结局';
|
case 'hidden': return '🔮 隐藏结局';
|
||||||
|
case 'rewrite': return '🤖 AI改写结局';
|
||||||
default: return '📖 普通结局';
|
default: return '📖 普通结局';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,6 +246,7 @@ export default class EndingScene extends BaseScene {
|
|||||||
case 'good': return `rgba(100, 255, 150, ${alpha})`;
|
case 'good': return `rgba(100, 255, 150, ${alpha})`;
|
||||||
case 'bad': return `rgba(255, 100, 100, ${alpha})`;
|
case 'bad': return `rgba(255, 100, 100, ${alpha})`;
|
||||||
case 'hidden': return `rgba(255, 215, 0, ${alpha})`;
|
case 'hidden': return `rgba(255, 215, 0, ${alpha})`;
|
||||||
|
case 'rewrite': return `rgba(168, 85, 247, ${alpha})`;
|
||||||
default: return `rgba(150, 150, 255, ${alpha})`;
|
default: return `rgba(150, 150, 255, ${alpha})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -538,6 +538,21 @@ export default class StoryScene extends BaseScene {
|
|||||||
if (this.waitingForClick) {
|
if (this.waitingForClick) {
|
||||||
this.waitingForClick = false;
|
this.waitingForClick = false;
|
||||||
|
|
||||||
|
// AI改写内容 - 直接跳转到新结局
|
||||||
|
if (this.aiContent && this.aiContent.is_ending) {
|
||||||
|
console.log('AI改写内容:', JSON.stringify(this.aiContent));
|
||||||
|
this.main.sceneManager.switchScene('ending', {
|
||||||
|
storyId: this.storyId,
|
||||||
|
ending: {
|
||||||
|
name: this.aiContent.ending_name,
|
||||||
|
type: this.aiContent.ending_type,
|
||||||
|
content: this.aiContent.content,
|
||||||
|
score: 100
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否是结局
|
// 检查是否是结局
|
||||||
if (this.main.storyManager.isEnding()) {
|
if (this.main.storyManager.isEnding()) {
|
||||||
this.main.sceneManager.switchScene('ending', {
|
this.main.sceneManager.switchScene('ending', {
|
||||||
|
|||||||
@@ -10,20 +10,18 @@ const BASE_URL = 'http://localhost:3000/api';
|
|||||||
*/
|
*/
|
||||||
export function request(options) {
|
export function request(options) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeoutMs = options.timeout || 30000;
|
||||||
reject(new Error('请求超时'));
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
wx.request({
|
wx.request({
|
||||||
url: BASE_URL + options.url,
|
url: BASE_URL + options.url,
|
||||||
method: options.method || 'GET',
|
method: options.method || 'GET',
|
||||||
data: options.data || {},
|
data: options.data || {},
|
||||||
|
timeout: timeoutMs,
|
||||||
header: {
|
header: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...options.header
|
...options.header
|
||||||
},
|
},
|
||||||
success(res) {
|
success(res) {
|
||||||
clearTimeout(timeout);
|
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
resolve(res.data.data);
|
resolve(res.data.data);
|
||||||
} else {
|
} else {
|
||||||
@@ -31,7 +29,6 @@ export function request(options) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
fail(err) {
|
fail(err) {
|
||||||
clearTimeout(timeout);
|
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -48,8 +45,8 @@ export function get(url, data) {
|
|||||||
/**
|
/**
|
||||||
* POST请求
|
* POST请求
|
||||||
*/
|
*/
|
||||||
export function post(url, data) {
|
export function post(url, data, options = {}) {
|
||||||
return request({ url, method: 'POST', data });
|
return request({ url, method: 'POST', data, ...options });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { request, get, post };
|
export default { request, get, post };
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ PORT=3000
|
|||||||
DB_HOST=localhost
|
DB_HOST=localhost
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_USER=root
|
DB_USER=root
|
||||||
DB_PASSWORD=your_password
|
DB_PASSWORD=liang20020523
|
||||||
DB_NAME=stardom_story
|
DB_NAME=stardom_story
|
||||||
|
|
||||||
# 微信小游戏配置
|
# 微信小游戏配置
|
||||||
|
|||||||
150
server/AI_CONFIG.md
Normal file
150
server/AI_CONFIG.md
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# AI服务配置指南
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 复制配置文件
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 选择 AI服务商
|
||||||
|
|
||||||
|
目前支持 4 种 AI服务,**任选其一**即可:
|
||||||
|
|
||||||
|
#### 方案 A:DeepSeek(推荐,性价比高)
|
||||||
|
```env
|
||||||
|
AI_SERVICE_ENABLED=true
|
||||||
|
AI_PROVIDER=deepseek
|
||||||
|
DEEPSEEK_API_KEY=sk-a685e8a0e97e41e4b3cb70fa6fcc3af1
|
||||||
|
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
|
||||||
|
DEEPSEEK_MODEL=deepseek-chat
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方案 B:通义千问(国内访问快)
|
||||||
|
```env
|
||||||
|
AI_SERVICE_ENABLED=true
|
||||||
|
AI_PROVIDER=qwen
|
||||||
|
DASHSCOPE_API_KEY=你的 dashscope API Key
|
||||||
|
QWEN_MODEL=qwen-plus
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方案 C:OpenAI(国际版)
|
||||||
|
```env
|
||||||
|
AI_SERVICE_ENABLED=true
|
||||||
|
AI_PROVIDER=openai
|
||||||
|
OPENAI_API_KEY=sk-your-openai-key
|
||||||
|
OPENAI_BASE_URL=https://api.openai.com/v1
|
||||||
|
OPENAI_MODEL=gpt-3.5-turbo
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方案 D:Claude(高质量)
|
||||||
|
```env
|
||||||
|
AI_SERVICE_ENABLED=true
|
||||||
|
AI_PROVIDER=claude
|
||||||
|
CLAUDE_API_KEY=你的 claude API Key
|
||||||
|
CLAUDE_MODEL=claude-3-haiku-20240307
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 安装依赖
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 启动服务
|
||||||
|
```bash
|
||||||
|
python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## DeepSeek 详细配置
|
||||||
|
|
||||||
|
### API Key 获取
|
||||||
|
1. 访问 https://platform.deepseek.com/
|
||||||
|
2. 注册/登录账号
|
||||||
|
3. 进入控制台 → API Keys
|
||||||
|
4. 创建新的 API Key
|
||||||
|
5. 复制到 `.env` 文件的 `DEEPSEEK_API_KEY`
|
||||||
|
|
||||||
|
### 计费说明
|
||||||
|
- **价格**: 输入¥0.002/1K tokens,输出¥0.002/1K tokens(截至 2024 年)
|
||||||
|
- **免费额度**: 新用户注册赠送¥14 元体验金
|
||||||
|
- **充值**: 最低充值¥10 元起
|
||||||
|
|
||||||
|
### 限流说明
|
||||||
|
- **QPS**: 默认 3 次/秒
|
||||||
|
- **RPM**: 默认 60 次/分钟
|
||||||
|
- **TPM**: 默认 200K tokens/分钟
|
||||||
|
|
||||||
|
如需提升限额,联系官方客服。
|
||||||
|
|
||||||
|
## 测试 AI 功能
|
||||||
|
|
||||||
|
### 方法 1:通过小游戏界面
|
||||||
|
1. 启动后端服务
|
||||||
|
2. 打开微信开发者工具
|
||||||
|
3. 进入任意故事的结局页
|
||||||
|
4. 点击"✨ AI改写结局"
|
||||||
|
5. 选择标签或输入自定义指令
|
||||||
|
6. 点击"✨ 开始改写"
|
||||||
|
|
||||||
|
### 方法 2:直接调用 API
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/stories/1/rewrite \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"ending_name": "双向奔赴",
|
||||||
|
"ending_content": "原结局内容...",
|
||||||
|
"prompt": "让主角逆袭成功"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: AI_SERVICE_ENABLED=false 会怎样?
|
||||||
|
A: 将使用模拟数据(随机模板),不会调用真实AI API,不产生费用。
|
||||||
|
|
||||||
|
### Q: 可以混合使用多个 AI服务吗?
|
||||||
|
A: 不建议。每次只能选择一个提供商。如需切换,修改 `AI_PROVIDER` 后重启服务。
|
||||||
|
|
||||||
|
### Q: 调用失败怎么办?
|
||||||
|
A: 系统会自动降级到模拟模式,保证用户体验不受影响。
|
||||||
|
|
||||||
|
### Q: 如何查看 Token 消耗?
|
||||||
|
A: API 返回的 `tokens_used` 字段包含本次消耗的 token 数量。可在日志中查看。
|
||||||
|
|
||||||
|
## 安全建议
|
||||||
|
|
||||||
|
1. **不要提交`.env` 文件到 Git**
|
||||||
|
```bash
|
||||||
|
echo ".env" >> .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **定期轮换 API Key**
|
||||||
|
- 每 3 个月更换一次
|
||||||
|
- 发现异常立即更换
|
||||||
|
|
||||||
|
3. **设置预算告警**
|
||||||
|
- 在 AI 平台设置每月消费上限
|
||||||
|
- 开启短信/邮件通知
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
### 降低延迟
|
||||||
|
- 选择地理位置近的 API端点
|
||||||
|
- 使用 CDN 加速(如 Cloudflare)
|
||||||
|
- 启用连接池复用
|
||||||
|
|
||||||
|
### 降低成本
|
||||||
|
- 调整 `temperature` 参数(0.7-0.9 之间)
|
||||||
|
- 限制 `max_tokens`(300-500 足够)
|
||||||
|
- 缓存相似请求的结果
|
||||||
|
|
||||||
|
## 监控指标
|
||||||
|
|
||||||
|
建议监控以下指标:
|
||||||
|
- AI 调用成功率
|
||||||
|
- 平均响应时间
|
||||||
|
- Token 消耗速率
|
||||||
|
- 每日调用次数
|
||||||
|
|
||||||
|
可通过 Prometheus + Grafana 搭建监控系统。
|
||||||
@@ -19,10 +19,23 @@ class Settings(BaseSettings):
|
|||||||
server_port: int = 3000
|
server_port: int = 3000
|
||||||
debug: bool = True
|
debug: bool = True
|
||||||
|
|
||||||
# AI服务配置(预留)
|
# AI 服务配置
|
||||||
|
ai_service_enabled: bool = True
|
||||||
|
ai_provider: str = "deepseek"
|
||||||
|
|
||||||
|
# DeepSeek 配置
|
||||||
|
deepseek_api_key: str = ""
|
||||||
|
deepseek_base_url: str = "https://api.deepseek.com/v1"
|
||||||
|
deepseek_model: str = "deepseek-chat"
|
||||||
|
|
||||||
|
# OpenAI 配置(备用)
|
||||||
openai_api_key: str = ""
|
openai_api_key: str = ""
|
||||||
openai_base_url: str = "https://api.openai.com/v1"
|
openai_base_url: str = "https://api.openai.com/v1"
|
||||||
|
|
||||||
|
# 微信小游戏配置(预留)
|
||||||
|
wx_appid: str = ""
|
||||||
|
wx_secret: str = ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def database_url(self) -> str:
|
def database_url(self) -> str:
|
||||||
return f"mysql+aiomysql://{self.db_user}:{self.db_password}@{self.db_host}:{self.db_port}/{self.db_name}"
|
return f"mysql+aiomysql://{self.db_user}:{self.db_password}@{self.db_host}:{self.db_port}/{self.db_name}"
|
||||||
|
|||||||
@@ -187,6 +187,9 @@ async def toggle_like(story_id: int, request: LikeRequest, db: AsyncSession = De
|
|||||||
@router.post("/{story_id}/rewrite")
|
@router.post("/{story_id}/rewrite")
|
||||||
async def ai_rewrite_ending(story_id: int, request: RewriteRequest, db: AsyncSession = Depends(get_db)):
|
async def ai_rewrite_ending(story_id: int, request: RewriteRequest, db: AsyncSession = Depends(get_db)):
|
||||||
"""AI改写结局"""
|
"""AI改写结局"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
if not request.prompt:
|
if not request.prompt:
|
||||||
raise HTTPException(status_code=400, detail="请输入改写指令")
|
raise HTTPException(status_code=400, detail="请输入改写指令")
|
||||||
|
|
||||||
@@ -194,7 +197,54 @@ async def ai_rewrite_ending(story_id: int, request: RewriteRequest, db: AsyncSes
|
|||||||
result = await db.execute(select(Story).where(Story.id == story_id))
|
result = await db.execute(select(Story).where(Story.id == story_id))
|
||||||
story = result.scalar_one_or_none()
|
story = result.scalar_one_or_none()
|
||||||
|
|
||||||
# 模拟AI生成(后续替换为真实API调用)
|
if not story:
|
||||||
|
raise HTTPException(status_code=404, detail="故事不存在")
|
||||||
|
|
||||||
|
# 调用 AI 服务
|
||||||
|
from app.services.ai import ai_service
|
||||||
|
|
||||||
|
ai_result = await ai_service.rewrite_ending(
|
||||||
|
story_title=story.title,
|
||||||
|
story_category=story.category or "未知",
|
||||||
|
ending_name=request.ending_name or "未知结局",
|
||||||
|
ending_content=request.ending_content or "",
|
||||||
|
user_prompt=request.prompt
|
||||||
|
)
|
||||||
|
|
||||||
|
if ai_result and ai_result.get("content"):
|
||||||
|
content = ai_result["content"]
|
||||||
|
ending_name = f"{request.ending_name}(AI改写)"
|
||||||
|
|
||||||
|
# 尝试解析 JSON 格式的返回
|
||||||
|
try:
|
||||||
|
# 提取 JSON 部分
|
||||||
|
json_match = re.search(r'\{[^{}]*"ending_name"[^{}]*"content"[^{}]*\}', content, re.DOTALL)
|
||||||
|
if json_match:
|
||||||
|
parsed = json.loads(json_match.group())
|
||||||
|
ending_name = parsed.get("ending_name", ending_name)
|
||||||
|
content = parsed.get("content", content)
|
||||||
|
else:
|
||||||
|
# 尝试直接解析整个内容
|
||||||
|
parsed = json.loads(content)
|
||||||
|
ending_name = parsed.get("ending_name", ending_name)
|
||||||
|
content = parsed.get("content", content)
|
||||||
|
except (json.JSONDecodeError, AttributeError):
|
||||||
|
# 解析失败,使用原始内容
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"content": content,
|
||||||
|
"speaker": "旁白",
|
||||||
|
"is_ending": True,
|
||||||
|
"ending_name": ending_name,
|
||||||
|
"ending_type": "rewrite",
|
||||||
|
"tokens_used": ai_result.get("tokens_used", 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# AI 服务不可用时的降级处理
|
||||||
templates = [
|
templates = [
|
||||||
f"根据你的愿望「{request.prompt}」,故事有了新的发展...\n\n",
|
f"根据你的愿望「{request.prompt}」,故事有了新的发展...\n\n",
|
||||||
f"命运的齿轮开始转动,{request.prompt}...\n\n",
|
f"命运的齿轮开始转动,{request.prompt}...\n\n",
|
||||||
@@ -205,8 +255,7 @@ async def ai_rewrite_ending(story_id: int, request: RewriteRequest, db: AsyncSes
|
|||||||
new_content = (
|
new_content = (
|
||||||
template +
|
template +
|
||||||
"原本的结局被改写,新的故事在这里展开。\n\n" +
|
"原本的结局被改写,新的故事在这里展开。\n\n" +
|
||||||
f"【AI改写提示】这是基于「{request.prompt}」生成的新结局。\n" +
|
f"【提示】AI服务暂时不可用,这是模板内容。"
|
||||||
"实际部署时,这里将由AI大模型根据上下文生成更精彩的内容。"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
211
server/app/services/ai.py
Normal file
211
server/app/services/ai.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
"""
|
||||||
|
AI服务封装模块
|
||||||
|
支持多种AI提供商:DeepSeek, OpenAI, Claude, 通义千问
|
||||||
|
"""
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
class AIService:
|
||||||
|
def __init__(self):
|
||||||
|
from app.config import get_settings
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
self.enabled = settings.ai_service_enabled
|
||||||
|
self.provider = settings.ai_provider
|
||||||
|
|
||||||
|
# 根据提供商初始化配置
|
||||||
|
if self.provider == "deepseek":
|
||||||
|
self.api_key = settings.deepseek_api_key
|
||||||
|
self.base_url = settings.deepseek_base_url
|
||||||
|
self.model = settings.deepseek_model
|
||||||
|
elif self.provider == "openai":
|
||||||
|
self.api_key = settings.openai_api_key
|
||||||
|
self.base_url = settings.openai_base_url
|
||||||
|
self.model = "gpt-3.5-turbo"
|
||||||
|
elif self.provider == "claude":
|
||||||
|
# Claude 需要从环境变量读取
|
||||||
|
import os
|
||||||
|
self.api_key = os.getenv("CLAUDE_API_KEY", "")
|
||||||
|
self.base_url = "https://api.anthropic.com"
|
||||||
|
self.model = "claude-3-haiku-20240307"
|
||||||
|
elif self.provider == "qwen":
|
||||||
|
import os
|
||||||
|
self.api_key = os.getenv("DASHSCOPE_API_KEY", "")
|
||||||
|
self.base_url = "https://dashscope.aliyuncs.com/api/v1"
|
||||||
|
self.model = "qwen-plus"
|
||||||
|
else:
|
||||||
|
self.api_key = None
|
||||||
|
self.base_url = None
|
||||||
|
self.model = None
|
||||||
|
|
||||||
|
async def rewrite_ending(
|
||||||
|
self,
|
||||||
|
story_title: str,
|
||||||
|
story_category: str,
|
||||||
|
ending_name: str,
|
||||||
|
ending_content: str,
|
||||||
|
user_prompt: str
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
AI改写结局
|
||||||
|
:return: {"content": str, "tokens_used": int} 或 None
|
||||||
|
"""
|
||||||
|
if not self.enabled or not self.api_key:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 构建Prompt
|
||||||
|
system_prompt = """你是一个专业的互动故事创作专家。根据用户的改写指令,重新创作故事结局。
|
||||||
|
要求:
|
||||||
|
1. 保持原故事的世界观和人物性格
|
||||||
|
2. 结局要有张力和情感冲击
|
||||||
|
3. 结局内容字数控制在200-400字
|
||||||
|
4. 为新结局取一个4-8字的新名字,体现改写后的剧情走向
|
||||||
|
5. 输出格式必须是JSON:{"ending_name": "新结局名称", "content": "结局内容"}"""
|
||||||
|
|
||||||
|
user_prompt_text = f"""故事标题:{story_title}
|
||||||
|
故事分类:{story_category}
|
||||||
|
原结局名称:{ending_name}
|
||||||
|
原结局内容:{ending_content[:500]}
|
||||||
|
---
|
||||||
|
用户改写指令:{user_prompt}
|
||||||
|
---
|
||||||
|
请创作新的结局(输出JSON格式):"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.provider == "openai":
|
||||||
|
return await self._call_openai(system_prompt, user_prompt_text)
|
||||||
|
elif self.provider == "claude":
|
||||||
|
return await self._call_claude(user_prompt_text)
|
||||||
|
elif self.provider == "qwen":
|
||||||
|
return await self._call_qwen(system_prompt, user_prompt_text)
|
||||||
|
elif self.provider == "deepseek":
|
||||||
|
return await self._call_deepseek(system_prompt, user_prompt_text)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"AI调用失败:{e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _call_openai(self, system_prompt: str, user_prompt: str) -> Optional[Dict]:
|
||||||
|
"""调用OpenAI API"""
|
||||||
|
url = f"{self.base_url}/chat/completions"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"model": self.model,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": user_prompt}
|
||||||
|
],
|
||||||
|
"temperature": 0.8,
|
||||||
|
"max_tokens": 500
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
response = await client.post(url, headers=headers, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
content = result["choices"][0]["message"]["content"]
|
||||||
|
tokens = result["usage"]["total_tokens"]
|
||||||
|
|
||||||
|
return {"content": content.strip(), "tokens_used": tokens}
|
||||||
|
|
||||||
|
async def _call_claude(self, prompt: str) -> Optional[Dict]:
|
||||||
|
"""调用Claude API"""
|
||||||
|
url = "https://api.anthropic.com/v1/messages"
|
||||||
|
headers = {
|
||||||
|
"x-api-key": self.api_key,
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
"content-type": "application/json"
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"model": self.model,
|
||||||
|
"max_tokens": 1024,
|
||||||
|
"messages": [{"role": "user", "content": prompt}]
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
response = await client.post(url, headers=headers, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
content = result["content"][0]["text"]
|
||||||
|
tokens = result.get("usage", {}).get("output_tokens", 0)
|
||||||
|
|
||||||
|
return {"content": content.strip(), "tokens_used": tokens}
|
||||||
|
|
||||||
|
async def _call_qwen(self, system_prompt: str, user_prompt: str) -> Optional[Dict]:
|
||||||
|
"""调用通义千问API"""
|
||||||
|
url = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"model": self.model,
|
||||||
|
"input": {
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": user_prompt}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"result_format": "message",
|
||||||
|
"temperature": 0.8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
response = await client.post(url, headers=headers, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
content = result["output"]["choices"][0]["message"]["content"]
|
||||||
|
tokens = result.get("usage", {}).get("total_tokens", 0)
|
||||||
|
|
||||||
|
return {"content": content.strip(), "tokens_used": tokens}
|
||||||
|
|
||||||
|
async def _call_deepseek(self, system_prompt: str, user_prompt: str) -> Optional[Dict]:
|
||||||
|
"""调用 DeepSeek API"""
|
||||||
|
url = f"{self.base_url}/chat/completions"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"model": self.model,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": user_prompt}
|
||||||
|
],
|
||||||
|
"temperature": 0.8,
|
||||||
|
"max_tokens": 500
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
try:
|
||||||
|
response = await client.post(url, headers=headers, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
if "choices" in result and len(result["choices"]) > 0:
|
||||||
|
content = result["choices"][0]["message"]["content"]
|
||||||
|
tokens = result.get("usage", {}).get("total_tokens", 0)
|
||||||
|
|
||||||
|
return {"content": content.strip(), "tokens_used": tokens}
|
||||||
|
else:
|
||||||
|
print(f"DeepSeek API 返回异常:{result}")
|
||||||
|
return None
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
print(f"DeepSeek HTTP 错误:{e.response.status_code} - {e.response.text}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DeepSeek 调用失败:{e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# 单例模式
|
||||||
|
ai_service = AIService()
|
||||||
40
server/init_db.py
Normal file
40
server/init_db.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import asyncio
|
||||||
|
import aiomysql
|
||||||
|
|
||||||
|
async def init_db():
|
||||||
|
# 连接 MySQL
|
||||||
|
conn = await aiomysql.connect(
|
||||||
|
host='localhost',
|
||||||
|
port=3306,
|
||||||
|
user='root',
|
||||||
|
password='liang20020523',
|
||||||
|
db='ai_game',
|
||||||
|
charset='utf8mb4'
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with conn.cursor() as cursor:
|
||||||
|
# 读取 SQL 文件
|
||||||
|
with open('sql/schema_v2.sql', 'r', encoding='utf-8') as f:
|
||||||
|
sql_content = f.read()
|
||||||
|
|
||||||
|
# 分割 SQL 语句(按分号分隔)
|
||||||
|
statements = [s.strip() for s in sql_content.split(';') if s.strip()]
|
||||||
|
|
||||||
|
# 执行每个语句
|
||||||
|
for stmt in statements:
|
||||||
|
if stmt and not stmt.startswith('--'):
|
||||||
|
try:
|
||||||
|
await cursor.execute(stmt)
|
||||||
|
print(f"✓ 执行成功")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ 执行失败:{e}")
|
||||||
|
|
||||||
|
await conn.commit()
|
||||||
|
print("\n数据库初始化完成!")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(init_db())
|
||||||
15
server/test_db.py
Normal file
15
server/test_db.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import asyncio
|
||||||
|
from app.database import AsyncSessionLocal
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
async def test():
|
||||||
|
try:
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
result = await session.execute(text('SELECT 1'))
|
||||||
|
print("数据库连接成功:", result.scalar())
|
||||||
|
except Exception as e:
|
||||||
|
print("数据库连接失败:", e)
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
asyncio.run(test())
|
||||||
Reference in New Issue
Block a user