diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py index 5f16834..deac8a1 100644 --- a/backend/app/api/settings.py +++ b/backend/app/api/settings.py @@ -58,6 +58,7 @@ def get_settings() -> dict: "has_ai_key": bool(secrets_store.get_ai_key()), "ai_model": secrets_store.get_ai_config("ai_model", settings.ai_model), "ai_daily_token_budget": int(secrets_store.get_ai_config("ai_daily_token_budget", str(settings.ai_daily_token_budget)) or settings.ai_daily_token_budget), + "ai_user_agent": secrets_store.get_ai_config("ai_user_agent", settings.ai_user_agent), } @@ -212,6 +213,7 @@ class AiSettingsIn(BaseModel): api_key: str | None = None model: str = "" daily_token_budget: int = 500_000 + user_agent: str = "" @router.post("/ai") @@ -238,6 +240,9 @@ def save_ai_settings(req: AiSettingsIn) -> dict: settings.ai_model = req.model updates["ai_daily_token_budget"] = req.daily_token_budget settings.ai_daily_token_budget = req.daily_token_budget + # user_agent 允许清空(回到默认浏览器 UA),故无条件持久化 + updates["ai_user_agent"] = req.user_agent + settings.ai_user_agent = req.user_agent if updates: secrets_store.save(updates) diff --git a/backend/app/api/strategy.py b/backend/app/api/strategy.py index dae6ddc..205784b 100644 --- a/backend/app/api/strategy.py +++ b/backend/app/api/strategy.py @@ -324,7 +324,12 @@ async def ai_test(request: Request): return {"ok": False, "error": "未配置 API Key"} try: - client = AsyncOpenAI(api_key=ai_key, base_url=settings.ai_base_url) + # User-Agent: 默认浏览器标识,绕过 Cloudflare 等 CDN/WAF 的 Bot 拦截(Issue #8)。 + client = AsyncOpenAI( + api_key=ai_key, + base_url=settings.ai_base_url, + default_headers={"User-Agent": settings.ai_user_agent or "Mozilla/5.0"}, + ) resp = await client.chat.completions.create( model=settings.ai_model, messages=[{"role": "user", "content": "回复 OK"}], diff --git a/backend/app/config.py b/backend/app/config.py index 2dcfe8b..922410d 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -71,6 +71,13 @@ class Settings(BaseSettings): ai_api_key: str = "" ai_model: str = "gpt-5.5" ai_daily_token_budget: int = 5_000_000 + # 默认浏览器风格 UA,绕过 Cloudflare 等 CDN/WAF 的 Bot 拦截(Issue #8)。 + # 用户可在 AI 设置页按需修改。 + ai_user_agent: str = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/131.0.0.0 Safari/537.36" + ) # Server host: str = "0.0.0.0" diff --git a/backend/app/strategy/ai_generator.py b/backend/app/strategy/ai_generator.py index 38fa054..f774a4c 100644 --- a/backend/app/strategy/ai_generator.py +++ b/backend/app/strategy/ai_generator.py @@ -83,11 +83,17 @@ class AIStrategyGenerator: if not ai_key: raise RuntimeError("AI API Key 未配置,请在设置页面配置") + # User-Agent: 默认浏览器标识,绕过 Cloudflare 等 CDN/WAF 的 Bot 拦截(Issue #8)。 + # 用户可在 AI 设置页自定义。 + from app.config import settings + user_agent = secrets_store.get_ai_config("ai_user_agent", "") or settings.ai_user_agent + client = AsyncOpenAI( api_key=ai_key, base_url=secrets_store.get_ai_config("ai_base_url", "https://api.alysc.top"), timeout=180.0, max_retries=2, + default_headers={"User-Agent": user_agent}, ) # 使用流式请求:CDN 收到首个 token 后会持续转发,不会因等待超时 stream = await client.chat.completions.create( diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index baddeed..72a4e00 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -566,6 +566,7 @@ export interface SettingsState { has_ai_key: boolean ai_model: string ai_daily_token_budget: number + ai_user_agent: string } /** 保存 TickFlow Key 的响应(先探后存) */ @@ -636,7 +637,7 @@ export const api = { ), /** 保存 AI 配置 */ - saveAiSettings: (ai: { provider?: string; base_url?: string; api_key?: string; model?: string; daily_token_budget?: number }) => + saveAiSettings: (ai: { provider?: string; base_url?: string; api_key?: string; model?: string; daily_token_budget?: number; user_agent?: string }) => request<{ ok: boolean }>('/api/settings/ai', { method: 'POST', body: JSON.stringify(ai), diff --git a/frontend/src/pages/settings/AI.tsx b/frontend/src/pages/settings/AI.tsx index 2bfbeba..4e75385 100644 --- a/frontend/src/pages/settings/AI.tsx +++ b/frontend/src/pages/settings/AI.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { Save, Loader2, Check, Wifi, WifiOff, Eye, EyeOff, Shield } from 'lucide-react' +import { Save, Loader2, Check, Wifi, WifiOff, Eye, EyeOff, Shield, Shuffle } from 'lucide-react' import { useSettings } from '@/lib/useSharedQueries' import { api } from '@/lib/api' import { QK } from '@/lib/queryKeys' @@ -21,6 +21,10 @@ export function SettingsAIPanel() { const [apiKey, setApiKey] = useState('') const [model, setModel] = useState('') const [tokenBudget, setTokenBudget] = useState(5_000_000) + // 自定义 User-Agent 开关:关闭 → 后端用内置默认浏览器 UA(开箱绕过 CDN 拦截); + // 开启 → 用下方文本框的 UA,留空时随机生成。 + const [customUa, setCustomUa] = useState(false) + const [userAgent, setUserAgent] = useState('') const [showKey, setShowKey] = useState(false) const [saved, setSaved] = useState(false) @@ -28,17 +32,35 @@ export function SettingsAIPanel() { const [testing, setTesting] = useState(false) const [testResult, setTestResult] = useState<{ ok: boolean; msg: string } | null>(null) + // 随机生成一个近期桌面端 Chrome UA(Win/Mac/Linux 随机) + const genRandomUa = () => { + const major = 128 + Math.floor(Math.random() * 8) // 128~135 + const platforms = [ + `Windows NT 10.0; Win64; x64`, + `Macintosh; Intel Mac OS X 10_15_7`, + `X11; Linux x86_64`, + ] + const pf = platforms[Math.floor(Math.random() * platforms.length)] + setUserAgent(`Mozilla/5.0 (${pf}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${major}.0.0.0 Safari/537.36`) + } + useEffect(() => { if (!s) return setProvider(s.ai_provider ?? 'openai_compat') setBaseUrl(s.ai_base_url ?? '') setModel(s.ai_model ?? '') setTokenBudget(s.ai_daily_token_budget ?? 500_000) + // 有已保存的自定义 UA → 开关默认开启;否则关闭(用后端内置默认) + const ua = s.ai_user_agent ?? '' + setCustomUa(!!ua) + setUserAgent(ua) }, [s]) const save = useMutation({ mutationFn: () => api.saveAiSettings({ provider, base_url: baseUrl, api_key: apiKey || undefined, model, daily_token_budget: tokenBudget, + // 关闭开关 → 提交空串,后端回退内置默认 UA + user_agent: customUa ? userAgent : '', }), onSuccess: () => { setSaved(true); setApiKey(''); qc.invalidateQueries({ queryKey: QK.settings }) @@ -50,7 +72,7 @@ export function SettingsAIPanel() { setTesting(true); setTestResult(null) try { // 先保存当前配置(不保存 Key 仅用于测试时临时存) - if (apiKey) await api.saveAiSettings({ provider, base_url: baseUrl, api_key: apiKey, model, daily_token_budget: tokenBudget }) + if (apiKey) await api.saveAiSettings({ provider, base_url: baseUrl, api_key: apiKey, model, daily_token_budget: tokenBudget, user_agent: customUa ? userAgent : '' }) const r = await api.strategyAiTest() setTestResult({ ok: r.ok, msg: r.ok ? `连通成功 · 模型: ${r.model}${r.usage ? ` · 消耗 ${r.usage.prompt + r.usage.completion} tokens` : ''}` : (r.error ?? '未知错误') }) } catch (e: any) { @@ -178,6 +200,39 @@ export function SettingsAIPanel() { 超出后仅发出提醒,不阻止 AI 调用 + + {/* 请求头 User-Agent */} +