feat: 同步知识库与工作台相关改动
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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]) + "..."
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user