feat: 添加注释
This commit is contained in:
+40
-10
@@ -1,3 +1,5 @@
|
||||
// Package handler 实现了面向 Wails 前端的中间件逻辑。
|
||||
// Expert handler 专门处理 "搜索" 与 "AI 回答" 的请求。
|
||||
package handler
|
||||
|
||||
import (
|
||||
@@ -13,17 +15,21 @@ import (
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// Expert handles search and AI streaming for the active library.
|
||||
// Expert 是负责搜索与处理大型 RAG 对话链路的控制器结构。
|
||||
type Expert struct {
|
||||
ctx context.Context
|
||||
stopMu sync.Mutex
|
||||
ctx context.Context
|
||||
// stopMu: 读写锁,防止用户连续快速点击 "Stop" 导致多线程 nil pointer 异常。
|
||||
stopMu sync.Mutex
|
||||
// stopCancel: 持有当前正在进行的 Context,可以在任意时刻中止发给 OpenAI 兼容后端的网络请求。
|
||||
stopCancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewExpert() *Expert { return &Expert{} }
|
||||
func (e *Expert) SetContext(ctx context.Context) { e.ctx = ctx }
|
||||
|
||||
// SearchExpert fuzzy-searches the active knowledge library.
|
||||
// SearchExpert 是核心的关键词输入搜索,它在活跃的本地 SQLite 库中采用 OR LIKE 的朴素算法进行查询。
|
||||
// 若无结果直接返回空数组,并不引起任何异常。
|
||||
// 这里的空结果触发降级推荐,是由 service 层自行封装处理后抛出来的(带有 IsFallback=true)。
|
||||
func (e *Expert) SearchExpert(query string) []service.SearchResult {
|
||||
results, err := service.SearchKnowledge(query)
|
||||
if err != nil {
|
||||
@@ -33,28 +39,41 @@ func (e *Expert) SearchExpert(query string) []service.SearchResult {
|
||||
return results
|
||||
}
|
||||
|
||||
// AskDeepSeek performs RAG + streaming AI call.
|
||||
// AskDeepSeek 是系统最核心的 RAG 流式问答接口。
|
||||
// 它调用了 server-sent events (SSE) 进行块级读取请求。
|
||||
//
|
||||
// 工作流设计:
|
||||
// 1. 将 query 丢进 buildKnowledgeContext 中提取上下文参考(这保证即使用户点击“直接提问”,AI也能带入相关的环境信息)。
|
||||
// 2. ResolveAIConfig 会去 settings.db 读取用户的 API 密钥。如果他填了个空密钥,就会走全局兜底的公钥(Config.yaml 里那个)。
|
||||
// 3. 开启协程发 HTTP 请求,主路在此执行 block 循环:一收到一个 channel 片段就利用 runtime.EventsEmit() 通知前端更新,达到"打字机动画"的感觉。
|
||||
// 4. __STOPPED__ 与 __ERROR__ 是特殊的哨兵符号,用于控制前端何时该取消动画或者该降级展示。
|
||||
func (e *Expert) AskDeepSeek(query, rawAnswer string) string {
|
||||
aiCfg := service.ResolveAIConfig()
|
||||
knowledgeCtx := e.buildKnowledgeContext(query)
|
||||
|
||||
var userMsg string
|
||||
if rawAnswer != "" {
|
||||
// 如果有了答案(代表由点击搜索结果的"AI润色"触发),原句当作参考材料传入。
|
||||
userMsg = fmt.Sprintf("用户问题:%s\n\n原始参考答案:%s", query, rawAnswer)
|
||||
} else {
|
||||
userMsg = fmt.Sprintf("用户问题:%s\n\n请直接回答上述问题。", query)
|
||||
}
|
||||
messages := service.BuildRAGMessages(knowledgeCtx, userMsg, aiCfg.SystemPrompt)
|
||||
|
||||
messages := service.BuildRAGMessages(knowledgeCtx, userMsg, aiCfg.SystemPrompt)
|
||||
streamCh := make(chan string, 64)
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
// 创建可取消的上下文,用于停止生成
|
||||
streamCtx, cancel := context.WithCancel(e.ctx)
|
||||
e.setStopCancel(cancel)
|
||||
e.setStopCancel(cancel) // 将这个 cancel 函数放进结构体中
|
||||
|
||||
// 开一个 goroutine 去跑耗时的 HTTP 请求。
|
||||
go func() {
|
||||
// 结束后负责释放资源并关闭 channel 发送。
|
||||
defer func() { cancel(); close(streamCh) }()
|
||||
if err := service.CallDeepSeekStream(streamCtx, aiCfg, messages, streamCh); err != nil {
|
||||
// 如果因为主动调用了 StopGeneration(手动 cancel),将进入 Canceled 判断
|
||||
if streamCtx.Err() == context.Canceled {
|
||||
streamCh <- "__STOPPED__"
|
||||
} else {
|
||||
@@ -64,23 +83,28 @@ func (e *Expert) AskDeepSeek(query, rawAnswer string) string {
|
||||
}
|
||||
}()
|
||||
|
||||
// 主控:消费 channel 内的字符直到通道关闭返回
|
||||
for chunk := range streamCh {
|
||||
switch chunk {
|
||||
case "__ERROR__":
|
||||
// 如果调用抛错(可能是没钱了也可以是超时网络波动),使用 fallback 向前端传递异常发生。
|
||||
runtime.EventsEmit(e.ctx, "ai:fallback", rawAnswer)
|
||||
return rawAnswer
|
||||
case "__STOPPED__":
|
||||
runtime.EventsEmit(e.ctx, "ai:done", sb.String())
|
||||
return sb.String()
|
||||
default:
|
||||
sb.WriteString(chunk)
|
||||
runtime.EventsEmit(e.ctx, "ai:chunk", chunk)
|
||||
sb.WriteString(chunk) // 先存全量方便后续持久化
|
||||
runtime.EventsEmit(e.ctx, "ai:chunk", chunk) // 把字往前端丢
|
||||
}
|
||||
}
|
||||
// 到头了(或者服务端关连接了),抛送 done 信号表示可以关闭等待动画
|
||||
runtime.EventsEmit(e.ctx, "ai:done", sb.String())
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// StopGeneration 安全中止生成的流程(并发安全)。
|
||||
// 它的实现是直接取消底层的 HTTP Context,让 Read 立即退出不走阻塞。
|
||||
func (e *Expert) StopGeneration() {
|
||||
e.stopMu.Lock()
|
||||
defer e.stopMu.Unlock()
|
||||
@@ -90,11 +114,16 @@ func (e *Expert) StopGeneration() {
|
||||
}
|
||||
}
|
||||
|
||||
// GetDBStatus 返回当前 db 是否 ready(初始化完毕并且至少打开了一个库)。
|
||||
func (e *Expert) GetDBStatus() bool { return database.IsReady() }
|
||||
|
||||
// ToggleTopmost 在 macOS / Windows 上能直接使软件置顶
|
||||
// 这保证用户在别的屏幕操作发帖或复制消息的时候 Sidebar 依然不被埋没。
|
||||
func (e *Expert) ToggleTopmost(enabled bool) {
|
||||
runtime.WindowSetAlwaysOnTop(e.ctx, enabled)
|
||||
}
|
||||
|
||||
// buildKnowledgeContext 是一个辅助函数:提取最近 3 条相关查询用于丰富 System Prompt。
|
||||
func (e *Expert) buildKnowledgeContext(query string) string {
|
||||
results, err := service.SearchKnowledge(query)
|
||||
if err != nil || len(results) == 0 {
|
||||
@@ -112,11 +141,12 @@ func (e *Expert) buildKnowledgeContext(query string) string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// setStopCancel 加锁写入 cancel。
|
||||
func (e *Expert) setStopCancel(fn context.CancelFunc) {
|
||||
e.stopMu.Lock()
|
||||
defer e.stopMu.Unlock()
|
||||
if e.stopCancel != nil {
|
||||
e.stopCancel()
|
||||
e.stopCancel() // 确保上一次的上下文被干掉防止泄露
|
||||
}
|
||||
e.stopCancel = fn
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user