diff --git a/client/js/data/StoryManager.js b/client/js/data/StoryManager.js index 81c8854..4c5d81b 100644 --- a/client/js/data/StoryManager.js +++ b/client/js/data/StoryManager.js @@ -9,6 +9,7 @@ export default class StoryManager { this.currentStory = null; this.currentNodeKey = 'start'; this.categories = []; + this.pathHistory = []; // 记录用户走过的路径 } /** @@ -56,6 +57,7 @@ export default class StoryManager { try { this.currentStory = await get(`/stories/${storyId}`); this.currentNodeKey = 'start'; + this.pathHistory = []; // 重置路径历史 // 记录游玩次数 await post(`/stories/${storyId}/play`); @@ -85,6 +87,14 @@ export default class StoryManager { } const choice = currentNode.choices[choiceIndex]; + + // 记录路径历史 + this.pathHistory.push({ + nodeKey: this.currentNodeKey, + content: currentNode.content, + choice: choice.text + }); + this.currentNodeKey = choice.nextNodeKey; return this.getCurrentNode(); @@ -145,6 +155,39 @@ export default class StoryManager { } } + /** + * AI改写中间章节,生成新的剧情分支 + * @returns {Object|null} 成功返回新节点,失败返回 null(不改变当前状态) + */ + async rewriteBranch(storyId, prompt, userId) { + try { + const currentNode = this.getCurrentNode(); + const result = await post(`/stories/${storyId}/rewrite-branch`, { + userId: userId, + currentNodeKey: this.currentNodeKey, + pathHistory: this.pathHistory, + currentContent: currentNode?.content || '', + prompt: prompt + }, { timeout: 300000 }); // 5分钟超时,AI生成需要较长时间 + + // 检查是否有有效的 nodes + if (result && result.nodes) { + // AI 成功,将新分支合并到当前故事中 + Object.assign(this.currentStory.nodes, result.nodes); + // 跳转到新分支的入口节点 + this.currentNodeKey = result.entryNodeKey || 'branch_1'; + return this.getCurrentNode(); + } + + // AI 失败,返回 null + console.log('AI服务不可用:', result?.error || '未知错误'); + return null; + } catch (error) { + console.error('AI改写分支失败:', error?.errMsg || error?.message || JSON.stringify(error)); + return null; + } + } + /** * AI续写故事 */ diff --git a/client/js/scenes/ChapterScene.js b/client/js/scenes/ChapterScene.js index 1b15d80..796fb39 100644 --- a/client/js/scenes/ChapterScene.js +++ b/client/js/scenes/ChapterScene.js @@ -71,8 +71,10 @@ export default class ChapterScene extends BaseScene { const cardHeight = 85; const gap = 12; const headerHeight = 80; - const contentHeight = this.nodeList.length * (cardHeight + gap) + headerHeight; - this.maxScrollY = Math.max(0, contentHeight - this.screenHeight + 20); + const bottomPadding = 50; // 底部留出空间 + const contentHeight = this.nodeList.length * (cardHeight + gap) + headerHeight + bottomPadding; + this.maxScrollY = Math.max(0, contentHeight - this.screenHeight); + console.log('[ChapterScene] nodeList长度:', this.nodeList.length, 'contentHeight:', contentHeight, 'screenHeight:', this.screenHeight, 'maxScrollY:', this.maxScrollY); } update() {} @@ -173,9 +175,17 @@ export default class ChapterScene extends BaseScene { if (this.maxScrollY > 0) { const scrollBarHeight = 50; const scrollBarY = startY + (this.scrollY / this.maxScrollY) * (this.screenHeight - startY - scrollBarHeight - 20); - ctx.fillStyle = 'rgba(255,255,255,0.2)'; - this.roundRect(ctx, this.screenWidth - 5, scrollBarY, 3, scrollBarHeight, 1.5); + ctx.fillStyle = 'rgba(255,255,255,0.4)'; + this.roundRect(ctx, this.screenWidth - 8, scrollBarY, 5, scrollBarHeight, 2.5); ctx.fill(); + + // 如果还没滚动到底部,显示提示 + if (this.scrollY < this.maxScrollY - 10) { + ctx.fillStyle = 'rgba(255,255,255,0.4)'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('↑ 上滑查看更多 ↑', this.screenWidth / 2, this.screenHeight - 15); + } } } diff --git a/client/js/scenes/StoryScene.js b/client/js/scenes/StoryScene.js index b82d0b4..a8a9bc6 100644 --- a/client/js/scenes/StoryScene.js +++ b/client/js/scenes/StoryScene.js @@ -29,6 +29,8 @@ export default class StoryScene extends BaseScene { // 场景图相关 this.sceneImage = null; this.sceneColors = this.generateSceneColors(); + // AI改写相关 + this.isAIRewriting = false; } // 根据场景生成氛围色 @@ -246,16 +248,33 @@ export default class StoryScene extends BaseScene { ctx.textAlign = 'center'; ctx.font = 'bold 15px sans-serif'; ctx.fillStyle = this.sceneColors.accent; - const title = this.story.title.length > 10 ? this.story.title.substring(0, 10) + '...' : this.story.title; + const title = this.story.title.length > 8 ? this.story.title.substring(0, 8) + '...' : this.story.title; ctx.fillText(title, this.screenWidth / 2, 35); } - // 进度指示 - ctx.textAlign = 'right'; - ctx.font = '12px sans-serif'; - ctx.fillStyle = 'rgba(255,255,255,0.5)'; - const progress = this.main.storyManager.getProgress ? this.main.storyManager.getProgress() : ''; - ctx.fillText(progress, this.screenWidth - 15, 35); + // AI改写按钮(右上角) + const btnX = this.screenWidth - 70; + const btnY = 18; + const btnW = 55; + const btnH = 26; + + // 按钮背景 + if (this.isAIRewriting) { + ctx.fillStyle = 'rgba(255,255,255,0.2)'; + } else { + const gradient = ctx.createLinearGradient(btnX, btnY, btnX + btnW, btnY); + gradient.addColorStop(0, '#a855f7'); + gradient.addColorStop(1, '#ec4899'); + ctx.fillStyle = gradient; + } + this.roundRect(ctx, btnX, btnY, btnW, btnH, 13); + ctx.fill(); + + // 按钮文字 + ctx.fillStyle = '#ffffff'; + ctx.font = '11px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(this.isAIRewriting ? '生成中' : 'AI改写', btnX + btnW / 2, btnY + 17); } renderDialogBox(ctx) { @@ -300,14 +319,14 @@ export default class StoryScene extends BaseScene { const textY = boxY + 65; const lineHeight = 26; const maxWidth = this.screenWidth - padding * 2; - const visibleHeight = boxHeight - 105; // 可见区域高度 + const visibleHeight = boxHeight - 90; // 增加可见区域高度 ctx.font = '15px sans-serif'; const allLines = this.getWrappedLines(ctx, this.displayText, maxWidth); const totalTextHeight = allLines.length * lineHeight; // 计算最大滚动距离 - this.maxScrollY = Math.max(0, totalTextHeight - visibleHeight); + this.maxScrollY = Math.max(0, totalTextHeight - visibleHeight + 30); // 自动滚动到最新内容(打字时) if (this.isTyping) { @@ -334,9 +353,17 @@ export default class StoryScene extends BaseScene { if (this.maxScrollY > 0) { const scrollBarHeight = 40; const scrollBarY = boxY + 55 + (this.textScrollY / this.maxScrollY) * (visibleHeight - scrollBarHeight); - ctx.fillStyle = 'rgba(255,255,255,0.2)'; - this.roundRect(ctx, this.screenWidth - 6, scrollBarY, 3, scrollBarHeight, 1.5); + ctx.fillStyle = 'rgba(255,255,255,0.3)'; + this.roundRect(ctx, this.screenWidth - 8, scrollBarY, 4, scrollBarHeight, 2); ctx.fill(); + + // 如果还没滚动到底部,显示滚动提示 + if (this.textScrollY < this.maxScrollY - 10 && !this.isTyping) { + ctx.fillStyle = 'rgba(255,255,255,0.5)'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('↑ 上滑查看更多 ↑', this.screenWidth / 2, this.screenHeight - 25); + } } // 打字机光标 @@ -496,13 +523,15 @@ export default class StoryScene extends BaseScene { const touch = e.touches[0]; // 滑动对话框内容 - if (this.isDragging && this.maxScrollY > 0) { + if (this.isDragging) { const deltaY = this.lastTouchY - touch.clientY; if (Math.abs(deltaY) > 2) { this.hasMoved = true; } - this.textScrollY += deltaY; - this.textScrollY = Math.max(0, Math.min(this.textScrollY, this.maxScrollY)); + if (this.maxScrollY > 0) { + this.textScrollY += deltaY; + this.textScrollY = Math.max(0, Math.min(this.textScrollY, this.maxScrollY)); + } this.lastTouchY = touch.clientY; } } @@ -525,6 +554,18 @@ export default class StoryScene extends BaseScene { return; } + // AI改写按钮点击 + const btnX = this.screenWidth - 70; + const btnY = 18; + const btnW = 55; + const btnH = 26; + if (y >= btnY && y <= btnY + btnH && x >= btnX && x <= btnX + btnW) { + if (!this.isAIRewriting) { + this.showAIRewriteInput(); + } + return; + } + // 加速打字 if (this.isTyping) { this.displayText = this.targetText; @@ -638,6 +679,71 @@ export default class StoryScene extends BaseScene { ctx.closePath(); } + /** + * 显示AI改写输入框 + */ + showAIRewriteInput() { + wx.showModal({ + title: 'AI改写剧情', + editable: true, + placeholderText: '输入你的改写指令,如"让主角暴富"', + success: (res) => { + if (res.confirm && res.content) { + this.doAIRewrite(res.content); + } + } + }); + } + + /** + * 执行AI改写 + */ + async doAIRewrite(prompt) { + if (this.isAIRewriting) return; + + this.isAIRewriting = true; + this.main.showLoading('AI正在改写剧情...'); + + try { + const userId = this.main.userManager.userId || 0; + const newNode = await this.main.storyManager.rewriteBranch( + this.storyId, + prompt, + userId + ); + + this.main.hideLoading(); + + if (newNode) { + // 成功获取新分支,开始播放 + this.currentNode = newNode; + this.startTypewriter(newNode.content); + wx.showToast({ + title: '改写成功!', + icon: 'success', + duration: 1500 + }); + } else { + // AI 失败,继续原故事 + wx.showToast({ + title: 'AI暂时不可用,继续原故事', + icon: 'none', + duration: 2000 + }); + } + } catch (error) { + this.main.hideLoading(); + console.error('AI改写出错:', error); + wx.showToast({ + title: '网络错误,请重试', + icon: 'none', + duration: 2000 + }); + } finally { + this.isAIRewriting = false; + } + } + destroy() { if (this.main.userManager.isLoggedIn && this.story) { this.main.userManager.saveProgress( diff --git a/client/project.config.json b/client/project.config.json index e0fa724..2e0aabf 100644 --- a/client/project.config.json +++ b/client/project.config.json @@ -42,5 +42,7 @@ "ignore": [], "include": [] }, - "editorSetting": {} + "editorSetting": {}, + "libVersion": "3.14.3", + "isGameTourist": false } \ No newline at end of file diff --git a/server/app/__pycache__/config.cpython-310.pyc b/server/app/__pycache__/config.cpython-310.pyc index 94ca8b8..f7196ad 100644 Binary files a/server/app/__pycache__/config.cpython-310.pyc and b/server/app/__pycache__/config.cpython-310.pyc differ diff --git a/server/app/routers/__pycache__/story.cpython-310.pyc b/server/app/routers/__pycache__/story.cpython-310.pyc index 724bd19..9d71bd6 100644 Binary files a/server/app/routers/__pycache__/story.cpython-310.pyc and b/server/app/routers/__pycache__/story.cpython-310.pyc differ diff --git a/server/app/routers/story.py b/server/app/routers/story.py index fb122fe..3f1686c 100644 --- a/server/app/routers/story.py +++ b/server/app/routers/story.py @@ -5,7 +5,7 @@ import random from fastapi import APIRouter, Depends, Query, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, update, func, distinct -from typing import Optional +from typing import Optional, List from pydantic import BaseModel from app.database import get_db @@ -25,6 +25,20 @@ class RewriteRequest(BaseModel): prompt: str +class PathHistoryItem(BaseModel): + nodeKey: str + content: str = "" + choice: str = "" + + +class RewriteBranchRequest(BaseModel): + userId: int + currentNodeKey: str + pathHistory: List[PathHistoryItem] + currentContent: str + prompt: str + + # ========== API接口 ========== @router.get("") @@ -268,3 +282,59 @@ async def ai_rewrite_ending(story_id: int, request: RewriteRequest, db: AsyncSes "ending_type": "rewrite" } } + + +@router.post("/{story_id}/rewrite-branch") +async def ai_rewrite_branch( + story_id: int, + request: RewriteBranchRequest, + db: AsyncSession = Depends(get_db) +): + """AI改写中间章节,生成新的剧情分支""" + if not request.prompt: + raise HTTPException(status_code=400, detail="请输入改写指令") + + # 获取故事信息 + result = await db.execute(select(Story).where(Story.id == story_id)) + story = result.scalar_one_or_none() + + if not story: + raise HTTPException(status_code=404, detail="故事不存在") + + # 将 Pydantic 模型转换为字典列表 + path_history = [ + {"nodeKey": item.nodeKey, "content": item.content, "choice": item.choice} + for item in request.pathHistory + ] + + # 调用 AI 服务 + from app.services.ai import ai_service + + ai_result = await ai_service.rewrite_branch( + story_title=story.title, + story_category=story.category or "未知", + path_history=path_history, + current_content=request.currentContent, + user_prompt=request.prompt + ) + + if ai_result and ai_result.get("nodes"): + return { + "code": 0, + "data": { + "nodes": ai_result["nodes"], + "entryNodeKey": ai_result.get("entryNodeKey", "branch_1"), + "tokensUsed": ai_result.get("tokens_used", 0) + } + } + + # AI 服务不可用时,返回空结果(不使用兜底模板) + return { + "code": 0, + "data": { + "nodes": None, + "entryNodeKey": None, + "tokensUsed": 0, + "error": "AI服务暂时不可用" + } + } \ No newline at end of file diff --git a/server/app/services/__pycache__/ai.cpython-310.pyc b/server/app/services/__pycache__/ai.cpython-310.pyc new file mode 100644 index 0000000..6401d73 Binary files /dev/null and b/server/app/services/__pycache__/ai.cpython-310.pyc differ diff --git a/server/app/services/ai.py b/server/app/services/ai.py index e14df4f..2fd790c 100644 --- a/server/app/services/ai.py +++ b/server/app/services/ai.py @@ -2,8 +2,10 @@ AI服务封装模块 支持多种AI提供商:DeepSeek, OpenAI, Claude, 通义千问 """ -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, List import httpx +import json +import re class AIService: def __init__(self): @@ -13,11 +15,14 @@ class AIService: self.enabled = settings.ai_service_enabled self.provider = settings.ai_provider + print(f"[AI服务初始化] enabled={self.enabled}, provider={self.provider}") + # 根据提供商初始化配置 if self.provider == "deepseek": self.api_key = settings.deepseek_api_key self.base_url = settings.deepseek_base_url self.model = settings.deepseek_model + print(f"[AI服务初始化] DeepSeek配置: api_key={self.api_key[:20] + '...' if self.api_key else 'None'}, base_url={self.base_url}, model={self.model}") elif self.provider == "openai": self.api_key = settings.openai_api_key self.base_url = settings.openai_base_url @@ -86,6 +91,351 @@ class AIService: return None + async def rewrite_branch( + self, + story_title: str, + story_category: str, + path_history: List[Dict[str, str]], + current_content: str, + user_prompt: str + ) -> Optional[Dict[str, Any]]: + """ + AI改写中间章节,生成新的剧情分支 + """ + print(f"\n[rewrite_branch] ========== 开始调用 ==========") + print(f"[rewrite_branch] story_title={story_title}, category={story_category}") + 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] 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 + + # 构建路径历史文本 + path_text = "" + for i, item in enumerate(path_history, 1): + path_text += f"第{i}段:{item.get('content', '')}\n" + if item.get('choice'): + path_text += f" → 用户选择:{item['choice']}\n" + + # 构建系统提示词 + system_prompt = """你是一个专业的互动故事续写专家。用户正在玩一个互动故事,想要在当前位置改变剧情走向。 + +【任务】 +请从当前节点开始,创作新的剧情分支。新剧情的第一句话必须紧接当前节点最后的情节,仿佛是同一段故事的自然延续,不能有任何跳跃感。 + +【写作要求】 +1. 第一个节点必须紧密衔接当前剧情,像是同一段话的下一句 +2. 生成 4-6 个新节点,形成有层次的剧情发展(起承转合) +3. 每个节点内容 150-300 字,要分 2-3 个自然段(用\n\n分隔),包含:场景描写、人物对话、心理活动 +4. 每个非结局节点有 2 个选项,选项要有明显的剧情差异和后果 +5. 必须以结局收尾,结局内容要 200-400 字,分 2-3 段,有情感冲击力 +6. 严格符合用户的改写意图,围绕用户指令展开剧情 +7. 保持原故事的人物性格、语言风格和世界观 +8. 对话要自然生动,描写要有画面感 + +【重要】内容分段示例: +"content": "他的声音在耳边响起,像是一阵温柔的风。\n\n\"我喜欢你。\"他说,目光坚定地看着你。\n\n你的心跳漏了一拍,一时间不知该如何回应。" + +【输出格式】(严格JSON,不要有任何额外文字) +{ + "nodes": { + "branch_1": { + "content": "新剧情第一段(150-300字)...", + "speaker": "旁白", + "choices": [ + {"text": "选项A(5-15字)", "nextNodeKey": "branch_2a"}, + {"text": "选项B(5-15字)", "nextNodeKey": "branch_2b"} + ] + }, + "branch_2a": { + "content": "...", + "speaker": "旁白", + "choices": [...] + }, + "branch_ending_good": { + "content": "好结局内容(200-400字)...", + "speaker": "旁白", + "is_ending": true, + "ending_name": "结局名称(4-8字)", + "ending_type": "good" + } + }, + "entryNodeKey": "branch_1" +}""" + + # 构建用户提示词 + user_prompt_text = f"""【原故事信息】 +故事标题:{story_title} +故事分类:{story_category} + +【用户已走过的剧情】 +{path_text} + +【当前节点】 +{current_content} + +【用户改写指令】 +{user_prompt} + +请创作新的剧情分支(输出JSON格式):""" + + print(f"[rewrite_branch] 提示词构建完成,开始调用AI...") + print(f"[rewrite_branch] provider={self.provider}") + + try: + result = None + if self.provider == "openai": + print(f"[rewrite_branch] 调用 OpenAI...") + result = await self._call_openai_long(system_prompt, user_prompt_text) + elif self.provider == "claude": + print(f"[rewrite_branch] 调用 Claude...") + result = await self._call_claude(f"{system_prompt}\n\n{user_prompt_text}") + elif self.provider == "qwen": + print(f"[rewrite_branch] 调用 Qwen...") + result = await self._call_qwen_long(system_prompt, user_prompt_text) + elif self.provider == "deepseek": + print(f"[rewrite_branch] 调用 DeepSeek...") + result = await self._call_deepseek_long(system_prompt, user_prompt_text) + + print(f"[rewrite_branch] AI调用完成,result存在={result is not None}") + + if result and result.get("content"): + print(f"[rewrite_branch] AI返回内容长度={len(result.get('content', ''))}") + print(f"[rewrite_branch] AI返回内容前500字: {result.get('content', '')[:500]}") + + # 解析JSON响应 + parsed = self._parse_branch_json(result["content"]) + print(f"[rewrite_branch] JSON解析结果: parsed存在={parsed is not None}") + + if parsed: + parsed["tokens_used"] = result.get("tokens_used", 0) + print(f"[rewrite_branch] 成功! nodes数量={len(parsed.get('nodes', {}))}, tokens={parsed.get('tokens_used')}") + return parsed + else: + print(f"[rewrite_branch] JSON解析失败!") + else: + print(f"[rewrite_branch] AI返回为空或无content") + + return None + except Exception as e: + print(f"[rewrite_branch] 异常: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + return None + + def _parse_branch_json(self, content: str) -> Optional[Dict]: + """解析AI返回的分支JSON""" + print(f"[_parse_branch_json] 开始解析,内容长度={len(content)}") + + # 移除 markdown 代码块标记 + clean_content = content.strip() + if clean_content.startswith('```'): + # 移除开头的 ```json 或 ``` + clean_content = re.sub(r'^```(?:json)?\s*', '', clean_content) + # 移除结尾的 ``` + clean_content = re.sub(r'\s*```$', '', clean_content) + + try: + # 尝试直接解析 + result = json.loads(clean_content) + print(f"[_parse_branch_json] 直接解析成功!") + return result + except json.JSONDecodeError as e: + print(f"[_parse_branch_json] 直接解析失败: {e}") + + # 尝试提取JSON块 + try: + # 匹配 { ... } 结构 + brace_match = re.search(r'\{[\s\S]*\}', clean_content) + if brace_match: + json_str = brace_match.group(0) + print(f"[_parse_branch_json] 找到花括号块,尝试解析...") + + try: + result = json.loads(json_str) + print(f"[_parse_branch_json] 花括号块解析成功!") + return result + except json.JSONDecodeError as e: + print(f"[_parse_branch_json] 花括号块解析失败: {e}") + # 打印错误位置附近的内容 + error_pos = e.pos if hasattr(e, 'pos') else 0 + start = max(0, error_pos - 100) + end = min(len(json_str), error_pos + 100) + print(f"[_parse_branch_json] 错误位置附近内容: ...{json_str[start:end]}...") + + # 尝试修复不完整的 JSON + print(f"[_parse_branch_json] 尝试修复不完整的JSON...") + fixed_json = self._try_fix_incomplete_json(json_str) + if fixed_json: + print(f"[_parse_branch_json] JSON修复成功!") + return fixed_json + + except Exception as e: + print(f"[_parse_branch_json] 提取解析异常: {e}") + + print(f"[_parse_branch_json] 所有解析方法都失败了") + return None + + def _try_fix_incomplete_json(self, json_str: str) -> Optional[Dict]: + """尝试修复不完整的JSON(被截断的情况)""" + try: + # 找到已完成的节点,截断不完整的部分 + # 查找最后一个完整的节点(以 } 结尾,后面跟着逗号或闭括号) + + # 先找到 "nodes": { 的位置 + nodes_match = re.search(r'"nodes"\s*:\s*\{', json_str) + if not nodes_match: + return None + + nodes_start = nodes_match.end() + + # 找所有完整的 branch 节点 + branch_pattern = r'"branch_\w+"\s*:\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}' + branches = list(re.finditer(branch_pattern, json_str[nodes_start:])) + + if not branches: + return None + + # 取最后一个完整的节点的结束位置 + last_complete_end = nodes_start + branches[-1].end() + + # 构建修复后的 JSON + # 截取到最后一个完整节点,然后补全结构 + truncated = json_str[:last_complete_end] + + # 补全 JSON 结构 + fixed = truncated + '\n },\n "entryNodeKey": "branch_1"\n}' + + print(f"[_try_fix_incomplete_json] 修复后的JSON长度: {len(fixed)}") + result = json.loads(fixed) + + # 验证结果结构 + if "nodes" in result and len(result["nodes"]) > 0: + print(f"[_try_fix_incomplete_json] 修复后节点数: {len(result['nodes'])}") + return result + + except Exception as e: + print(f"[_try_fix_incomplete_json] 修复失败: {e}") + + return None + + async def _call_deepseek_long(self, system_prompt: str, user_prompt: str) -> Optional[Dict]: + """调用 DeepSeek API (长文本版本)""" + print(f"[_call_deepseek_long] 开始调用...") + print(f"[_call_deepseek_long] base_url={self.base_url}") + print(f"[_call_deepseek_long] model={self.model}") + + 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.85, + "max_tokens": 6000 # 增加输出长度,确保JSON完整 + } + + print(f"[_call_deepseek_long] system_prompt长度={len(system_prompt)}") + print(f"[_call_deepseek_long] user_prompt长度={len(user_prompt)}") + + async with httpx.AsyncClient(timeout=300.0) as client: + try: + print(f"[_call_deepseek_long] 发送请求到 {url}...") + response = await client.post(url, headers=headers, json=data) + print(f"[_call_deepseek_long] 响应状态码: {response.status_code}") + + response.raise_for_status() + result = response.json() + + print(f"[_call_deepseek_long] 响应JSON keys: {result.keys()}") + + if "choices" in result and len(result["choices"]) > 0: + content = result["choices"][0]["message"]["content"] + tokens = result.get("usage", {}).get("total_tokens", 0) + print(f"[_call_deepseek_long] 成功! content长度={len(content)}, tokens={tokens}") + return {"content": content.strip(), "tokens_used": tokens} + else: + print(f"[_call_deepseek_long] 响应异常,无choices: {result}") + return None + except httpx.HTTPStatusError as e: + print(f"[_call_deepseek_long] HTTP错误: {e.response.status_code} - {e.response.text}") + return None + except httpx.TimeoutException as e: + print(f"[_call_deepseek_long] 请求超时: {e}") + return None + except Exception as e: + print(f"[_call_deepseek_long] 其他错误: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + return None + + async def _call_openai_long(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": 2000 + } + + async with httpx.AsyncClient(timeout=60.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_qwen_long(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, + "max_tokens": 2000 + } + } + + async with httpx.AsyncClient(timeout=60.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_openai(self, system_prompt: str, user_prompt: str) -> Optional[Dict]: """调用OpenAI API""" url = f"{self.base_url}/chat/completions"