diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index 9938b90..1345577 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -87,7 +87,7 @@ func main() { if err != nil { log.Printf("Warning: Failed to initialize upload service: %v (files will not be available)", err) } - knowledgeService := service.NewKnowledgeService(knowledgeRepo, modelRepo, uploadService, cfg.PythonServiceURL) + knowledgeService := service.NewKnowledgeService(knowledgeRepo, modelRepo, uploadService, cfg.PythonServiceURL, cfg.AICoreServiceAddr) // 6. 初始化 Handler dbHandler := handler.NewDatabaseHandler(dbService) diff --git a/server/config/config.yaml b/server/config/config.yaml index 7f16dd8..05e185b 100644 --- a/server/config/config.yaml +++ b/server/config/config.yaml @@ -1,9 +1,17 @@ # 本地开发配置 port: "8082" jwt_secret: "dev-secret-key" -# Docker 内访问用 db:3306,本地访问用 localhost:6036 -database_url: "root:root@tcp(localhost:6036)/x_agents?charset=utf8mb4&parseTime=True&loc=Local" + +# 数据库配置 +database_host: "localhost" +database_port: "6036" +database_user: "root" +database_password: "root" +database_name: "x_agents" + +# AI 服务配置 python_service_url: "http://localhost:8081" +ai_core_service_addr: "localhost:50051" # 文件上传配置 (local 或 minio) upload_mode: "local" diff --git a/server/go.mod b/server/go.mod index 47aef32..22d149b 100644 --- a/server/go.mod +++ b/server/go.mod @@ -70,7 +70,9 @@ require ( golang.org/x/net v0.48.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.32.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/grpc v1.79.2 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/server/go.sum b/server/go.sum index 163dea2..1902d11 100644 --- a/server/go.sum +++ b/server/go.sum @@ -44,6 +44,7 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -185,9 +186,16 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/server/internal/config/config.go b/server/internal/config/config.go index 27f6920..15bc133 100644 --- a/server/internal/config/config.go +++ b/server/internal/config/config.go @@ -14,8 +14,14 @@ import ( type Config struct { Port string JWTSecret string - DatabaseURL string + DatabaseHost string + DatabasePort string + DatabaseUser string + DatabasePassword string + DatabaseName string + DatabaseURL string // 拼接后的完整连接字符串 PythonServiceURL string + AICoreServiceAddr string // AI-Core gRPC 服务地址,如 "localhost:50051" // 文件上传配置 UploadMode string // "local" 或 "minio" UploadLocalPath string // 本地存储路径,如 "resource/files" @@ -39,7 +45,13 @@ func Load() *Config { viper.SetDefault("port", "8080") viper.SetDefault("jwt_secret", "your-secret-key-change-in-production") viper.SetDefault("python_service_url", "http://localhost:8081") - viper.SetDefault("database_url", "root:root@tcp(localhost:3306)/x_agents?charset=utf8mb4&parseTime=True&loc=Local") + viper.SetDefault("ai_core_service_addr", "localhost:50051") + // 数据库默认配置 + viper.SetDefault("database_host", "localhost") + viper.SetDefault("database_port", "3306") + viper.SetDefault("database_user", "root") + viper.SetDefault("database_password", "root") + viper.SetDefault("database_name", "x_agents") // 文件上传默认配置 viper.SetDefault("upload_mode", "local") viper.SetDefault("upload_local_path", "resource/files") @@ -54,11 +66,26 @@ func Load() *Config { log.Printf("Using default config: %v", err) } + // 拼接数据库连接字符串 + dbHost := viper.GetString("database_host") + dbPort := viper.GetString("database_port") + dbUser := viper.GetString("database_user") + dbPassword := viper.GetString("database_password") + dbName := viper.GetString("database_name") + databaseURL := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", + dbUser, dbPassword, dbHost, dbPort, dbName) + return &Config{ - Port: viper.GetString("port"), - JWTSecret: viper.GetString("jwt_secret"), - DatabaseURL: viper.GetString("database_url"), - PythonServiceURL: viper.GetString("python_service_url"), + Port: viper.GetString("port"), + JWTSecret: viper.GetString("jwt_secret"), + DatabaseURL: databaseURL, + DatabaseHost: dbHost, + DatabasePort: dbPort, + DatabaseUser: dbUser, + DatabasePassword: dbPassword, + DatabaseName: dbName, + PythonServiceURL: viper.GetString("python_service_url"), + AICoreServiceAddr: viper.GetString("ai_core_service_addr"), // 文件上传配置 UploadMode: viper.GetString("upload_mode"), UploadLocalPath: viper.GetString("upload_local_path"), diff --git a/server/internal/model/knowledge_info.go b/server/internal/model/knowledge_info.go index 04fa167..5d1b039 100644 --- a/server/internal/model/knowledge_info.go +++ b/server/internal/model/knowledge_info.go @@ -87,6 +87,7 @@ type KnowledgeDocument struct { FileKey string `json:"file_key" gorm:"type:varchar(500)"` FileURL string `json:"file_url" gorm:"type:varchar(500)"` // 文件访问 URL FileSize int64 `json:"file_size" gorm:"type:bigint;default:0"` + Content string `json:"content" gorm:"type:longtext"` // Markdown 内容(AI-Core 解析结果) Status string `json:"status" gorm:"type:varchar(20);default:parsing"` // parsing / parsed / failed ChunkCount int `json:"chunk_count" gorm:"default:0"` UploadedAt time.Time `json:"uploaded_at"` diff --git a/server/internal/service/document_parser_client.go b/server/internal/service/document_parser_client.go new file mode 100644 index 0000000..8d1108a --- /dev/null +++ b/server/internal/service/document_parser_client.go @@ -0,0 +1,132 @@ +package service + +import ( + "context" + "fmt" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +// AICoreClient AI-Core 文档解析服务客户端 +type AICoreClient struct { + conn *grpc.ClientConn + address string +} + +// ParseResult 解析结果 +type ParseResult struct { + Success bool + Content string + Message string + ContentLength int32 + FileType string + ParserEngine string +} + +// NewAICoreClient 创建 AI-Core 客户端 +func NewAICoreClient(address string) (*AICoreClient, error) { + return &AICoreClient{address: address}, nil +} + +// Connect 连接到 gRPC 服务 +func (c *AICoreClient) Connect() error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + conn, err := grpc.DialContext(ctx, c.address, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithBlock(), + ) + if err != nil { + return fmt.Errorf("failed to connect to AI-Core service: %w", err) + } + c.conn = conn + return nil +} + +// Close 关闭连接 +func (c *AICoreClient) Close() { + if c.conn != nil { + c.conn.Close() + } +} + +// ParseDocument 解析文档 +func (c *AICoreClient) ParseDocument(fileURL, fileName, fileType string) (*ParseResult, error) { + if c.conn == nil { + if err := c.Connect(); err != nil { + return nil, err + } + } + + // 使用 gRPC raw bytes 调用 + // 由于没有生成 protobuf 代码,使用 raw bytes 方式调用 + client := NewDocumentParserClient(c.conn) + + req := &ParseRequest{ + FileUrl: fileURL, + FileName: fileName, + FileType: fileType, + } + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + resp, err := client.ParseDocument(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to parse document: %w", err) + } + + return &ParseResult{ + Success: resp.Success, + Content: resp.Content, + Message: resp.Message, + ContentLength: resp.ContentLength, + FileType: resp.FileType, + ParserEngine: resp.ParserEngine, + }, nil +} + +// 以下是手动定义的 protobuf messages(与 proto 文件一致) +// 不需要生成 .pb.go 文件,直接手动定义 + +type ParseRequest struct { + FileUrl string + FileName string + FileType string + ParserEngine string +} + +type ParseResponse struct { + Success bool + Content string + Message string + ContentLength int32 + FileType string + ParserEngine string +} + +// DocumentParserClient gRPC 客户端接口(手动实现) +type DocumentParserClient interface { + ParseDocument(ctx context.Context, in *ParseRequest, opts ...grpc.CallOption) (*ParseResponse, error) +} + +type documentParserClient struct { + cc grpc.ClientConnInterface +} + +// NewDocumentParserClient 创建 DocumentParser 客户端 +func NewDocumentParserClient(cc grpc.ClientConnInterface) DocumentParserClient { + return &documentParserClient{cc: cc} +} + +func (c *documentParserClient) ParseDocument(ctx context.Context, in *ParseRequest, opts ...grpc.CallOption) (*ParseResponse, error) { + out := new(ParseResponse) + err := c.cc.Invoke(ctx, "/docparser.DocumentParser/ParseDocument", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} diff --git a/server/internal/service/knowledge_service.go b/server/internal/service/knowledge_service.go index 631ee72..d180bd3 100644 --- a/server/internal/service/knowledge_service.go +++ b/server/internal/service/knowledge_service.go @@ -24,18 +24,21 @@ func init() { } type KnowledgeService struct { - repo *repository.KnowledgeRepository - modelRepo *repository.ModelRepository - uploadService *UploadService + repo *repository.KnowledgeRepository + modelRepo *repository.ModelRepository + uploadService *UploadService pythonServiceURL string + aiCoreClient *AICoreClient } -func NewKnowledgeService(repo *repository.KnowledgeRepository, modelRepo *repository.ModelRepository, uploadService *UploadService, pythonServiceURL string) *KnowledgeService { +func NewKnowledgeService(repo *repository.KnowledgeRepository, modelRepo *repository.ModelRepository, uploadService *UploadService, pythonServiceURL, aiCoreServiceAddr string) *KnowledgeService { + aiCoreClient, _ := NewAICoreClient(aiCoreServiceAddr) return &KnowledgeService{ - repo: repo, - modelRepo: modelRepo, - uploadService: uploadService, + repo: repo, + modelRepo: modelRepo, + uploadService: uploadService, pythonServiceURL: pythonServiceURL, + aiCoreClient: aiCoreClient, } } @@ -227,6 +230,9 @@ func (s *KnowledgeService) UploadDocument(kbID string, file *multipart.FileHeade // 异步调用 Python 服务解析文档 go s.parseDocument(kbID, doc.ID, result.URL, kb.ParsingConfig) + // 异步调用 AI-Core gRPC 服务解析文档(获取 Markdown) + go s.parseDocumentWithAICore(doc.ID, result.URL, doc.Name) + return doc, result.URL, nil } @@ -284,6 +290,32 @@ func (s *KnowledgeService) parseDocument(kbID, docID, fileURL string, config mod } } +// parseDocumentWithAICore 调用 AI-Core gRPC 服务解析文档 +func (s *KnowledgeService) parseDocumentWithAICore(docID, fileURL, fileName string) { + if s.aiCoreClient == nil { + knowledgeDebugLog.Printf("[AICore] AI-Core 客户端未初始化") + return + } + + knowledgeDebugLog.Printf("[AICore] 开始解析文档: docID=%s, fileURL=%s, fileName=%s", docID, fileURL, fileName) + + result, err := s.aiCoreClient.ParseDocument(fileURL, fileName, "") + if err != nil { + knowledgeDebugLog.Printf("[AICore] 解析失败: docID=%s, err=%v", docID, err) + return + } + + if result.Success && result.Content != "" { + knowledgeDebugLog.Printf("[AICore] 解析成功: docID=%s, contentLength=%d", docID, len(result.Content)) + // 更新文档的 Content 字段 + s.repo.UpdateDocument(docID, map[string]interface{}{ + "content": result.Content, + }) + } else { + knowledgeDebugLog.Printf("[AICore] 解析返回失败: docID=%s, message=%s", docID, result.Message) + } +} + // DeleteDocument 删除文档 func (s *KnowledgeService) DeleteDocument(kbID, docID string) error { // 验证文档存在