This commit is contained in:
parent
0792830c3d
commit
c2b8706bf2
|
|
@ -182,11 +182,74 @@ class TradingService:
|
|||
|
||||
except Exception as e:
|
||||
logger.error(f"Analysis failed for {ticker}: {str(e)}", exc_info=True)
|
||||
|
||||
# Check if it's a rate limit error
|
||||
error_message = str(e)
|
||||
error_type = "general"
|
||||
retry_after = None
|
||||
quota_limit = None
|
||||
quota_metric = None
|
||||
|
||||
# Detect OpenAI/Gemini Rate Limit Errors
|
||||
if "Error code: 429" in error_message or "RateLimitError" in str(type(e).__name__):
|
||||
error_type = "rate_limit"
|
||||
|
||||
# Extract quota details from error message
|
||||
import re
|
||||
|
||||
# Extract limit (e.g., "limit: 20")
|
||||
limit_match = re.search(r'limit:\s*(\d+)', error_message)
|
||||
if limit_match:
|
||||
quota_limit = int(limit_match.group(1))
|
||||
|
||||
# Extract model name
|
||||
model_match = re.search(r'model:\s*([\w\-\.]+)', error_message)
|
||||
model_name = model_match.group(1) if model_match else "unknown"
|
||||
|
||||
# Extract retry time (e.g., "retry in 37.312655565s" or "retryDelay": "37s")
|
||||
retry_match = re.search(r'retry in ([\d\.]+)s', error_message)
|
||||
if not retry_match:
|
||||
retry_match = re.search(r'"retryDelay":\s*"(\d+)s"', error_message)
|
||||
|
||||
if retry_match:
|
||||
retry_after = int(float(retry_match.group(1)))
|
||||
|
||||
# Extract quota metric name
|
||||
metric_match = re.search(r'quotaMetric["\']:\s*["\']([^"\']+)', error_message)
|
||||
if metric_match:
|
||||
quota_metric = metric_match.group(1)
|
||||
|
||||
# Create user-friendly message
|
||||
if quota_limit and model_name:
|
||||
error_message = (
|
||||
f"API Rate Limit Exceeded: You've reached the quota limit of {quota_limit} requests "
|
||||
f"for model '{model_name}'. "
|
||||
)
|
||||
if retry_after:
|
||||
minutes = retry_after // 60
|
||||
seconds = retry_after % 60
|
||||
if minutes > 0:
|
||||
error_message += f"Please retry in {minutes} minute(s) and {seconds} second(s). "
|
||||
else:
|
||||
error_message += f"Please retry in {seconds} second(s). "
|
||||
error_message += (
|
||||
"Consider upgrading to a paid plan for higher limits, or reduce the number of "
|
||||
"analysts/research depth to minimize API calls."
|
||||
)
|
||||
else:
|
||||
error_message = (
|
||||
"API Rate Limit Exceeded: You've exceeded your quota. "
|
||||
"Please wait before retrying, or consider upgrading to a paid plan."
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "error",
|
||||
"ticker": ticker,
|
||||
"analysis_date": analysis_date,
|
||||
"error": str(e),
|
||||
"error": error_message,
|
||||
"error_type": error_type,
|
||||
"retry_after": retry_after,
|
||||
"quota_limit": quota_limit,
|
||||
}
|
||||
|
||||
def get_available_analysts(self) -> List[str]:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { useEffect } from "react";
|
|||
import { useRouter } from "next/navigation";
|
||||
import { AnalysisForm } from "@/components/analysis/AnalysisForm";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { ErrorAlert } from "@/components/shared/ErrorAlert";
|
||||
import { useAnalysis } from "@/hooks/useAnalysis";
|
||||
import { useAnalysisContext } from "@/context/AnalysisContext";
|
||||
import type { AnalysisRequest } from "@/lib/types";
|
||||
|
|
@ -62,12 +63,7 @@ export default function AnalysisPage() {
|
|||
<LoadingSpinner message="正在執行分析... 這可能需要幾分鐘時間。" />
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||
<h3 className="text-red-800 dark:text-red-300 font-semibold mb-2">錯誤</h3>
|
||||
<p className="text-red-600 dark:text-red-400">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
{error && <ErrorAlert error={error} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* Error Alert Component
|
||||
* Displays user-friendly error messages with special handling for rate limits
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { AlertCircle, Clock, TrendingUp } from "lucide-react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
interface ErrorAlertProps {
|
||||
error: string | {
|
||||
error: string;
|
||||
error_type?: string;
|
||||
retry_after?: number;
|
||||
quota_limit?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function ErrorAlert({ error }: ErrorAlertProps) {
|
||||
// Parse error data
|
||||
const isRateLimit = typeof error === "object" && error.error_type === "rate_limit";
|
||||
const errorMessage = typeof error === "string" ? error : error.error;
|
||||
const retryAfter = typeof error === "object" ? error.retry_after : null;
|
||||
const quotaLimit = typeof error === "object" ? error.quota_limit : null;
|
||||
|
||||
// Calculate retry time display
|
||||
const getRetryTimeDisplay = (seconds: number | null) => {
|
||||
if (!seconds) return null;
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
|
||||
if (minutes > 0) {
|
||||
return `${minutes} 分 ${remainingSeconds} 秒`;
|
||||
}
|
||||
return `${remainingSeconds} 秒`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={`p-6 border-2 ${
|
||||
isRateLimit
|
||||
? "bg-orange-50 dark:bg-orange-900/20 border-orange-300 dark:border-orange-700"
|
||||
: "bg-red-50 dark:bg-red-900/20 border-red-300 dark:border-red-700"
|
||||
}`}>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
isRateLimit
|
||||
? "bg-orange-100 dark:bg-orange-800"
|
||||
: "bg-red-100 dark:bg-red-800"
|
||||
}`}>
|
||||
{isRateLimit ? (
|
||||
<Clock className="w-5 h-5 text-orange-600 dark:text-orange-400" />
|
||||
) : (
|
||||
<AlertCircle className="w-5 h-5 text-red-600 dark:text-red-400" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className={`font-semibold text-lg mb-2 ${
|
||||
isRateLimit
|
||||
? "text-orange-900 dark:text-orange-200"
|
||||
: "text-red-900 dark:text-red-200"
|
||||
}`}>
|
||||
{isRateLimit ? "API 請求額度已達上限" : "錯誤"}
|
||||
</h3>
|
||||
|
||||
<p className={`mb-4 ${
|
||||
isRateLimit
|
||||
? "text-orange-800 dark:text-orange-300"
|
||||
: "text-red-800 dark:text-red-300"
|
||||
}`}>
|
||||
{errorMessage}
|
||||
</p>
|
||||
|
||||
{isRateLimit && (
|
||||
<div className="space-y-3 mt-4">
|
||||
{/* Retry Information */}
|
||||
{retryAfter && (
|
||||
<div className="flex items-start gap-2 bg-white/50 dark:bg-gray-900/50 p-3 rounded-lg">
|
||||
<Clock className="w-4 h-4 text-orange-600 dark:text-orange-400 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
建議等待時間
|
||||
</p>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
請在 <span className="font-bold text-orange-600 dark:text-orange-400">
|
||||
{getRetryTimeDisplay(retryAfter)}
|
||||
</span> 後重試
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quota Information */}
|
||||
{quotaLimit && (
|
||||
<div className="flex items-start gap-2 bg-white/50 dark:bg-gray-900/50 p-3 rounded-lg">
|
||||
<TrendingUp className="w-4 h-4 text-orange-600 dark:text-orange-400 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
每日額度限制
|
||||
</p>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
當前計劃:每日 {quotaLimit} 次請求
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Solutions */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 p-4 rounded-lg mt-4">
|
||||
<p className="text-sm font-semibold text-blue-900 dark:text-blue-200 mb-2">
|
||||
💡 解決方案:
|
||||
</p>
|
||||
<ul className="text-sm text-blue-800 dark:text-blue-300 space-y-1.5 list-disc list-inside">
|
||||
<li>等待額度重置(通常為每日重置)</li>
|
||||
<li>升級至付費方案以獲得更高額度</li>
|
||||
<li>減少分析師數量或研究深度以降低 API 呼叫次數</li>
|
||||
<li>使用不同的 API 金鑰(如果有多個帳戶)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -9,7 +9,12 @@ import type { AnalysisRequest, AnalysisResponse } from "@/lib/types";
|
|||
|
||||
export function useAnalysis() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | {
|
||||
error: string;
|
||||
error_type?: string;
|
||||
retry_after?: number;
|
||||
quota_limit?: number;
|
||||
} | null>(null);
|
||||
const [result, setResult] = useState<AnalysisResponse | null>(null);
|
||||
const [taskId, setTaskId] = useState<string | null>(null);
|
||||
const [progress, setProgress] = useState<string | null>(null);
|
||||
|
|
@ -42,7 +47,17 @@ export function useAnalysis() {
|
|||
|
||||
// Check if failed
|
||||
if (status.status === "failed") {
|
||||
setError(status.error || "Analysis failed");
|
||||
// Check if we have structured error data from result
|
||||
if (status.result && status.result.error) {
|
||||
setError({
|
||||
error: status.result.error,
|
||||
error_type: status.result.error_type,
|
||||
retry_after: status.result.retry_after,
|
||||
quota_limit: status.result.quota_limit,
|
||||
});
|
||||
} else {
|
||||
setError(status.error || "Analysis failed");
|
||||
}
|
||||
setLoading(false);
|
||||
setProgress(null);
|
||||
// Stop polling
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ export interface AnalysisResponse {
|
|||
decision?: any;
|
||||
reports?: any;
|
||||
error?: string;
|
||||
error_type?: string;
|
||||
retry_after?: number;
|
||||
quota_limit?: number;
|
||||
price_data?: PriceData[];
|
||||
price_stats?: PriceStats;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue