feat: 同步知识库与工作台相关改动

This commit is contained in:
Jackzhou
2026-08-24 10:40:15 +08:00
parent 8d0588827d
commit 4f33b036d1
50 changed files with 8034 additions and 273 deletions
@@ -0,0 +1,149 @@
package ai
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"eaisalestrain/backend/internal/config"
)
type KnowledgeClassifyResult struct {
Intent string `json:"intent"`
Score float64 `json:"score"`
Reason string `json:"reason"`
}
type KnowledgeSearchItem struct {
ID uint `json:"id"`
Title string `json:"title"`
Snippet string `json:"snippet"`
Content string `json:"content"`
ChunkIndex int `json:"chunk_index"`
SourceType string `json:"source_type"`
SourceID string `json:"source_id"`
KnowledgeSpaceKey string `json:"knowledge_space_key"`
KnowledgeSpaceName string `json:"knowledge_space_name"`
Score float64 `json:"score"`
}
type KnowledgeIndexItem struct {
ID uint `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
ChunkIndex int `json:"chunk_index"`
SourceType string `json:"source_type"`
SourceID string `json:"source_id"`
KnowledgeSpaceKey string `json:"knowledge_space_key"`
KnowledgeSpaceName string `json:"knowledge_space_name"`
}
func KnowledgeServiceEnabled(cfg *config.Config) bool {
return cfg != nil && strings.TrimSpace(cfg.KnowledgeServiceURL) != ""
}
func KnowledgeClassify(cfg *config.Config, query string) (*KnowledgeClassifyResult, error) {
var resp struct {
Data KnowledgeClassifyResult `json:"data"`
}
if err := postKnowledgeService(cfg, "/classify", map[string]any{
"query": query,
}, &resp); err != nil {
return nil, err
}
return &resp.Data, nil
}
func KnowledgeSearch(cfg *config.Config, query, spaceKey string, topK int) ([]KnowledgeSearchItem, error) {
var resp struct {
Data struct {
Items []KnowledgeSearchItem `json:"items"`
} `json:"data"`
}
if err := postKnowledgeService(cfg, "/search", map[string]any{
"query": query,
"space_key": spaceKey,
"top_k": topK,
}, &resp); err != nil {
return nil, err
}
return resp.Data.Items, nil
}
func KnowledgeRebuildIndex(cfg *config.Config, items []KnowledgeIndexItem) error {
var resp map[string]any
return postKnowledgeService(cfg, "/index/rebuild", map[string]any{
"index_dir": cfg.KnowledgeIndexDir,
"items": items,
}, &resp)
}
func postKnowledgeService(cfg *config.Config, path string, payload any, out any) error {
if !KnowledgeServiceEnabled(cfg) {
return fmt.Errorf("knowledge service disabled")
}
b, err := json.Marshal(payload)
if err != nil {
return err
}
url := strings.TrimRight(cfg.KnowledgeServiceURL, "/") + path
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(b))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
// #region debug-point D:knowledge-service-request
if payload, err := json.Marshal(map[string]any{
"sessionId": "knowledge-chat-401",
"runId": "pre-fix",
"hypothesisId": "D",
"location": "backend-go/internal/ai/knowledge_service_client.go:postKnowledgeService:request",
"msg": "[DEBUG] knowledge service request",
"data": map[string]any{
"url": url,
"path": path,
},
"ts": time.Now().UnixMilli(),
}); err == nil {
go http.Post("http://127.0.0.1:7777/event", "application/json", strings.NewReader(string(payload)))
}
// #endregion
client := &http.Client{Timeout: 60 * time.Second}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 300 {
// #region debug-point D:knowledge-service-non200
if payload, err := json.Marshal(map[string]any{
"sessionId": "knowledge-chat-401",
"runId": "pre-fix",
"hypothesisId": "D",
"location": "backend-go/internal/ai/knowledge_service_client.go:postKnowledgeService:non200",
"msg": "[DEBUG] knowledge service non-200",
"data": map[string]any{
"url": url,
"status": resp.StatusCode,
"bodyPreview": truncate(string(body), 240),
},
"ts": time.Now().UnixMilli(),
}); err == nil {
go http.Post("http://127.0.0.1:7777/event", "application/json", strings.NewReader(string(payload)))
}
// #endregion
return fmt.Errorf("knowledge service %d: %s", resp.StatusCode, truncate(string(body), 200))
}
if out == nil || len(body) == 0 {
return nil
}
if err := json.Unmarshal(body, out); err != nil {
return fmt.Errorf("knowledge service parse failed: %w", err)
}
return nil
}
+103 -12
View File
@@ -44,12 +44,12 @@ type ChatResult struct {
// Client 低层 HTTP 客户端(基于 RouteConfig)
type Client struct {
baseURL string
apiKey string
model string
maxTokens int
baseURL string
apiKey string
model string
maxTokens int
temperature float64
hc *http.Client
hc *http.Client
}
// NewClient 从 RouteConfig 创建客户端
@@ -100,7 +100,45 @@ func (c *Client) post(path string, body any) (*http.Response, error) {
for k, v := range c.headers() {
req.Header.Set(k, v)
}
return c.hc.Do(req)
// #region debug-point C:llm-post
if payload, err := json.Marshal(map[string]any{
"sessionId": "knowledge-chat-401",
"runId": "pre-fix",
"hypothesisId": "C",
"location": "backend-go/internal/ai/llm.go:Client.post:request",
"msg": "[DEBUG] llm client request",
"data": map[string]any{
"url": c.url(path),
"model": c.model,
"hasAPIKey": c.apiKey != "",
"authHeader": req.Header.Get("Authorization") != "",
},
"ts": time.Now().UnixMilli(),
}); err == nil {
go http.Post("http://127.0.0.1:7777/event", "application/json", strings.NewReader(string(payload)))
}
// #endregion
resp, err := c.hc.Do(req)
if err != nil {
return nil, err
}
// #region debug-point C:llm-response
if payload, err := json.Marshal(map[string]any{
"sessionId": "knowledge-chat-401",
"runId": "pre-fix",
"hypothesisId": "C",
"location": "backend-go/internal/ai/llm.go:Client.post:response",
"msg": "[DEBUG] llm client response",
"data": map[string]any{
"url": c.url(path),
"status": resp.StatusCode,
},
"ts": time.Now().UnixMilli(),
}); err == nil {
go http.Post("http://127.0.0.1:7777/event", "application/json", strings.NewReader(string(payload)))
}
// #endregion
return resp, nil
}
// ──────────────────────────────────────────────
@@ -222,6 +260,24 @@ func (c *Client) GenerateStream(messages []Message, onChunk func(string)) error
if resp.StatusCode != http.StatusOK {
data, _ := io.ReadAll(resp.Body)
// #region debug-point C:llm-non200
if payload, err := json.Marshal(map[string]any{
"sessionId": "knowledge-chat-401",
"runId": "pre-fix",
"hypothesisId": "C",
"location": "backend-go/internal/ai/llm.go:GenerateStream:non200",
"msg": "[DEBUG] llm stream non-200",
"data": map[string]any{
"status": resp.StatusCode,
"bodyPreview": truncate(string(data), 240),
"model": c.model,
"baseURL": c.baseURL,
},
"ts": time.Now().UnixMilli(),
}); err == nil {
go http.Post("http://127.0.0.1:7777/event", "application/json", strings.NewReader(string(payload)))
}
// #endregion
return fmt.Errorf("LLM 返回 %d: %s", resp.StatusCode, truncate(string(data), 200))
}
@@ -335,6 +391,41 @@ func GenerateFullWithFallback(primary *config.RouteConfig, messages []Message) (
return nil, nil, fmt.Errorf("所有路由均失败: %w", lastErr)
}
// GenerateStreamWithFallback 流式 + 回退链:返回实际命中的路由
func GenerateStreamWithFallback(primary *config.RouteConfig, messages []Message, onChunk func(string)) (*config.RouteConfig, error) {
chain := []*config.RouteConfig{primary}
if fallbacks, err := config.GetFallbackRoutes(primary.RouteID); err == nil && len(fallbacks) > 0 {
chain = append(chain, fallbacks...)
}
var lastErr error
for _, route := range chain {
if requiresAPIKey(route) && strings.TrimSpace(route.APIKey) == "" {
lastErr = fmt.Errorf("[%s] 未配置 API Key", route.RouteID)
continue
}
client := NewClient(route)
if err := client.GenerateStream(messages, onChunk); err == nil {
return route, nil
} else {
lastErr = fmt.Errorf("[%s] %w", route.RouteID, err)
}
}
return nil, fmt.Errorf("所有路由均失败: %w", lastErr)
}
func requiresAPIKey(route *config.RouteConfig) bool {
if route == nil {
return false
}
baseURL := strings.ToLower(strings.TrimSpace(route.BaseURL))
if strings.Contains(baseURL, "openrouter.ai") || strings.Contains(baseURL, "openai.com") {
return true
}
provider := strings.ToLower(strings.TrimSpace(route.Provider))
return provider == "openrouter" || provider == "openai"
}
// ──────────────────────────────────────────────
// 配置解析(兼容旧接口)
// ──────────────────────────────────────────────
@@ -375,11 +466,11 @@ func ResolveLLM(cfg *config.Config) (LLMConfig, bool) {
}
c := LLMConfig{
BaseURL: baseURL,
APIKey: apiKey,
Model: modelName,
EmbedModel: embedModel,
MaxTokens: 2048,
BaseURL: baseURL,
APIKey: apiKey,
Model: modelName,
EmbedModel: embedModel,
MaxTokens: 2048,
Temperature: 0.7,
}
if c.BaseURL == "" || c.Model == "" {
@@ -394,4 +485,4 @@ func truncate(s string, n int) string {
return s
}
return string(r[:n]) + "..."
}
}