12713257d0
UserPromptSubmit 不是工具事件,prompt 文本作为顶层 JSON 字段传递, 而非 tool_input.prompt。新增 Prompt 字段到结构体并从 event.Prompt 读取。
159 lines
4.7 KiB
Go
159 lines
4.7 KiB
Go
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"`
|
|
Prompt string `json:"prompt,omitempty"` // UserPromptSubmit 事件的 prompt 文本
|
|
}
|
|
|
|
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.Prompt
|
|
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))
|
|
}
|