package main import ( "bytes" "database/sql" "encoding/json" "fmt" "log" "math/rand" "net/http" "path/filepath" "regexp" "sync/atomic" "strings" "time" "unicode" "unicode/utf8" _ "modernc.org/sqlite" ) const cpaURL = "https://cpa.1216.top/v1/chat/completions" const cpaModel = "deepseek-v4-pro" const minKnowledgeRunes = 120 type knowledgeData struct { Content string `json:"content"` Keyword string `json:"keyword"` } var knowledgeDB *sql.DB func initKnowledgeDB() { dbPath := filepath.Join(configDir(), "knowledge.db") var err error knowledgeDB, err = sql.Open("sqlite", dbPath) if err != nil { log.Println("知识库打开失败:", err) return } knowledgeDB.SetMaxOpenConns(1) _, err = knowledgeDB.Exec(`CREATE TABLE IF NOT EXISTS knowledge_cards ( id INTEGER PRIMARY KEY AUTOINCREMENT, keyword TEXT NOT NULL, content TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`) if err != nil { log.Println("知识库建表失败:", err) } cleanPromptLeakedCards() } // cleanPromptLeakedCards 清理数据库中 prompt 泄露的垃圾卡片 func cleanPromptLeakedCards() { if knowledgeDB == nil { return } for _, sig := range promptLeakSignals { result, err := knowledgeDB.Exec( "DELETE FROM knowledge_cards WHERE content LIKE ?", "%"+sig+"%", ) if err == nil { if n, _ := result.RowsAffected(); n > 0 { log.Printf("清理 prompt 泄露卡片: 特征=%q, 删除=%d条", sig, n) } } } } func saveKnowledgeCard(keyword, content string) { if knowledgeDB == nil { return } _, err := knowledgeDB.Exec("INSERT INTO knowledge_cards (keyword, content) VALUES (?, ?)", keyword, content) if err != nil { log.Println("知识保存失败:", err) } } func getRandomKnowledgeCard(keyword string) string { if knowledgeDB == nil { return "" } rows, err := knowledgeDB.Query( "SELECT content FROM knowledge_cards WHERE keyword = ? ORDER BY RANDOM() LIMIT 20", keyword, ) if err != nil { return "" } defer rows.Close() for rows.Next() { var content string if rows.Scan(&content) == nil { content = normalizeKnowledgeContent(content) if isQualityKnowledgeCard(content) { return content } } } return "" } func getKnowledgeCardCount(keyword string) int { if knowledgeDB == nil { return 0 } var count int err := knowledgeDB.QueryRow( "SELECT COUNT(*) FROM knowledge_cards WHERE keyword = ?", keyword, ).Scan(&count) if err != nil { return 0 } return count } // systemPromptCodeMark 用于在 Go raw string 中拼接反引号 const systemPromptCodeMark = "`" const knowledgeSystemPrompt = `你是一位在一线写了 15 年底层系统代码的工程师,只在踩过真实生产级坑之后才写知识卡片。 内容铁律: - 必须是大多数人不知道的冷知识、反直觉行为、或版本变更导致的隐蔽 break - 必须包含至少一个精确数值(阈值/延迟/版本号/缓冲区大小/默认值等) - 必须讲清因果链:触发条件 → 内部机制 → 实际后果 → 正确做法 - 禁止泛泛而谈,禁止复述文档,禁止入门级内容 - 每次从不同角度切入,不要重复之前的知识点 格式: - Markdown:**加粗**重点、` + systemPromptCodeMark + `行内代码` + systemPromptCodeMark + `标记术语、简短列表 - 120-200字,紧凑无废话 - 不要 emoji` func fetchKnowledgeFromLLM(keyword string, cfg *Config) string { basePrompt := buildKnowledgePrompt(keyword, cfg.KnowledgePrompt) messages := []map[string]string{ {"role": "system", "content": knowledgeSystemPrompt}, {"role": "user", "content": basePrompt}, } body := map[string]interface{}{ "model": cpaModel, "max_completion_tokens": 2048, "temperature": 0.8, "messages": messages, } content := requestKnowledgeCompletion(body) content = normalizeKnowledgeContent(content) if isQualityKnowledgeCard(content) { return content } log.Printf("知识卡片质量不足,重试: %q", content) messages = append(messages, map[string]string{ "role": "assistant", "content": content, }, map[string]string{ "role": "user", "content": fmt.Sprintf( "上面这条「%s」知识卡片质量不行:要么太短、要么没有具体数值、要么信息密度不够。"+ "请换一个完全不同的角度重写,必须包含:一个精确数值 + 一个底层机制 + 一个生产踩坑场景。", keyword, ), }) body["messages"] = messages content = requestKnowledgeCompletion(body) content = normalizeKnowledgeContent(content) if isQualityKnowledgeCard(content) { return content } return "" } const promptCodeMark = "`" // 知识卡片随机切入角度,确保每次生成不同内容 var knowledgeAngles = []string{ "性能陷阱与调优", "并发/竞态/死锁", "内存泄漏与 GC", "网络协议底层行为", "安全漏洞与防御", "错误处理的隐蔽坑", "版本升级导致的 break change", "配置默认值的陷阱", "日志/监控/可观测性", "序列化/反序列化边界", "连接池/资源耗尽", "编译器/运行时的反直觉行为", "缓存一致性与失效", "时区/时间处理陷阱", "文件系统/IO 边界条件", } func buildKnowledgePrompt(keyword, customPrompt string) string { angle := knowledgeAngles[rand.Intn(len(knowledgeAngles))] basePrompt := fmt.Sprintf(`围绕「%s」从「%s」角度写一条面向资深开发者的技术知识卡片。 要求: - 120-180字,信息密度极高 - 必须是反常识、冷门陷阱、或版本差异等非常规知识点 - 必须包含至少一个精确数值(如:默认超时 30s、缓冲区 4KB、Go 1.21 修复等) - 必须讲清因果链:什么场景触发 → 底层发生了什么 → 有什么后果 → 正确做法 - 用 Markdown 格式:**加粗重点**、`+promptCodeMark+`行内代码`+promptCodeMark+`标记术语 - 不要写常见知识点,不要复述官方文档`, keyword, angle) if customPrompt != "" { basePrompt += "\n附加风格要求(不能覆盖上面的字数和质量要求):" + customPrompt } return basePrompt } func requestKnowledgeCompletion(body map[string]interface{}) string { jsonData, _ := json.Marshal(body) req, err := http.NewRequest("POST", cpaURL, bytes.NewReader(jsonData)) if err != nil { log.Println("知识API请求创建失败:", err) return "" } key := loadConfig().cpaKey() if key == "" { log.Println("未配置知识卡片 API Key") return "" } req.Header.Set("Authorization", "Bearer "+key) req.Header.Set("Content-Type", "application/json") resp, err := httpClient.Do(req) if err != nil { log.Println("知识API请求失败:", err) return "" } defer resp.Body.Close() if resp.StatusCode != 200 { log.Printf("知识API返回状态码: %d", resp.StatusCode) if resp.StatusCode == 429 { // 限流:等待后用新 body 重试 time.Sleep(3 * time.Second) req2, err2 := http.NewRequest("POST", cpaURL, bytes.NewReader(jsonData)) if err2 != nil { return "" } req2.Header.Set("Authorization", "Bearer "+key) req2.Header.Set("Content-Type", "application/json") resp2, err2 := httpClient.Do(req2) if err2 != nil { log.Println("知识API重试失败:", err2) return "" } defer resp2.Body.Close() if resp2.StatusCode != 200 { log.Printf("知识API重试状态码: %d", resp2.StatusCode) return "" } resp = resp2 } else { return "" } } var result struct { Choices []struct { Message struct { Content string `json:"content"` ReasoningContent string `json:"reasoning_content"` } `json:"message"` } `json:"choices"` } if json.NewDecoder(resp.Body).Decode(&result) != nil { log.Println("知识API响应解析失败") return "" } if len(result.Choices) == 0 { log.Println("知识API: choices 为空") return "" } if len(result.Choices) > 0 { c := result.Choices[0].Message.Content if c == "" { c = result.Choices[0].Message.ReasoningContent } return c } return "" } // normalizeKnowledgeContent 清洗 LLM 输出的格式残留,保留 Markdown 标记 func normalizeKnowledgeContent(content string) string { content = strings.TrimSpace(content) // 去除整段代码块包裹 if strings.HasPrefix(content, "```") && strings.HasSuffix(content, "```") { content = strings.TrimPrefix(content, "```") // 去除语言标识行 if idx := strings.Index(content, "\n"); idx >= 0 && !strings.Contains(content[:idx], " ") { content = content[idx+1:] } content = strings.TrimSuffix(content, "```") content = strings.TrimSpace(content) } // 去除常见前缀 content = strings.TrimPrefix(content, "知识卡片:") content = strings.TrimPrefix(content, "知识卡片:") content = strings.TrimPrefix(content, "正文:") content = strings.TrimPrefix(content, "正文:") // 统一换行 content = strings.ReplaceAll(content, "\r\n", "\n") // 连续空行压缩为两个换行(Markdown 段落分隔) for strings.Contains(content, "\n\n\n") { content = strings.ReplaceAll(content, "\n\n\n", "\n\n") } // 行尾空白清理 lines := strings.Split(content, "\n") for i, line := range lines { lines[i] = strings.TrimRight(line, " \t") } content = strings.Join(lines, "\n") return strings.TrimSpace(content) } var numberRe = regexp.MustCompile(`\d+`) // weakPhrases 空泛表述列表 var weakPhrases = []string{ "很重要", "非常重要", "很有用", "提升效率", "值得关注", "可以帮助", "广泛应用", "具有重要作用", "不可忽视", "具有重要意义", } // promptLeakSignals prompt 泄露特征词,命中任意一条即判定为 prompt 而非知识内容 var promptLeakSignals = []string{ "围绕「", "写一条", "角度写", "面向资深开发者", "信息密度极高", "120-180字", "必须包含至少一个精确数值", "必须讲清因果链", "不要写常见知识点", "不要复述官方文档", "附加风格要求", "围绕「", "从「", "」角度", } // isQualityKnowledgeCard 检查质量:字数≥120、有空泛表述上限、必须含数字、信息密度、prompt 泄露检测 func isQualityKnowledgeCard(content string) bool { runes := utf8.RuneCountInString(content) if runes < minKnowledgeRunes { return false } // prompt 泄露检测:包含 prompt 指令特征的内容直接拒绝 for _, sig := range promptLeakSignals { if strings.Contains(content, sig) { log.Printf("知识卡片 prompt 泄露检测命中 %q,丢弃", sig) return false } } // 必须包含数字(确保有具体数值) if !numberRe.MatchString(content) { return false } // 空泛表述检查 weakHits := 0 for _, phrase := range weakPhrases { if strings.Contains(content, phrase) { weakHits++ } } if weakHits >= 3 { return false } // 必须有标点(非空内容) if !strings.ContainsAny(content, "。;;::,,") { return false } // 信息密度:非中文字符(代码、数字、标点、Markdown)占比 ≥ 15% nonCJK := 0 for _, r := range content { if !unicode.Is(unicode.Han, r) && !unicode.IsSpace(r) { nonCJK++ } } if float64(nonCJK)/float64(runes) < 0.10 { return false } return true } func pushKnowledgeJSON(content, keyword string) { data, _ := json.Marshal(knowledgeData{Content: content, Keyword: keyword}) evalJS(fmt.Sprintf(`if(window.updateKnowledgeFromGo) window.updateKnowledgeFromGo(%s)`, string(data))) } var lastFetchTime int64 // unix timestamp, 限流冷却 func fetchAndPushKnowledge() { // 30s 冷却,防止频繁点击触发限流 now := time.Now().Unix() if now-atomic.LoadInt64(&lastFetchTime) < 30 { // 冷却中,回退显示缓存 cfg := loadConfig() if cached := getRandomKnowledgeCard(cfg.KnowledgeKeyword); cached != "" { pushKnowledgeJSON(cached, cfg.KnowledgeKeyword) } return } atomic.StoreInt64(&lastFetchTime, now) cfg := loadConfig() keyword := cfg.KnowledgeKeyword if keyword == "" { return } var content string count := getKnowledgeCardCount(keyword) if count > 0 && rand.Intn(10) < 3 { content = getRandomKnowledgeCard(keyword) } if content == "" { content = fetchKnowledgeFromLLM(keyword, cfg) if content != "" { saveKnowledgeCard(keyword, content) } } if content == "" && count > 0 { content = getRandomKnowledgeCard(keyword) } if content == "" { pushKnowledgeJSON("知识卡片加载失败,稍后重试", keyword) return } pushKnowledgeJSON(content, keyword) preview := content if len(preview) > 30 { preview = preview[:30] + "..." } log.Println("知识卡片已推送:", preview) } func pushKnowledgeLoading(keyword string) { pushKnowledgeJSON("加载中...", keyword) } func pushKnowledgePlaceholder() { pushKnowledgeJSON("请设置知识关键字", "") } func knowledgeLoop() { initKnowledgeDB() cfg := loadConfig() if cfg.KnowledgeKeyword != "" && !cfg.HideKnowledge { if cached := getRandomKnowledgeCard(cfg.KnowledgeKeyword); cached != "" { pushKnowledgeJSON(cached, cfg.KnowledgeKeyword) } else { pushKnowledgeLoading(cfg.KnowledgeKeyword) } } else if cfg.KnowledgeKeyword == "" { pushKnowledgePlaceholder() } time.Sleep(3 * time.Second) cfg = loadConfig() if cfg.KnowledgeKeyword != "" && !cfg.HideKnowledge { fetchAndPushKnowledge() } ticker := time.NewTicker(30 * time.Minute) for range ticker.C { cfg := loadConfig() if cfg.KnowledgeKeyword != "" && !cfg.HideKnowledge { fetchAndPushKnowledge() } } } func triggerKnowledgeRefresh() { go fetchAndPushKnowledge() }