package main // search.go 全局搜索(Ctrl+K 命令面板):项目 / 待办 / 工单 / AI 会话各取前 8 条。 import "strings" // escapeLike 转义 LIKE 通配符,配合 ESCAPE '\' 使用。 func escapeLike(s string) string { r := strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`) return r.Replace(s) } func (a *App) GlobalSearch(q string) ([]SearchHit, error) { if e := a.ready(); e != nil { return nil, e } q = strings.TrimSpace(q) out := []SearchHit{} if q == "" { return out, nil } pat := "%" + escapeLike(q) + "%" type src struct { kind, query string args int // pat 占位数量 } for _, s := range []src{ {"project", `SELECT id,name,path,'' FROM projects WHERE name LIKE ? ESCAPE '\' OR path LIKE ? ESCAPE '\' ORDER BY updated_at DESC LIMIT 8`, 2}, {"todo", `SELECT id,title,status,priority FROM todos WHERE deleted=0 AND (title LIKE ? ESCAPE '\' OR content LIKE ? ESCAPE '\') ORDER BY updated_at DESC LIMIT 8`, 2}, {"ticket", `SELECT id,title,status,priority FROM tickets WHERE deleted=0 AND (title LIKE ? ESCAPE '\' OR description LIKE ? ESCAPE '\') ORDER BY updated_at DESC LIMIT 8`, 2}, {"conversation", `SELECT id,title,provider,'' FROM ai_conversations WHERE title LIKE ? ESCAPE '\' ORDER BY updated_at DESC LIMIT 8`, 1}, } { args := make([]any, s.args) for i := range args { args[i] = pat } rows, e := a.store.db.Query(s.query, args...) if e != nil { return nil, e } for rows.Next() { h := SearchHit{Kind: s.kind} if e := rows.Scan(&h.ID, &h.Title, &h.Sub, &h.Extra); e != nil { rows.Close() return nil, e } out = append(out, h) } rows.Close() if e := rows.Err(); e != nil { return nil, e } } return out, nil }