feat: improve bot status configuration summary (#1174)

This commit is contained in:
mumu
2026-05-02 17:03:47 +08:00
committed by GitHub
parent 8ca0771908
commit 267d5fd63e
5 changed files with 368 additions and 11 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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 列表,限制敏感操作
```

View File

@@ -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:

View 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()