From 37ebe19f1e93e4e622f2441fba5c0395a1e50273 Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Thu, 25 Jun 2026 15:53:15 +0800 Subject: [PATCH] =?UTF-8?q?refactor(ai):=20=E9=85=8D=E7=BD=AE=E9=A1=B5?= =?UTF-8?q?=E5=8D=95=E5=88=97=E5=8D=A1=E7=89=87=E9=87=8D=E6=9E=84=20+=20?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E6=97=A0=E7=94=A8=E7=9A=84=20Token=20?= =?UTF-8?q?=E9=A2=84=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AI 设置页改为单列分卡片布局(Card/Field/Toggle 辅助组件),自定义配置卡 头部标注「Chat Completions」 - 移除从未实际生效的 ai_daily_token_budget(后端 config/api/settings + 前端 Settings 接口与保存参数 + UI) - 测试结果不再展示 token 消耗,仅显示「连通成功 · 模型: xxx」 --- backend/app/api/settings.py | 4 - backend/app/config.py | 1 - backend/uv.lock | 2 +- frontend/src/lib/api.ts | 3 +- frontend/src/pages/settings/AI.tsx | 245 +++++++++++++++++++---------- 5 files changed, 160 insertions(+), 95 deletions(-) diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py index deac8a1..1e0700a 100644 --- a/backend/app/api/settings.py +++ b/backend/app/api/settings.py @@ -57,7 +57,6 @@ def get_settings() -> dict: "ai_api_key_masked": secrets_store.mask(secrets_store.get_ai_key()), "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,7 +211,6 @@ class AiSettingsIn(BaseModel): base_url: str = "" api_key: str | None = None model: str = "" - daily_token_budget: int = 500_000 user_agent: str = "" @@ -238,8 +236,6 @@ def save_ai_settings(req: AiSettingsIn) -> dict: if req.model: updates["ai_model"] = req.model 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 diff --git a/backend/app/config.py b/backend/app/config.py index 922410d..16ee8f9 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -70,7 +70,6 @@ class Settings(BaseSettings): ai_base_url: str = "https://api.alysc.top" 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 = ( diff --git a/backend/uv.lock b/backend/uv.lock index c6b9e52..09cb06f 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -2470,7 +2470,7 @@ all = [ [[package]] name = "tickflow-stock-panel-backend" -version = "0.1.35" +version = "0.1.44" source = { editable = "." } dependencies = [ { name = "apscheduler" }, diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 72a4e00..298aa4e 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -565,7 +565,6 @@ export interface SettingsState { ai_api_key_masked: string has_ai_key: boolean ai_model: string - ai_daily_token_budget: number ai_user_agent: string } @@ -637,7 +636,7 @@ export const api = { ), /** 保存 AI 配置 */ - saveAiSettings: (ai: { provider?: string; base_url?: string; api_key?: string; model?: string; daily_token_budget?: number; user_agent?: string }) => + saveAiSettings: (ai: { provider?: string; base_url?: string; api_key?: string; model?: string; 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 407c7d6..172b53e 100644 --- a/frontend/src/pages/settings/AI.tsx +++ b/frontend/src/pages/settings/AI.tsx @@ -1,13 +1,22 @@ import { useState, useEffect } from 'react' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { Save, Loader2, Check, Wifi, WifiOff, Eye, EyeOff, Shield, Shuffle } from 'lucide-react' +import { + Save, Loader2, Check, Wifi, WifiOff, Eye, EyeOff, Shield, + Shuffle, Plug, Zap, Settings2, ExternalLink, +} from 'lucide-react' import { useSettings } from '@/lib/useSharedQueries' import { api } from '@/lib/api' import { QK } from '@/lib/queryKeys' +// 统一的输入框样式(与项目其他设置页一致) +const INPUT_CLS = + 'w-full h-9 px-2.5 rounded-lg bg-base border-0 ring-1 ring-border/30 text-xs font-mono text-foreground placeholder:text-muted/30 focus:outline-none focus:ring-2 focus:ring-accent/30 transition-shadow' + const PRESETS: { label: string; url: string; model: string; website: string; websiteLabel: string; description: string; partner?: boolean; promo?: string }[] = [ - { label: 'DeepSeek', url: 'https://api.deepseek.com/v1', model: 'deepseek-chat', website: 'https://www.deepseek.com/', websiteLabel: 'deepseek.com', description: 'DeepSeek 官方 OpenAI 兼容接口。' }, - { label: '通义千问', url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-plus', website: 'https://tongyi.aliyun.com/', websiteLabel: 'tongyi.aliyun.com', description: '阿里云 DashScope 兼容模式接口。' }, + { label: 'DeepSeek', url: 'https://api.deepseek.com', model: 'deepseek-v4-pro', website: 'https://www.deepseek.com/', websiteLabel: 'deepseek.com', description: 'DeepSeek 官方 OpenAI 兼容接口。' }, + { label: '通义千问', url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-3.6plus', website: 'https://tongyi.aliyun.com/', websiteLabel: 'tongyi.aliyun.com', description: '阿里云 DashScope 兼容模式接口。' }, + { label: '智谱 GLM', url: 'https://open.bigmodel.cn/api/paas/v4', model: 'glm-5.2', website: 'https://open.bigmodel.cn/', websiteLabel: 'open.bigmodel.cn', description: '智谱 AI 官方 OpenAI 兼容接口。' }, + { label: 'Kimi', url: 'https://api.moonshot.cn/v1', model: 'kimi-k2.6', website: 'https://platform.moonshot.cn/', websiteLabel: 'platform.moonshot.cn', description: '月之暗面 Moonshot 官方 OpenAI 兼容接口,支持超长上下文。' }, { label: '炸鸡中转站', url: 'https://code.alysc.top/v1', model: 'gpt-5.5', website: 'https://code.alysc.top/sign-up?aff=1afk', websiteLabel: 'code.alysc.top', description: 'OpenAI 兼容中转服务,适合直接使用国际模型。', partner: true, promo: '通过链接邀请注册赠送免费额度 · 国际模型最低0.01倍率' }, ] @@ -20,7 +29,6 @@ export function SettingsAIPanel() { const [baseUrl, setBaseUrl] = useState('') 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) @@ -49,7 +57,6 @@ export function SettingsAIPanel() { 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) @@ -58,7 +65,7 @@ export function SettingsAIPanel() { const save = useMutation({ mutationFn: () => api.saveAiSettings({ - provider, base_url: baseUrl, api_key: apiKey || undefined, model, daily_token_budget: tokenBudget, + provider, base_url: baseUrl, api_key: apiKey || undefined, model, // 关闭开关 → 提交空串,后端回退内置默认 UA user_agent: customUa ? userAgent : '', }), @@ -72,9 +79,9 @@ 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, user_agent: customUa ? userAgent : '' }) + if (apiKey) await api.saveAiSettings({ provider, base_url: baseUrl, api_key: apiKey, model, 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 ?? '未知错误') }) + setTestResult({ ok: r.ok, msg: r.ok ? `连通成功 · 模型: ${r.model}` : (r.error ?? '未知错误') }) } catch (e: any) { setTestResult({ ok: false, msg: String(e?.message ?? '测试失败') }) } finally { setTesting(false) } @@ -86,44 +93,49 @@ export function SettingsAIPanel() { const configured = s?.has_ai_key const selectedPreset = PRESETS.find(p => p.url === baseUrl) + const canSave = !!baseUrl && !!model return ( -
- {/* ===== 状态横幅 ===== */} -
-
- {configured ? : } -
-
-
{configured ? 'AI 已连接' : 'AI 未配置'}
-
- {configured ? `${s?.ai_model} · ${s?.ai_api_key_masked}` : '配置 API Key 后即可使用 AI 策略定制'} -
-
- {configured && ( +
+ {/* ===== ① 连接状态 ===== */} + + className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn bg-elevated hover:bg-elevated/80 text-xs text-secondary transition-colors duration-150 ease-smooth disabled:opacity-50"> {testing ? : } {testing ? '测试中' : '测试'} - )} -
- - {/* 测试结果 */} - {testResult && ( -
-
- {testResult.msg} + ) + }> +
+
+ {configured ? : } +
+
+
+ {configured ? 'AI 已连接' : 'AI 未配置'} +
+
+ {configured ? `${s?.ai_model} · ${s?.ai_api_key_masked}` : '配置 API Key 后即可使用 AI 策略定制'} +
+
- )} - {/* ===== 快速预设 ===== */} -
-
快速配置
-
+ {/* 测试结果 */} + {testResult && ( +
+
+ {testResult.msg} +
+ )} + + + {/* ===== ② 快速预设 ===== */} + +
{PRESETS.map(p => (
-
+ - {/* Token 预算 */} -
- -
- setTokenBudget(Math.max(10000, Number(e.target.value) || 0))} - min={10000} step={100000} - className="w-44 h-8 px-2.5 rounded-lg bg-base border-0 ring-1 ring-border/30 text-xs font-mono text-foreground focus:outline-none focus:ring-2 focus:ring-accent/30 transition-shadow" /> - 超出后仅发出提醒,不阻止 AI 调用 -
-
+ {/* 分隔线 */} +
- {/* 请求头 User-Agent */} + {/* 自定义 User-Agent */}
- - + + setCustomUa(v => !v)} /> +
-
+
{customUa ? '当前使用下方自定义 UA 调用 AI API。' : '默认已使用内置浏览器标识,可绕过 Cloudflare 等 CDN/WAF 拦截。仅在默认标识被拦截时才需开启自定义。'} @@ -222,29 +225,29 @@ export function SettingsAIPanel() { {customUa && (
setUserAgent(e.target.value)} - placeholder="留空点击「随机生成」或直接粘贴浏览器 UA" - className="flex-1 h-8 px-2.5 rounded-lg bg-base border-0 ring-1 ring-border/30 text-xs font-mono text-foreground placeholder:text-muted/30 focus:outline-none focus:ring-2 focus:ring-accent/30 transition-shadow" /> + placeholder="留空点击「随机」或直接粘贴浏览器 UA" + className={`${INPUT_CLS} flex-1`} />
)}
-
+ {/* ===== 安全提示 ===== */} -
+
-
+
API Key 仅保存在本机项目文件,不上传至任何服务器。请妥善保管,勿泄露给他人。
{/* ===== 保存 ===== */} -
) } + + +// ===== 通用卡片(与 Keys 页风格统一) ===== + +interface CardProps { + icon: React.ComponentType<{ className?: string }> + title: string + right?: React.ReactNode + children: React.ReactNode +} + +function Card({ icon: Icon, title, right, children }: CardProps) { + return ( +
+
+
+ +

{title}

+
+ {right} +
+ {children} +
+ ) +} + +// ===== 表单字段(统一 label + 输入框样式) ===== + +function Field({ label, hint, inline, children }: { + label: string + hint?: string + inline?: boolean + children: React.ReactNode +}) { + if (inline) { + return ( +
+
+
{label}
+ {hint &&
{hint}
} +
+ {children} +
+ ) + } + return ( +
+
{label}
+ {children} + {hint &&
{hint}
} +
+ ) +} + +// ===== 开关 ===== + +function Toggle({ checked, onChange }: { checked: boolean; onChange: () => void }) { + return ( + + ) +}