package main import ( "encoding/json" "fmt" "log" "net/http" "path/filepath" ) // ClaudeCodeHookEvent represents the JSON payload from Claude Code hooks type ClaudeCodeHookEvent struct { SessionID string `json:"session_id"` CWD string `json:"cwd"` HookEventName string `json:"hook_event_name"` ToolName string `json:"tool_name,omitempty"` ToolInput map[string]interface{} `json:"tool_input,omitempty"` Matcher string `json:"matcher,omitempty"` StopHookActive bool `json:"stop_hook_active,omitempty"` } type HookHandler struct { config Config db *PromptDB // 可能为 nil,未配置 MySQL 时跳过数据库写入 } func NewHookHandler(config Config, db *PromptDB) *HookHandler { return &HookHandler{config: config, db: db} } func (h *HookHandler) HandleHook(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var event ClaudeCodeHookEvent if err := json.NewDecoder(r.Body).Decode(&event); err != nil { log.Printf("failed to decode hook event: %v", err) http.Error(w, "bad request", http.StatusBadRequest) return } log.Printf("received hook: event=%s, matcher=%s", event.HookEventName, event.Matcher) var title, message string var priority int switch event.HookEventName { case "Notification": title, message, priority = h.handleNotification(event) case "Stop": title, message, priority = h.handleStop(event) case "PreToolUse": title, message, priority = h.handlePreToolUse(event) case "UserPromptSubmit": h.handleUserPromptSubmit(event) w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `{"status":"ok"}`) return default: log.Printf("ignored event: %s", event.HookEventName) w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `{"status":"ignored"}`) return } if err := SendMessage(h.config.GotifyURL, h.config.GotifyToken, title, message, priority); err != nil { log.Printf("failed to send gotify message: %v", err) http.Error(w, "internal error", http.StatusInternalServerError) return } log.Printf("notification sent: %s", title) w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `{"status":"ok"}`) } func (h *HookHandler) handleNotification(event ClaudeCodeHookEvent) (title, message string, priority int) { switch event.Matcher { case "permission_prompt": title = "Claude Code - 需要权限" message = "Claude 需要你批准一个操作" priority = 7 case "idle_prompt": title = "Claude Code - 等待输入" message = "Claude 完成工作,等待你的下一步指令" priority = 5 case "auth_success": title = "Claude Code - 认证成功" message = "身份验证完成" priority = 3 default: title = "Claude Code - 通知" message = "Claude Code 需要你的关注" priority = 5 } return title, message, priority } func (h *HookHandler) handlePreToolUse(event ClaudeCodeHookEvent) (title, message string, priority int) { switch event.ToolName { case "AskUserQuestion": title = "Claude Code - 等待回答" if question, ok := event.ToolInput["question"].(string); ok && question != "" { message = "Claude 向你提问: " + question } else { message = "Claude 向你提出了一个问题,等待你的回答" } priority = 6 default: title = "Claude Code - 工具调用" message = fmt.Sprintf("Claude 正在使用 %s", event.ToolName) priority = 4 } return title, message, priority } func (h *HookHandler) handleStop(event ClaudeCodeHookEvent) (title, message string, priority int) { if event.StopHookActive { // Avoid infinite loop: Stop hook triggered too many times title = "Claude Code - 循环停止" message = "Stop hook 连续触发多次,已自动停止" priority = 8 } else { title = "Claude Code - 任务完成" message = "Claude 完成了本轮回复" priority = 5 } return title, message, priority } // handleUserPromptSubmit 将用户提交的 prompt 写入数据库 func (h *HookHandler) handleUserPromptSubmit(event ClaudeCodeHookEvent) { if h.db == nil { log.Printf("mysql not configured, skip saving prompt for session %s", event.SessionID) return } prompt, _ := event.ToolInput["prompt"].(string) if prompt == "" { log.Printf("empty prompt in UserPromptSubmit, session=%s", event.SessionID) return } // 从 cwd 提取项目名称(最后一级目录) projectName := filepath.Base(event.CWD) if err := h.db.SavePrompt(event.SessionID, projectName, prompt); err != nil { log.Printf("failed to save prompt: %v", err) return } log.Printf("prompt saved: session=%s, project=%s, len=%d", event.SessionID, projectName, len(prompt)) }