diff --git a/.env.example b/.env.example index 1374cf2..fb56025 100644 --- a/.env.example +++ b/.env.example @@ -30,4 +30,26 @@ FILE_UPLOAD_LIMIT=10 # API请求限制 RATE_LIMIT_WINDOW_MS=900000 -RATE_LIMIT_MAX_REQUESTS=100 \ No newline at end of file +RATE_LIMIT_MAX_REQUESTS=100 + +# AI解读功能配置 +# AI API密钥(请替换为您的实际API Key) +VITE_AI_API_KEY=your-openai-api-key-here + +# AI API服务地址 +VITE_AI_API_URL=https://api.openai.com/v1/chat/completions + +# AI模型名称 +VITE_AI_MODEL_NAME=gpt-3.5-turbo + +# AI请求最大Token数 +VITE_AI_MAX_TOKENS=2000 + +# AI温度参数(0-2,控制回答的创造性) +VITE_AI_TEMPERATURE=0.7 + +# AI请求超时时间(毫秒) +VITE_AI_TIMEOUT=30000 + +# AI流式响应 +VITE_AI_STREAM=true \ No newline at end of file diff --git a/AI_INTERPRETATION_GUIDE.md b/AI_INTERPRETATION_GUIDE.md new file mode 100644 index 0000000..cacc088 --- /dev/null +++ b/AI_INTERPRETATION_GUIDE.md @@ -0,0 +1,204 @@ +# AI解读功能使用指南 + +## 功能概述 + +AI解读功能是本系统的智能增强特性,通过调用大语言模型(如GPT)对传统命理分析结果进行深度解读和现代化阐释,为用户提供更加通俗易懂、贴近现代生活的指导建议。 + +## 主要特性 + +### 🧠 智能解读 +- 对八字、紫微斗数、易经占卜结果进行AI深度分析 +- 将古典命理术语转换为现代语言 +- 提供实用的人生指导和建议 + +### ⚙️ 灵活配置 +- 支持多种AI服务提供商(OpenAI、Azure OpenAI等) +- 可自定义模型参数(温度、最大Token数等) +- 支持自定义提示词模板 + +### 💾 结果缓存 +- 自动保存AI解读结果到本地存储 +- 避免重复调用,节省API费用 +- 支持重新解读功能 + +## 使用方法 + +### 1. 配置AI服务 + +#### 方法一:环境变量配置(推荐) +在项目根目录的 `.env` 文件中添加以下配置: + +```env +# AI API密钥 +VITE_AI_API_KEY=your-openai-api-key-here + +# AI API服务地址 +VITE_AI_API_URL=https://api.openai.com/v1/chat/completions + +# AI模型名称 +VITE_AI_MODEL_NAME=gpt-3.5-turbo + +# AI请求最大Token数 +VITE_AI_MAX_TOKENS=2000 + +# AI温度参数(0-2,控制回答的创造性) +VITE_AI_TEMPERATURE=0.7 + +# AI请求超时时间(毫秒) +VITE_AI_TIMEOUT=30000 +``` + +#### 方法二:界面配置 +1. 在分析结果页面点击「AI解读」按钮 +2. 如果未配置,系统会提示配置AI设置 +3. 点击「配置」按钮打开配置界面 +4. 填写API Key、API地址等信息 +5. 点击「测试连接」验证配置 +6. 保存配置 + +### 2. 使用AI解读 + +#### 在分析结果页面 +1. 完成命理分析后,在结果页面找到「AI智能解读」区域 +2. 点击「AI解读」按钮 +3. 等待AI分析完成 +4. 查看AI解读结果 + +#### 在历史记录页面 +1. 进入历史记录页面 +2. 在记录列表中点击「AI解读」按钮,或 +3. 点击「查看」进入详情页,然后使用AI解读功能 + +### 3. 管理解读结果 + +- **查看解读**:点击「查看AI解读」展开/收起解读内容 +- **重新解读**:点击「重新解读」按钮获取新的AI分析 +- **配置管理**:点击「配置」按钮修改AI设置 + +## 支持的AI服务商 + +### OpenAI +- **API地址**:`https://api.openai.com/v1/chat/completions` +- **推荐模型**:`gpt-3.5-turbo`、`gpt-4` +- **获取API Key**:访问 [OpenAI官网](https://platform.openai.com/api-keys) + +### Azure OpenAI +- **API地址**:`https://your-resource.openai.azure.com/openai/deployments/your-deployment/chat/completions?api-version=2023-12-01-preview` +- **认证方式**:需要在请求头中使用 `api-key` 而不是 `Authorization` + +### 其他兼容服务 +任何兼容OpenAI Chat Completions API格式的服务都可以使用,包括: +- Anthropic Claude(通过代理) +- 本地部署的开源模型 +- 其他云服务商的API + +## 提示词模板 + +系统为不同的分析类型预设了专业的提示词模板: + +### 八字分析提示词 +``` +你是一位专业的八字命理大师,请对以下八字分析结果进行深度解读和补充说明。 + +请从以下几个方面进行解读: +1. 命格特点的深层含义 +2. 五行平衡对人生的具体影响 +3. 大运流年的关键转折点 +4. 实用的人生建议和注意事项 +5. 现代生活中的应用指导 + +请用通俗易懂的语言,结合现代生活实际,给出具有指导意义的解读。 +``` + +### 紫微斗数提示词 +``` +你是一位资深的紫微斗数专家,请对以下紫微斗数分析结果进行专业解读。 + +请重点分析: +1. 命宫主星的性格特质解析 +2. 十二宫位的相互影响 +3. 大限小限的运势变化 +4. 桃花、财帛、事业等重点宫位分析 +5. 现实生活中的应用建议 + +请结合现代社会背景,提供实用的人生指导。 +``` + +### 易经占卜提示词 +``` +你是一位精通易经的占卜大师,请对以下易经占卜结果进行深入解读。 + +请从以下角度分析: +1. 卦象的深层寓意 +2. 爻辞的具体指导意义 +3. 变卦的发展趋势 +4. 针对问题的具体建议 +5. 行动时机和注意事项 + +请用现代语言解释古典智慧,提供切实可行的指导。 +``` + +## 费用说明 + +- AI解读功能需要调用第三方AI服务,会产生API费用 +- 费用由您的AI服务商账户承担 +- 建议合理设置Token限制以控制费用 +- 系统会缓存解读结果,避免重复调用 + +## 常见问题 + +### Q: 为什么AI解读按钮是灰色的? +A: 这表示AI配置不完整,请检查API Key、API地址等配置是否正确。 + +### Q: AI解读失败怎么办? +A: 请检查: +1. 网络连接是否正常 +2. API Key是否有效 +3. API地址是否正确 +4. 账户余额是否充足 + +### Q: 可以使用免费的AI服务吗? +A: 可以使用任何兼容OpenAI API格式的服务,包括一些提供免费额度的服务。 + +### Q: AI解读结果不满意怎么办? +A: 可以: +1. 点击「重新解读」获取新的分析 +2. 调整温度参数改变AI的创造性 +3. 修改提示词模板以获得更符合需求的解读 + +### Q: 解读结果会保存吗? +A: 是的,解读结果会自动保存到浏览器本地存储中,下次查看同一分析时会直接显示已保存的解读。 + +## 技术实现 + +### 架构设计 +- **配置管理**:`src/config/aiConfig.ts` +- **服务层**:`src/services/aiInterpretationService.ts` +- **UI组件**:`src/components/ui/AIInterpretationButton.tsx` +- **配置界面**:`src/components/ui/AIConfigModal.tsx` + +### 数据流程 +1. 用户触发AI解读 +2. 系统检查配置有效性 +3. 将分析结果转换为Markdown格式 +4. 使用提示词模板构建请求 +5. 调用AI API获取解读 +6. 显示结果并保存到本地存储 + +### 安全考虑 +- API Key等敏感信息存储在本地 +- 支持环境变量配置避免硬编码 +- 请求超时保护 +- 错误处理和用户友好提示 + +## 更新日志 + +### v1.0.0 +- 初始版本发布 +- 支持八字、紫微斗数、易经三种分析类型的AI解读 +- 提供完整的配置界面和结果管理功能 +- 集成到分析结果页面和历史记录页面 + +--- + +如有其他问题或建议,请联系开发团队。 \ No newline at end of file diff --git a/src/components/CompleteBaziAnalysis.tsx b/src/components/CompleteBaziAnalysis.tsx index 38eeeea..e05e43d 100644 --- a/src/components/CompleteBaziAnalysis.tsx +++ b/src/components/CompleteBaziAnalysis.tsx @@ -4,6 +4,8 @@ import { Calendar, Star, BookOpen, Sparkles, User, BarChart3, Zap, TrendingUp, L import { Card, CardContent, CardHeader, CardTitle } from './ui/Card'; import { BackToTop } from './ui/BackToTop'; import DownloadButton from './ui/DownloadButton'; +import AIInterpretationButton from './ui/AIInterpretationButton'; +import AIConfigModal from './ui/AIConfigModal'; import { localApi } from '../lib/localApi'; interface CompleteBaziAnalysisProps { @@ -20,6 +22,7 @@ const CompleteBaziAnalysis: React.FC = ({ birthDate, const [isLoading, setIsLoading] = useState(!propAnalysisData); const [error, setError] = useState(null); const [analysisData, setAnalysisData] = useState(propAnalysisData || null); + const [showAIConfig, setShowAIConfig] = useState(false); // 五行颜色配置 const elementColors: { [key: string]: string } = { @@ -278,15 +281,26 @@ const CompleteBaziAnalysis: React.FC = ({ birthDate,
- {/* 下载按钮 */} -
- + {/* 下载和AI解读按钮 */} +
+
+ setShowAIConfig(true)} + className="sticky top-4 z-10" + /> +
+
+ +
{/* 标题和基本信息 */} @@ -1047,6 +1061,16 @@ const CompleteBaziAnalysis: React.FC = ({ birthDate, {/* 回到顶部按钮 */} + + {/* AI配置模态框 */} + setShowAIConfig(false)} + onConfigSaved={() => { + setShowAIConfig(false); + // 可以在这里添加配置保存后的逻辑 + }} + />
); }; diff --git a/src/components/CompleteYijingAnalysis.tsx b/src/components/CompleteYijingAnalysis.tsx index 4b24276..c4dfe41 100644 --- a/src/components/CompleteYijingAnalysis.tsx +++ b/src/components/CompleteYijingAnalysis.tsx @@ -3,6 +3,8 @@ import { Calendar, Star, BookOpen, Sparkles, User, BarChart3, Zap, TrendingUp, L import { Card, CardContent, CardHeader, CardTitle } from './ui/Card'; import { BackToTop } from './ui/BackToTop'; import DownloadButton from './ui/DownloadButton'; +import AIInterpretationButton from './ui/AIInterpretationButton'; +import AIConfigModal from './ui/AIConfigModal'; import { localApi } from '../lib/localApi'; interface CompleteYijingAnalysisProps { @@ -21,6 +23,7 @@ const CompleteYijingAnalysis: React.FC = ({ const [isLoading, setIsLoading] = useState(!propAnalysisData); const [error, setError] = useState(null); const [analysisData, setAnalysisData] = useState(propAnalysisData || null); + const [showAIConfig, setShowAIConfig] = useState(false); // 卦象颜色配置 const hexagramColors: { [key: string]: string } = { @@ -266,15 +269,26 @@ const CompleteYijingAnalysis: React.FC = ({
- {/* 下载按钮 */} -
- + {/* 下载和AI解读按钮 */} +
+
+ setShowAIConfig(true)} + className="sticky top-4 z-10" + /> +
+
+ +
{/* 标题和基本信息 */} @@ -748,10 +762,22 @@ const CompleteYijingAnalysis: React.FC = ({
+ +
{/* 返回顶部按钮 */} + + {/* AI配置模态框 */} + setShowAIConfig(false)} + onConfigSaved={() => { + setShowAIConfig(false); + // 可以在这里添加配置保存后的逻辑 + }} + />
); }; diff --git a/src/components/CompleteZiweiAnalysis.tsx b/src/components/CompleteZiweiAnalysis.tsx index 0cb0c68..2c63c43 100644 --- a/src/components/CompleteZiweiAnalysis.tsx +++ b/src/components/CompleteZiweiAnalysis.tsx @@ -6,6 +6,8 @@ import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } import { ChineseLoading } from './ui/ChineseLoading'; import { BackToTop } from './ui/BackToTop'; import DownloadButton from './ui/DownloadButton'; +import AIInterpretationButton from './ui/AIInterpretationButton'; +import AIConfigModal from './ui/AIConfigModal'; import { localApi } from '../lib/localApi'; import { cn } from '../lib/utils'; @@ -23,6 +25,7 @@ const CompleteZiweiAnalysis: React.FC = ({ birthDate const [isLoading, setIsLoading] = useState(!propAnalysisData); const [error, setError] = useState(null); const [analysisData, setAnalysisData] = useState(propAnalysisData || null); + const [showAIConfig, setShowAIConfig] = useState(false); // 四化飞星详细解释 const sihuaExplanations = { @@ -581,15 +584,26 @@ const CompleteZiweiAnalysis: React.FC = ({ birthDate
- {/* 下载按钮 */} -
- + {/* 下载和AI解读按钮 */} +
+
+ setShowAIConfig(true)} + className="sticky top-4 z-10" + /> +
+
+ +
{/* 标题和基本信息 */} @@ -1562,10 +1576,22 @@ const CompleteZiweiAnalysis: React.FC = ({ birthDate
+ +
{/* 回到顶部按钮 */} + + {/* AI配置模态框 */} + setShowAIConfig(false)} + onConfigSaved={() => { + setShowAIConfig(false); + // 可以在这里添加配置保存后的逻辑 + }} + />
); }; diff --git a/src/components/ui/AIConfigModal.tsx b/src/components/ui/AIConfigModal.tsx new file mode 100644 index 0000000..70d1aca --- /dev/null +++ b/src/components/ui/AIConfigModal.tsx @@ -0,0 +1,353 @@ +import React, { useState, useEffect } from 'react'; +import { X, Settings, Eye, EyeOff, Save, TestTube, CheckCircle, AlertCircle } from 'lucide-react'; +import { ChineseButton } from './ChineseButton'; +import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from './ChineseCard'; +import { getAIConfig, saveAIConfig, validateAIConfig, AIConfig, defaultAIConfig } from '../../config/aiConfig'; +import { toast } from 'sonner'; +import { cn } from '../../lib/utils'; + +interface AIConfigModalProps { + isOpen: boolean; + onClose: () => void; + onConfigSaved?: () => void; +} + +const AIConfigModal: React.FC = ({ + isOpen, + onClose, + onConfigSaved +}) => { + const [config, setConfig] = useState({ + apiKey: '', + apiUrl: '', + modelName: '', + maxTokens: 2000, + temperature: 0.7, + timeout: 30000 + }); + const [showApiKey, setShowApiKey] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [isTesting, setIsTesting] = useState(false); + const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null); + + // 加载当前配置 + useEffect(() => { + if (isOpen) { + const currentConfig = getAIConfig(); + setConfig(currentConfig); + setTestResult(null); + } + }, [isOpen]); + + // 处理输入变化 + const handleInputChange = (field: keyof AIConfig, value: string | number) => { + setConfig(prev => ({ + ...prev, + [field]: value + })); + setTestResult(null); // 清除测试结果 + }; + + // 保存配置 + const handleSave = async () => { + if (!validateAIConfig(config)) { + toast.error('请填写完整的配置信息'); + return; + } + + setIsSaving(true); + try { + saveAIConfig(config); + toast.success('AI配置保存成功'); + if (onConfigSaved) { + onConfigSaved(); + } + onClose(); + } catch (error: any) { + toast.error(`保存配置失败: ${error.message}`); + } finally { + setIsSaving(false); + } + }; + + // 测试配置 + const handleTest = async () => { + if (!validateAIConfig(config)) { + toast.error('请先填写完整的配置信息'); + return; + } + + setIsTesting(true); + setTestResult(null); + + try { + const testRequest = { + model: config.modelName, + messages: [ + { + role: 'user', + content: '请回复"配置测试成功"来确认连接正常。' + } + ], + max_tokens: 50, + temperature: 0.1 + }; + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), config.timeout); + + const response = await fetch(config.apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${config.apiKey}` + }, + body: JSON.stringify(testRequest), + signal: controller.signal + }); + + clearTimeout(timeoutId); + + if (response.ok) { + const data = await response.json(); + if (data.choices && data.choices[0] && data.choices[0].message) { + setTestResult({ + success: true, + message: '连接测试成功!AI API配置正确。' + }); + toast.success('配置测试成功'); + } else { + throw new Error('API响应格式异常'); + } + } else { + const errorData = await response.json().catch(() => ({})); + throw new Error(`API请求失败: ${response.status} ${errorData.error?.message || response.statusText}`); + } + } catch (error: any) { + let errorMessage = '连接测试失败'; + if (error.name === 'AbortError') { + errorMessage = '请求超时,请检查网络连接和API地址'; + } else if (error.message) { + errorMessage = error.message; + } + + setTestResult({ + success: false, + message: errorMessage + }); + toast.error('配置测试失败'); + } finally { + setIsTesting(false); + } + }; + + // 重置为默认配置 + const handleReset = () => { + setConfig({ + ...defaultAIConfig // 使用完整的默认配置,包括API Key + }); + setTestResult(null); + }; + + if (!isOpen) return null; + + return ( +
+
+ + +
+ + + AI解读配置 + + + + +
+
+ + + {/* API Key */} +
+ +
+ handleInputChange('apiKey', e.target.value)} + placeholder="请输入您的API Key" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 pr-10" + /> + +
+
+ + {/* API URL */} +
+ + handleInputChange('apiUrl', e.target.value)} + placeholder="https://api.openai.com/v1/chat/completions" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ + {/* Model Name */} +
+ + handleInputChange('modelName', e.target.value)} + placeholder="gpt-3.5-turbo" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ + {/* Advanced Settings */} +
+
+ + handleInputChange('maxTokens', parseInt(e.target.value) || 2000)} + min="100" + max="4000" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ +
+ + handleInputChange('temperature', parseFloat(e.target.value) || 0.7)} + min="0" + max="2" + step="0.1" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ +
+ + handleInputChange('timeout', parseInt(e.target.value) || 30000)} + min="5000" + max="120000" + step="1000" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+ + {/* Stream Setting */} +
+ +
+ + + 启用后将实时显示AI生成的内容 + +
+
+ + {/* Test Result */} + {testResult && ( +
+ {testResult.success ? ( + + ) : ( + + )} + {testResult.message} +
+ )} + + {/* Action Buttons */} +
+ + 重置默认 + + +
+ + + {isTesting ? '测试中...' : '测试连接'} + + + + + {isSaving ? '保存中...' : '保存配置'} + +
+
+
+
+
+
+ ); +}; + +export default AIConfigModal; \ No newline at end of file diff --git a/src/components/ui/AIInterpretationButton.tsx b/src/components/ui/AIInterpretationButton.tsx new file mode 100644 index 0000000..3b00a3a --- /dev/null +++ b/src/components/ui/AIInterpretationButton.tsx @@ -0,0 +1,474 @@ +import React, { useState, useEffect } from 'react'; +import { Brain, Loader2, Sparkles, AlertCircle, CheckCircle, Settings, RefreshCw, Eye, X } from 'lucide-react'; +import { ChineseButton } from './ChineseButton'; +import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from './ChineseCard'; +import { cn } from '../../lib/utils'; +import { + requestAIInterpretation, + saveAIInterpretation, + getAIInterpretation, + AIInterpretationResult, + AIInterpretationRequest +} from '../../services/aiInterpretationService'; +import { getAIConfig, validateAIConfig, getPromptTemplate } from '../../config/aiConfig'; +import { toast } from 'sonner'; + +interface AIInterpretationButtonProps { + analysisData: any; + analysisType: 'bazi' | 'ziwei' | 'yijing'; + analysisId?: string; // 用于缓存解读结果 + className?: string; + variant?: 'default' | 'outline' | 'ghost'; + size?: 'sm' | 'md' | 'lg'; + showConfigButton?: boolean; // 是否显示配置按钮 + onConfigClick?: () => void; // 配置按钮点击回调 +} + +const AIInterpretationButton: React.FC = ({ + analysisData, + analysisType, + analysisId, + className, + variant = 'default', + size = 'md', + showConfigButton = true, + onConfigClick +}) => { + const [isLoading, setIsLoading] = useState(false); + const [interpretation, setInterpretation] = useState(null); + const [showResult, setShowResult] = useState(false); + const [isConfigValid, setIsConfigValid] = useState(false); + const [debugInfo, setDebugInfo] = useState(null); + const [requestStartTime, setRequestStartTime] = useState(null); + const [streamingContent, setStreamingContent] = useState(''); // 流式内容 + + // 检查AI配置是否有效 + useEffect(() => { + const config = getAIConfig(); + setIsConfigValid(validateAIConfig(config)); + }, []); + + // 加载已保存的解读结果 + useEffect(() => { + if (analysisId) { + const savedInterpretation = getAIInterpretation(analysisId); + if (savedInterpretation) { + setInterpretation(savedInterpretation); + } + } + }, [analysisId]); + + // 处理AI解读请求 + const handleAIInterpretation = async () => { + if (!isConfigValid) { + toast.error('AI配置不完整,请先配置API设置'); + if (onConfigClick) { + onConfigClick(); + } + return; + } + + if (!analysisData) { + toast.error('没有可解读的分析数据'); + return; + } + + setIsLoading(true); + setRequestStartTime(Date.now()); + + // 获取用户配置的AI设置 + const currentConfig = getAIConfig(); + + setDebugInfo({ + status: '开始请求', + startTime: new Date().toLocaleString(), + config: { + apiUrl: currentConfig.apiUrl, + modelName: currentConfig.modelName, + maxTokens: currentConfig.maxTokens, + temperature: currentConfig.temperature, + timeout: currentConfig.timeout, + apiKeyLength: currentConfig.apiKey?.length || 0 + }, + analysisType, + analysisDataSize: JSON.stringify(analysisData).length + }); + + try { + const request: AIInterpretationRequest = { + analysisType, + analysisContent: analysisData, + onStreamUpdate: currentConfig.stream ? (content: string) => { + setStreamingContent(content); + setShowResult(true); // 开始流式输出时就显示结果区域 + } : undefined + }; + + // 获取提示词用于调试显示 + const analysisMarkdown = typeof request.analysisContent === 'string' + ? request.analysisContent + : JSON.stringify(request.analysisContent, null, 2); + + const promptTemplate = getPromptTemplate(request.analysisType); + const fullPrompt = promptTemplate.replace('{analysisContent}', analysisMarkdown); + + // 生成curl命令用于调试 + const requestBody = { + model: currentConfig.modelName, + messages: [{ role: 'user', content: fullPrompt }], + max_tokens: currentConfig.maxTokens, + temperature: currentConfig.temperature + }; + + const curlCommand = `curl -X POST "${currentConfig.apiUrl}" \\ + -H "Content-Type: application/json" \\ + -H "Authorization: Bearer ${currentConfig.apiKey.substring(0, 10)}..." \\ + -d '${JSON.stringify(requestBody, null, 2).replace(/'/g, "'\"'\"'")}'`; + + setDebugInfo(prev => ({ + ...prev, + status: '发送请求中', + requestTime: new Date().toLocaleString(), + apiParams: { + model: currentConfig.modelName, + maxTokens: currentConfig.maxTokens, + temperature: currentConfig.temperature, + promptLength: fullPrompt.length, + promptPreview: fullPrompt.substring(0, 300) + '...', + fullPrompt: fullPrompt, // 完整的prompt用于调试 + requestBody: JSON.stringify(requestBody, null, 2), + curlCommand: curlCommand + } + })); + + const result = await requestAIInterpretation(request); + + const endTime = Date.now(); + const duration = requestStartTime ? endTime - requestStartTime : 0; + + console.log('🐛 调试时间计算 (成功):', { + requestStartTime, + endTime, + duration, + durationSeconds: duration / 1000 + }); + + setDebugInfo(prev => ({ + ...prev, + status: result.success ? '请求成功' : '请求失败', + endTime: new Date().toLocaleString(), + duration: `${duration}ms (${(duration / 1000).toFixed(1)}秒)`, + result: { + success: result.success, + contentLength: result.content?.length || 0, + error: result.error, + model: result.model, + tokensUsed: result.tokensUsed, + actualDuration: duration, + startTime: requestStartTime, + endTime: endTime + } + })); + + if (result.success) { + console.log('AI解读成功,结果:', result); + setInterpretation(result); + setShowResult(true); + setStreamingContent(''); // 清空流式内容,使用最终结果 + + // 保存解读结果 + if (analysisId) { + saveAIInterpretation(analysisId, result); + } + + toast.success(`AI解读完成,耗时${duration}ms`); + } else { + console.error('AI解读失败:', result.error); + toast.error(`AI解读失败: ${result.error}`); + setStreamingContent(''); // 清空流式内容 + } + } catch (error: any) { + const endTime = Date.now(); + const duration = requestStartTime ? endTime - requestStartTime : 0; + + console.log('🐛 调试时间计算:', { + requestStartTime, + endTime, + duration, + durationSeconds: duration / 1000 + }); + + setDebugInfo(prev => ({ + ...prev, + status: '请求异常', + endTime: new Date().toLocaleString(), + duration: `${duration}ms (${(duration / 1000).toFixed(1)}秒)`, + error: { + name: error.name, + message: error.message, + stack: error.stack?.substring(0, 500), + actualDuration: duration, + startTime: requestStartTime, + endTime: endTime + } + })); + + console.error('AI解读出错:', error); + toast.error(`解读过程出错: ${error.message || '未知错误'}`); + setStreamingContent(''); // 清空流式内容 + } finally { + setIsLoading(false); + // 不要立即清除requestStartTime,保留用于调试 + // setRequestStartTime(null); + } + }; + + // 重新解读 + const handleReinterpret = () => { + setInterpretation(null); + setShowResult(false); + handleAIInterpretation(); + }; + + // 获取分析类型显示名称 + const getAnalysisTypeName = (type: string) => { + const names = { + 'bazi': '八字', + 'ziwei': '紫微斗数', + 'yijing': '易经' + }; + return names[type as keyof typeof names] || '命理'; + }; + + return ( +
+ {/* AI解读按钮区域 */} +
+ setShowResult(!showResult) : handleAIInterpretation} + disabled={isLoading || (!isConfigValid && !interpretation)} + className={cn( + 'px-3 sm:px-6 text-xs sm:text-sm', + !isConfigValid && !interpretation && 'opacity-50 cursor-not-allowed' + )} + > + {isLoading ? ( + + ) : ( + + )} + + {isLoading + ? 'AI解读中...' + : interpretation + ? (showResult ? '隐藏解读' : 'AI解读') + : 'AI解读' + } + + + + {/* 重新解读按钮 */} + {interpretation && ( + + + 重新解读 + + )} + + {/* 配置按钮 */} + {showConfigButton && onConfigClick && ( + + + 配置 + + )} +
+ + {/* 配置提示 */} + {!isConfigValid && !interpretation && ( +
+ +
+

需要配置AI设置

+

请先配置API Key、API地址等信息才能使用AI解读功能

+
+
+ )} + + {/* 调试信息 */} + {debugInfo && ( +
+
+
🔍 AI解读调试信息
+ +
+
+
状态: {debugInfo.status}
+
开始时间: {debugInfo.startTime}
+ {debugInfo.endTime &&
结束时间: {debugInfo.endTime}
} + {debugInfo.duration &&
耗时: {debugInfo.duration}
} +
分析类型: {debugInfo.analysisType}
+
数据大小: {debugInfo.analysisDataSize} 字符
+ + {debugInfo.config && ( +
+ 配置信息 +
+
API地址: {debugInfo.config.apiUrl}
+
模型: {debugInfo.config.modelName}
+
最大Token: {debugInfo.config.maxTokens}
+
温度: {debugInfo.config.temperature}
+
超时: {debugInfo.config.timeout}ms
+
API Key长度: {debugInfo.config.apiKeyLength}
+
+
+ )} + + {debugInfo.apiParams && ( +
+ API调用参数 +
+
模型: {debugInfo.apiParams.model}
+
最大Token: {debugInfo.apiParams.maxTokens}
+
温度: {debugInfo.apiParams.temperature}
+
Prompt长度: {debugInfo.apiParams.promptLength} 字符
+
Prompt预览:
+
{debugInfo.apiParams.promptPreview}
+ +
+ 查看完整Prompt +
{debugInfo.apiParams.fullPrompt}
+
+ +
+ 查看请求体JSON +
{debugInfo.apiParams.requestBody}
+
+ +
+ 🔧 API调用指令 (curl) +
+
复制以下命令到终端执行以手动测试API:
+
{debugInfo.apiParams.curlCommand}
+ +
+
+
+
+ )} + + {debugInfo.result && ( +
+ 响应信息 +
+
成功: {debugInfo.result.success ? '是' : '否'}
+
内容长度: {debugInfo.result.contentLength}
+
使用模型: {debugInfo.result.model || 'N/A'}
+
消耗Token: {debugInfo.result.tokensUsed || 'N/A'}
+ {debugInfo.result.error &&
错误: {debugInfo.result.error}
} +
+
时间调试:
+
开始时间戳: {debugInfo.result.startTime}
+
结束时间戳: {debugInfo.result.endTime}
+
实际耗时: {debugInfo.result.actualDuration}ms
+
+
+
+ )} + + {debugInfo.error && ( +
+ 错误详情 +
+
错误类型: {debugInfo.error.name}
+
错误信息: {debugInfo.error.message}
+ {debugInfo.error.stack && ( +
堆栈:
{debugInfo.error.stack}
+ )} +
+
时间调试:
+
开始时间戳: {debugInfo.error.startTime}
+
结束时间戳: {debugInfo.error.endTime}
+
实际耗时: {debugInfo.error.actualDuration}ms
+
+
+
+ )} +
+
+ )} + + {/* AI解读结果显示 */} + {(interpretation || streamingContent) && showResult && ( + + + + {isLoading ? ( + + ) : ( + + )} + AI智能解读 - {getAnalysisTypeName(analysisType)} + {isLoading && streamingContent && ( + 正在生成中... + )} + + {interpretation && ( +
+ 解读时间: {new Date(interpretation.timestamp).toLocaleString('zh-CN')} + {interpretation.model && 模型: {interpretation.model}} + {interpretation.tokensUsed && 消耗Token: {interpretation.tokensUsed}} +
+ )} +
+ + {interpretation && !interpretation.success ? ( +
+ +
+

解读失败

+

{interpretation.error}

+
+
+ ) : ( +
+
+ {streamingContent || interpretation?.content} + {isLoading && streamingContent && ( + + )} +
+
+ )} +
+
+ )} +
+ ); +}; + +export default AIInterpretationButton; \ No newline at end of file diff --git a/src/config/aiConfig.ts b/src/config/aiConfig.ts new file mode 100644 index 0000000..e6ce883 --- /dev/null +++ b/src/config/aiConfig.ts @@ -0,0 +1,108 @@ +// AI解读功能配置文件 +export interface AIConfig { + apiKey: string; + apiUrl: string; + modelName: string; + maxTokens: number; + temperature: number; + timeout: number; + stream: boolean; +} + +// 默认AI配置 +export const defaultAIConfig: AIConfig = { + apiKey: import.meta.env.VITE_AI_API_KEY || 'dee444451bdf4232920a88ef430ce753.Z4SAbECrSnf5JMq7', + apiUrl: import.meta.env.VITE_AI_API_URL || 'https://open.bigmodel.cn/api/paas/v4/chat/completions', + modelName: import.meta.env.VITE_AI_MODEL_NAME || 'GLM-4.5', + maxTokens: parseInt(import.meta.env.VITE_AI_MAX_TOKENS || '50000'), + temperature: parseFloat(import.meta.env.VITE_AI_TEMPERATURE || '0.6'), + timeout: parseInt(import.meta.env.VITE_AI_TIMEOUT || '120000'), + stream: import.meta.env.VITE_AI_STREAM === 'false' ? false : true +}; + +// AI解读提示词模板 +export const aiPromptTemplates = { + bazi: `你是一位专业的八字命理大师,请对以下八字分析结果进行深度解读和补充说明。 + +请从以下几个方面进行解读: +1. 命格特点的深层含义 +2. 五行平衡对人生的具体影响 +3. 大运流年的关键转折点 +4. 实用的人生建议和注意事项 +5. 现代生活中的应用指导 + +请用通俗易懂的语言,结合现代生活实际,给出具有指导意义的解读。 + +八字分析结果: +{analysisContent} + +请提供详细的AI解读:`, + + ziwei: `你是一位资深的紫微斗数专家,请对以下紫微斗数分析结果进行专业解读。 + +请重点分析: +1. 命宫主星的性格特质解析 +2. 十二宫位的相互影响 +3. 大限小限的运势变化 +4. 桃花、财帛、事业等重点宫位分析 +5. 现实生活中的应用建议 + +请结合现代社会背景,提供实用的人生指导。 + +紫微斗数分析结果: +{analysisContent} + +请提供专业的AI解读:`, + + yijing: `你是一位精通易经的占卜大师,请对以下易经占卜结果进行深入解读。 + +请从以下角度分析: +1. 卦象的深层寓意 +2. 爻辞的具体指导意义 +3. 变卦的发展趋势 +4. 针对问题的具体建议 +5. 行动时机和注意事项 + +请用现代语言解释古典智慧,提供切实可行的指导。 + +易经占卜结果: +{analysisContent} + +请提供智慧的AI解读:` +}; + +// 获取AI配置 +export const getAIConfig = (): AIConfig => { + // 可以从localStorage或其他存储中读取用户自定义配置 + const savedConfig = localStorage.getItem('ai-config'); + if (savedConfig) { + try { + const parsedConfig = JSON.parse(savedConfig); + return { ...defaultAIConfig, ...parsedConfig }; + } catch (error) { + console.warn('解析AI配置失败,使用默认配置:', error); + } + } + return defaultAIConfig; +}; + +// 保存AI配置 +export const saveAIConfig = (config: Partial): void => { + try { + const currentConfig = getAIConfig(); + const newConfig = { ...currentConfig, ...config }; + localStorage.setItem('ai-config', JSON.stringify(newConfig)); + } catch (error) { + console.error('保存AI配置失败:', error); + } +}; + +// 验证AI配置 +export const validateAIConfig = (config: AIConfig): boolean => { + return !!(config.apiKey && config.apiUrl && config.modelName); +}; + +// 获取提示词模板 +export const getPromptTemplate = (analysisType: 'bazi' | 'ziwei' | 'yijing'): string => { + return aiPromptTemplates[analysisType] || aiPromptTemplates.bazi; +}; \ No newline at end of file diff --git a/src/pages/HistoryPage.tsx b/src/pages/HistoryPage.tsx index 798207a..8ff3dc6 100644 --- a/src/pages/HistoryPage.tsx +++ b/src/pages/HistoryPage.tsx @@ -170,6 +170,7 @@ const HistoryPage: React.FC = () => {
+ { getInputDataValue(selectedReading.input_data, 'divination_method', 'time') : undefined} preAnalysisData={selectedReading.analysis} /> + ); } @@ -272,6 +274,7 @@ const HistoryPage: React.FC = () => { 查看 + { )} + + ); }; diff --git a/src/services/aiInterpretationService.ts b/src/services/aiInterpretationService.ts new file mode 100644 index 0000000..7fe9bd1 --- /dev/null +++ b/src/services/aiInterpretationService.ts @@ -0,0 +1,536 @@ +import { getAIConfig, validateAIConfig, getPromptTemplate } from '../config/aiConfig'; + +// AI解读结果接口 +export interface AIInterpretationResult { + success: boolean; + content?: string; + error?: string; + timestamp: string; + model?: string; + tokensUsed?: number; +} + +// AI解读请求参数 +export interface AIInterpretationRequest { + analysisType: 'bazi' | 'ziwei' | 'yijing'; + analysisContent: string; + customPrompt?: string; + onStreamUpdate?: (content: string) => void; // 流式更新回调 +} + +// 将分析结果转换为Markdown格式 +const convertAnalysisToMarkdown = (analysisData: any, analysisType: string): string => { + try { + let markdown = `# ${getAnalysisTitle(analysisType)}分析结果\n\n`; + + // 根据不同分析类型生成不同的Markdown内容 + switch (analysisType) { + case 'bazi': + markdown += generateBaziMarkdown(analysisData); + break; + case 'ziwei': + markdown += generateZiweiMarkdown(analysisData); + break; + case 'yijing': + markdown += generateYijingMarkdown(analysisData); + break; + default: + markdown += JSON.stringify(analysisData, null, 2); + } + + return markdown; + } catch (error) { + console.error('转换分析结果为Markdown失败:', error); + return JSON.stringify(analysisData, null, 2); + } +}; + +// 生成八字分析的Markdown +const generateBaziMarkdown = (data: any): string => { + let markdown = ''; + + if (data.basic_info) { + markdown += '## 基本信息\n\n'; + if (data.basic_info.bazi_chart) { + markdown += '### 八字排盘\n'; + const chart = data.basic_info.bazi_chart; + markdown += `- 年柱: ${chart.year_pillar?.stem}${chart.year_pillar?.branch}\n`; + markdown += `- 月柱: ${chart.month_pillar?.stem}${chart.month_pillar?.branch}\n`; + markdown += `- 日柱: ${chart.day_pillar?.stem}${chart.day_pillar?.branch}\n`; + markdown += `- 时柱: ${chart.hour_pillar?.stem}${chart.hour_pillar?.branch}\n\n`; + } + + if (data.basic_info.pillar_interpretations) { + markdown += '### 四柱解释\n'; + const interpretations = data.basic_info.pillar_interpretations; + markdown += `**年柱**: ${interpretations.year_pillar}\n\n`; + markdown += `**月柱**: ${interpretations.month_pillar}\n\n`; + markdown += `**日柱**: ${interpretations.day_pillar}\n\n`; + markdown += `**时柱**: ${interpretations.hour_pillar}\n\n`; + } + } + + if (data.geju_analysis) { + markdown += '## 格局分析\n\n'; + markdown += `${data.geju_analysis.pattern_analysis || ''}\n\n`; + } + + if (data.dayun_analysis) { + markdown += '## 大运分析\n\n'; + if (data.dayun_analysis.current_dayun) { + markdown += `**当前大运**: ${data.dayun_analysis.current_dayun.period} (${data.dayun_analysis.current_dayun.age_range})\n`; + markdown += `${data.dayun_analysis.current_dayun.analysis}\n\n`; + } + } + + if (data.life_guidance) { + markdown += '## 人生指导\n\n'; + markdown += `${data.life_guidance.overall_summary || ''}\n\n`; + } + + return markdown; +}; + +// 生成紫微斗数分析的Markdown +const generateZiweiMarkdown = (data: any): string => { + let markdown = ''; + + if (data.basic_chart) { + markdown += '## 基本命盘\n\n'; + markdown += `${JSON.stringify(data.basic_chart, null, 2)}\n\n`; + } + + if (data.palace_analysis) { + markdown += '## 宫位分析\n\n'; + markdown += `${data.palace_analysis}\n\n`; + } + + return markdown; +}; + +// 生成易经分析的Markdown +const generateYijingMarkdown = (data: any): string => { + let markdown = ''; + + // 基本信息 + if (data.basic_info) { + markdown += '## 占卜基本信息\n\n'; + if (data.basic_info.divination_data) { + markdown += `**问题**: ${data.basic_info.divination_data.question}\n`; + markdown += `**占卜方法**: ${data.basic_info.divination_data.method}\n`; + markdown += `**占卜时间**: ${data.basic_info.divination_data.divination_time}\n\n`; + } + } + + // 卦象信息 + if (data.basic_info?.hexagram_info) { + const hexInfo = data.basic_info.hexagram_info; + markdown += '## 卦象信息\n\n'; + + // 本卦信息 + markdown += `**本卦**: ${hexInfo.main_hexagram} (第${hexInfo.main_hexagram_number}卦)\n`; + markdown += `**卦象符号**: ${hexInfo.main_hexagram_symbol}\n`; + markdown += `**卦辞**: ${hexInfo.hexagram_description}\n`; + + // 卦象结构 + if (hexInfo.hexagram_structure) { + markdown += `**上卦**: ${hexInfo.hexagram_structure.upper_trigram}\n`; + markdown += `**下卦**: ${hexInfo.hexagram_structure.lower_trigram}\n`; + } + + // 变卦信息 + if (hexInfo.changing_hexagram && hexInfo.changing_hexagram !== '无') { + markdown += `**变卦**: ${hexInfo.changing_hexagram}\n`; + markdown += `**变卦符号**: ${hexInfo.changing_hexagram_symbol}\n`; + } else { + markdown += `**变卦**: 无变卦\n`; + } + + markdown += '\n'; + } + + // 详细分析 + if (data.detailed_analysis) { + const analysis = data.detailed_analysis; + + // 卦象分析 + if (analysis.hexagram_analysis) { + markdown += '## 卦象分析\n\n'; + markdown += `**主要含义**: ${analysis.hexagram_analysis.primary_meaning}\n`; + markdown += `**卦辞解释**: ${analysis.hexagram_analysis.judgment}\n`; + markdown += `**象传**: ${analysis.hexagram_analysis.image}\n`; + if (analysis.hexagram_analysis.trigram_analysis) { + markdown += `**卦象分析**: ${analysis.hexagram_analysis.trigram_analysis}\n`; + } + markdown += '\n'; + } + + // 动爻分析 + if (analysis.changing_lines_analysis) { + markdown += '## 动爻分析\n\n'; + markdown += `**动爻数量**: ${analysis.changing_lines_analysis.changing_lines_count}爻\n`; + if (analysis.changing_lines_analysis.changing_line_position) { + markdown += `**动爻位置**: ${analysis.changing_lines_analysis.changing_line_position}\n`; + } + if (analysis.changing_lines_analysis.line_meanings) { + markdown += `**爻辞含义**: ${analysis.changing_lines_analysis.line_meanings}\n`; + } + markdown += '\n'; + } + + // 变卦分析 + if (analysis.changing_hexagram_analysis) { + markdown += '## 变卦分析\n\n'; + markdown += `**变化含义**: ${analysis.changing_hexagram_analysis.meaning}\n`; + markdown += `**转化洞察**: ${analysis.changing_hexagram_analysis.transformation_insight}\n`; + markdown += `**指导建议**: ${analysis.changing_hexagram_analysis.guidance}\n`; + markdown += `**时机把握**: ${analysis.changing_hexagram_analysis.timing}\n`; + markdown += '\n'; + } + + // 高级分析(互卦、错卦、综卦) + if (analysis.advanced_analysis) { + markdown += '## 高级卦象分析\n\n'; + + if (analysis.advanced_analysis.inter_hexagram) { + markdown += `**互卦**: ${analysis.advanced_analysis.inter_hexagram.name}\n`; + markdown += `互卦分析: ${analysis.advanced_analysis.inter_hexagram.analysis}\n\n`; + } + + if (analysis.advanced_analysis.opposite_hexagram) { + markdown += `**错卦**: ${analysis.advanced_analysis.opposite_hexagram.name}\n`; + markdown += `错卦分析: ${analysis.advanced_analysis.opposite_hexagram.analysis}\n\n`; + } + + if (analysis.advanced_analysis.reverse_hexagram) { + markdown += `**综卦**: ${analysis.advanced_analysis.reverse_hexagram.name}\n`; + markdown += `综卦分析: ${analysis.advanced_analysis.reverse_hexagram.analysis}\n\n`; + } + } + + // 五行分析 + if (analysis.hexagram_analysis?.five_elements) { + const elements = analysis.hexagram_analysis.five_elements; + markdown += '## 五行分析\n\n'; + markdown += `**上卦五行**: ${elements.upper_element}\n`; + markdown += `**下卦五行**: ${elements.lower_element}\n`; + markdown += `**五行关系**: ${elements.relationship}\n`; + markdown += `**五行平衡**: ${elements.balance}\n\n`; + } + } + + // 综合解读 + if (data.comprehensive_interpretation) { + markdown += '## 综合解读\n\n'; + markdown += `${data.comprehensive_interpretation}\n\n`; + } + + // 实用建议 + if (data.practical_guidance) { + markdown += '## 实用建议\n\n'; + if (data.practical_guidance.immediate_actions) { + markdown += `**近期行动**: ${data.practical_guidance.immediate_actions}\n`; + } + if (data.practical_guidance.long_term_strategy) { + markdown += `**长期策略**: ${data.practical_guidance.long_term_strategy}\n`; + } + if (data.practical_guidance.timing_advice) { + markdown += `**时机建议**: ${data.practical_guidance.timing_advice}\n`; + } + markdown += '\n'; + } + + return markdown; +}; + +// 获取分析类型标题 +const getAnalysisTitle = (analysisType: string): string => { + const titles = { + 'bazi': '八字命理', + 'ziwei': '紫微斗数', + 'yijing': '易经占卜' + }; + return titles[analysisType as keyof typeof titles] || '命理'; +}; + +// 调用AI API进行解读 +export const requestAIInterpretation = async (request: AIInterpretationRequest): Promise => { + const startTime = Date.now(); + + try { + // 获取AI配置 + const config = getAIConfig(); + + // 验证配置 + if (!validateAIConfig(config)) { + return { + success: false, + error: 'AI配置不完整,请检查API Key、API地址和模型名称设置', + timestamp: new Date().toISOString() + }; + } + + // 转换分析内容为Markdown + const analysisMarkdown = typeof request.analysisContent === 'string' + ? request.analysisContent + : convertAnalysisToMarkdown(request.analysisContent, request.analysisType); + + console.log('🔄 分析内容转换为Markdown:', { + originalType: typeof request.analysisContent, + markdownLength: analysisMarkdown.length, + preview: analysisMarkdown.substring(0, 200) + '...' + }); + + // 获取提示词模板 + const promptTemplate = request.customPrompt || getPromptTemplate(request.analysisType); + const prompt = promptTemplate.replace('{analysisContent}', analysisMarkdown); + + console.log('📝 构建AI提示词:', { + templateLength: promptTemplate.length, + finalPromptLength: prompt.length, + analysisType: request.analysisType + }); + + // 构建请求体 + const requestBody = { + model: config.modelName, + messages: [ + { + role: 'user', + content: prompt + } + ], + max_tokens: config.maxTokens, + temperature: config.temperature, + stream: config.stream + }; + + console.log('🚀 准备发送API请求:', { + url: config.apiUrl, + model: config.modelName, + maxTokens: config.maxTokens, + temperature: config.temperature, + timeout: config.timeout, + messageLength: prompt.length, + timestamp: new Date().toISOString() + }); + + // 发送请求 + const controller = new AbortController(); + const timeoutId = setTimeout(() => { + console.log('⏰ 请求超时,正在中止请求...'); + controller.abort(); + }, config.timeout); + + const requestStartTime = Date.now(); + console.log('📡 开始发送HTTP请求...', { + method: 'POST', + url: config.apiUrl, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${config.apiKey.substring(0, 10)}...` + }, + bodySize: JSON.stringify(requestBody).length + }); + + const response = await fetch(config.apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${config.apiKey}` + }, + body: JSON.stringify(requestBody), + signal: controller.signal + }); + + clearTimeout(timeoutId); + const requestDuration = Date.now() - requestStartTime; + + console.log('📨 收到HTTP响应:', { + status: response.status, + statusText: response.statusText, + ok: response.ok, + duration: `${requestDuration}ms`, + headers: Object.fromEntries(response.headers.entries()) + }); + + if (!response.ok) { + let errorData; + try { + errorData = await response.json(); + console.log('❌ API错误响应:', errorData); + } catch (parseError) { + console.log('❌ 无法解析错误响应:', parseError); + errorData = {}; + } + + const errorMessage = `API请求失败: ${response.status} ${response.statusText}. ${errorData.error?.message || ''}`; + console.log('❌ 请求失败:', errorMessage); + throw new Error(errorMessage); + } + + let content = ''; + let tokensUsed = 0; + let model = config.modelName; + + if (config.stream) { + // 处理流式响应 + console.log('📡 开始处理流式响应...'); + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error('无法获取响应流'); + } + + const decoder = new TextDecoder(); + let buffer = ''; + + try { + while (true) { + const { done, value } = await reader.read(); + + if (done) { + console.log('📡 流式响应完成'); + break; + } + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; // 保留不完整的行 + + for (const line of lines) { + if (line.trim() === '') continue; + if (line.startsWith('data: ')) { + const data = line.slice(6); + if (data === '[DONE]') { + console.log('📡 收到流式结束标记'); + break; + } + + try { + const parsed = JSON.parse(data); + if (parsed.choices && parsed.choices[0] && parsed.choices[0].delta) { + const delta = parsed.choices[0].delta; + if (delta.content) { + content += delta.content; + // 调用流式更新回调 + if (request.onStreamUpdate) { + request.onStreamUpdate(content); + } + } + } + + // 获取使用情况和模型信息 + if (parsed.usage) { + tokensUsed = parsed.usage.total_tokens; + } + if (parsed.model) { + model = parsed.model; + } + } catch (parseError) { + console.warn('解析流式数据失败:', parseError, 'data:', data); + } + } + } + } + } finally { + reader.releaseLock(); + } + + console.log('📄 流式AI解读完成:', { + contentLength: content.length, + tokensUsed, + model, + totalDuration: `${Date.now() - startTime}ms` + }); + + } else { + // 处理非流式响应 + const data = await response.json(); + + console.log('✅ AI API成功响应:', { + id: data.id, + object: data.object, + created: data.created, + model: data.model, + usage: data.usage, + choicesCount: data.choices?.length || 0, + totalDuration: `${Date.now() - startTime}ms` + }); + + if (!data.choices || !data.choices[0] || !data.choices[0].message) { + console.log('❌ AI响应格式异常:', data); + throw new Error('AI响应格式异常'); + } + + content = data.choices[0].message.content; + tokensUsed = data.usage?.total_tokens; + model = data.model || config.modelName; + + console.log('📄 AI解读内容:', { + contentLength: content?.length || 0, + tokensUsed, + finishReason: data.choices[0].finish_reason, + contentPreview: content?.substring(0, 100) + '...' + }); + } + + return { + success: true, + content, + timestamp: new Date().toISOString(), + model, + tokensUsed + }; + + } catch (error: any) { + console.error('AI解读请求失败:', error); + + let errorMessage = '未知错误'; + if (error.name === 'AbortError') { + errorMessage = '请求超时,请稍后重试'; + } else if (error.message) { + errorMessage = error.message; + } + + return { + success: false, + error: errorMessage, + timestamp: new Date().toISOString() + }; + } +}; + +// 保存AI解读结果到本地存储 +export const saveAIInterpretation = (analysisId: string, result: AIInterpretationResult): void => { + try { + const key = `ai-interpretation-${analysisId}`; + localStorage.setItem(key, JSON.stringify(result)); + } catch (error) { + console.error('保存AI解读结果失败:', error); + } +}; + +// 从本地存储获取AI解读结果 +export const getAIInterpretation = (analysisId: string): AIInterpretationResult | null => { + try { + const key = `ai-interpretation-${analysisId}`; + const saved = localStorage.getItem(key); + if (saved) { + return JSON.parse(saved); + } + } catch (error) { + console.error('获取AI解读结果失败:', error); + } + return null; +}; + +// 清除AI解读结果 +export const clearAIInterpretation = (analysisId: string): void => { + try { + const key = `ai-interpretation-${analysisId}`; + localStorage.removeItem(key); + } catch (error) { + console.error('清除AI解读结果失败:', error); + } +}; \ No newline at end of file