feat: 集成 AI-Core gRPC 文档解析服务

- 新增 AICoreClient 客户端
- 添加 document_parser_client.go
- 知识库服务集成 AI-Core 解析
- 配置中添加 ai_core_service_addr

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 12:50:27 +08:00
parent 0d4fd6b425
commit dc1c825d2e
8 changed files with 227 additions and 17 deletions

View File

@@ -87,7 +87,7 @@ func main() {
if err != nil { if err != nil {
log.Printf("Warning: Failed to initialize upload service: %v (files will not be available)", err) 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 // 6. 初始化 Handler
dbHandler := handler.NewDatabaseHandler(dbService) dbHandler := handler.NewDatabaseHandler(dbService)

View File

@@ -1,9 +1,17 @@
# 本地开发配置 # 本地开发配置
port: "8082" port: "8082"
jwt_secret: "dev-secret-key" 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" python_service_url: "http://localhost:8081"
ai_core_service_addr: "localhost:50051"
# 文件上传配置 (local 或 minio) # 文件上传配置 (local 或 minio)
upload_mode: "local" upload_mode: "local"

View File

@@ -70,7 +70,9 @@ require (
golang.org/x/net v0.48.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.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/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@@ -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 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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.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/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 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= 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.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 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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 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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -14,8 +14,14 @@ import (
type Config struct { type Config struct {
Port string Port string
JWTSecret string JWTSecret string
DatabaseURL string DatabaseHost string
DatabasePort string
DatabaseUser string
DatabasePassword string
DatabaseName string
DatabaseURL string // 拼接后的完整连接字符串
PythonServiceURL string PythonServiceURL string
AICoreServiceAddr string // AI-Core gRPC 服务地址,如 "localhost:50051"
// 文件上传配置 // 文件上传配置
UploadMode string // "local" 或 "minio" UploadMode string // "local" 或 "minio"
UploadLocalPath string // 本地存储路径,如 "resource/files" UploadLocalPath string // 本地存储路径,如 "resource/files"
@@ -39,7 +45,13 @@ func Load() *Config {
viper.SetDefault("port", "8080") viper.SetDefault("port", "8080")
viper.SetDefault("jwt_secret", "your-secret-key-change-in-production") viper.SetDefault("jwt_secret", "your-secret-key-change-in-production")
viper.SetDefault("python_service_url", "http://localhost:8081") 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_mode", "local")
viper.SetDefault("upload_local_path", "resource/files") viper.SetDefault("upload_local_path", "resource/files")
@@ -54,11 +66,26 @@ func Load() *Config {
log.Printf("Using default config: %v", err) 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{ return &Config{
Port: viper.GetString("port"), Port: viper.GetString("port"),
JWTSecret: viper.GetString("jwt_secret"), JWTSecret: viper.GetString("jwt_secret"),
DatabaseURL: viper.GetString("database_url"), DatabaseURL: databaseURL,
PythonServiceURL: viper.GetString("python_service_url"), 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"), UploadMode: viper.GetString("upload_mode"),
UploadLocalPath: viper.GetString("upload_local_path"), UploadLocalPath: viper.GetString("upload_local_path"),

View File

@@ -87,6 +87,7 @@ type KnowledgeDocument struct {
FileKey string `json:"file_key" gorm:"type:varchar(500)"` FileKey string `json:"file_key" gorm:"type:varchar(500)"`
FileURL string `json:"file_url" gorm:"type:varchar(500)"` // 文件访问 URL FileURL string `json:"file_url" gorm:"type:varchar(500)"` // 文件访问 URL
FileSize int64 `json:"file_size" gorm:"type:bigint;default:0"` 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 Status string `json:"status" gorm:"type:varchar(20);default:parsing"` // parsing / parsed / failed
ChunkCount int `json:"chunk_count" gorm:"default:0"` ChunkCount int `json:"chunk_count" gorm:"default:0"`
UploadedAt time.Time `json:"uploaded_at"` UploadedAt time.Time `json:"uploaded_at"`

View File

@@ -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
}

View File

@@ -24,18 +24,21 @@ func init() {
} }
type KnowledgeService struct { type KnowledgeService struct {
repo *repository.KnowledgeRepository repo *repository.KnowledgeRepository
modelRepo *repository.ModelRepository modelRepo *repository.ModelRepository
uploadService *UploadService uploadService *UploadService
pythonServiceURL string 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{ return &KnowledgeService{
repo: repo, repo: repo,
modelRepo: modelRepo, modelRepo: modelRepo,
uploadService: uploadService, uploadService: uploadService,
pythonServiceURL: pythonServiceURL, pythonServiceURL: pythonServiceURL,
aiCoreClient: aiCoreClient,
} }
} }
@@ -227,6 +230,9 @@ func (s *KnowledgeService) UploadDocument(kbID string, file *multipart.FileHeade
// 异步调用 Python 服务解析文档 // 异步调用 Python 服务解析文档
go s.parseDocument(kbID, doc.ID, result.URL, kb.ParsingConfig) 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 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 删除文档 // DeleteDocument 删除文档
func (s *KnowledgeService) DeleteDocument(kbID, docID string) error { func (s *KnowledgeService) DeleteDocument(kbID, docID string) error {
// 验证文档存在 // 验证文档存在