docs/bot-command_EN.md
This document covers the bot module architecture, supported commands, webhook routes, and how to configure platform integrations.
Glossary: "Enterprise bot" in this context means a chatbot that receives commands via webhook from a messaging platform (Feishu / DingTalk / WeChat Work / Telegram) and calls the analysis pipeline to reply inline.
flowchart TB
subgraph Platforms [Messaging Platforms]
FS[Feishu]
DT[DingTalk]
WC[WeChat Work]
TG[Telegram]
More[More platforms...]
end
subgraph BotModule [bot/ module]
WH[Webhook Server]
Adapters[Platform Adapters]
Dispatcher[Command Dispatcher]
Commands[Command Handlers]
end
subgraph Core [Core Modules]
AS[AnalysisService]
MA[MarketAnalyzer]
NS[NotificationService]
end
FS -->|POST /bot/feishu| WH
DT -->|POST /bot/dingtalk| WH
WC -->|POST /bot/wecom| WH
TG -->|POST /bot/telegram| WH
WH --> Adapters
Adapters -->|Unified message format| Dispatcher
Dispatcher --> Commands
Commands --> AS
Commands --> MA
Commands --> NS
bot/
├── __init__.py # Module entry, exports main classes
├── models.py # Unified message/response models
├── dispatcher.py # Command dispatcher (core)
├── handler.py # Webhook handler functions (one per platform)
├── commands/ # Command handlers
│ ├── __init__.py
│ ├── base.py # Abstract base class for commands
│ ├── analyze.py # /analyze — stock/index analysis
│ ├── ask.py # /ask — single-turn question
│ ├── batch.py # /batch — batch watchlist analysis
│ ├── chat.py # /chat — multi-turn strategy chat
│ ├── market.py # /market — market review
│ ├── help.py # /help — help text
│ └── status.py # /status — system status
└── platforms/ # Platform adapters
├── __init__.py
├── base.py # Abstract base class for platforms
├── dingtalk.py # DingTalk bot
├── dingtalk_stream.py # DingTalk Stream bot
└── feishu_stream.py # Feishu (Lark) Stream bot
bot/models.py)@dataclass
class BotMessage:
platform: str # Platform ID: feishu / dingtalk / wecom / telegram
user_id: str # Sender ID
user_name: str # Sender display name
chat_id: str # Conversation ID (group or DM)
chat_type: str # Conversation type: group / private
content: str # Message text
raw_data: Dict # Raw request data (platform-specific)
timestamp: datetime
mentioned: bool = False # Whether the bot was @-mentioned
@dataclass
class BotResponse:
text: str
markdown: bool = False # Whether the response is Markdown
at_user: bool = True # Whether to @-mention the sender
bot/platforms/base.py)class BotPlatform(ABC):
@property
@abstractmethod
def platform_name(self) -> str: ...
@abstractmethod
def verify_request(self, headers: Dict, body: bytes) -> bool:
"""Verify request signature (security check)"""
...
@abstractmethod
def parse_message(self, data: Dict) -> Optional[BotMessage]:
"""Parse platform message into unified format"""
...
@abstractmethod
def format_response(self, response: BotResponse, message: BotMessage) -> WebhookResponse:
"""Convert unified response to platform format"""
...
bot/commands/base.py)class BotCommand(ABC):
@property
@abstractmethod
def name(self) -> str: ... # e.g. 'analyze'
@property
@abstractmethod
def aliases(self) -> List[str]: ... # e.g. ['a', 'analyse']
@property
@abstractmethod
def description(self) -> str: ...
@property
@abstractmethod
def usage(self) -> str: ...
@abstractmethod
def execute(self, message: BotMessage, args: List[str]) -> BotResponse: ...
| Command | Description | Example |
|---|---|---|
/analyze | Analyze a specific stock or a registered index | /analyze AAPL, /analyze 600519, /analyze sh000016, /analyze 上证50 |
/ask | Single-turn question about a stock or the market | /ask what is RSI for AAPL |
/batch | Batch-analyze your configured watchlist | /batch |
/chat | Multi-turn strategy chat (maintains conversation context) | /chat |
/market | Market review (A-shares / US stocks) | /market |
/help | Show help text | /help |
/status | Show system status | /status |
Stock code formats: A-shares use 6-digit codes (e.g.
600519); HK stocks prefixhk(e.g.hk00700); US stocks use ticker symbols (e.g.AAPL,TSLA).
Registered index inputs: explicit codes (
sh000016), CSI aliases (930955.CSIconverges tocsi930955) and registered Chinese names (上证50) are all accepted. Index inputs are submitted as a structuredAnalysisTargetthat flows through to the analysis pipeline, sosh000016is never rewritten toSH000016. Unregistered CSI forms (e.g.930956.CSI), code shapes the legacy gate rejected (12345, bare00700,600519.SH, unregisteredsh999999) and unrecognized names return an explicit error without submitting a task; ambiguous registered names require an explicit code instead of guessing. Stock inputs (A-share 6-digit,HK+5-digit, US 1-5 letters, e.g.usfd→USFD) keep the legacy code path. Stock-name inputs (e.g.贵州茅台) are newly exposed by this Bot entry: they reuse the existing name resolver (resolve_name_to_code) and then submit the legacy code without a structured target.
scripts/smoke_bot_index_entry.py)Walks the real CommandDispatcher.dispatch_async -> AnalyzeCommand -> TaskService -> StockAnalysisPipeline online for a single target, covering the SH.000016 / 上证50 / 930955.CSI matrix scenarios without any webhook transport (no mocked online dependencies, no dry-run).
Run with a single target argument (--timeout defaults to 900 seconds):
.venv\Scripts\python.exe scripts/smoke_bot_index_entry.py "SH.000016"
.venv\Scripts\python.exe scripts/smoke_bot_index_entry.py "上证50"
.venv\Scripts\python.exe scripts/smoke_bot_index_entry.py "930955.CSI"
Process responsibilities: a worker subprocess submits and polls the in-process TaskService and prints single-line E2E_EVENT {json} events (phase=submitted|completed|failed|timeout, with target, plus task_id/stock_code/result/error per phase); the parent only enforces the deadline, cleans up the process tree and maps exit codes (0=success / 1=failure / 124=timeout).
Failure semantics: a failed task or a completed-but-incomplete result (canonical code or registered name not matching the matrix expectation, or any of analysis_summary/operation_advice/trend_prediction empty or whitespace-only) emits a failed event and exits non-zero; on timeout the process tree is killed (Windows taskkill /T /F, POSIX process group) — a successful cleanup emits a timeout event and exits 124, a failed cleanup emits a failed event carrying the cleanup error and exits 1 — never rolling back DB / reports / notifications side effects. On a user Ctrl-C the parent also cleans up the process tree first: a successful cleanup propagates the interrupt, a failed cleanup emits a failed event carrying the cleanup error and exits 1 — a cleanup failure is never silently swallowed. The worker's expected code/name come from the script's built-in authoritative matrix map (SH.000016/上证50 → sh000016/上证50, 930955.CSI → csi930955/红利低波100) — never from the response or the result's self-reported identity: a submission whose extra.stock_code does not match the expectation fails with an explicit mismatch error carrying the expected and actual values (the failed event also carries the structured actual stock_code), and targets outside the matrix as well as non-positive --timeout are rejected before any submission or subprocess spawn (argument/input error, exit code 2). Unexpected worker exceptions (dispatch / poll / event serialization) emit a failed event and exit 1 with the exception evidence kept on stderr; KeyboardInterrupt is not treated as an ordinary failure; the parent normalizes any other worker exit code to 1 so the runtime contract only exposes 0 / 1 / 124. The parent spawns the worker with an internal --worker flag, not via environment variables.
Prerequisites: network plus configured data-source and AI credentials (online chain). This script is not part of the offline gate and does not run in CI.
/status and LLM configuration diagnostics/status/status follows runtime precedence:
LITELLM_CONFIG (LiteLLM YAML)LLM_CHANNELSGEMINI_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY / DEEPSEEK_API_KEY)LITELLM_MODEL or AGENT_LITELLM_MODEL) has no configured source in the active layer, /status shows AI 服务未配置 and keeps the explicit reason line.litellm>=1.80.10,!=1.82.7,!=1.82.8,<1.99.0; current status semantics are aligned with this constraint.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.LITELLM_CONFIG or LLM_CHANNELS is active, lower-priority legacy provider keys are ignored as the active source for that run (no silent downgrade).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.
| Route | Method | Status | Notes |
|---|---|---|---|
/bot/dingtalk | POST | Ready | DingtalkPlatform is registered in ALL_PLATFORMS |
/bot/feishu | POST | Stream only | Use feishu_stream.py; no Webhook adapter in ALL_PLATFORMS |
/bot/wecom | POST | Not implemented | Handler exists but no platform adapter |
/bot/telegram | POST | Not implemented | Handler exists but no platform adapter |
To mount the DingTalk webhook in your FastAPI app:
from bot.handler import handle_dingtalk_webhook
@app.post("/bot/dingtalk")
async def dingtalk_webhook(request: Request):
headers = dict(request.headers)
body = await request.body()
return handle_dingtalk_webhook(headers, body)
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:
# --- Bot general ---
BOT_ENABLED=false
BOT_COMMAND_PREFIX=/
# --- Feishu (Lark) bot ---
FEISHU_APP_ID=
FEISHU_APP_SECRET=
FEISHU_DOMAIN=feishu # feishu (China) or lark (international/Lark)
FEISHU_VERIFICATION_TOKEN= # Event verification token
FEISHU_ENCRYPT_KEY= # Encryption key (optional)
# --- DingTalk bot ---
DINGTALK_APP_KEY=
DINGTALK_APP_SECRET=
# --- WeChat Work bot (in development) ---
WECOM_TOKEN=
WECOM_ENCODING_AES_KEY=
# --- Telegram bot ---
TELEGRAM_BOT_TOKEN= # Get from @BotFather
TELEGRAM_WEBHOOK_SECRET= # Webhook secret token
bot/platforms/.BotPlatform and implement verify_request, parse_message, format_response.api/app.py) instead of api/v1/router.py, so the callback path stays /bot/<platform> rather than /api/v1/bot/<platform>.bot/commands/.BotCommand and implement the execute method.