refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
This commit is contained in:
@@ -6,11 +6,12 @@ import (
|
||||
)
|
||||
|
||||
type UploadInfo struct {
|
||||
Prefix string
|
||||
Filename string
|
||||
FileSize int64
|
||||
MimeType string
|
||||
Principal *dto.AuthPrincipal
|
||||
Prefix string
|
||||
ConversationID int64
|
||||
Filename string
|
||||
FileSize int64
|
||||
MimeType string
|
||||
Principal *dto.AuthPrincipal
|
||||
}
|
||||
|
||||
type StoredFile struct {
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"code.tczkiot.com/wlw/ai-agent/contract"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/config"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
|
||||
"io"
|
||||
)
|
||||
|
||||
var providers = make(map[enums.AssetProvider]FileStorageProvider)
|
||||
var hostStorage contract.FileStorage
|
||||
|
||||
type FileStorageProvider interface {
|
||||
ProviderType() enums.AssetProvider
|
||||
@@ -18,7 +23,24 @@ type FileStorageProvider interface {
|
||||
Read(key string) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
// SetHostStorage lets the embedding system own file persistence. Passing nil
|
||||
// preserves the standalone module's built-in local/OSS providers.
|
||||
func SetHostStorage(value contract.FileStorage) {
|
||||
hostStorage = value
|
||||
providers = make(map[enums.AssetProvider]FileStorageProvider)
|
||||
}
|
||||
|
||||
func GetDefault() (FileStorageProvider, error) {
|
||||
if hostStorage != nil {
|
||||
provider, err := hostStorage.DefaultProvider(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if provider == "" {
|
||||
return nil, fmt.Errorf("host file storage returned an empty default provider")
|
||||
}
|
||||
return GetProvider(enums.AssetProvider(provider))
|
||||
}
|
||||
return NewProvider(config.Current().Storage.Default)
|
||||
}
|
||||
|
||||
@@ -37,6 +59,12 @@ func GetProvider(providerType enums.AssetProvider) (FileStorageProvider, error)
|
||||
}
|
||||
|
||||
func NewProvider(provider enums.AssetProvider) (FileStorageProvider, error) {
|
||||
if hostStorage != nil {
|
||||
if provider == "" {
|
||||
return GetDefault()
|
||||
}
|
||||
return &hostFileStorageProvider{provider: provider}, nil
|
||||
}
|
||||
cfg := config.Current().Storage
|
||||
|
||||
switch provider {
|
||||
@@ -48,3 +76,45 @@ func NewProvider(provider enums.AssetProvider) (FileStorageProvider, error) {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0082")
|
||||
}
|
||||
}
|
||||
|
||||
type hostFileStorageProvider struct {
|
||||
provider enums.AssetProvider
|
||||
}
|
||||
|
||||
func (p *hostFileStorageProvider) ProviderType() enums.AssetProvider {
|
||||
return p.provider
|
||||
}
|
||||
|
||||
func (p *hostFileStorageProvider) Upload(reader io.Reader, key string, info UploadInfo) (*StoredFile, error) {
|
||||
accessURL, err := hostStorage.Upload(
|
||||
context.Background(), string(p.provider), key, info.Filename, info.MimeType, info.FileSize, reader,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &StoredFile{
|
||||
Provider: p.provider,
|
||||
StorageKey: key,
|
||||
URL: accessURL,
|
||||
Filename: info.Filename,
|
||||
FileSize: info.FileSize,
|
||||
MimeType: info.MimeType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *hostFileStorageProvider) GetURL(key string) string {
|
||||
value, _ := hostStorage.URL(context.Background(), string(p.provider), key)
|
||||
return value
|
||||
}
|
||||
|
||||
func (p *hostFileStorageProvider) GetSignedURL(key string) string {
|
||||
return p.GetURL(key)
|
||||
}
|
||||
|
||||
func (p *hostFileStorageProvider) Delete(key string) error {
|
||||
return hostStorage.Delete(context.Background(), string(p.provider), key)
|
||||
}
|
||||
|
||||
func (p *hostFileStorageProvider) Read(key string) (io.ReadCloser, error) {
|
||||
return hostStorage.Open(context.Background(), string(p.provider), key)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeHostStorage struct {
|
||||
files map[string][]byte
|
||||
}
|
||||
|
||||
func (s *fakeHostStorage) DefaultProvider(context.Context) (string, error) {
|
||||
return "system", nil
|
||||
}
|
||||
|
||||
func (s *fakeHostStorage) Upload(
|
||||
_ context.Context,
|
||||
provider, key, _, _ string,
|
||||
_ int64,
|
||||
reader io.Reader,
|
||||
) (string, error) {
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s.files[provider+":"+key] = data
|
||||
return "/files/" + key, nil
|
||||
}
|
||||
|
||||
func (s *fakeHostStorage) Open(_ context.Context, provider, key string) (io.ReadCloser, error) {
|
||||
return io.NopCloser(bytes.NewReader(s.files[provider+":"+key])), nil
|
||||
}
|
||||
|
||||
func (*fakeHostStorage) URL(_ context.Context, _ string, key string) (string, error) {
|
||||
return "/files/" + key, nil
|
||||
}
|
||||
|
||||
func (s *fakeHostStorage) Delete(_ context.Context, provider, key string) error {
|
||||
delete(s.files, provider+":"+key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestHostStorageProvider(t *testing.T) {
|
||||
host := &fakeHostStorage{files: make(map[string][]byte)}
|
||||
SetHostStorage(host)
|
||||
t.Cleanup(func() { SetHostStorage(nil) })
|
||||
|
||||
provider, err := GetDefault()
|
||||
if err != nil {
|
||||
t.Fatalf("GetDefault() error = %v", err)
|
||||
}
|
||||
if got := string(provider.ProviderType()); got != "system" {
|
||||
t.Fatalf("ProviderType() = %q, want system", got)
|
||||
}
|
||||
|
||||
stored, err := provider.Upload(strings.NewReader("hello"), "chat/a.txt", UploadInfo{
|
||||
Filename: "a.txt",
|
||||
FileSize: 5,
|
||||
MimeType: "text/plain",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Upload() error = %v", err)
|
||||
}
|
||||
if stored.URL != "/files/chat/a.txt" {
|
||||
t.Fatalf("stored URL = %q", stored.URL)
|
||||
}
|
||||
|
||||
reader, err := provider.Read("chat/a.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Read() error = %v", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil || string(data) != "hello" {
|
||||
t.Fatalf("Read() = %q, %v", data, err)
|
||||
}
|
||||
|
||||
if err = provider.Delete("chat/a.txt"); err != nil {
|
||||
t.Fatalf("Delete() error = %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user