Skip to content

Commit 80b195f

Browse files
yazelinclaude
andauthored
feat: bot multi-mode platform with restricted mode and admin diagnostics (#129)
* feat: add bot slash command router and multi-mode platform specs 建立 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> * feat: implement identity router for unbound user routing (Group 3) - 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> * feat: add bot-restricted and bot-debug agent definitions (Group 4) - 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> * feat: implement rate limiter for unbound users (Group 5) - 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> * feat: add debug-skill with 5 diagnostic scripts (Group 6) - 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> * feat: implement /debug admin diagnostic command (Group 7) - 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> * feat: add is_public flag to knowledge base and library folder access 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> * feat: add integration tests for bot multi-mode platform (Group 9) 23 個整合測試覆蓋: - reject 策略回歸測試(Line/Telegram 個人/群組) - restricted 策略受限模式完整流程 - 知識庫 public_only 過濾邏輯驗證 - /debug 指令權限控制(管理員/非管理員/群組/未綁定) - rate limiter 超限阻斷與 key 格式 - 跨功能整合(config、models、指令別名) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: archive bot-multi-mode-platform and sync specs 歸檔已完成的 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> * fix: resolve critical bugs found in code review 修復 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> * fix: harden bot multi-mode platform security and robustness - 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> * test: add restricted mode routing coverage for linebot_router 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> * fix: address code review findings (Gemini + self-review) - 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> * test: add Telegram restricted mode routing tests for coverage 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> * fix: address code review H1-H4, M1-M4 findings - 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> * fix: ensure Telegram _ensure_bot_user/_ensure_bot_group return str 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> * refactor: remove dead code from reset command migration - 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> * fix: add reply fallback, deduplicate user query, and add /debug AI log - 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> * refactor: extract get_command_user_context shared helper 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> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0f131ae commit 80b195f

50 files changed

Lines changed: 4864 additions & 201 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""新增 bot_usage_tracking 資料表
2+
3+
追蹤未綁定用戶的訊息使用量,支援 rate limiting。
4+
5+
Revision ID: 009
6+
"""
7+
8+
from alembic import op
9+
10+
revision = "009"
11+
down_revision = "008"
12+
branch_labels = None
13+
depends_on = None
14+
15+
16+
def upgrade() -> None:
17+
op.execute("""
18+
CREATE TABLE bot_usage_tracking (
19+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
20+
bot_user_id UUID NOT NULL REFERENCES bot_users(id) ON DELETE CASCADE,
21+
period_type VARCHAR(10) NOT NULL,
22+
period_key VARCHAR(20) NOT NULL,
23+
message_count INT NOT NULL DEFAULT 0,
24+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
25+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
26+
UNIQUE(bot_user_id, period_type, period_key)
27+
)
28+
""")
29+
# UNIQUE 約束已隱含建立唯一索引,無需額外 CREATE INDEX
30+
31+
32+
def downgrade() -> None:
33+
op.execute("DROP TABLE IF EXISTS bot_usage_tracking")

backend/src/ching_tech_os/api/linebot_router.py

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@
7979
check_line_access,
8080
update_group_settings,
8181
reply_text,
82+
push_text,
83+
get_line_user_record,
8284
)
8385
from ..services.linebot_ai import handle_text_message
8486

@@ -259,21 +261,85 @@ async def process_message_event(event: MessageEvent) -> None:
259261

260262
if not has_access:
261263
if deny_reason == "user_not_bound":
262-
# 個人對話:回覆提示訊息
263-
if not is_group and event.reply_token:
264+
# 身份分流:根據策略決定拒絕或進入受限模式
265+
from ..services.bot.identity_router import (
266+
route_unbound,
267+
handle_restricted_mode,
268+
)
269+
270+
route_result = route_unbound(
271+
platform_type="line", is_group=is_group
272+
)
273+
if route_result.action == "reject":
274+
if event.reply_token:
275+
try:
276+
await reply_text(event.reply_token, route_result.reply_text)
277+
except Exception as e:
278+
logger.warning(f"回覆未綁定訊息失敗: {e}")
279+
elif route_result.action == "restricted":
280+
# 受限模式:先攔截斜線指令
281+
from ..services.bot.commands import router as cmd_router
282+
283+
parsed_cmd = cmd_router.parse(content)
284+
if parsed_cmd:
285+
cmd, cmd_args = parsed_cmd
286+
from ..services.bot.commands import CommandContext
287+
288+
cmd_ctx = CommandContext(
289+
platform_type="line",
290+
platform_user_id=line_user_id,
291+
bot_user_id=str(user_uuid),
292+
ctos_user_id=None, # 未綁定
293+
is_admin=False,
294+
is_group=is_group,
295+
group_id=str(group_uuid) if group_uuid else None,
296+
reply_token=event.reply_token,
297+
raw_args=cmd_args,
298+
)
299+
cmd_reply = await cmd_router.dispatch(cmd, cmd_args, cmd_ctx)
300+
if cmd_reply and event.reply_token:
301+
try:
302+
await reply_text(event.reply_token, cmd_reply)
303+
except Exception:
304+
await push_text(line_user_id, cmd_reply)
305+
return
306+
307+
# 受限模式 AI 處理
264308
try:
265-
await reply_text(
266-
event.reply_token,
267-
"請先在 CTOS 系統綁定您的 Line 帳號才能使用此服務。\n\n"
268-
"步驟:\n"
269-
"1. 登入 CTOS 系統\n"
270-
"2. 進入 Line Bot 管理頁面\n"
271-
"3. 點擊「綁定 Line 帳號」產生驗證碼\n"
272-
"4. 將驗證碼發送給我完成綁定",
309+
# 取得使用者顯示名稱
310+
display_name = None
311+
user_row = await get_line_user_record(
312+
line_user_id, "display_name"
313+
)
314+
if user_row:
315+
display_name = user_row["display_name"]
316+
317+
reply = await handle_restricted_mode(
318+
content=content,
319+
platform_user_id=line_user_id,
320+
bot_user_id=str(user_uuid),
321+
is_group=is_group,
322+
line_group_id=group_uuid,
323+
message_uuid=message_uuid,
324+
user_display_name=display_name,
273325
)
326+
if reply:
327+
# reply_token 可能在長時間 AI 處理後過期,
328+
# 先嘗試 reply,失敗則 fallback 到 push
329+
if event.reply_token:
330+
try:
331+
await reply_text(event.reply_token, reply)
332+
except Exception:
333+
await push_text(line_user_id, reply)
334+
else:
335+
await push_text(line_user_id, reply)
274336
except Exception as e:
275-
logger.warning(f"回覆未綁定訊息失敗: {e}")
276-
# 群組對話:靜默不回應
337+
logger.error(f"受限模式 AI 處理失敗: {e}", exc_info=True)
338+
try:
339+
await push_text(line_user_id, "抱歉,處理訊息時發生錯誤,請稍後再試。")
340+
except Exception as push_e:
341+
logger.warning(f"推送錯誤訊息失敗: {push_e}")
342+
# silent: 群組靜默忽略
277343
elif deny_reason == "group_not_allowed":
278344
# 群組未開啟 AI 回應,靜默不回應
279345
pass

backend/src/ching_tech_os/config.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,38 @@ class Settings:
150150
"ctos", # 系統簡稱
151151
]
152152

153+
# ===================
154+
# Bot 多模式平台設定
155+
# ===================
156+
# 未綁定用戶策略:reject(預設,拒絕並提示綁定)/ restricted(走受限模式 Agent)
157+
bot_unbound_user_policy: str = _get_env("BOT_UNBOUND_USER_POLICY", "reject")
158+
# 受限模式使用的 AI 模型(控制成本,預設用較輕量的 haiku)
159+
bot_restricted_model: str = _get_env("BOT_RESTRICTED_MODEL", "haiku")
160+
# Debug 模式使用的 AI 模型
161+
bot_debug_model: str = _get_env("BOT_DEBUG_MODEL", "sonnet")
162+
# 頻率限制開關(僅在 restricted 模式下生效)
163+
bot_rate_limit_enabled: bool = _get_env_bool("BOT_RATE_LIMIT_ENABLED", True)
164+
# 每小時訊息上限(未綁定用戶)
165+
bot_rate_limit_hourly: int = _get_env_int("BOT_RATE_LIMIT_HOURLY", 20)
166+
# 每日訊息上限(未綁定用戶)
167+
bot_rate_limit_daily: int = _get_env_int("BOT_RATE_LIMIT_DAILY", 50)
168+
# 驗證 hourly <= daily(若設定不合理則修正)
169+
if bot_rate_limit_hourly > bot_rate_limit_daily:
170+
logger.warning(
171+
"BOT_RATE_LIMIT_HOURLY (%d) > BOT_RATE_LIMIT_DAILY (%d),"
172+
"將 hourly 限制調整為與 daily 相同",
173+
bot_rate_limit_hourly,
174+
bot_rate_limit_daily,
175+
)
176+
bot_rate_limit_hourly = bot_rate_limit_daily
177+
178+
# 圖書館公開資料夾(逗號分隔,未綁定用戶只能看到這些資料夾)
179+
library_public_folders: list[str] = [
180+
f.strip()
181+
for f in _get_env("LIBRARY_PUBLIC_FOLDERS", "產品資料,教育訓練").split(",")
182+
if f.strip()
183+
]
184+
153185
# ===================
154186
# Telegram Bot 設定
155187
# ===================

backend/src/ching_tech_os/main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ async def lifespan(app: FastAPI):
117117
app.state.skillhub_client = SkillHubClient()
118118
await init_db_pool()
119119

120+
# 註冊 Bot 斜線指令
121+
from .services.bot.command_handlers import register_builtin_commands
122+
register_builtin_commands()
123+
120124
# 啟動模組初始化(依啟停狀態)
121125
for module_id, info in get_module_registry().items():
122126
if not is_module_enabled(module_id):

backend/src/ching_tech_os/models/knowledge.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class KnowledgeCreate(BaseModel):
6666
category: str = "technical"
6767
scope: str = "personal" # 預設為個人知識(global、personal 或 project)
6868
project_id: str | None = None # 關聯專案 UUID(scope=project 時使用)
69+
is_public: bool = False # 是否允許未綁定用戶查詢
6970
tags: KnowledgeTags = Field(default_factory=KnowledgeTags)
7071
source: KnowledgeSource | None = None
7172
related: list[str] = Field(default_factory=list)
@@ -81,6 +82,7 @@ class KnowledgeUpdate(BaseModel):
8182
category: str | None = None
8283
scope: str | None = None # global, personal, project
8384
owner: str | None = None # 擁有者帳號(設為空字串可清除)
85+
is_public: bool | None = None # 是否允許未綁定用戶查詢
8486
tags: KnowledgeTags | None = None
8587
source: KnowledgeSource | None = None
8688
related: list[str] | None = None
@@ -96,6 +98,7 @@ class KnowledgeResponse(BaseModel):
9698
scope: str = "global" # global、personal 或 project
9799
owner: str | None = None # 擁有者帳號(個人知識用)
98100
project_id: str | None = None # 關聯專案 UUID(專案知識用)
101+
is_public: bool = False # 是否允許未綁定用戶查詢
99102
tags: KnowledgeTags
100103
source: KnowledgeSource
101104
related: list[str]
@@ -116,6 +119,7 @@ class KnowledgeListItem(BaseModel):
116119
scope: str = "global" # global、personal 或 project
117120
owner: str | None = None # 擁有者帳號(個人知識用)
118121
project_id: str | None = None # 關聯專案 UUID(專案知識用)
122+
is_public: bool = False # 是否允許未綁定用戶查詢
119123
tags: KnowledgeTags
120124
author: str
121125
updated_at: date
@@ -176,6 +180,7 @@ class IndexEntry(BaseModel):
176180
scope: str = "global" # global、personal 或 project
177181
owner: str | None = None # 擁有者帳號(個人知識用)
178182
project_id: str | None = None # 關聯專案 UUID(專案知識用)
183+
is_public: bool = False # 是否允許未綁定用戶查詢
179184
tags: KnowledgeTags
180185
author: str
181186
created_at: str
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"""內建指令 handler
2+
3+
註冊所有內建的斜線指令到 CommandRouter。
4+
"""
5+
6+
from __future__ import annotations
7+
8+
import logging
9+
import re
10+
import time
11+
12+
from .commands import CommandContext, SlashCommand, router
13+
from ..bot_line.trigger import reset_conversation
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
async def _handle_reset(ctx: CommandContext) -> str | None:
19+
"""重置對話歷史"""
20+
# 群組檢查已由 CommandRouter.dispatch() 的 private_only 處理
21+
await reset_conversation(ctx.platform_user_id)
22+
return "已清除對話歷史,開始新對話!有什麼可以幫你的嗎?"
23+
24+
25+
async def _handle_debug(ctx: CommandContext) -> str | None:
26+
"""管理員系統診斷指令
27+
28+
使用 bot-debug Agent 和 debug-skill 腳本分析系統狀態。
29+
"""
30+
from .. import ai_manager
31+
from ..claude_agent import call_claude
32+
from ..bot.ai import parse_ai_response
33+
from ...config import settings
34+
35+
# 取得 bot-debug Agent
36+
agent = await ai_manager.get_agent_by_name("bot-debug")
37+
if not agent:
38+
return "⚠️ bot-debug Agent 不存在,請確認系統已正確初始化。"
39+
40+
# 取得 system prompt
41+
system_prompt_data = agent.get("system_prompt")
42+
if isinstance(system_prompt_data, dict):
43+
system_prompt = system_prompt_data.get("content", "")
44+
else:
45+
system_prompt = ""
46+
47+
if not system_prompt:
48+
return "⚠️ bot-debug Agent 缺少 system_prompt 設定。"
49+
50+
# 取得 Agent 定義的工具
51+
agent_tools = agent.get("tools") or ["run_skill_script"]
52+
53+
# 準備 prompt(使用者的問題描述,或預設「分析系統目前狀態」)
54+
user_problem = ctx.raw_args.strip() if ctx.raw_args else ""
55+
if user_problem:
56+
prompt = f"管理員問題:{user_problem}"
57+
else:
58+
prompt = "請執行系統綜合健康檢查(check-system-health),分析目前系統狀態並回報結果。"
59+
60+
# 呼叫 Claude CLI
61+
model = settings.bot_debug_model
62+
start_time = time.time()
63+
64+
try:
65+
response = await call_claude(
66+
prompt=prompt,
67+
model=model,
68+
system_prompt=system_prompt,
69+
timeout=180, # 3 分鐘(診斷腳本可能需要時間)
70+
tools=agent_tools,
71+
ctos_user_id=ctx.ctos_user_id,
72+
)
73+
except Exception:
74+
logger.exception("Debug 指令 AI 呼叫失敗")
75+
return "⚠️ 診斷執行失敗,請查看系統日誌。"
76+
77+
duration_ms = int((time.time() - start_time) * 1000)
78+
logger.info(f"/debug 診斷完成,耗時 {duration_ms}ms")
79+
80+
# 記錄 AI Log
81+
try:
82+
from ..linebot_ai import log_linebot_ai_call
83+
84+
await log_linebot_ai_call(
85+
message_uuid=None,
86+
line_group_id=None,
87+
is_group=False,
88+
input_prompt=prompt,
89+
history=None,
90+
system_prompt=system_prompt,
91+
allowed_tools=agent_tools,
92+
model=model,
93+
response=response,
94+
duration_ms=duration_ms,
95+
context_type_override="bot-debug",
96+
)
97+
except Exception:
98+
logger.warning("記錄 /debug AI Log 失敗", exc_info=True)
99+
100+
# 解析回應
101+
reply_text, _files = parse_ai_response(response.message)
102+
103+
if not reply_text:
104+
return "診斷完成,但未產生回報內容。"
105+
106+
# 過濾 FILE_MESSAGE(debug 不需要檔案傳送)
107+
reply_text = re.sub(r"\[FILE_MESSAGE:[^\]]+\]", "", reply_text).strip()
108+
109+
return reply_text or "診斷完成,但未產生回報內容。"
110+
111+
112+
def register_builtin_commands() -> None:
113+
"""註冊所有內建指令"""
114+
115+
# /reset 指令(包含所有別名)
116+
router.register(
117+
SlashCommand(
118+
name="reset",
119+
aliases=["新對話", "新对话", "清除對話", "清除对话", "忘記", "忘记"],
120+
handler=_handle_reset,
121+
require_bound=False, # 受限模式用戶也可以重置
122+
require_admin=False,
123+
private_only=True,
124+
)
125+
)
126+
127+
# /debug 指令(管理員專用,僅限個人對話)
128+
router.register(
129+
SlashCommand(
130+
name="debug",
131+
aliases=["診斷", "diag"],
132+
handler=_handle_debug,
133+
require_bound=True,
134+
require_admin=True,
135+
private_only=True,
136+
)
137+
)
138+
139+
logger.info(f"已註冊 {len({id(v) for v in router._commands.values()})} 個內建指令")

0 commit comments

Comments
 (0)