ai enpowered

This commit is contained in:
patdelphi
2025-08-21 22:59:35 +08:00
parent c7534db874
commit 7910dd4bbf
10 changed files with 1806 additions and 28 deletions

View File

@@ -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<CompleteBaziAnalysisProps> = ({ birthDate,
const [isLoading, setIsLoading] = useState(!propAnalysisData);
const [error, setError] = useState<string | null>(null);
const [analysisData, setAnalysisData] = useState<any>(propAnalysisData || null);
const [showAIConfig, setShowAIConfig] = useState(false);
// 五行颜色配置
const elementColors: { [key: string]: string } = {
@@ -278,15 +281,26 @@ const CompleteBaziAnalysis: React.FC<CompleteBaziAnalysisProps> = ({ birthDate,
<div className="min-h-screen bg-gradient-to-br from-red-50 to-yellow-50 py-8">
<div className="max-w-7xl mx-auto px-4 space-y-8" id="bazi-analysis-content" data-export-content>
{/* 下载按钮 */}
<div className="flex justify-end no-export" data-no-export>
<DownloadButton
analysisData={analysisData}
analysisType="bazi"
userName={birthDate.name}
targetElementId="bazi-analysis-content"
className="sticky top-4 z-10"
/>
{/* 下载和AI解读按钮 */}
<div className="flex justify-between items-start no-export" data-no-export>
<div className="flex-1">
<AIInterpretationButton
analysisData={analysisData}
analysisType="bazi"
analysisId={`bazi-${birthDate.date}-${birthDate.time}`}
onConfigClick={() => setShowAIConfig(true)}
className="sticky top-4 z-10"
/>
</div>
<div className="ml-4">
<DownloadButton
analysisData={analysisData}
analysisType="bazi"
userName={birthDate.name}
targetElementId="bazi-analysis-content"
className="sticky top-4 z-10"
/>
</div>
</div>
{/* 标题和基本信息 */}
@@ -1047,6 +1061,16 @@ const CompleteBaziAnalysis: React.FC<CompleteBaziAnalysisProps> = ({ birthDate,
{/* 回到顶部按钮 */}
<BackToTop />
{/* AI配置模态框 */}
<AIConfigModal
isOpen={showAIConfig}
onClose={() => setShowAIConfig(false)}
onConfigSaved={() => {
setShowAIConfig(false);
// 可以在这里添加配置保存后的逻辑
}}
/>
</div>
);
};

View File

@@ -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<CompleteYijingAnalysisProps> = ({
const [isLoading, setIsLoading] = useState(!propAnalysisData);
const [error, setError] = useState<string | null>(null);
const [analysisData, setAnalysisData] = useState<any>(propAnalysisData || null);
const [showAIConfig, setShowAIConfig] = useState(false);
// 卦象颜色配置
const hexagramColors: { [key: string]: string } = {
@@ -266,15 +269,26 @@ const CompleteYijingAnalysis: React.FC<CompleteYijingAnalysisProps> = ({
<div className="min-h-screen bg-gradient-to-br from-red-50 to-yellow-50 py-8">
<div className="max-w-7xl mx-auto px-4 space-y-8" id="yijing-analysis-content" data-export-content>
{/* 下载按钮 */}
<div className="flex justify-end no-export" data-no-export>
<DownloadButton
analysisData={analysisData}
analysisType="yijing"
userName={question ? `占卜_${question.substring(0, 10)}` : 'user'}
targetElementId="yijing-analysis-content"
className="sticky top-4 z-10"
/>
{/* 下载和AI解读按钮 */}
<div className="flex justify-between items-start no-export" data-no-export>
<div className="flex-1">
<AIInterpretationButton
analysisData={analysisData}
analysisType="yijing"
analysisId={`yijing-${question || 'general'}-${Date.now()}`}
onConfigClick={() => setShowAIConfig(true)}
className="sticky top-4 z-10"
/>
</div>
<div className="ml-4">
<DownloadButton
analysisData={analysisData}
analysisType="yijing"
userName={question ? `占卜_${question.substring(0, 10)}` : 'user'}
targetElementId="yijing-analysis-content"
className="sticky top-4 z-10"
/>
</div>
</div>
{/* 标题和基本信息 */}
@@ -748,10 +762,22 @@ const CompleteYijingAnalysis: React.FC<CompleteYijingAnalysisProps> = ({
</div>
</CardContent>
</Card>
</div>
{/* 返回顶部按钮 */}
<BackToTop />
{/* AI配置模态框 */}
<AIConfigModal
isOpen={showAIConfig}
onClose={() => setShowAIConfig(false)}
onConfigSaved={() => {
setShowAIConfig(false);
// 可以在这里添加配置保存后的逻辑
}}
/>
</div>
);
};

View File

@@ -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<CompleteZiweiAnalysisProps> = ({ birthDate
const [isLoading, setIsLoading] = useState(!propAnalysisData);
const [error, setError] = useState<string | null>(null);
const [analysisData, setAnalysisData] = useState<any>(propAnalysisData || null);
const [showAIConfig, setShowAIConfig] = useState(false);
// 四化飞星详细解释
const sihuaExplanations = {
@@ -581,15 +584,26 @@ const CompleteZiweiAnalysis: React.FC<CompleteZiweiAnalysisProps> = ({ birthDate
<div className="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-8">
<div className="max-w-7xl mx-auto px-4 space-y-8" id="ziwei-analysis-content" data-export-content>
{/* 下载按钮 */}
<div className="flex justify-end no-export" data-no-export>
<DownloadButton
analysisData={analysisData}
analysisType="ziwei"
userName={birthDate.name}
targetElementId="ziwei-analysis-content"
className="sticky top-4 z-10"
/>
{/* 下载和AI解读按钮 */}
<div className="flex justify-between items-start no-export" data-no-export>
<div className="flex-1">
<AIInterpretationButton
analysisData={analysisData}
analysisType="ziwei"
analysisId={`ziwei-${birthDate.date}-${birthDate.time}`}
onConfigClick={() => setShowAIConfig(true)}
className="sticky top-4 z-10"
/>
</div>
<div className="ml-4">
<DownloadButton
analysisData={analysisData}
analysisType="ziwei"
userName={birthDate.name}
targetElementId="ziwei-analysis-content"
className="sticky top-4 z-10"
/>
</div>
</div>
{/* 标题和基本信息 */}
@@ -1562,10 +1576,22 @@ const CompleteZiweiAnalysis: React.FC<CompleteZiweiAnalysisProps> = ({ birthDate
</div>
</CardContent>
</Card>
</div>
{/* 回到顶部按钮 */}
<BackToTop />
{/* AI配置模态框 */}
<AIConfigModal
isOpen={showAIConfig}
onClose={() => setShowAIConfig(false)}
onConfigSaved={() => {
setShowAIConfig(false);
// 可以在这里添加配置保存后的逻辑
}}
/>
</div>
);
};

View File

@@ -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<AIConfigModalProps> = ({
isOpen,
onClose,
onConfigSaved
}) => {
const [config, setConfig] = useState<AIConfig>({
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 (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<ChineseCard className="border-0 shadow-none">
<ChineseCardHeader className="border-b">
<div className="flex items-center justify-between">
<ChineseCardTitle className="flex items-center space-x-2">
<Settings className="h-5 w-5 text-blue-600" />
<span>AI解读配置</span>
</ChineseCardTitle>
<ChineseButton
variant="ghost"
size="sm"
onClick={onClose}
className="p-1"
>
<X className="h-4 w-4" />
</ChineseButton>
</div>
</ChineseCardHeader>
<ChineseCardContent className="space-y-6 p-6">
{/* API Key */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">
API Key <span className="text-red-500">*</span>
</label>
<div className="relative">
<input
type={showApiKey ? 'text' : 'password'}
value={config.apiKey}
onChange={(e) => 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"
/>
<button
type="button"
onClick={() => setShowApiKey(!showApiKey)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
{/* API URL */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">
API地址 <span className="text-red-500">*</span>
</label>
<input
type="url"
value={config.apiUrl}
onChange={(e) => 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"
/>
</div>
{/* Model Name */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">
<span className="text-red-500">*</span>
</label>
<input
type="text"
value={config.modelName}
onChange={(e) => 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"
/>
</div>
{/* Advanced Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Token数</label>
<input
type="number"
value={config.maxTokens}
onChange={(e) => 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"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700"></label>
<input
type="number"
value={config.temperature}
onChange={(e) => 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"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">(ms)</label>
<input
type="number"
value={config.timeout}
onChange={(e) => 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"
/>
</div>
</div>
{/* Stream Setting */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700"></label>
<div className="flex items-center space-x-3">
<label className="flex items-center cursor-pointer">
<input
type="checkbox"
checked={config.stream}
onChange={(e) => handleInputChange('stream', e.target.checked)}
className="sr-only"
/>
<div className={cn(
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
config.stream ? 'bg-blue-600' : 'bg-gray-200'
)}>
<span className={cn(
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
config.stream ? 'translate-x-6' : 'translate-x-1'
)} />
</div>
<span className="ml-2 text-sm text-gray-600">
{config.stream ? '启用' : '禁用'}
</span>
</label>
<span className="text-xs text-gray-500">
AI生成的内容
</span>
</div>
</div>
{/* Test Result */}
{testResult && (
<div className={cn(
'flex items-center space-x-2 p-3 rounded-lg border',
testResult.success
? 'bg-green-50 border-green-200 text-green-800'
: 'bg-red-50 border-red-200 text-red-800'
)}>
{testResult.success ? (
<CheckCircle className="h-4 w-4 flex-shrink-0" />
) : (
<AlertCircle className="h-4 w-4 flex-shrink-0" />
)}
<span className="text-sm">{testResult.message}</span>
</div>
)}
{/* Action Buttons */}
<div className="flex items-center justify-between pt-4 border-t">
<ChineseButton
variant="outline"
onClick={handleReset}
className="text-gray-600"
>
</ChineseButton>
<div className="flex items-center space-x-3">
<ChineseButton
variant="outline"
onClick={handleTest}
disabled={isTesting || !validateAIConfig(config)}
className="flex items-center space-x-2"
>
<TestTube className={cn('h-4 w-4', isTesting && 'animate-pulse')} />
<span>{isTesting ? '测试中...' : '测试连接'}</span>
</ChineseButton>
<ChineseButton
onClick={handleSave}
disabled={isSaving || !validateAIConfig(config)}
className="flex items-center space-x-2"
>
<Save className={cn('h-4 w-4', isSaving && 'animate-pulse')} />
<span>{isSaving ? '保存中...' : '保存配置'}</span>
</ChineseButton>
</div>
</div>
</ChineseCardContent>
</ChineseCard>
</div>
</div>
);
};
export default AIConfigModal;

View File

@@ -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<AIInterpretationButtonProps> = ({
analysisData,
analysisType,
analysisId,
className,
variant = 'default',
size = 'md',
showConfigButton = true,
onConfigClick
}) => {
const [isLoading, setIsLoading] = useState(false);
const [interpretation, setInterpretation] = useState<AIInterpretationResult | null>(null);
const [showResult, setShowResult] = useState(false);
const [isConfigValid, setIsConfigValid] = useState(false);
const [debugInfo, setDebugInfo] = useState<any>(null);
const [requestStartTime, setRequestStartTime] = useState<number | null>(null);
const [streamingContent, setStreamingContent] = useState<string>(''); // 流式内容
// 检查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 (
<div className={cn('space-y-4', className)}>
{/* AI解读按钮区域 */}
<div className="flex items-center space-x-2">
<ChineseButton
variant="outline"
size="md"
onClick={interpretation ? () => 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 ? (
<Loader2 className="mr-1 h-3 w-3 sm:h-4 sm:w-4 animate-spin" />
) : (
<Eye className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
)}
<span className="hidden sm:inline">
{isLoading
? 'AI解读中...'
: interpretation
? (showResult ? '隐藏解读' : 'AI解读')
: 'AI解读'
}
</span>
</ChineseButton>
{/* 重新解读按钮 */}
{interpretation && (
<ChineseButton
variant="outline"
size={size}
onClick={handleReinterpret}
disabled={isLoading}
className="flex items-center space-x-1"
>
<RefreshCw className={cn('h-3 w-3', isLoading && 'animate-spin')} />
<span className="text-xs"></span>
</ChineseButton>
)}
{/* 配置按钮 */}
{showConfigButton && onConfigClick && (
<ChineseButton
variant="ghost"
size={size}
onClick={onConfigClick}
className="flex items-center space-x-1 text-gray-500 hover:text-gray-700"
>
<Settings className="h-3 w-3" />
<span className="text-xs"></span>
</ChineseButton>
)}
</div>
{/* 配置提示 */}
{!isConfigValid && !interpretation && (
<div className="flex items-center space-x-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<AlertCircle className="h-4 w-4 text-yellow-600 flex-shrink-0" />
<div className="text-sm text-yellow-800">
<p className="font-medium">AI设置</p>
<p className="text-xs mt-1">API KeyAPI地址等信息才能使用AI解读功能</p>
</div>
</div>
)}
{/* 调试信息 */}
{debugInfo && (
<div className="text-xs text-gray-500 p-3 bg-gray-100 rounded border">
<div className="flex justify-between items-center mb-2">
<div className="font-bold">🔍 AI解读调试信息</div>
<button
onClick={() => setDebugInfo(null)}
className="text-gray-400 hover:text-gray-600 p-1"
title="清除调试信息"
>
<X className="h-3 w-3" />
</button>
</div>
<div className="space-y-1">
<div><strong>:</strong> {debugInfo.status}</div>
<div><strong>:</strong> {debugInfo.startTime}</div>
{debugInfo.endTime && <div><strong>:</strong> {debugInfo.endTime}</div>}
{debugInfo.duration && <div><strong>:</strong> {debugInfo.duration}</div>}
<div><strong>:</strong> {debugInfo.analysisType}</div>
<div><strong>:</strong> {debugInfo.analysisDataSize} </div>
{debugInfo.config && (
<details className="mt-2">
<summary className="cursor-pointer font-medium"></summary>
<div className="ml-2 mt-1 space-y-1">
<div><strong>API地址:</strong> {debugInfo.config.apiUrl}</div>
<div><strong>:</strong> {debugInfo.config.modelName}</div>
<div><strong>Token:</strong> {debugInfo.config.maxTokens}</div>
<div><strong>:</strong> {debugInfo.config.temperature}</div>
<div><strong>:</strong> {debugInfo.config.timeout}ms</div>
<div><strong>API Key长度:</strong> {debugInfo.config.apiKeyLength}</div>
</div>
</details>
)}
{debugInfo.apiParams && (
<details className="mt-2">
<summary className="cursor-pointer font-medium">API调用参数</summary>
<div className="ml-2 mt-1 space-y-1">
<div><strong>:</strong> {debugInfo.apiParams.model}</div>
<div><strong>Token:</strong> {debugInfo.apiParams.maxTokens}</div>
<div><strong>:</strong> {debugInfo.apiParams.temperature}</div>
<div><strong>Prompt长度:</strong> {debugInfo.apiParams.promptLength} </div>
<div><strong>Prompt预览:</strong></div>
<pre className="text-xs mt-1 p-2 bg-white rounded border whitespace-pre-wrap max-h-32 overflow-y-auto">{debugInfo.apiParams.promptPreview}</pre>
<details className="mt-2">
<summary className="cursor-pointer text-xs">Prompt</summary>
<pre className="text-xs mt-1 p-2 bg-white rounded border whitespace-pre-wrap max-h-64 overflow-y-auto">{debugInfo.apiParams.fullPrompt}</pre>
</details>
<details className="mt-2">
<summary className="cursor-pointer text-xs">JSON</summary>
<pre className="text-xs mt-1 p-2 bg-white rounded border whitespace-pre-wrap max-h-64 overflow-y-auto">{debugInfo.apiParams.requestBody}</pre>
</details>
<details className="mt-2">
<summary className="cursor-pointer text-xs font-medium text-blue-600">🔧 API调用指令 (curl)</summary>
<div className="mt-1">
<div className="text-xs text-gray-600 mb-1">API:</div>
<pre className="text-xs p-2 bg-black text-green-400 rounded border whitespace-pre-wrap max-h-64 overflow-y-auto font-mono">{debugInfo.apiParams.curlCommand}</pre>
<button
onClick={() => navigator.clipboard.writeText(debugInfo.apiParams.curlCommand)}
className="mt-1 px-2 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600"
>
curl命令
</button>
</div>
</details>
</div>
</details>
)}
{debugInfo.result && (
<details className="mt-2">
<summary className="cursor-pointer font-medium"></summary>
<div className="ml-2 mt-1 space-y-1">
<div><strong>:</strong> {debugInfo.result.success ? '是' : '否'}</div>
<div><strong>:</strong> {debugInfo.result.contentLength}</div>
<div><strong>使:</strong> {debugInfo.result.model || 'N/A'}</div>
<div><strong>Token:</strong> {debugInfo.result.tokensUsed || 'N/A'}</div>
{debugInfo.result.error && <div><strong>:</strong> {debugInfo.result.error}</div>}
<div className="mt-2 p-2 bg-yellow-50 rounded text-xs">
<div><strong>:</strong></div>
<div>: {debugInfo.result.startTime}</div>
<div>: {debugInfo.result.endTime}</div>
<div>: {debugInfo.result.actualDuration}ms</div>
</div>
</div>
</details>
)}
{debugInfo.error && (
<details className="mt-2">
<summary className="cursor-pointer font-medium text-red-600"></summary>
<div className="ml-2 mt-1 space-y-1 text-red-600">
<div><strong>:</strong> {debugInfo.error.name}</div>
<div><strong>:</strong> {debugInfo.error.message}</div>
{debugInfo.error.stack && (
<div><strong>:</strong> <pre className="text-xs mt-1 whitespace-pre-wrap">{debugInfo.error.stack}</pre></div>
)}
<div className="mt-2 p-2 bg-yellow-50 rounded text-xs text-black">
<div><strong>:</strong></div>
<div>: {debugInfo.error.startTime}</div>
<div>: {debugInfo.error.endTime}</div>
<div>: {debugInfo.error.actualDuration}ms</div>
</div>
</div>
</details>
)}
</div>
</div>
)}
{/* AI解读结果显示 */}
{(interpretation || streamingContent) && showResult && (
<ChineseCard className="border-2 border-purple-200 bg-gradient-to-br from-purple-50 to-blue-50">
<ChineseCardHeader>
<ChineseCardTitle className="flex items-center space-x-2 text-purple-800">
{isLoading ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
<Sparkles className="h-5 w-5" />
)}
<span>AI智能解读 - {getAnalysisTypeName(analysisType)}</span>
{isLoading && streamingContent && (
<span className="ml-2 text-sm font-normal text-purple-600">...</span>
)}
</ChineseCardTitle>
{interpretation && (
<div className="flex items-center space-x-4 text-xs text-gray-500 mt-2">
<span>: {new Date(interpretation.timestamp).toLocaleString('zh-CN')}</span>
{interpretation.model && <span>: {interpretation.model}</span>}
{interpretation.tokensUsed && <span>Token: {interpretation.tokensUsed}</span>}
</div>
)}
</ChineseCardHeader>
<ChineseCardContent>
{interpretation && !interpretation.success ? (
<div className="flex items-center space-x-2 p-4 bg-red-50 border border-red-200 rounded-lg">
<AlertCircle className="h-4 w-4 text-red-600 flex-shrink-0" />
<div className="text-sm text-red-800">
<p className="font-medium"></p>
<p className="text-xs mt-1">{interpretation.error}</p>
</div>
</div>
) : (
<div className="prose prose-sm max-w-none">
<div className="whitespace-pre-wrap text-gray-800 leading-relaxed">
{streamingContent || interpretation?.content}
{isLoading && streamingContent && (
<span className="inline-block w-2 h-5 bg-purple-600 animate-pulse ml-1"></span>
)}
</div>
</div>
)}
</ChineseCardContent>
</ChineseCard>
)}
</div>
);
};
export default AIInterpretationButton;

108
src/config/aiConfig.ts Normal file
View File

@@ -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<AIConfig>): 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;
};

View File

@@ -170,6 +170,7 @@ const HistoryPage: React.FC = () => {
</div>
</div>
<AnalysisResultDisplay
analysisResult={selectedReading.analysis}
analysisType={selectedReading.reading_type as 'bazi' | 'ziwei' | 'yijing'}
@@ -186,6 +187,7 @@ const HistoryPage: React.FC = () => {
getInputDataValue(selectedReading.input_data, 'divination_method', 'time') : undefined}
preAnalysisData={selectedReading.analysis}
/>
</div>
);
}
@@ -272,6 +274,7 @@ const HistoryPage: React.FC = () => {
<Eye className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
<span className="hidden sm:inline"></span>
</ChineseButton>
<DownloadButton
analysisData={{
...(reading.analysis || reading.results),
@@ -299,6 +302,8 @@ const HistoryPage: React.FC = () => {
)}
</ChineseCardContent>
</ChineseCard>
</div>
);
};

View File

@@ -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<AIInterpretationResult> => {
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);
}
};