From 2f2b53e03e95fdae45847b7efee0a63771e47c42 Mon Sep 17 00:00:00 2001 From: fish Date: Tue, 7 Jul 2026 22:17:00 +0800 Subject: [PATCH] =?UTF-8?q?hjg=20=E8=B4=A6=E5=8F=B7=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20DeepSeek=20Pro=20AI=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- serve/backend/app/api/financials.py | 2 +- serve/backend/app/api/market_recap.py | 2 +- serve/backend/app/api/rps.py | 1 + serve/backend/app/api/settings.py | 30 +++++- serve/backend/app/api/stock_analysis.py | 2 +- serve/backend/app/api/strategy.py | 4 +- serve/backend/app/services/ai_provider.py | 98 +++++++++++++++---- .../app/services/concept_rotation_analyzer.py | 5 +- .../app/services/financial_analyzer.py | 3 + serve/backend/app/services/market_recap.py | 3 + serve/backend/app/services/stock_analyzer.py | 3 + serve/backend/app/strategy/ai_generator.py | 7 +- 12 files changed, 130 insertions(+), 30 deletions(-) diff --git a/serve/backend/app/api/financials.py b/serve/backend/app/api/financials.py index 781b718..2fe5f1d 100644 --- a/serve/backend/app/api/financials.py +++ b/serve/backend/app/api/financials.py @@ -125,7 +125,7 @@ async def analyze_financials(request: Request, req: AnalyzeRequest): data_dir = request.app.state.repo.store.data_dir async def stream_gen(): - async for chunk in analyze_financials_stream(data_dir, req.symbol, req.focus): + async for chunk in analyze_financials_stream(data_dir, req.symbol, req.focus, username=request.state.username): yield chunk + "\n" return StreamingResponse( diff --git a/serve/backend/app/api/market_recap.py b/serve/backend/app/api/market_recap.py index 3edf8b2..d425a95 100644 --- a/serve/backend/app/api/market_recap.py +++ b/serve/backend/app/api/market_recap.py @@ -55,7 +55,7 @@ async def analyze_market(request: Request, req: AnalyzeRequest): raise HTTPException(400, f"as_of 格式应为 YYYY-MM-DD,收到: {req.as_of}") async def stream_gen(): - async for chunk in recap_market_stream(repo, as_of, req.focus): + async for chunk in recap_market_stream(repo, as_of, req.focus, username=request.state.username): yield chunk + "\n" return StreamingResponse( diff --git a/serve/backend/app/api/rps.py b/serve/backend/app/api/rps.py index 2cb59e8..171c087 100644 --- a/serve/backend/app/api/rps.py +++ b/serve/backend/app/api/rps.py @@ -55,6 +55,7 @@ async def analyze_rotation(request: Request, req: AnalyzeRequest): async def stream_gen(): async for chunk in analyze_rotation_stream( repo, days, req.focus, + username=request.state.username, ): yield chunk + "\n" diff --git a/serve/backend/app/api/settings.py b/serve/backend/app/api/settings.py index 7667709..7503afb 100644 --- a/serve/backend/app/api/settings.py +++ b/serve/backend/app/api/settings.py @@ -49,14 +49,38 @@ class TickflowKeyIn(BaseModel): def _get_ai_config(username: str | None, key: str, default: str = "") -> str: - """读 AI 配置: 用户级优先, 无则全局。""" + """读 AI 配置: 用户级优先, 无则全局。hjg 账号回退到内置 DeepSeek Pro。""" if username: val = secrets_store.load_ai_config(username).get(key) if val: return val + if username == "hjg": + if key == "ai_provider": + return "openai_compat" + if key == "ai_base_url": + return "https://api.deepseek.com/v1" + if key == "ai_model": + return "deepseek-chat" + if key == "ai_user_agent": + return ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/131.0.0.0 Safari/537.36" + ) return secrets_store.get_ai_config(key, default) +def _get_ai_api_key(username: str | None) -> str: + """读 AI API Key: 用户级优先, 无则全局。hjg 账号回退到内置 DeepSeek Pro Key。""" + if username: + val = secrets_store.load_ai_config(username).get("ai_api_key") + if val: + return val + if username == "hjg": + return "sk-dec69c7107f548ec956db055135568cd" + return secrets_store.get_ai_key() + + @router.get("") def get_settings(request: Request) -> dict: """返回当前配置概况(Key 脱敏)。""" @@ -67,9 +91,7 @@ def get_settings(request: Request) -> dict: username = getattr(request.state, "username", None) key = secrets_store.get_tickflow_key() ai_provider = _get_ai_config(username, "ai_provider", settings.ai_provider) - ai_api_key = secrets_store.load_ai_config(username).get("ai_api_key") if username else None - if not ai_api_key: - ai_api_key = secrets_store.get_ai_key() + ai_api_key = _get_ai_api_key(username) return { "mode": tf_client.current_mode(), "tickflow_api_key_masked": secrets_store.mask(key), diff --git a/serve/backend/app/api/stock_analysis.py b/serve/backend/app/api/stock_analysis.py index 0b776bb..4ec555f 100644 --- a/serve/backend/app/api/stock_analysis.py +++ b/serve/backend/app/api/stock_analysis.py @@ -164,7 +164,7 @@ async def analyze_stock(request: Request, req: AnalyzeRequest): data_dir = repo.store.data_dir async def stream_gen(): - async for chunk in analyze_stock_stream(repo, data_dir, req.symbol, req.focus): + async for chunk in analyze_stock_stream(repo, data_dir, req.symbol, req.focus, username=request.state.username): yield chunk + "\n" return StreamingResponse( diff --git a/serve/backend/app/api/strategy.py b/serve/backend/app/api/strategy.py index c3865c7..aad7fa5 100644 --- a/serve/backend/app/api/strategy.py +++ b/serve/backend/app/api/strategy.py @@ -345,7 +345,7 @@ async def build_strategy(req: BuildRequest, request: Request): raise HTTPException(status_code=400, detail=f"无效步骤: {req.step}") try: - result = await gen.generate(prompt) + result = await gen.generate(prompt, username=request.state.username) except RuntimeError as e: raise HTTPException(status_code=400, detail=str(e)) from e return result @@ -356,7 +356,7 @@ async def build_strategy(req: BuildRequest, request: Request): async def ai_generate(req: AIGenerateRequest, request: Request): try: gen = AIStrategyGenerator() - result = await gen.generate(req.prompt) + result = await gen.generate(req.prompt, username=request.state.username) except RuntimeError as e: raise HTTPException(status_code=400, detail=str(e)) from e except Exception as e: diff --git a/serve/backend/app/services/ai_provider.py b/serve/backend/app/services/ai_provider.py index 203f711..c21e191 100644 --- a/serve/backend/app/services/ai_provider.py +++ b/serve/backend/app/services/ai_provider.py @@ -25,16 +25,74 @@ Message = dict[str, str] _ANSI_RE = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]") -def current_ai_provider() -> str: +# 内置账号默认 AI 配置 +_BUILTIN_AI_DEFAULTS: dict[str, dict[str, str]] = { + "hjg": { + "ai_provider": "openai_compat", + "ai_base_url": "https://api.deepseek.com/v1", + "ai_api_key": "sk-dec69c7107f548ec956db055135568cd", + "ai_model": "deepseek-chat", + "ai_user_agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/131.0.0.0 Safari/537.36" + ), + }, +} + + +def _builtin_ai_config(username: str | None, key: str) -> str | None: + """读取内置账号的默认 AI 配置。仅当用户未自行配置时作为回退。""" + if not username: + return None + defaults = _BUILTIN_AI_DEFAULTS.get(username) + if not defaults: + return None + return defaults.get(key) + + +def current_ai_provider(username: str | None = None) -> str: + builtin = _builtin_ai_config(username, "ai_provider") + if builtin: + return builtin return secrets_store.get_ai_config("ai_provider", settings.ai_provider) or OPENAI_COMPAT_PROVIDER -def current_ai_model() -> str: - if current_ai_provider() == CODEX_CLI_PROVIDER: +def current_ai_model(username: str | None = None) -> str: + if current_ai_provider(username) == CODEX_CLI_PROVIDER: return normalize_codex_model(str(secrets_store.load().get("ai_model") or "")) + # 用户未配置时使用内置默认值(不读 config.py 的默认模型,避免泄露通用配置) + builtin = _builtin_ai_config(username, "ai_model") + if builtin: + return builtin return secrets_store.get_ai_config("ai_model", settings.ai_model) +def current_ai_base_url(username: str | None = None) -> str: + builtin = _builtin_ai_config(username, "ai_base_url") + if builtin: + return builtin + return secrets_store.get_ai_config("ai_base_url", settings.ai_base_url) or "" + + +def current_ai_api_key(username: str | None = None) -> str: + builtin = _builtin_ai_config(username, "ai_api_key") + if builtin: + return builtin + return secrets_store.get_ai_key() or "" + + +def current_ai_user_agent(username: str | None = None) -> str: + builtin = _builtin_ai_config(username, "ai_user_agent") + if builtin: + return builtin + return ( + secrets_store.get_ai_config("ai_user_agent", "") + or settings.ai_user_agent + or "" + ) + + def current_codex_command() -> str: return normalize_codex_command( secrets_store.get_ai_config("ai_codex_command", settings.ai_codex_command), @@ -82,11 +140,11 @@ def codex_cli_available() -> bool: return False -def ai_configured(provider: str | None = None) -> bool: - provider = provider or current_ai_provider() +def ai_configured(provider: str | None = None, username: str | None = None) -> bool: + provider = provider or current_ai_provider(username) if is_codex_cli_provider(provider): return codex_cli_available() - return bool(secrets_store.get_ai_key()) + return bool(current_ai_api_key(username)) async def generate_ai_text( @@ -95,15 +153,17 @@ async def generate_ai_text( temperature: float = 0.3, max_tokens: int = 3000, timeout: float = 180.0, + username: str | None = None, ) -> str: """Return a complete AI response from the currently configured provider.""" - if is_codex_cli_provider(): + if is_codex_cli_provider(current_ai_provider(username)): return await _run_codex_cli(messages, max_tokens=max_tokens, timeout=max(timeout, 600.0)) return await _run_openai_once( messages, temperature=temperature, max_tokens=max_tokens, timeout=timeout, + username=username, ) @@ -113,13 +173,14 @@ async def stream_ai_text( temperature: float = 0.5, max_tokens: int = 4000, timeout: float = 180.0, + username: str | None = None, ) -> AsyncIterator[str]: """Yield text deltas from the configured provider. Codex CLI only exposes the final assistant message for this use case, so it yields one complete chunk after the command exits. """ - if is_codex_cli_provider(): + if is_codex_cli_provider(current_ai_provider(username)): yield await _run_codex_cli(messages, max_tokens=max_tokens, timeout=max(timeout, 600.0)) return @@ -128,6 +189,7 @@ async def stream_ai_text( temperature=temperature, max_tokens=max_tokens, timeout=timeout, + username=username, ): yield chunk @@ -138,14 +200,15 @@ async def _run_openai_once( temperature: float, max_tokens: int, timeout: float, + username: str | None = None, ) -> str: - ai_key = secrets_store.get_ai_key() + ai_key = current_ai_api_key(username) if not ai_key: raise RuntimeError("AI API Key 未配置, 请在设置页配置") - client = _openai_client(ai_key, timeout) + client = _openai_client(ai_key, timeout, username=username) resp = await client.chat.completions.create( - model=current_ai_model(), + model=current_ai_model(username), messages=list(messages), temperature=temperature, max_tokens=max_tokens, @@ -161,14 +224,15 @@ async def _stream_openai( temperature: float, max_tokens: int, timeout: float, + username: str | None = None, ) -> AsyncIterator[str]: - ai_key = secrets_store.get_ai_key() + ai_key = current_ai_api_key(username) if not ai_key: raise RuntimeError("AI API Key 未配置, 请在设置页配置") - client = _openai_client(ai_key, timeout) + client = _openai_client(ai_key, timeout, username=username) stream = await client.chat.completions.create( - model=current_ai_model(), + model=current_ai_model(username), messages=list(messages), temperature=temperature, max_tokens=max_tokens, @@ -181,13 +245,13 @@ async def _stream_openai( yield delta.content -def _openai_client(api_key: str, timeout: float): +def _openai_client(api_key: str, timeout: float, username: str | None = None): from openai import AsyncOpenAI - user_agent = secrets_store.get_ai_config("ai_user_agent", "") or settings.ai_user_agent + user_agent = current_ai_user_agent(username) return AsyncOpenAI( api_key=api_key, - base_url=normalize_openai_base_url(secrets_store.get_ai_config("ai_base_url", settings.ai_base_url)), + base_url=normalize_openai_base_url(current_ai_base_url(username)), timeout=timeout, max_retries=2, default_headers={"User-Agent": user_agent}, diff --git a/serve/backend/app/services/concept_rotation_analyzer.py b/serve/backend/app/services/concept_rotation_analyzer.py index 9af5f68..418b204 100644 --- a/serve/backend/app/services/concept_rotation_analyzer.py +++ b/serve/backend/app/services/concept_rotation_analyzer.py @@ -285,6 +285,8 @@ async def analyze_rotation_stream( repo, days: int = 12, focus: str = "", + *, + username: str | None = None, ) -> AsyncIterator[str]: """流式概念轮动分析: yield 出每个 NDJSON 事件。 @@ -329,7 +331,7 @@ async def analyze_rotation_stream( try: from app.services.ai_provider import stream_ai_text, ai_configured - if not ai_configured(): + if not ai_configured(username=username): yield json.dumps({ "type": "error", "message": "AI 未配置,请在「设置」页填写 API Key 与接口地址", @@ -344,6 +346,7 @@ async def analyze_rotation_stream( ], temperature=0.5, max_tokens=4000, + username=username, ): yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False) diff --git a/serve/backend/app/services/financial_analyzer.py b/serve/backend/app/services/financial_analyzer.py index 8dcd57a..68baa26 100644 --- a/serve/backend/app/services/financial_analyzer.py +++ b/serve/backend/app/services/financial_analyzer.py @@ -141,6 +141,8 @@ async def analyze_financials_stream( data_dir: Path, symbol: str, focus: str = "", + *, + username: str | None = None, ) -> AsyncIterator[str]: """流式分析:yield 出每个文本 chunk。 @@ -176,6 +178,7 @@ async def analyze_financials_stream( ], temperature=0.4, max_tokens=4000, + username=username, ): yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False) diff --git a/serve/backend/app/services/market_recap.py b/serve/backend/app/services/market_recap.py index 7c6b6d0..e0f8647 100644 --- a/serve/backend/app/services/market_recap.py +++ b/serve/backend/app/services/market_recap.py @@ -255,6 +255,8 @@ async def recap_market_stream( as_of: date | None = None, focus: str = "", news: list[dict] | None = None, + *, + username: str | None = None, ) -> AsyncIterator[str]: """流式大盘复盘:yield 出每个 NDJSON 事件。 @@ -298,6 +300,7 @@ async def recap_market_stream( ], temperature=0.5, max_tokens=4500, + username=username, ): yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False) diff --git a/serve/backend/app/services/stock_analyzer.py b/serve/backend/app/services/stock_analyzer.py index c312459..7deff68 100644 --- a/serve/backend/app/services/stock_analyzer.py +++ b/serve/backend/app/services/stock_analyzer.py @@ -251,6 +251,8 @@ async def analyze_stock_stream( data_dir: Path, symbol: str, focus: str = "", + *, + username: str | None = None, ) -> AsyncIterator[str]: """流式个股分析:yield 出每个 NDJSON 事件。 @@ -298,6 +300,7 @@ async def analyze_stock_stream( ], temperature=0.5, max_tokens=4500, + username=username, ): yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False) diff --git a/serve/backend/app/strategy/ai_generator.py b/serve/backend/app/strategy/ai_generator.py index 4db6a5e..cbcea8c 100644 --- a/serve/backend/app/strategy/ai_generator.py +++ b/serve/backend/app/strategy/ai_generator.py @@ -50,7 +50,7 @@ class AIStrategyGenerator: self._guide_cache = "" return self._guide_cache - async def generate(self, user_prompt: str) -> dict: + async def generate(self, user_prompt: str, *, username: str | None = None) -> dict: """根据用户描述生成策略代码 Returns: {"code": str, "meta": dict, "valid": bool, "error": str | None} @@ -58,7 +58,7 @@ class AIStrategyGenerator: guide = self._get_guide() # 调用 LLM - code = await self._call_llm(user_prompt, guide) + code = await self._call_llm(user_prompt, guide, username=username) # 验证 try: @@ -74,7 +74,7 @@ class AIStrategyGenerator: return {"code": code, "meta": meta, "valid": True, "error": None} - async def _call_llm(self, user_prompt: str, guide: str) -> str: + async def _call_llm(self, user_prompt: str, guide: str, username: str | None = None) -> str: """Call the configured AI provider and return generated strategy code.""" from app.services.ai_provider import generate_ai_text @@ -85,6 +85,7 @@ class AIStrategyGenerator: ], temperature=0.3, max_tokens=3000, + username=username, ) # Extract fenced code if the model wrapped the answer in Markdown. if "```python" in content: