mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
feat: improve bot status configuration summary (#1174)
This commit is contained in:
@@ -58,6 +58,8 @@ class StatusCommand(BotCommand):
|
||||
|
||||
def _collect_status(self, config) -> dict:
|
||||
"""收集系统状态信息"""
|
||||
from src.config import _uses_direct_env_provider, get_configured_llm_models
|
||||
|
||||
status = {
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
||||
@@ -67,8 +69,39 @@ class StatusCommand(BotCommand):
|
||||
}
|
||||
|
||||
# AI 配置状态
|
||||
status["ai_gemini"] = bool(config.gemini_api_key)
|
||||
status["ai_openai"] = bool(config.openai_api_key)
|
||||
llm_channels = getattr(config, "llm_channels", []) or []
|
||||
llm_model_list = getattr(config, "llm_model_list", []) or []
|
||||
llm_model = (getattr(config, "litellm_model", "") or "").strip()
|
||||
agent_model = (getattr(config, "agent_litellm_model", "") or "").strip()
|
||||
status["ai_primary_model"] = llm_model
|
||||
status["ai_agent_model"] = agent_model or ("继承主模型" if llm_model else "")
|
||||
status["ai_channels"] = [
|
||||
str(channel.get("name") or "").strip()
|
||||
for channel in llm_channels
|
||||
if str(channel.get("name") or "").strip()
|
||||
]
|
||||
status["ai_yaml"] = (
|
||||
getattr(config, "llm_models_source", "") == "litellm_config"
|
||||
and bool(llm_model_list)
|
||||
)
|
||||
status["ai_legacy_keys"] = {
|
||||
"Gemini": bool(getattr(config, "gemini_api_keys", [])),
|
||||
"OpenAI": bool(getattr(config, "openai_api_keys", [])),
|
||||
"Anthropic": bool(getattr(config, "anthropic_api_keys", [])),
|
||||
"DeepSeek": bool(getattr(config, "deepseek_api_keys", [])),
|
||||
}
|
||||
has_direct_env_model = bool(llm_model) and _uses_direct_env_provider(llm_model)
|
||||
available_router_model_set = set(get_configured_llm_models(llm_model_list))
|
||||
primary_model_reachable = not (
|
||||
available_router_model_set
|
||||
and llm_model
|
||||
and not _uses_direct_env_provider(llm_model)
|
||||
and llm_model not in available_router_model_set
|
||||
)
|
||||
status["ai_available"] = bool(
|
||||
llm_model
|
||||
and (has_direct_env_model or (llm_model_list and primary_model_reachable))
|
||||
)
|
||||
|
||||
# 搜索服务状态
|
||||
status["search_bocha"] = len(config.bocha_api_keys) > 0
|
||||
@@ -83,6 +116,29 @@ class StatusCommand(BotCommand):
|
||||
status["notify_feishu"] = bool(config.feishu_webhook_url)
|
||||
status["notify_telegram"] = bool(config.telegram_bot_token and config.telegram_chat_id)
|
||||
status["notify_email"] = bool(config.email_sender and config.email_password)
|
||||
status["notify_custom"] = bool(getattr(config, "custom_webhook_urls", []))
|
||||
status["notify_discord"] = bool(
|
||||
getattr(config, "discord_webhook_url", None)
|
||||
or (
|
||||
getattr(config, "discord_bot_token", None)
|
||||
and getattr(config, "discord_main_channel_id", None)
|
||||
)
|
||||
)
|
||||
status["notify_slack"] = bool(
|
||||
getattr(config, "slack_webhook_url", None)
|
||||
or (
|
||||
getattr(config, "slack_bot_token", None)
|
||||
and getattr(config, "slack_channel_id", None)
|
||||
)
|
||||
)
|
||||
status["notify_push"] = bool(
|
||||
getattr(config, "pushplus_token", None)
|
||||
or (
|
||||
getattr(config, "pushover_user_key", None)
|
||||
and getattr(config, "pushover_api_token", None)
|
||||
)
|
||||
or getattr(config, "serverchan3_sendkey", None)
|
||||
)
|
||||
|
||||
return status
|
||||
|
||||
@@ -114,8 +170,15 @@ class StatusCommand(BotCommand):
|
||||
lines.extend([
|
||||
"",
|
||||
"**🤖 AI 分析服务**",
|
||||
f"• Gemini API: {icon(status['ai_gemini'])}",
|
||||
f"• OpenAI API: {icon(status['ai_openai'])}",
|
||||
f"• 主模型: {status['ai_primary_model'] or '未配置'}",
|
||||
f"• Agent 模型: {status['ai_agent_model'] or '未配置'}",
|
||||
f"• LLM 渠道: {', '.join(status['ai_channels']) if status['ai_channels'] else '未配置'}",
|
||||
f"• LiteLLM YAML: {icon(status['ai_yaml'])}",
|
||||
"• Legacy Key: "
|
||||
+ ", ".join(
|
||||
f"{name}{icon(enabled)}"
|
||||
for name, enabled in status["ai_legacy_keys"].items()
|
||||
),
|
||||
"",
|
||||
"**🔍 搜索服务**",
|
||||
f"• Bocha: {icon(status['search_bocha'])}",
|
||||
@@ -130,11 +193,14 @@ class StatusCommand(BotCommand):
|
||||
f"• 飞书: {icon(status['notify_feishu'])}",
|
||||
f"• Telegram: {icon(status['notify_telegram'])}",
|
||||
f"• 邮件: {icon(status['notify_email'])}",
|
||||
f"• 自定义 Webhook: {icon(status['notify_custom'])}",
|
||||
f"• Discord: {icon(status['notify_discord'])}",
|
||||
f"• Slack: {icon(status['notify_slack'])}",
|
||||
f"• PushPlus/Pushover/Server酱3: {icon(status['notify_push'])}",
|
||||
])
|
||||
|
||||
# AI 服务总体状态
|
||||
ai_available = status['ai_gemini'] or status['ai_openai']
|
||||
if ai_available:
|
||||
if status["ai_available"]:
|
||||
lines.extend([
|
||||
"",
|
||||
"---",
|
||||
@@ -145,7 +211,7 @@ class StatusCommand(BotCommand):
|
||||
"",
|
||||
"---",
|
||||
"⚠️ **AI 服务未配置,分析功能不可用**",
|
||||
"请配置 Gemini 或 OpenAI API Key",
|
||||
"请配置 LITELLM_MODEL、LLM_CHANNELS、LITELLM_CONFIG 或任一 provider API Key",
|
||||
])
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
- [修复] 统一持仓快照输出现价/市值/浮盈亏/收益率与价格元信息,并为 LLM 渠道测试补充结构化诊断与设置页排障提示。
|
||||
- [文档] 补充 LLM 渠道编辑器的官方来源、依赖兼容窗口、保存时的运行时模型清理规则,以及旧配置回退路径说明。
|
||||
- [测试] 补齐 task_queue 运行时配置同步回归证据,明确 `tests/test_task_queue_config_sync.py` 作为本轮验收项。
|
||||
- [改进] Bot `/status` 展示统一 LLM 主模型、Agent 模型、渠道模式、YAML 配置和更多通知渠道状态。
|
||||
|
||||
## [3.14.2] - 2026-04-30
|
||||
|
||||
|
||||
@@ -195,7 +195,34 @@ class CommandDispatcher:
|
||||
|
||||
| /status | /s, 状态 | 系统状态 | `/status` |
|
||||
|
||||
## 五、Webhook 路由
|
||||
## 五、`/status` 与模型配置诊断说明
|
||||
|
||||
### 可配置层级与可用性判断依据
|
||||
|
||||
- `/status` 显示的 LLM 可用性遵循系统统一运行时优先级:
|
||||
- `LITELLM_CONFIG`(LiteLLM YAML)
|
||||
- `LLM_CHANNELS`
|
||||
- legacy provider 键(`GEMINI_API_KEY` / `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `DEEPSEEK_API_KEY`)
|
||||
- 当主模型(`LITELLM_MODEL` 或 `AGENT_LITELLM_MODEL`)在当前激活层无可用来源时,会展示“AI 服务未配置”,并保留用户可见原因行。
|
||||
- 本仓库 `requirements.txt` 的运行时依赖窗口为 `litellm>=1.80.10,<1.82.7`,该窗口内本链路以现有兼容行为为准。
|
||||
- 该诊断规则与 `GET /api/v1/system/config/setup/status` 的 LLM 检查保持一致:`LITELLM_CONFIG`/`LLM_CHANNELS` 为高优先级;模式切换时不会做静默迁移,切回旧模式由用户显式恢复历史值或回滚。
|
||||
|
||||
### 回退与迁移边界
|
||||
|
||||
- `LITELLM_CONFIG` 与 `LLM_CHANNELS` 任一生效时,下层 legacy 配置会被该层忽略(不会继续作为本次调用来源)。
|
||||
- 本次修复仅增强诊断,不进行 silent migration:不会主动清空/删除 `GEMINI_*`、`OPENAI_*`、`ANTHROPIC_*`、`LITELLM_*` 的历史值,仅在可用性诊断上提示。
|
||||
|
||||
### 官方兼容来源(用于排障核对)
|
||||
|
||||
- LiteLLM 官网:<https://docs.litellm.ai/>
|
||||
- LiteLLM OpenAI Compatible 说明:<https://docs.litellm.ai/docs/providers/openai_compatible>
|
||||
- OpenAI Chat API:<https://platform.openai.com/docs/api-reference/chat>
|
||||
- DeepSeek API 文档:<https://api-docs.deepseek.com/>
|
||||
- Kimi Moonshot 兼容说明:<https://platform.moonshot.ai/docs/guide/compatibility>
|
||||
- Gemini OpenAI 兼容说明:<https://ai.google.dev/gemini-api/docs/openai>
|
||||
- Ollama API 文档:<https://github.com/ollama/ollama/blob/main/docs/api.md>
|
||||
|
||||
## 六、Webhook 路由
|
||||
|
||||
在 [api/v1/router.py](../api/v1/router.py) 中注册路由:
|
||||
|
||||
@@ -262,4 +289,3 @@ telegram_webhook_secret: str # 新增:Webhook 密钥
|
||||
bot_rate_limit_window: int = 60 # 频率限制:窗口时间(秒)
|
||||
bot_admin_users: List[str] = field(default_factory=list) # 管理员用户 ID 列表,限制敏感操作
|
||||
```
|
||||
|
||||
|
||||
@@ -164,7 +164,34 @@ class BotCommand(ABC):
|
||||
|
||||
---
|
||||
|
||||
## 5. Webhook Routes
|
||||
## 5. `/status` and LLM configuration diagnostics
|
||||
|
||||
### Configuration precedence for readiness in `/status`
|
||||
|
||||
- The AI availability displayed by `/status` follows runtime precedence:
|
||||
- `LITELLM_CONFIG` (LiteLLM YAML)
|
||||
- `LLM_CHANNELS`
|
||||
- legacy provider keys (`GEMINI_API_KEY` / `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `DEEPSEEK_API_KEY`)
|
||||
- If the primary model (`LITELLM_MODEL` or `AGENT_LITELLM_MODEL`) has no configured source in the active layer, `/status` shows `AI 服务未配置` and keeps the explicit reason line.
|
||||
- Runtime dependency range in this repository is `litellm>=1.80.10,<1.82.7`; current status semantics are aligned with this window.
|
||||
- This diagnostic follows the same readiness rules as `GET /api/v1/system/config/setup/status` for LLM checks: channels/yaml are active higher priority than legacy keys, and no silent migration is performed when toggling modes.
|
||||
|
||||
### Fallback and migration boundary
|
||||
|
||||
- When `LITELLM_CONFIG` or `LLM_CHANNELS` is active, lower-priority legacy provider keys are ignored as the active source for that run (no silent downgrade).
|
||||
- This change only improves diagnosis and does not perform automatic migration: legacy configuration values are not deleted or rewritten during startup or status collection.
|
||||
|
||||
### Official compatibility references (for triage)
|
||||
|
||||
- LiteLLM docs: https://docs.litellm.ai/
|
||||
- LiteLLM OpenAI-compatible provider: https://docs.litellm.ai/docs/providers/openai_compatible
|
||||
- OpenAI Chat API: https://platform.openai.com/docs/api-reference/chat
|
||||
- DeepSeek API docs: https://api-docs.deepseek.com/
|
||||
- Kimi Moonshot compatibility: https://platform.moonshot.ai/docs/guide/compatibility
|
||||
- Gemini OpenAI compatibility: https://ai.google.dev/gemini-api/docs/openai
|
||||
- Ollama API docs: https://github.com/ollama/ollama/blob/main/docs/api.md
|
||||
|
||||
## 6. Webhook Routes
|
||||
|
||||
Handler functions for each platform live in `bot/handler.py`.
|
||||
These routes are **not yet wired** into the FastAPI application — you must mount them manually.
|
||||
@@ -190,7 +217,7 @@ async def dingtalk_webhook(request: Request):
|
||||
|
||||
---
|
||||
|
||||
## 6. Configuration
|
||||
## 7. Configuration
|
||||
|
||||
Add the following to your `.env`. Some of these bot-specific keys are already listed in `.env.example` (for example the DingTalk and Feishu app credentials), while others are not, so treat this section as a consolidated reference for bot setup:
|
||||
|
||||
|
||||
237
tests/test_bot_status_command.py
Normal file
237
tests/test_bot_status_command.py
Normal file
@@ -0,0 +1,237 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for bot /status command output."""
|
||||
|
||||
from bot.commands.status import StatusCommand
|
||||
from src.config import Config
|
||||
|
||||
|
||||
def test_status_command_reports_unified_llm_and_notification_channels():
|
||||
model_list = [
|
||||
{
|
||||
"model_name": "deepseek/deepseek-v4-flash",
|
||||
"litellm_params": {
|
||||
"model": "deepseek/deepseek-v4-flash",
|
||||
"api_key": "sk-test",
|
||||
},
|
||||
}
|
||||
]
|
||||
config = Config(
|
||||
stock_list=["600519", "AAPL"],
|
||||
litellm_model="deepseek/deepseek-v4-flash",
|
||||
agent_litellm_model="openai/gpt-4o-mini",
|
||||
llm_channels=[
|
||||
{
|
||||
"name": "deepseek",
|
||||
"models": ["deepseek/deepseek-v4-flash"],
|
||||
}
|
||||
],
|
||||
llm_models_source="llm_channels",
|
||||
llm_model_list=model_list,
|
||||
custom_webhook_urls=["https://example.com/webhook"],
|
||||
slack_webhook_url="https://hooks.slack.com/services/T/B/C",
|
||||
serverchan3_sendkey="SCT123",
|
||||
)
|
||||
command = StatusCommand()
|
||||
|
||||
status = command._collect_status(config)
|
||||
text = command._format_status(status, "telegram")
|
||||
|
||||
assert status["ai_available"] is True
|
||||
assert "主模型: deepseek/deepseek-v4-flash" in text
|
||||
assert "Agent 模型: openai/gpt-4o-mini" in text
|
||||
assert "LLM 渠道: deepseek" in text
|
||||
assert "自定义 Webhook: ✅" in text
|
||||
assert "Slack: ✅" in text
|
||||
assert "PushPlus/Pushover/Server酱3: ✅" in text
|
||||
assert "系统就绪" in text
|
||||
|
||||
|
||||
def test_status_command_warns_when_no_llm_source_configured():
|
||||
config = Config(stock_list=["600519"])
|
||||
command = StatusCommand()
|
||||
|
||||
status = command._collect_status(config)
|
||||
text = command._format_status(status, "telegram")
|
||||
|
||||
assert status["ai_available"] is False
|
||||
assert "主模型: 未配置" in text
|
||||
assert "AI 服务未配置" in text
|
||||
assert "LITELLM_MODEL" in text
|
||||
|
||||
|
||||
def test_status_command_does_not_treat_managed_model_name_as_ready():
|
||||
config = Config(
|
||||
stock_list=["600519"],
|
||||
litellm_model="openai/gpt-4o-mini",
|
||||
llm_model_list=[],
|
||||
)
|
||||
command = StatusCommand()
|
||||
|
||||
status = command._collect_status(config)
|
||||
text = command._format_status(status, "telegram")
|
||||
|
||||
assert status["ai_available"] is False
|
||||
assert "AI 服务未配置" in text
|
||||
|
||||
|
||||
def test_status_command_keeps_channel_mode_priority_over_legacy_keys():
|
||||
config = Config(
|
||||
stock_list=["600519"],
|
||||
litellm_model="openai/gpt-4o-mini",
|
||||
llm_channels=[
|
||||
{
|
||||
"name": "deepseek",
|
||||
"models": ["deepseek/deepseek-v4-flash"],
|
||||
}
|
||||
],
|
||||
llm_models_source="llm_channels",
|
||||
llm_model_list=[
|
||||
{
|
||||
"model_name": "deepseek/deepseek-v4-flash",
|
||||
"litellm_params": {
|
||||
"model": "deepseek/deepseek-v4-flash",
|
||||
"api_key": "sk-test",
|
||||
},
|
||||
}
|
||||
],
|
||||
openai_api_keys=["openai-legacy-key"],
|
||||
)
|
||||
command = StatusCommand()
|
||||
|
||||
status = command._collect_status(config)
|
||||
text = command._format_status(status, "telegram")
|
||||
|
||||
assert status["ai_available"] is False
|
||||
assert "AI 服务未配置" in text
|
||||
assert "主模型: openai/gpt-4o-mini" in text
|
||||
|
||||
|
||||
def test_status_command_requires_primary_model_in_configured_router_models():
|
||||
config = Config(
|
||||
stock_list=["600519"],
|
||||
litellm_model="openai/gpt-4o-mini",
|
||||
llm_channels=[
|
||||
{
|
||||
"name": "deepseek",
|
||||
"models": ["deepseek/deepseek-v4-flash"],
|
||||
}
|
||||
],
|
||||
llm_models_source="llm_channels",
|
||||
llm_model_list=[
|
||||
{
|
||||
"model_name": "deepseek/deepseek-v4-flash",
|
||||
"litellm_params": {
|
||||
"model": "deepseek/deepseek-v4-flash",
|
||||
"api_key": "sk-test",
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
command = StatusCommand()
|
||||
|
||||
status = command._collect_status(config)
|
||||
text = command._format_status(status, "telegram")
|
||||
|
||||
assert status["ai_available"] is False
|
||||
assert "AI 服务未配置" in text
|
||||
assert "系统就绪" not in text
|
||||
|
||||
|
||||
def test_status_command_requires_primary_model_for_yaml_router_models():
|
||||
config = Config(
|
||||
stock_list=["600519"],
|
||||
litellm_model="",
|
||||
llm_models_source="litellm_config",
|
||||
llm_model_list=[
|
||||
{
|
||||
"model_name": "yaml-primary",
|
||||
"litellm_params": {
|
||||
"model": "openai/gpt-4o-mini",
|
||||
"api_key": "sk-test",
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
command = StatusCommand()
|
||||
|
||||
status = command._collect_status(config)
|
||||
text = command._format_status(status, "telegram")
|
||||
|
||||
assert status["ai_yaml"] is True
|
||||
assert status["ai_available"] is False
|
||||
assert "主模型: 未配置" in text
|
||||
assert "AI 服务未配置" in text
|
||||
assert "系统就绪" not in text
|
||||
|
||||
|
||||
def test_status_command_does_not_treat_invalid_yaml_path_as_active():
|
||||
config = Config(
|
||||
stock_list=["600519"],
|
||||
litellm_config_path="missing.yaml",
|
||||
llm_models_source="legacy_env",
|
||||
llm_model_list=[],
|
||||
)
|
||||
command = StatusCommand()
|
||||
|
||||
status = command._collect_status(config)
|
||||
text = command._format_status(status, "telegram")
|
||||
|
||||
assert status["ai_yaml"] is False
|
||||
assert status["ai_available"] is False
|
||||
assert "LiteLLM YAML: ❌" in text
|
||||
assert "AI 服务未配置" in text
|
||||
|
||||
|
||||
def test_status_command_treats_direct_env_provider_model_as_ready():
|
||||
config = Config(
|
||||
stock_list=["600519"],
|
||||
litellm_model="cohere/command-r-plus",
|
||||
llm_model_list=[],
|
||||
)
|
||||
command = StatusCommand()
|
||||
|
||||
status = command._collect_status(config)
|
||||
text = command._format_status(status, "telegram")
|
||||
|
||||
assert status["ai_available"] is True
|
||||
assert "系统就绪" in text
|
||||
|
||||
|
||||
def test_status_command_supports_legacy_key_compatibility_without_explicit_litellm_model(monkeypatch, tmp_path):
|
||||
# When only legacy OpenAI-compatible keys are configured and LITELLM_MODEL is unset,
|
||||
# runtime still infers a usable model path. /status should reflect this compatibility
|
||||
# path instead of reporting hard failure.
|
||||
env_file = tmp_path / ".env"
|
||||
env_file.write_text("", encoding="utf-8")
|
||||
monkeypatch.setenv("ENV_FILE", str(env_file))
|
||||
for key in (
|
||||
"GEMINI_API_KEYS",
|
||||
"GEMINI_API_KEY",
|
||||
"ANTHROPIC_API_KEYS",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"DEEPSEEK_API_KEYS",
|
||||
"DEEPSEEK_API_KEY",
|
||||
"OPENAI_API_KEYS",
|
||||
"AIHUBMIX_KEY",
|
||||
"LITELLM_MODEL",
|
||||
"LLM_CHANNELS",
|
||||
"LITELLM_CONFIG",
|
||||
):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "sk-legacy-test-key")
|
||||
monkeypatch.setenv("OPENAI_MODEL", "gpt-4o-mini")
|
||||
|
||||
Config.reset_instance()
|
||||
try:
|
||||
config = Config.get_instance()
|
||||
command = StatusCommand()
|
||||
|
||||
status = command._collect_status(config)
|
||||
text = command._format_status(status, "telegram")
|
||||
|
||||
assert status["ai_available"] is True
|
||||
assert "主模型: openai/gpt-4o-mini" in text
|
||||
assert "AI 服务未配置" not in text
|
||||
finally:
|
||||
Config.reset_instance()
|
||||
Reference in New Issue
Block a user