-
Notifications
You must be signed in to change notification settings - Fork 8
feat: bot multi-mode platform with restricted mode and admin diagnostics #129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5726b8f
7a0f638
e3693a1
edcdb8d
431f7f8
2ded10f
68ab06f
e525b05
61fcd96
51257c6
3ff6110
ff451c3
1b6d17d
baefd55
c46e3bc
db95651
092ec28
4e412d9
943bc68
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| """新增 bot_usage_tracking 資料表 | ||
|
|
||
| 追蹤未綁定用戶的訊息使用量,支援 rate limiting。 | ||
|
|
||
| Revision ID: 009 | ||
| """ | ||
|
|
||
| from alembic import op | ||
|
|
||
| revision = "009" | ||
| down_revision = "008" | ||
| branch_labels = None | ||
| depends_on = None | ||
|
|
||
|
|
||
| def upgrade() -> None: | ||
| op.execute(""" | ||
| CREATE TABLE bot_usage_tracking ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| bot_user_id UUID NOT NULL REFERENCES bot_users(id) ON DELETE CASCADE, | ||
| period_type VARCHAR(10) NOT NULL, | ||
| period_key VARCHAR(20) NOT NULL, | ||
| message_count INT NOT NULL DEFAULT 0, | ||
| created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||
| updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||
| UNIQUE(bot_user_id, period_type, period_key) | ||
| ) | ||
| """) | ||
| # UNIQUE 約束已隱含建立唯一索引,無需額外 CREATE INDEX | ||
|
|
||
|
|
||
| def downgrade() -> None: | ||
| op.execute("DROP TABLE IF EXISTS bot_usage_tracking") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| """內建指令 handler | ||
|
|
||
| 註冊所有內建的斜線指令到 CommandRouter。 | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| import re | ||
| import time | ||
|
|
||
| from .commands import CommandContext, SlashCommand, router | ||
| from ..bot_line.trigger import reset_conversation | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| async def _handle_reset(ctx: CommandContext) -> str | None: | ||
| """重置對話歷史""" | ||
| # 群組檢查已由 CommandRouter.dispatch() 的 private_only 處理 | ||
| await reset_conversation(ctx.platform_user_id) | ||
| return "已清除對話歷史,開始新對話!有什麼可以幫你的嗎?" | ||
|
|
||
|
|
||
| async def _handle_debug(ctx: CommandContext) -> str | None: | ||
| """管理員系統診斷指令 | ||
|
|
||
| 使用 bot-debug Agent 和 debug-skill 腳本分析系統狀態。 | ||
| """ | ||
| from .. import ai_manager | ||
| from ..claude_agent import call_claude | ||
| from ..bot.ai import parse_ai_response | ||
| from ...config import settings | ||
|
|
||
| # 取得 bot-debug Agent | ||
| agent = await ai_manager.get_agent_by_name("bot-debug") | ||
| if not agent: | ||
| return "⚠️ bot-debug Agent 不存在,請確認系統已正確初始化。" | ||
|
|
||
| # 取得 system prompt | ||
| system_prompt_data = agent.get("system_prompt") | ||
| if isinstance(system_prompt_data, dict): | ||
| system_prompt = system_prompt_data.get("content", "") | ||
| else: | ||
| system_prompt = "" | ||
|
|
||
| if not system_prompt: | ||
| return "⚠️ bot-debug Agent 缺少 system_prompt 設定。" | ||
|
|
||
| # 取得 Agent 定義的工具 | ||
| agent_tools = agent.get("tools") or ["run_skill_script"] | ||
|
|
||
| # 準備 prompt(使用者的問題描述,或預設「分析系統目前狀態」) | ||
| user_problem = ctx.raw_args.strip() if ctx.raw_args else "" | ||
| if user_problem: | ||
| prompt = f"管理員問題:{user_problem}" | ||
|
Comment on lines
+54
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||
| else: | ||
| prompt = "請執行系統綜合健康檢查(check-system-health),分析目前系統狀態並回報結果。" | ||
|
|
||
| # 呼叫 Claude CLI | ||
| model = settings.bot_debug_model | ||
| start_time = time.time() | ||
|
|
||
| try: | ||
|
Comment on lines
+54
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Untrusted user input ( |
||
| response = await call_claude( | ||
| prompt=prompt, | ||
| model=model, | ||
| system_prompt=system_prompt, | ||
| timeout=180, # 3 分鐘(診斷腳本可能需要時間) | ||
| tools=agent_tools, | ||
| ctos_user_id=ctx.ctos_user_id, | ||
| ) | ||
| except Exception: | ||
| logger.exception("Debug 指令 AI 呼叫失敗") | ||
| return "⚠️ 診斷執行失敗,請查看系統日誌。" | ||
|
|
||
| duration_ms = int((time.time() - start_time) * 1000) | ||
| logger.info(f"/debug 診斷完成,耗時 {duration_ms}ms") | ||
|
|
||
| # 記錄 AI Log | ||
| try: | ||
| from ..linebot_ai import log_linebot_ai_call | ||
|
|
||
| await log_linebot_ai_call( | ||
| message_uuid=None, | ||
| line_group_id=None, | ||
| is_group=False, | ||
| input_prompt=prompt, | ||
| history=None, | ||
| system_prompt=system_prompt, | ||
| allowed_tools=agent_tools, | ||
| model=model, | ||
| response=response, | ||
| duration_ms=duration_ms, | ||
| context_type_override="bot-debug", | ||
| ) | ||
| except Exception: | ||
| logger.warning("記錄 /debug AI Log 失敗", exc_info=True) | ||
|
|
||
| # 解析回應 | ||
| reply_text, _files = parse_ai_response(response.message) | ||
|
|
||
| if not reply_text: | ||
| return "診斷完成,但未產生回報內容。" | ||
|
|
||
| # 過濾 FILE_MESSAGE(debug 不需要檔案傳送) | ||
| reply_text = re.sub(r"\[FILE_MESSAGE:[^\]]+\]", "", reply_text).strip() | ||
|
|
||
| return reply_text or "診斷完成,但未產生回報內容。" | ||
|
|
||
|
|
||
| def register_builtin_commands() -> None: | ||
| """註冊所有內建指令""" | ||
|
|
||
| # /reset 指令(包含所有別名) | ||
| router.register( | ||
| SlashCommand( | ||
| name="reset", | ||
| aliases=["新對話", "新对话", "清除對話", "清除对话", "忘記", "忘记"], | ||
| handler=_handle_reset, | ||
| require_bound=False, # 受限模式用戶也可以重置 | ||
| require_admin=False, | ||
| private_only=True, | ||
| ) | ||
| ) | ||
|
|
||
| # /debug 指令(管理員專用,僅限個人對話) | ||
| router.register( | ||
| SlashCommand( | ||
| name="debug", | ||
| aliases=["診斷", "diag"], | ||
| handler=_handle_debug, | ||
| require_bound=True, | ||
| require_admin=True, | ||
| private_only=True, | ||
| ) | ||
| ) | ||
|
|
||
| logger.info(f"已註冊 {len({id(v) for v in router._commands.values()})} 個內建指令") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
這個
elif route_result.action == "restricted":區塊的邏輯相當複雜且長,在單一函式中混合了指令處理和 AI 處理兩種不同的路徑。為了提高可讀性和可維護性,建議將這段邏輯提取到一個獨立的輔助函式中,例如
_handle_restricted_mode_event。這樣可以讓process_message_event的主流程更清晰,只負責分派事件,而將受限模式的具體處理細節封裝起來。雖然無法在此提供可直接套用的程式碼建議來新增函式,但重構的方向是將 lines 280-341 的內容移至一個新的
async def _handle_restricted_mode_event(...)函式中,並在原處呼叫它。