init: initial commit
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"AI-Expert-Sidebar/internal/database"
|
||||
"AI-Expert-Sidebar/internal/service"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// Expert handles search and AI streaming for the active library.
|
||||
type Expert struct {
|
||||
ctx context.Context
|
||||
stopMu sync.Mutex
|
||||
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.
|
||||
func (e *Expert) SearchExpert(query string) []service.SearchResult {
|
||||
results, err := service.SearchKnowledge(query)
|
||||
if err != nil {
|
||||
log.Printf("[SearchExpert] %v", err)
|
||||
return nil
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// AskDeepSeek performs RAG + streaming AI call.
|
||||
func (e *Expert) AskDeepSeek(query, rawAnswer string) string {
|
||||
aiCfg := service.ResolveAIConfig()
|
||||
knowledgeCtx := e.buildKnowledgeContext(query)
|
||||
|
||||
var userMsg string
|
||||
if rawAnswer != "" {
|
||||
userMsg = fmt.Sprintf("用户问题:%s\n\n原始参考答案:%s", query, rawAnswer)
|
||||
} else {
|
||||
userMsg = fmt.Sprintf("用户问题:%s\n\n请直接回答上述问题。", query)
|
||||
}
|
||||
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)
|
||||
|
||||
go func() {
|
||||
defer func() { cancel(); close(streamCh) }()
|
||||
if err := service.CallDeepSeekStream(streamCtx, aiCfg, messages, streamCh); err != nil {
|
||||
if streamCtx.Err() == context.Canceled {
|
||||
streamCh <- "__STOPPED__"
|
||||
} else {
|
||||
log.Printf("[AskDeepSeek] %v", err)
|
||||
streamCh <- "__ERROR__"
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for chunk := range streamCh {
|
||||
switch chunk {
|
||||
case "__ERROR__":
|
||||
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)
|
||||
}
|
||||
}
|
||||
runtime.EventsEmit(e.ctx, "ai:done", sb.String())
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (e *Expert) StopGeneration() {
|
||||
e.stopMu.Lock()
|
||||
defer e.stopMu.Unlock()
|
||||
if e.stopCancel != nil {
|
||||
e.stopCancel()
|
||||
e.stopCancel = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Expert) GetDBStatus() bool { return database.IsReady() }
|
||||
func (e *Expert) ToggleTopmost(enabled bool) {
|
||||
runtime.WindowSetAlwaysOnTop(e.ctx, enabled)
|
||||
}
|
||||
|
||||
func (e *Expert) buildKnowledgeContext(query string) string {
|
||||
results, err := service.SearchKnowledge(query)
|
||||
if err != nil || len(results) == 0 {
|
||||
return "(无相关本地知识)"
|
||||
}
|
||||
limit := 3
|
||||
if len(results) < limit {
|
||||
limit = len(results)
|
||||
}
|
||||
var sb strings.Builder
|
||||
for i := 0; i < limit; i++ {
|
||||
r := results[i]
|
||||
sb.WriteString(fmt.Sprintf("%d. Q: %s\n A: %s\n 分类: %s\n", i+1, r.Question, r.Answer, r.Category))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (e *Expert) setStopCancel(fn context.CancelFunc) {
|
||||
e.stopMu.Lock()
|
||||
defer e.stopMu.Unlock()
|
||||
if e.stopCancel != nil {
|
||||
e.stopCancel()
|
||||
}
|
||||
e.stopCancel = fn
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"AI-Expert-Sidebar/internal/database"
|
||||
"AI-Expert-Sidebar/internal/service"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// LibraryHandler exposes library management and CSV import via Wails bindings.
|
||||
type LibraryHandler struct{ ctx context.Context }
|
||||
|
||||
func NewLibraryHandler() *LibraryHandler { return &LibraryHandler{} }
|
||||
func (h *LibraryHandler) SetContext(ctx context.Context) { h.ctx = ctx }
|
||||
|
||||
// ListLibraries returns all registered knowledge libraries.
|
||||
func (h *LibraryHandler) ListLibraries() []LibraryInfo {
|
||||
libs, err := service.ListLibraries()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]LibraryInfo, len(libs))
|
||||
for i, l := range libs {
|
||||
out[i] = LibraryInfo{
|
||||
ID: l.ID, Name: l.Name, Description: l.Description,
|
||||
EntryCount: l.EntryCount, IsActive: l.Name == database.GetActiveLibName(),
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetActiveLibrary returns the name of the currently active library.
|
||||
func (h *LibraryHandler) GetActiveLibrary() string {
|
||||
return database.GetActiveLibName()
|
||||
}
|
||||
|
||||
// CreateLibrary registers a new knowledge library.
|
||||
func (h *LibraryHandler) CreateLibrary(name, description string) string {
|
||||
if name == "" {
|
||||
return "名称不能为空"
|
||||
}
|
||||
lib, err := service.CreateLibrary(name, description)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
// Auto-switch to newly created library
|
||||
service.SwitchLibrary(lib.Name) //nolint
|
||||
return ""
|
||||
}
|
||||
|
||||
// SwitchLibrary makes the named library active.
|
||||
func (h *LibraryHandler) SwitchLibrary(name string) string {
|
||||
if err := service.SwitchLibrary(name); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// DeleteLibrary removes a library from the registry (file is kept).
|
||||
func (h *LibraryHandler) DeleteLibrary(name string) string {
|
||||
if name == database.GetActiveLibName() {
|
||||
return "不能删除当前使用中的知识库,请先切换到其他库"
|
||||
}
|
||||
if err := service.DeleteLibrary(name, false); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ImportCSV opens a native file dialog then imports CSV data into the active library.
|
||||
func (h *LibraryHandler) ImportCSV() service.ImportResult {
|
||||
filePath, err := runtime.OpenFileDialog(h.ctx, runtime.OpenDialogOptions{
|
||||
Title: "选择 CSV 文件",
|
||||
Filters: []runtime.FileFilter{
|
||||
{DisplayName: "CSV 文件", Pattern: "*.csv"},
|
||||
{DisplayName: "所有文件", Pattern: "*"},
|
||||
},
|
||||
})
|
||||
if err != nil || filePath == "" {
|
||||
return service.ImportResult{Error: "已取消"}
|
||||
}
|
||||
return service.ImportCSV(filePath)
|
||||
}
|
||||
|
||||
// LibraryInfo is the frontend-facing representation of a library.
|
||||
type LibraryInfo struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
EntryCount int `json:"entry_count"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"AI-Expert-Sidebar/internal/service"
|
||||
)
|
||||
|
||||
// SettingsHandler exposes AI settings CRUD via Wails bindings.
|
||||
type SettingsHandler struct{ ctx context.Context }
|
||||
|
||||
func NewSettingsHandler() *SettingsHandler { return &SettingsHandler{} }
|
||||
func (s *SettingsHandler) SetContext(ctx context.Context) { s.ctx = ctx }
|
||||
|
||||
// GetSettings returns the current local AI settings.
|
||||
func (s *SettingsHandler) GetSettings() *service.SettingsDTO {
|
||||
return service.GetSettings()
|
||||
}
|
||||
|
||||
// SaveSettings persists AI config to local settings.db.
|
||||
// Returns empty string on success, error message on failure.
|
||||
func (s *SettingsHandler) SaveSettings(dto service.SettingsDTO) string {
|
||||
if err := service.SaveSettings(dto); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetProviders returns built-in AI provider presets for the frontend dropdown.
|
||||
func (s *SettingsHandler) GetProviders() []ProviderPreset {
|
||||
return []ProviderPreset{
|
||||
{ID: "deepseek", Label: "DeepSeek", BaseURL: "https://api.deepseek.com/chat/completions", DefaultModel: "deepseek-chat"},
|
||||
{ID: "openai", Label: "OpenAI", BaseURL: "https://api.openai.com/v1/chat/completions", DefaultModel: "gpt-4o"},
|
||||
{ID: "grok", Label: "Grok (xAI)", BaseURL: "https://api.x.ai/v1/chat/completions", DefaultModel: "grok-3"},
|
||||
{ID: "custom", Label: "自定义", BaseURL: "", DefaultModel: ""},
|
||||
}
|
||||
}
|
||||
|
||||
// ProviderPreset describes a known AI provider with preset URL and model.
|
||||
type ProviderPreset struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
BaseURL string `json:"base_url"`
|
||||
DefaultModel string `json:"default_model"`
|
||||
}
|
||||
Reference in New Issue
Block a user