This commit is contained in:
parent
0792830c3d
commit
c2b8706bf2
|
|
@ -182,11 +182,74 @@ class TradingService:
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Analysis failed for {ticker}: {str(e)}", exc_info=True)
|
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 {
|
return {
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"ticker": ticker,
|
"ticker": ticker,
|
||||||
"analysis_date": analysis_date,
|
"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]:
|
def get_available_analysts(self) -> List[str]:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { AnalysisForm } from "@/components/analysis/AnalysisForm";
|
import { AnalysisForm } from "@/components/analysis/AnalysisForm";
|
||||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||||
|
import { ErrorAlert } from "@/components/shared/ErrorAlert";
|
||||||
import { useAnalysis } from "@/hooks/useAnalysis";
|
import { useAnalysis } from "@/hooks/useAnalysis";
|
||||||
import { useAnalysisContext } from "@/context/AnalysisContext";
|
import { useAnalysisContext } from "@/context/AnalysisContext";
|
||||||
import type { AnalysisRequest } from "@/lib/types";
|
import type { AnalysisRequest } from "@/lib/types";
|
||||||
|
|
@ -62,12 +63,7 @@ export default function AnalysisPage() {
|
||||||
<LoadingSpinner message="正在執行分析... 這可能需要幾分鐘時間。" />
|
<LoadingSpinner message="正在執行分析... 這可能需要幾分鐘時間。" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && <ErrorAlert error={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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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() {
|
export function useAnalysis() {
|
||||||
const [loading, setLoading] = useState(false);
|
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 [result, setResult] = useState<AnalysisResponse | null>(null);
|
||||||
const [taskId, setTaskId] = useState<string | null>(null);
|
const [taskId, setTaskId] = useState<string | null>(null);
|
||||||
const [progress, setProgress] = useState<string | null>(null);
|
const [progress, setProgress] = useState<string | null>(null);
|
||||||
|
|
@ -42,7 +47,17 @@ export function useAnalysis() {
|
||||||
|
|
||||||
// Check if failed
|
// Check if failed
|
||||||
if (status.status === "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);
|
setLoading(false);
|
||||||
setProgress(null);
|
setProgress(null);
|
||||||
// Stop polling
|
// Stop polling
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,9 @@ export interface AnalysisResponse {
|
||||||
decision?: any;
|
decision?: any;
|
||||||
reports?: any;
|
reports?: any;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
error_type?: string;
|
||||||
|
retry_after?: number;
|
||||||
|
quota_limit?: number;
|
||||||
price_data?: PriceData[];
|
price_data?: PriceData[];
|
||||||
price_stats?: PriceStats;
|
price_stats?: PriceStats;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue