优化: 知识卡片交互+deepseek模型+鼠标钩子

This commit is contained in:
2026-06-05 21:45:45 +08:00
parent 004dba7db6
commit 0302546267
10 changed files with 667 additions and 77 deletions

View File

@@ -9,16 +9,19 @@ import (
"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 = "glm-5.1"
const minKnowledgeRunes = 80
const cpaModel = "deepseek-v4-pro"
const minKnowledgeRunes = 120
type knowledgeData struct {
Content string `json:"content"`
@@ -96,31 +99,34 @@ func getKnowledgeCardCount(keyword string) int {
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": `你是一位资深技术专家,面向有多年经验的开发者撰写知识卡片。
内容标准:
- 必须包含可直接用于生产的硬核知识:底层原理、性能陷阱、边界条件、源码级细节
- 优先讲"为什么"而非"是什么"——机制、权衡、踩坑经验
- 用具体数值、代码片段(仅关键词级别)、或真实场景佐证
- 读者看完能立刻在项目中应用或避免踩坑
禁止:
- 入门级科普、"是什么"式定义、百科搬运
- 空泛鸡汤("很重要"、"提升效率"、"广泛应用"
- 标题、序号、Markdown标记、emoji`,
},
{"role": "system", "content": knowledgeSystemPrompt},
{"role": "user", "content": basePrompt},
}
body := map[string]interface{}{
"model": cpaModel,
"max_completion_tokens": 2048,
"temperature": 0.55,
"temperature": 0.8,
"messages": messages,
}
content := requestKnowledgeCompletion(body)
@@ -131,9 +137,13 @@ func fetchKnowledgeFromLLM(keyword string, cfg *Config) string {
log.Printf("知识卡片质量不足,重试: %q", content)
messages = append(messages, map[string]string{
"role": "assistant",
"content": content,
}, map[string]string{
"role": "user",
"content": fmt.Sprintf(
"上一条太短或信息密度不足。请重写一条「%s」知识卡片120-180个中文字符必须包含一个明确机制/原理、一个具体例子或应用场景、一个结论。只输出正文。",
"上面这条「%s」知识卡片质量不行要么太短、要么没有具体数值、要么信息密度不够。"+
"请换一个完全不同的角度重写,必须包含:一个精确数值 + 一个底层机制 + 一个生产踩坑场景。",
keyword,
),
})
@@ -147,14 +157,38 @@ func fetchKnowledgeFromLLM(keyword string, cfg *Config) string {
return ""
}
const promptCodeMark = "`"
// 知识卡片随机切入角度,确保每次生成不同内容
var knowledgeAngles = []string{
"性能陷阱与调优",
"并发/竞态/死锁",
"内存泄漏与 GC",
"网络协议底层行为",
"安全漏洞与防御",
"错误处理的隐蔽坑",
"版本升级导致的 break change",
"配置默认值的陷阱",
"日志/监控/可观测性",
"序列化/反序列化边界",
"连接池/资源耗尽",
"编译器/运行时的反直觉行为",
"缓存一致性与失效",
"时区/时间处理陷阱",
"文件系统/IO 边界条件",
}
func buildKnowledgePrompt(keyword, customPrompt string) string {
basePrompt := fmt.Sprintf(`围绕「%s」写一条面向资深开发者的技术知识卡片。
angle := knowledgeAngles[rand.Intn(len(knowledgeAngles))]
basePrompt := fmt.Sprintf(`围绕「%s」从「%s」角度写一条面向资深开发者的技术知识卡片。
要求:
- 100-160字2-3句连贯正文
- 必须触及底层机制、性能细节或生产踩坑经验
- 必须包含一个具体场景或数值佐证(如延迟数据、内存阈值、版本差异等)
- 直接输出正文不要标题、编号、Markdown`, keyword)
- 120-180字信息密度极高
- 必须是反常识、冷门陷阱、或版本差异等非常规知识点
- 必须包含至少一个精确数值(如:默认超时 30s、缓冲区 4KB、Go 1.21 修复等)
- 必须讲清因果链:什么场景触发 → 底层发生了什么 → 有什么后果 → 正确做法
- 用 Markdown 格式:**加粗重点**、`+promptCodeMark+`行内代码`+promptCodeMark+`标记术语
- 不要写常见知识点,不要复述官方文档`, keyword, angle)
if customPrompt != "" {
basePrompt += "\n附加风格要求不能覆盖上面的字数和质量要求" + customPrompt
}
@@ -186,7 +220,29 @@ func requestKnowledgeCompletion(body map[string]interface{}) string {
if resp.StatusCode != 200 {
log.Printf("知识API返回状态码: %d", resp.StatusCode)
return ""
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 {
@@ -201,6 +257,10 @@ func requestKnowledgeCompletion(body map[string]interface{}) string {
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 == "" {
@@ -211,29 +271,58 @@ func requestKnowledgeCompletion(body map[string]interface{}) string {
return ""
}
// normalizeKnowledgeContent 清洗 LLM 输出的格式残留Markdown标记、标题前缀、多余空白)
// normalizeKnowledgeContent 清洗 LLM 输出的格式残留,保留 Markdown 标记
func normalizeKnowledgeContent(content string) string {
content = strings.TrimSpace(content)
content = strings.Trim(content, "` \t\r\n")
// 去除整段代码块包裹
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")
lines := strings.FieldsFunc(content, func(r rune) bool {
return r == '\n' || r == '\r' || r == '\t'
})
content = strings.Join(lines, " ")
content = strings.Join(strings.Fields(content), " ")
// 连续空行压缩为两个换行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)
}
// isQualityKnowledgeCard 检查字数 ≥80 且空泛表述 <3 条
var numberRe = regexp.MustCompile(`\d+`)
// weakPhrases 空泛表述列表
var weakPhrases = []string{
"很重要", "非常重要", "很有用", "提升效率", "值得关注",
"可以帮助", "广泛应用", "具有重要作用", "不可忽视", "具有重要意义",
}
// isQualityKnowledgeCard 检查质量字数≥120、有空泛表述上限、必须含数字、信息密度
func isQualityKnowledgeCard(content string) bool {
if utf8.RuneCountInString(content) < minKnowledgeRunes {
runes := utf8.RuneCountInString(content)
if runes < minKnowledgeRunes {
return false
}
weakPhrases := []string{"很重要", "非常重要", "很有用", "提升效率", "值得关注", "可以帮助", "广泛应用"}
// 必须包含数字(确保有具体数值)
if !numberRe.MatchString(content) {
return false
}
// 空泛表述检查
weakHits := 0
for _, phrase := range weakPhrases {
if strings.Contains(content, phrase) {
@@ -243,7 +332,21 @@ func isQualityKnowledgeCard(content string) bool {
if weakHits >= 3 {
return false
}
return strings.ContainsAny(content, "。;;:,")
// 必须有标点(非空内容)
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) {
@@ -251,7 +354,21 @@ func pushKnowledgeJSON(content, keyword string) {
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 == "" {