hjg 账号默认使用 DeepSeek Pro AI 配置

This commit is contained in:
2026-07-07 22:17:00 +08:00
parent 490fb7ac0a
commit 2f2b53e03e
12 changed files with 130 additions and 30 deletions
+1 -1
View File
@@ -125,7 +125,7 @@ async def analyze_financials(request: Request, req: AnalyzeRequest):
data_dir = request.app.state.repo.store.data_dir data_dir = request.app.state.repo.store.data_dir
async def stream_gen(): 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" yield chunk + "\n"
return StreamingResponse( return StreamingResponse(
+1 -1
View File
@@ -55,7 +55,7 @@ async def analyze_market(request: Request, req: AnalyzeRequest):
raise HTTPException(400, f"as_of 格式应为 YYYY-MM-DD,收到: {req.as_of}") raise HTTPException(400, f"as_of 格式应为 YYYY-MM-DD,收到: {req.as_of}")
async def stream_gen(): 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" yield chunk + "\n"
return StreamingResponse( return StreamingResponse(
+1
View File
@@ -55,6 +55,7 @@ async def analyze_rotation(request: Request, req: AnalyzeRequest):
async def stream_gen(): async def stream_gen():
async for chunk in analyze_rotation_stream( async for chunk in analyze_rotation_stream(
repo, days, req.focus, repo, days, req.focus,
username=request.state.username,
): ):
yield chunk + "\n" yield chunk + "\n"
+26 -4
View File
@@ -49,14 +49,38 @@ class TickflowKeyIn(BaseModel):
def _get_ai_config(username: str | None, key: str, default: str = "") -> str: def _get_ai_config(username: str | None, key: str, default: str = "") -> str:
"""读 AI 配置: 用户级优先, 无则全局。""" """读 AI 配置: 用户级优先, 无则全局。hjg 账号回退到内置 DeepSeek Pro。"""
if username: if username:
val = secrets_store.load_ai_config(username).get(key) val = secrets_store.load_ai_config(username).get(key)
if val: if val:
return 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) 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("") @router.get("")
def get_settings(request: Request) -> dict: def get_settings(request: Request) -> dict:
"""返回当前配置概况(Key 脱敏)。""" """返回当前配置概况(Key 脱敏)。"""
@@ -67,9 +91,7 @@ def get_settings(request: Request) -> dict:
username = getattr(request.state, "username", None) username = getattr(request.state, "username", None)
key = secrets_store.get_tickflow_key() key = secrets_store.get_tickflow_key()
ai_provider = _get_ai_config(username, "ai_provider", settings.ai_provider) 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 ai_api_key = _get_ai_api_key(username)
if not ai_api_key:
ai_api_key = secrets_store.get_ai_key()
return { return {
"mode": tf_client.current_mode(), "mode": tf_client.current_mode(),
"tickflow_api_key_masked": secrets_store.mask(key), "tickflow_api_key_masked": secrets_store.mask(key),
+1 -1
View File
@@ -164,7 +164,7 @@ async def analyze_stock(request: Request, req: AnalyzeRequest):
data_dir = repo.store.data_dir data_dir = repo.store.data_dir
async def stream_gen(): 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" yield chunk + "\n"
return StreamingResponse( return StreamingResponse(
+2 -2
View File
@@ -345,7 +345,7 @@ async def build_strategy(req: BuildRequest, request: Request):
raise HTTPException(status_code=400, detail=f"无效步骤: {req.step}") raise HTTPException(status_code=400, detail=f"无效步骤: {req.step}")
try: try:
result = await gen.generate(prompt) result = await gen.generate(prompt, username=request.state.username)
except RuntimeError as e: except RuntimeError as e:
raise HTTPException(status_code=400, detail=str(e)) from e raise HTTPException(status_code=400, detail=str(e)) from e
return result return result
@@ -356,7 +356,7 @@ async def build_strategy(req: BuildRequest, request: Request):
async def ai_generate(req: AIGenerateRequest, request: Request): async def ai_generate(req: AIGenerateRequest, request: Request):
try: try:
gen = AIStrategyGenerator() gen = AIStrategyGenerator()
result = await gen.generate(req.prompt) result = await gen.generate(req.prompt, username=request.state.username)
except RuntimeError as e: except RuntimeError as e:
raise HTTPException(status_code=400, detail=str(e)) from e raise HTTPException(status_code=400, detail=str(e)) from e
except Exception as e: except Exception as e:
+81 -17
View File
@@ -25,16 +25,74 @@ Message = dict[str, str]
_ANSI_RE = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]") _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 return secrets_store.get_ai_config("ai_provider", settings.ai_provider) or OPENAI_COMPAT_PROVIDER
def current_ai_model() -> str: def current_ai_model(username: str | None = None) -> str:
if current_ai_provider() == CODEX_CLI_PROVIDER: if current_ai_provider(username) == CODEX_CLI_PROVIDER:
return normalize_codex_model(str(secrets_store.load().get("ai_model") or "")) 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) 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: def current_codex_command() -> str:
return normalize_codex_command( return normalize_codex_command(
secrets_store.get_ai_config("ai_codex_command", settings.ai_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 return False
def ai_configured(provider: str | None = None) -> bool: def ai_configured(provider: str | None = None, username: str | None = None) -> bool:
provider = provider or current_ai_provider() provider = provider or current_ai_provider(username)
if is_codex_cli_provider(provider): if is_codex_cli_provider(provider):
return codex_cli_available() return codex_cli_available()
return bool(secrets_store.get_ai_key()) return bool(current_ai_api_key(username))
async def generate_ai_text( async def generate_ai_text(
@@ -95,15 +153,17 @@ async def generate_ai_text(
temperature: float = 0.3, temperature: float = 0.3,
max_tokens: int = 3000, max_tokens: int = 3000,
timeout: float = 180.0, timeout: float = 180.0,
username: str | None = None,
) -> str: ) -> str:
"""Return a complete AI response from the currently configured provider.""" """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_codex_cli(messages, max_tokens=max_tokens, timeout=max(timeout, 600.0))
return await _run_openai_once( return await _run_openai_once(
messages, messages,
temperature=temperature, temperature=temperature,
max_tokens=max_tokens, max_tokens=max_tokens,
timeout=timeout, timeout=timeout,
username=username,
) )
@@ -113,13 +173,14 @@ async def stream_ai_text(
temperature: float = 0.5, temperature: float = 0.5,
max_tokens: int = 4000, max_tokens: int = 4000,
timeout: float = 180.0, timeout: float = 180.0,
username: str | None = None,
) -> AsyncIterator[str]: ) -> AsyncIterator[str]:
"""Yield text deltas from the configured provider. """Yield text deltas from the configured provider.
Codex CLI only exposes the final assistant message for this use case, so it Codex CLI only exposes the final assistant message for this use case, so it
yields one complete chunk after the command exits. 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)) yield await _run_codex_cli(messages, max_tokens=max_tokens, timeout=max(timeout, 600.0))
return return
@@ -128,6 +189,7 @@ async def stream_ai_text(
temperature=temperature, temperature=temperature,
max_tokens=max_tokens, max_tokens=max_tokens,
timeout=timeout, timeout=timeout,
username=username,
): ):
yield chunk yield chunk
@@ -138,14 +200,15 @@ async def _run_openai_once(
temperature: float, temperature: float,
max_tokens: int, max_tokens: int,
timeout: float, timeout: float,
username: str | None = None,
) -> str: ) -> str:
ai_key = secrets_store.get_ai_key() ai_key = current_ai_api_key(username)
if not ai_key: if not ai_key:
raise RuntimeError("AI API 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( resp = await client.chat.completions.create(
model=current_ai_model(), model=current_ai_model(username),
messages=list(messages), messages=list(messages),
temperature=temperature, temperature=temperature,
max_tokens=max_tokens, max_tokens=max_tokens,
@@ -161,14 +224,15 @@ async def _stream_openai(
temperature: float, temperature: float,
max_tokens: int, max_tokens: int,
timeout: float, timeout: float,
username: str | None = None,
) -> AsyncIterator[str]: ) -> AsyncIterator[str]:
ai_key = secrets_store.get_ai_key() ai_key = current_ai_api_key(username)
if not ai_key: if not ai_key:
raise RuntimeError("AI API 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( stream = await client.chat.completions.create(
model=current_ai_model(), model=current_ai_model(username),
messages=list(messages), messages=list(messages),
temperature=temperature, temperature=temperature,
max_tokens=max_tokens, max_tokens=max_tokens,
@@ -181,13 +245,13 @@ async def _stream_openai(
yield delta.content 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 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( return AsyncOpenAI(
api_key=api_key, 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, timeout=timeout,
max_retries=2, max_retries=2,
default_headers={"User-Agent": user_agent}, default_headers={"User-Agent": user_agent},
@@ -285,6 +285,8 @@ async def analyze_rotation_stream(
repo, repo,
days: int = 12, days: int = 12,
focus: str = "", focus: str = "",
*,
username: str | None = None,
) -> AsyncIterator[str]: ) -> AsyncIterator[str]:
"""流式概念轮动分析: yield 出每个 NDJSON 事件。 """流式概念轮动分析: yield 出每个 NDJSON 事件。
@@ -329,7 +331,7 @@ async def analyze_rotation_stream(
try: try:
from app.services.ai_provider import stream_ai_text, ai_configured from app.services.ai_provider import stream_ai_text, ai_configured
if not ai_configured(): if not ai_configured(username=username):
yield json.dumps({ yield json.dumps({
"type": "error", "type": "error",
"message": "AI 未配置,请在「设置」页填写 API Key 与接口地址", "message": "AI 未配置,请在「设置」页填写 API Key 与接口地址",
@@ -344,6 +346,7 @@ async def analyze_rotation_stream(
], ],
temperature=0.5, temperature=0.5,
max_tokens=4000, max_tokens=4000,
username=username,
): ):
yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False) yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False)
@@ -141,6 +141,8 @@ async def analyze_financials_stream(
data_dir: Path, data_dir: Path,
symbol: str, symbol: str,
focus: str = "", focus: str = "",
*,
username: str | None = None,
) -> AsyncIterator[str]: ) -> AsyncIterator[str]:
"""流式分析:yield 出每个文本 chunk。 """流式分析:yield 出每个文本 chunk。
@@ -176,6 +178,7 @@ async def analyze_financials_stream(
], ],
temperature=0.4, temperature=0.4,
max_tokens=4000, max_tokens=4000,
username=username,
): ):
yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False) yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False)
@@ -255,6 +255,8 @@ async def recap_market_stream(
as_of: date | None = None, as_of: date | None = None,
focus: str = "", focus: str = "",
news: list[dict] | None = None, news: list[dict] | None = None,
*,
username: str | None = None,
) -> AsyncIterator[str]: ) -> AsyncIterator[str]:
"""流式大盘复盘:yield 出每个 NDJSON 事件。 """流式大盘复盘:yield 出每个 NDJSON 事件。
@@ -298,6 +300,7 @@ async def recap_market_stream(
], ],
temperature=0.5, temperature=0.5,
max_tokens=4500, max_tokens=4500,
username=username,
): ):
yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False) yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False)
@@ -251,6 +251,8 @@ async def analyze_stock_stream(
data_dir: Path, data_dir: Path,
symbol: str, symbol: str,
focus: str = "", focus: str = "",
*,
username: str | None = None,
) -> AsyncIterator[str]: ) -> AsyncIterator[str]:
"""流式个股分析:yield 出每个 NDJSON 事件。 """流式个股分析:yield 出每个 NDJSON 事件。
@@ -298,6 +300,7 @@ async def analyze_stock_stream(
], ],
temperature=0.5, temperature=0.5,
max_tokens=4500, max_tokens=4500,
username=username,
): ):
yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False) yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False)
+4 -3
View File
@@ -50,7 +50,7 @@ class AIStrategyGenerator:
self._guide_cache = "" self._guide_cache = ""
return 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} Returns: {"code": str, "meta": dict, "valid": bool, "error": str | None}
@@ -58,7 +58,7 @@ class AIStrategyGenerator:
guide = self._get_guide() guide = self._get_guide()
# 调用 LLM # 调用 LLM
code = await self._call_llm(user_prompt, guide) code = await self._call_llm(user_prompt, guide, username=username)
# 验证 # 验证
try: try:
@@ -74,7 +74,7 @@ class AIStrategyGenerator:
return {"code": code, "meta": meta, "valid": True, "error": None} 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.""" """Call the configured AI provider and return generated strategy code."""
from app.services.ai_provider import generate_ai_text from app.services.ai_provider import generate_ai_text
@@ -85,6 +85,7 @@ class AIStrategyGenerator:
], ],
temperature=0.3, temperature=0.3,
max_tokens=3000, max_tokens=3000,
username=username,
) )
# Extract fenced code if the model wrapped the answer in Markdown. # Extract fenced code if the model wrapped the answer in Markdown.
if "```python" in content: if "```python" in content: