This commit is contained in:
MarkLo 2025-12-07 17:13:57 +08:00
parent 0792830c3d
commit c2b8706bf2
5 changed files with 213 additions and 9 deletions

View File

@ -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]:

View File

@ -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>

View File

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

View File

@ -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

View File

@ -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;
}