feat: bot multi-mode platform with restricted mode and admin diagnostics#129
feat: bot multi-mode platform with restricted mode and admin diagnostics#129
Conversation
建立 Bot 多模式平台基礎架構: - 新增斜線指令路由框架(commands.py),支援指令註冊、別名、權限檢查 - 將 /reset 指令遷移到 CommandRouter,Line/Telegram 共用 - 新增 bot_usage_tracking 資料表 migration(rate limiter 用) - 新增 BOT_UNBOUND_USER_POLICY 等 6 個環境變數設定 - 新增 OpenSpec change:proposal、design、specs、tasks 完整規劃 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add services/bot/identity_router.py with route_unbound() and handle_restricted_mode() - Modify linebot_router.py access control to use identity router for user_not_bound - Modify telegram handler access control to use identity router - Binding code verification remains prioritized before identity routing - 16 unit tests for route_unbound, get_unbound_policy, and handle_restricted_mode Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add BOT_RESTRICTED_PROMPT for unbound user restricted mode - Add BOT_DEBUG_PROMPT for admin diagnostic mode with debug-skill scripts - Add DEFAULT_BOT_MODE_AGENTS config (bot-restricted, bot-debug) - Refactor ensure_default_linebot_agents() into shared _ensure_agents() helper - Agents auto-created on startup, won't overwrite existing customizations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add services/bot/rate_limiter.py with check_rate_limit() and record_usage() - Integrate rate limit check at restricted mode entry point - Record usage after successful AI processing - PostgreSQL UPSERT for hourly/daily counters - 9 unit tests all passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- SKILL.md defining debug-skill with run_skill_script tool - check-server-logs: journalctl with lines/keyword params - check-ai-logs: query ai_logs table with limit/errors_only params - check-nginx-logs: docker logs with lines/type (access/error) params - check-db-status: connections, table sizes, database size - check-system-health: comprehensive health check with status report Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Register /debug command with aliases /診斷 /diag (admin-only, private-only) - Handler uses bot-debug Agent with BOT_DEBUG_MODEL - Default prompt triggers check-system-health when no problem description - 10 unit tests: handler logic + routing permissions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…control (Group 8) 知識庫新增 is_public 欄位支援公開存取控制: - models 新增 is_public 布林欄位(預設 false) - search_knowledge 新增 public_only 過濾(未綁定用戶自動啟用) - MCP search_knowledge 工具根據 ctos_user_id 自動設定 public_only - 前端知識庫編輯器新增「公開」勾選框 - list_library_folders 支援 LIBRARY_PUBLIC_FOLDERS 環境變數過濾 - 修復相關測試(rate limiter mock、CommandRouter 初始化、agent 數量調整) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
23 個整合測試覆蓋: - reject 策略回歸測試(Line/Telegram 個人/群組) - restricted 策略受限模式完整流程 - 知識庫 public_only 過濾邏輯驗證 - /debug 指令權限控制(管理員/非管理員/群組/未綁定) - rate limiter 超限阻斷與 key 格式 - 跨功能整合(config、models、指令別名) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
歸檔已完成的 bot-multi-mode-platform change,並同步 delta specs: - 新增 3 個 capability specs: bot-identity-router, bot-rate-limiter, bot-slash-commands - 更新 bot-platform spec: 新增 binding_status 欄位 - 更新 line-bot spec: 策略式未綁定用戶存取控制 - 更新 knowledge-base spec: is_public 公開存取旗標、圖書館公開資料夾 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
修復 code review 發現的重要問題: - parse_ai_response 呼叫方式錯誤(傳入 ClaudeResponse 而非 .message,回傳 tuple 被當 dict) - 未綁定用戶永遠被 _check_library_permission 拒絕(跳過完整權限檢查改用公開資料夾路徑) - _sanitize_path_segment 破壞多層路徑(改為分段清理後重組) - call_claude 在受限模式無 try/except(新增錯誤處理) - /debug 例外訊息洩漏內部細節(改用通用錯誤訊息) - 移除未使用的 record_usage import Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rate limiter: atomic check-and-increment in single transaction (TOCTOU fix), add cleanup_old_tracking() for data retention, fail-open on DB errors - Line router: intercept slash commands before restricted mode AI processing, fallback to push_text when reply_token expires during long AI calls - Commands: guard against missing handler (was non-async lambda default) - Debug scripts: sanitize error messages (no str(e) leak), validate int inputs with range clamping, whitelist nginx log_type, strict boolean for errors_only - SKILL.md: set requires_app to admin (was null/unrestricted) - Config: validate bot_rate_limit_hourly <= bot_rate_limit_daily - Update all tests for new atomic rate limiter API (708 tests pass) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary of ChangesHello @yazelin, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 此拉取請求引入了一個多模式 Bot 平台,旨在提升系統的靈活性和安全性。它透過身份分流機制,允許未綁定用戶在受限模式下使用部分 AI 功能,並對其進行頻率限制。同時,新增的斜線指令框架和管理員診斷工具,顯著增強了 Bot 的可擴展性和系統維護能力,並透過知識庫公開存取控制,精確管理資訊可見性。 Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
Cover lines 279-341: slash command interception, AI processing, reply_token expiry fallback, push_text fallback, error handling. Raises overall coverage from 84.33% to 87.83%. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces an important and well-structured multi-mode bot platform, featuring a clear identity router, a flexible command framework, and admin diagnostic tools, including a restricted mode for unbound users. While the implementation includes security controls like rate limiting and access checks, I've identified potential prompt injection vulnerabilities where user-supplied content is directly concatenated into LLM prompts. These should be mitigated by using proper delimiters and sanitizing platform-provided metadata like display names. Furthermore, I have some suggestions regarding database migration, exception handling, and code consistency to further enhance the quality of these changes.
| if user_display_name: | ||
| user_message = f"user[{user_display_name}]: {content}" | ||
| else: | ||
| user_message = f"user: {content}" |
There was a problem hiding this comment.
Potential prompt injection vulnerability. User-supplied content and the platform display name are directly concatenated into the LLM prompt. An attacker could manipulate their display name or the message content to inject instructions that bypass the 'restricted mode' constraints. It is recommended to sanitize the display name and use clear delimiters (e.g., XML-style tags) to separate user input from the rest of the prompt.
| user_problem = ctx.raw_args.strip() if ctx.raw_args else "" | ||
| if user_problem: | ||
| prompt = f"管理員問題:{user_problem}" |
There was a problem hiding this comment.
Potential prompt injection vulnerability. The user-provided problem description is directly embedded into the prompt sent to the diagnostic agent. Although this command is restricted to administrators, using delimiters to wrap the user input is a best practice to prevent unintended manipulation of the LLM's behavior.
| op.execute(""" | ||
| CREATE INDEX idx_bot_usage_tracking_user_period | ||
| ON bot_usage_tracking(bot_user_id, period_type, period_key) | ||
| """) |
| except Exception: | ||
| pass |
| return "診斷完成,但未產生回報內容。" | ||
|
|
||
| # 過濾 FILE_MESSAGE(debug 不需要檔案傳送) | ||
| import re |
| get_tools_for_user, | ||
| get_tool_routing_for_user, |
| # Telegram 專屬指令(不在 CommandRouter 中的) | ||
| if not is_group: | ||
| # /start 和 /help 指令(不需綁定即可使用) | ||
| cmd = text.strip().split("@")[0] # 處理 /start@botname 格式 | ||
| cmd = text.strip().split("@")[0] | ||
| if cmd == "/start": | ||
| await adapter.send_text(chat_id, START_MESSAGE) | ||
| return | ||
| if cmd == "/help": | ||
| await adapter.send_text(chat_id, HELP_MESSAGE) | ||
| return | ||
|
|
- Remove redundant index from migration 009 (UNIQUE already implies it) - Log warning instead of silent pass on push_text failure - Move `import re` to file top in identity_router and command_handlers - Remove unused imports (get_tools_for_user, get_tool_routing_for_user) - Add slash command interception to Telegram restricted mode Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cover slash command interception, AI processing, error handling, and silent routing in restricted mode to maintain 85% coverage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- H1: Rate limiter rollback on denial (avoid inflating counters) - H3: Remove dead code in Telegram restricted mode command parsing - H4: Rename reset_conversation param to platform_user_id (cross-platform) - M1: Telegram restricted mode now saves message and passes message_uuid - M2: Schedule cleanup_old_bot_tracking (daily at 04:30) - M4: Remove redundant group check in _handle_reset Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
asyncpg returns UUID objects from row["id"], but downstream code (CommandContext, rate_limiter, identity_router) expects str. Add str() conversion at the source to match Line-side behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
/gemini review |
- Remove is_reset_command block from process_message_with_ai (now handled by CommandRouter in handle_text_message) - Remove unused RESET_COMMANDS constant from Telegram handler - Remove unused check_rate_limit backward-compat wrapper - Fix misleading SQL comment in check-ai-logs.py - Remove related obsolete tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces multi-mode support for the Bot platform, including identity routing, restricted mode, frequency limiting, and administrator diagnostic commands. While the implementation follows security best practices in many areas (e.g., parameterized SQL queries, path traversal prevention) and features well-designed components like the command routing framework (commands.py), identity routing (identity_router.py), and atomic frequency limiting (rate_limiter.py), it introduces potential prompt injection vulnerabilities in the new identity router and command handlers by directly concatenating untrusted user input into LLM prompts. Overall, the code quality is high, but two suggestions were made regarding code structure to improve readability and maintainability.
| user_message = f"user[{user_display_name}]: {content}" | ||
| else: | ||
| user_message = f"user: {content}" | ||
|
|
||
| # 7. 組裝工具列表(僅 Agent 定義的工具 + 對應的 MCP 工具) | ||
| # 受限模式的 MCP 工具也受 Agent 設定限制 | ||
| mcp_tools = await get_mcp_tool_names(exclude_group_only=not is_group) | ||
| # 只保留 Agent 工具白名單中的 MCP 工具 | ||
| if agent_tools: | ||
| mcp_tool_set = set(mcp_tools) | ||
| allowed_mcp = [t for t in agent_tools if t in mcp_tool_set] | ||
| # Agent 工具中不在 MCP 列表中的可能是外部 skill | ||
| non_mcp_tools = [t for t in agent_tools if t not in mcp_tool_set] | ||
| all_tools = list(dict.fromkeys(allowed_mcp + non_mcp_tools)) | ||
| else: | ||
| all_tools = [] | ||
|
|
||
| # 取得需要的 MCP server(按需載入) | ||
| required_mcp_servers = set() | ||
| if all_tools: | ||
| # 受限模式使用空權限取得 MCP servers | ||
| required_mcp_servers = await get_mcp_servers_for_user(app_permissions) | ||
|
|
||
| # 8. 呼叫 Claude CLI | ||
| start_time = time.time() | ||
|
|
||
| try: | ||
| response = await call_claude( |
There was a problem hiding this comment.
Untrusted user input (content and user_display_name) is directly concatenated into the prompt sent to the LLM. This can lead to prompt injection attacks where an attacker can manipulate the LLM's behavior by providing specially crafted input. It is recommended to use structured prompts (e.g., separate system and user messages) and to sanitize or delimit user input to prevent it from being interpreted as instructions.
| user_problem = ctx.raw_args.strip() if ctx.raw_args else "" | ||
| if user_problem: | ||
| prompt = f"管理員問題:{user_problem}" | ||
| else: | ||
| prompt = "請執行系統綜合健康檢查(check-system-health),分析目前系統狀態並回報結果。" | ||
|
|
||
| # 呼叫 Claude CLI | ||
| model = settings.bot_debug_model | ||
| start_time = time.time() | ||
|
|
||
| try: |
There was a problem hiding this comment.
Untrusted user input (user_problem) is directly concatenated into the prompt sent to the LLM in the /debug command handler. Although this command is restricted to administrators, it still poses a risk of prompt injection if an administrator's account is compromised or if an administrator is tricked into providing malicious input. Structured prompts and input sanitization should be used.
| elif route_result.action == "restricted": | ||
| # 受限模式:先攔截斜線指令 | ||
| from ..services.bot.commands import router as cmd_router | ||
|
|
||
| parsed_cmd = cmd_router.parse(content) | ||
| if parsed_cmd: | ||
| cmd, cmd_args = parsed_cmd | ||
| from ..services.bot.commands import CommandContext | ||
|
|
||
| cmd_ctx = CommandContext( | ||
| platform_type="line", | ||
| platform_user_id=line_user_id, | ||
| bot_user_id=str(user_uuid), | ||
| ctos_user_id=None, # 未綁定 | ||
| is_admin=False, | ||
| is_group=is_group, | ||
| group_id=str(group_uuid) if group_uuid else None, | ||
| reply_token=event.reply_token, | ||
| raw_args=cmd_args, | ||
| ) | ||
| cmd_reply = await cmd_router.dispatch(cmd, cmd_args, cmd_ctx) | ||
| if cmd_reply and event.reply_token: | ||
| try: | ||
| await reply_text(event.reply_token, cmd_reply) | ||
| except Exception: | ||
| await push_text(line_user_id, cmd_reply) | ||
| return | ||
|
|
||
| # 受限模式 AI 處理 | ||
| try: | ||
| await reply_text( | ||
| event.reply_token, | ||
| "請先在 CTOS 系統綁定您的 Line 帳號才能使用此服務。\n\n" | ||
| "步驟:\n" | ||
| "1. 登入 CTOS 系統\n" | ||
| "2. 進入 Line Bot 管理頁面\n" | ||
| "3. 點擊「綁定 Line 帳號」產生驗證碼\n" | ||
| "4. 將驗證碼發送給我完成綁定", | ||
| # 取得使用者顯示名稱 | ||
| display_name = None | ||
| user_row = await get_line_user_record( | ||
| line_user_id, "display_name" | ||
| ) | ||
| if user_row: | ||
| display_name = user_row["display_name"] | ||
|
|
||
| reply = await handle_restricted_mode( | ||
| content=content, | ||
| platform_user_id=line_user_id, | ||
| bot_user_id=str(user_uuid), | ||
| is_group=is_group, | ||
| line_group_id=group_uuid, | ||
| message_uuid=message_uuid, | ||
| user_display_name=display_name, | ||
| ) | ||
| if reply: | ||
| # reply_token 可能在長時間 AI 處理後過期, | ||
| # 先嘗試 reply,失敗則 fallback 到 push | ||
| if event.reply_token: | ||
| try: | ||
| await reply_text(event.reply_token, reply) | ||
| except Exception: | ||
| await push_text(line_user_id, reply) | ||
| else: | ||
| await push_text(line_user_id, reply) | ||
| except Exception as e: | ||
| logger.warning(f"回覆未綁定訊息失敗: {e}") | ||
| # 群組對話:靜默不回應 | ||
| logger.error(f"受限模式 AI 處理失敗: {e}", exc_info=True) | ||
| try: | ||
| await push_text(line_user_id, "抱歉,處理訊息時發生錯誤,請稍後再試。") | ||
| except Exception as push_e: | ||
| logger.warning(f"推送錯誤訊息失敗: {push_e}") | ||
| # silent: 群組靜默忽略 |
There was a problem hiding this comment.
這個 elif route_result.action == "restricted": 區塊的邏輯相當複雜且長,在單一函式中混合了指令處理和 AI 處理兩種不同的路徑。
為了提高可讀性和可維護性,建議將這段邏輯提取到一個獨立的輔助函式中,例如 _handle_restricted_mode_event。這樣可以讓 process_message_event 的主流程更清晰,只負責分派事件,而將受限模式的具體處理細節封裝起來。
雖然無法在此提供可直接套用的程式碼建議來新增函式,但重構的方向是將 lines 280-341 的內容移至一個新的 async def _handle_restricted_mode_event(...) 函式中,並在原處呼叫它。
| if bot_user_id: | ||
| try: | ||
| async with get_connection() as conn: | ||
| row = await conn.fetchrow( | ||
| "SELECT user_id FROM bot_users WHERE id = $1", | ||
| bot_user_id, | ||
| ) | ||
| if row and row["user_id"]: | ||
| ctos_user_id = row["user_id"] | ||
| user_info = await get_user_role_and_permissions(ctos_user_id) | ||
| is_admin = user_info["role"] == "admin" | ||
| except Exception as e: | ||
| logger.error(f"查詢用戶綁定狀態失敗: {e}", exc_info=True) | ||
|
|
||
| ctx = CommandContext( | ||
| platform_type="telegram", |
There was a problem hiding this comment.
這段用於查詢用戶綁定狀態和管理員身份的邏輯,與 backend/src/ching_tech_os/services/linebot_ai.py 中 L1420-1430 的程式碼非常相似。
為了避免程式碼重複並提高可維護性,建議將這段邏輯提取到一個共用的輔助函式中,例如 _get_command_user_context(bot_user_id)。這個函式可以回傳一個包含 ctos_user_id 和 is_admin 的元組或字典。
這樣一來,linebot_ai.py 和 bot_telegram/handler.py 都可以呼叫這個共用函式來建構 CommandContext,從而簡化程式碼並確保兩邊的邏輯一致。
- H1: Command dispatch reply falls back to push_text when reply_token expires (e.g., /debug taking 3+ minutes) - M2: /debug command now records AI logs via log_linebot_ai_call with context_type "bot-debug"; make message_uuid param accept None - M3: Merge duplicate get_line_user_record queries in handle_text_message into a single call before command parse, reused for both paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract the "bot_user_id → ctos_user_id + is_admin" query pattern into a shared function in bot/commands.py, replacing the inline SQL in Telegram handler. Addresses Gemini review suggestion to deduplicate user context lookup across platforms. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add /agent slash command that allows admins to switch the AI agent used in a specific group or personal chat. Each group/user stores their own preference independently via active_agent_id FK on bot_users/bot_groups. Also fix MCP tool permission issue in bypassPermissions mode where ctos_user_id was not injected into tool parameters, causing "permission denied" errors for search_nas_files and read_document. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
BOT_UNBOUND_USER_POLICY設定(reject/restricted),決定未綁定用戶的處理路徑bot-restrictedAgent 使用受限功能(限制工具白名單、縮短對話歷史)/reset、/debug指令路由,支援 Line/Telegram 跨平台/debug指令搭配 debug-skill 腳本(server logs、AI logs、nginx、DB、system health)is_public欄位控制未綁定用戶可查詢的知識範圍LIBRARY_PUBLIC_FOLDERS環境變數控制未綁定用戶可瀏覽的資料夾Key Changes
identity_router.py、rate_limiter.py、commands.py、command_handlers.pydebug-skill/含 5 個診斷腳本linebot_router.py、telegram_handler.py整合身份分流bot_usage_tracking資料表 migrationis_public支援Test plan
🤖 Generated with Claude Code