memos/server/router/api/v1/workspace_service.go

457 lines
16 KiB
Go

package v1
import (
"context"
"fmt"
"github.com/pkg/errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/usememos/memos/plugin/ai"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
// GetWorkspaceProfile returns the workspace profile.
func (s *APIV1Service) GetWorkspaceProfile(ctx context.Context, _ *v1pb.GetWorkspaceProfileRequest) (*v1pb.WorkspaceProfile, error) {
workspaceProfile := &v1pb.WorkspaceProfile{
Version: s.Profile.Version,
Mode: s.Profile.Mode,
InstanceUrl: s.Profile.InstanceURL,
}
owner, err := s.GetInstanceOwner(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get instance owner: %v", err)
}
if owner != nil {
workspaceProfile.Owner = owner.Name
}
return workspaceProfile, nil
}
func (s *APIV1Service) GetWorkspaceSetting(ctx context.Context, request *v1pb.GetWorkspaceSettingRequest) (*v1pb.WorkspaceSetting, error) {
workspaceSettingKeyString, err := ExtractWorkspaceSettingKeyFromName(request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid workspace setting name: %v", err)
}
workspaceSettingKey := storepb.WorkspaceSettingKey(storepb.WorkspaceSettingKey_value[workspaceSettingKeyString])
// Get workspace setting from store with default value.
switch workspaceSettingKey {
case storepb.WorkspaceSettingKey_BASIC:
_, err = s.Store.GetWorkspaceBasicSetting(ctx)
case storepb.WorkspaceSettingKey_GENERAL:
_, err = s.Store.GetWorkspaceGeneralSetting(ctx)
case storepb.WorkspaceSettingKey_MEMO_RELATED:
_, err = s.Store.GetWorkspaceMemoRelatedSetting(ctx)
case storepb.WorkspaceSettingKey_STORAGE:
_, err = s.Store.GetWorkspaceStorageSetting(ctx)
case storepb.WorkspaceSettingKey_AI:
_, err = s.Store.GetWorkspaceAISetting(ctx)
default:
return nil, status.Errorf(codes.InvalidArgument, "unsupported workspace setting key: %v", workspaceSettingKey)
}
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get workspace setting: %v", err)
}
workspaceSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
Name: workspaceSettingKey.String(),
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get workspace setting: %v", err)
}
if workspaceSetting == nil {
return nil, status.Errorf(codes.NotFound, "workspace setting not found")
}
// For storage and AI settings, only host can get them.
if workspaceSetting.Key == storepb.WorkspaceSettingKey_STORAGE || workspaceSetting.Key == storepb.WorkspaceSettingKey_AI {
user, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if user == nil || user.Role != store.RoleHost {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
}
return convertWorkspaceSettingFromStore(workspaceSetting), nil
}
func (s *APIV1Service) UpdateWorkspaceSetting(ctx context.Context, request *v1pb.UpdateWorkspaceSettingRequest) (*v1pb.WorkspaceSetting, error) {
user, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if user.Role != store.RoleHost {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
// TODO: Apply update_mask if specified
_ = request.UpdateMask
updateSetting := convertWorkspaceSettingToStore(request.Setting)
workspaceSetting, err := s.Store.UpsertWorkspaceSetting(ctx, updateSetting)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to upsert workspace setting: %v", err)
}
return convertWorkspaceSettingFromStore(workspaceSetting), nil
}
func convertWorkspaceSettingFromStore(setting *storepb.WorkspaceSetting) *v1pb.WorkspaceSetting {
workspaceSetting := &v1pb.WorkspaceSetting{
Name: fmt.Sprintf("workspace/settings/%s", setting.Key.String()),
}
switch setting.Value.(type) {
case *storepb.WorkspaceSetting_GeneralSetting:
workspaceSetting.Value = &v1pb.WorkspaceSetting_GeneralSetting_{
GeneralSetting: convertWorkspaceGeneralSettingFromStore(setting.GetGeneralSetting()),
}
case *storepb.WorkspaceSetting_StorageSetting:
workspaceSetting.Value = &v1pb.WorkspaceSetting_StorageSetting_{
StorageSetting: convertWorkspaceStorageSettingFromStore(setting.GetStorageSetting()),
}
case *storepb.WorkspaceSetting_MemoRelatedSetting:
workspaceSetting.Value = &v1pb.WorkspaceSetting_MemoRelatedSetting_{
MemoRelatedSetting: convertWorkspaceMemoRelatedSettingFromStore(setting.GetMemoRelatedSetting()),
}
case *storepb.WorkspaceSetting_AiSetting:
workspaceSetting.Value = &v1pb.WorkspaceSetting_AiSetting_{
AiSetting: convertWorkspaceAISettingFromStore(setting.GetAiSetting()),
}
}
return workspaceSetting
}
func convertWorkspaceSettingToStore(setting *v1pb.WorkspaceSetting) *storepb.WorkspaceSetting {
settingKeyString, _ := ExtractWorkspaceSettingKeyFromName(setting.Name)
workspaceSetting := &storepb.WorkspaceSetting{
Key: storepb.WorkspaceSettingKey(storepb.WorkspaceSettingKey_value[settingKeyString]),
Value: &storepb.WorkspaceSetting_GeneralSetting{
GeneralSetting: convertWorkspaceGeneralSettingToStore(setting.GetGeneralSetting()),
},
}
switch workspaceSetting.Key {
case storepb.WorkspaceSettingKey_GENERAL:
workspaceSetting.Value = &storepb.WorkspaceSetting_GeneralSetting{
GeneralSetting: convertWorkspaceGeneralSettingToStore(setting.GetGeneralSetting()),
}
case storepb.WorkspaceSettingKey_STORAGE:
workspaceSetting.Value = &storepb.WorkspaceSetting_StorageSetting{
StorageSetting: convertWorkspaceStorageSettingToStore(setting.GetStorageSetting()),
}
case storepb.WorkspaceSettingKey_MEMO_RELATED:
workspaceSetting.Value = &storepb.WorkspaceSetting_MemoRelatedSetting{
MemoRelatedSetting: convertWorkspaceMemoRelatedSettingToStore(setting.GetMemoRelatedSetting()),
}
case storepb.WorkspaceSettingKey_AI:
workspaceSetting.Value = &storepb.WorkspaceSetting_AiSetting{
AiSetting: convertWorkspaceAISettingToStore(setting.GetAiSetting()),
}
}
return workspaceSetting
}
func convertWorkspaceGeneralSettingFromStore(setting *storepb.WorkspaceGeneralSetting) *v1pb.WorkspaceSetting_GeneralSetting {
if setting == nil {
return nil
}
// Backfill theme if empty
theme := setting.Theme
if theme == "" {
theme = "default"
}
generalSetting := &v1pb.WorkspaceSetting_GeneralSetting{
Theme: theme,
DisallowUserRegistration: setting.DisallowUserRegistration,
DisallowPasswordAuth: setting.DisallowPasswordAuth,
AdditionalScript: setting.AdditionalScript,
AdditionalStyle: setting.AdditionalStyle,
WeekStartDayOffset: setting.WeekStartDayOffset,
DisallowChangeUsername: setting.DisallowChangeUsername,
DisallowChangeNickname: setting.DisallowChangeNickname,
}
if setting.CustomProfile != nil {
generalSetting.CustomProfile = &v1pb.WorkspaceSetting_GeneralSetting_CustomProfile{
Title: setting.CustomProfile.Title,
Description: setting.CustomProfile.Description,
LogoUrl: setting.CustomProfile.LogoUrl,
Locale: setting.CustomProfile.Locale,
}
}
return generalSetting
}
func convertWorkspaceGeneralSettingToStore(setting *v1pb.WorkspaceSetting_GeneralSetting) *storepb.WorkspaceGeneralSetting {
if setting == nil {
return nil
}
generalSetting := &storepb.WorkspaceGeneralSetting{
Theme: setting.Theme,
DisallowUserRegistration: setting.DisallowUserRegistration,
DisallowPasswordAuth: setting.DisallowPasswordAuth,
AdditionalScript: setting.AdditionalScript,
AdditionalStyle: setting.AdditionalStyle,
WeekStartDayOffset: setting.WeekStartDayOffset,
DisallowChangeUsername: setting.DisallowChangeUsername,
DisallowChangeNickname: setting.DisallowChangeNickname,
}
if setting.CustomProfile != nil {
generalSetting.CustomProfile = &storepb.WorkspaceCustomProfile{
Title: setting.CustomProfile.Title,
Description: setting.CustomProfile.Description,
LogoUrl: setting.CustomProfile.LogoUrl,
Locale: setting.CustomProfile.Locale,
}
}
return generalSetting
}
func convertWorkspaceStorageSettingFromStore(settingpb *storepb.WorkspaceStorageSetting) *v1pb.WorkspaceSetting_StorageSetting {
if settingpb == nil {
return nil
}
setting := &v1pb.WorkspaceSetting_StorageSetting{
StorageType: v1pb.WorkspaceSetting_StorageSetting_StorageType(settingpb.StorageType),
FilepathTemplate: settingpb.FilepathTemplate,
UploadSizeLimitMb: settingpb.UploadSizeLimitMb,
}
if settingpb.S3Config != nil {
setting.S3Config = &v1pb.WorkspaceSetting_StorageSetting_S3Config{
AccessKeyId: settingpb.S3Config.AccessKeyId,
AccessKeySecret: settingpb.S3Config.AccessKeySecret,
Endpoint: settingpb.S3Config.Endpoint,
Region: settingpb.S3Config.Region,
Bucket: settingpb.S3Config.Bucket,
UsePathStyle: settingpb.S3Config.UsePathStyle,
}
}
return setting
}
func convertWorkspaceStorageSettingToStore(setting *v1pb.WorkspaceSetting_StorageSetting) *storepb.WorkspaceStorageSetting {
if setting == nil {
return nil
}
settingpb := &storepb.WorkspaceStorageSetting{
StorageType: storepb.WorkspaceStorageSetting_StorageType(setting.StorageType),
FilepathTemplate: setting.FilepathTemplate,
UploadSizeLimitMb: setting.UploadSizeLimitMb,
}
if setting.S3Config != nil {
settingpb.S3Config = &storepb.StorageS3Config{
AccessKeyId: setting.S3Config.AccessKeyId,
AccessKeySecret: setting.S3Config.AccessKeySecret,
Endpoint: setting.S3Config.Endpoint,
Region: setting.S3Config.Region,
Bucket: setting.S3Config.Bucket,
UsePathStyle: setting.S3Config.UsePathStyle,
}
}
return settingpb
}
func convertWorkspaceMemoRelatedSettingFromStore(setting *storepb.WorkspaceMemoRelatedSetting) *v1pb.WorkspaceSetting_MemoRelatedSetting {
if setting == nil {
return nil
}
return &v1pb.WorkspaceSetting_MemoRelatedSetting{
DisallowPublicVisibility: setting.DisallowPublicVisibility,
DisplayWithUpdateTime: setting.DisplayWithUpdateTime,
ContentLengthLimit: setting.ContentLengthLimit,
EnableDoubleClickEdit: setting.EnableDoubleClickEdit,
EnableLinkPreview: setting.EnableLinkPreview,
Reactions: setting.Reactions,
DisableMarkdownShortcuts: setting.DisableMarkdownShortcuts,
EnableBlurNsfwContent: setting.EnableBlurNsfwContent,
NsfwTags: setting.NsfwTags,
}
}
func convertWorkspaceMemoRelatedSettingToStore(setting *v1pb.WorkspaceSetting_MemoRelatedSetting) *storepb.WorkspaceMemoRelatedSetting {
if setting == nil {
return nil
}
return &storepb.WorkspaceMemoRelatedSetting{
DisallowPublicVisibility: setting.DisallowPublicVisibility,
DisplayWithUpdateTime: setting.DisplayWithUpdateTime,
ContentLengthLimit: setting.ContentLengthLimit,
EnableDoubleClickEdit: setting.EnableDoubleClickEdit,
EnableLinkPreview: setting.EnableLinkPreview,
Reactions: setting.Reactions,
DisableMarkdownShortcuts: setting.DisableMarkdownShortcuts,
EnableBlurNsfwContent: setting.EnableBlurNsfwContent,
NsfwTags: setting.NsfwTags,
}
}
func convertWorkspaceAISettingFromStore(setting *storepb.WorkspaceAISetting) *v1pb.WorkspaceSetting_AiSetting {
if setting == nil {
return &v1pb.WorkspaceSetting_AiSetting{
EnableAi: false,
BaseUrl: "",
ApiKey: "",
Model: "",
TimeoutSeconds: 10,
}
}
result := &v1pb.WorkspaceSetting_AiSetting{
EnableAi: setting.EnableAi,
BaseUrl: setting.BaseUrl,
ApiKey: setting.ApiKey,
Model: setting.Model,
TimeoutSeconds: setting.TimeoutSeconds,
}
if setting.TagRecommendation != nil {
result.TagRecommendation = &v1pb.WorkspaceSetting_TagRecommendationConfig{
Enabled: setting.TagRecommendation.Enabled,
SystemPrompt: setting.TagRecommendation.SystemPrompt,
RequestsPerMinute: setting.TagRecommendation.RequestsPerMinute,
}
}
return result
}
func convertWorkspaceAISettingToStore(setting *v1pb.WorkspaceSetting_AiSetting) *storepb.WorkspaceAISetting {
if setting == nil {
return &storepb.WorkspaceAISetting{
EnableAi: false,
BaseUrl: "",
ApiKey: "",
Model: "",
TimeoutSeconds: 10,
}
}
result := &storepb.WorkspaceAISetting{
EnableAi: setting.EnableAi,
BaseUrl: setting.BaseUrl,
ApiKey: setting.ApiKey,
Model: setting.Model,
TimeoutSeconds: setting.TimeoutSeconds,
}
if setting.TagRecommendation != nil {
result.TagRecommendation = &storepb.TagRecommendationConfig{
Enabled: setting.TagRecommendation.Enabled,
SystemPrompt: setting.TagRecommendation.SystemPrompt,
RequestsPerMinute: setting.TagRecommendation.RequestsPerMinute,
}
}
return result
}
var ownerCache *v1pb.User
func (s *APIV1Service) GetInstanceOwner(ctx context.Context) (*v1pb.User, error) {
if ownerCache != nil {
return ownerCache, nil
}
hostUserType := store.RoleHost
user, err := s.Store.GetUser(ctx, &store.FindUser{
Role: &hostUserType,
})
if err != nil {
return nil, errors.Wrapf(err, "failed to find owner")
}
if user == nil {
return nil, nil
}
ownerCache = convertUserFromStore(user)
return ownerCache, nil
}
// GetDefaultTagRecommendationPrompt returns the default system prompt for AI tag recommendations.
func (s *APIV1Service) GetDefaultTagRecommendationPrompt(ctx context.Context, _ *v1pb.GetDefaultTagRecommendationPromptRequest) (*v1pb.GetDefaultTagRecommendationPromptResponse, error) {
return &v1pb.GetDefaultTagRecommendationPromptResponse{
SystemPrompt: ai.GetDefaultSystemPrompt(),
}, nil
}
// TestAiConnection tests the AI API connection and configuration.
func (s *APIV1Service) TestAiConnection(ctx context.Context, request *v1pb.TestAiConnectionRequest) (*v1pb.TestAiConnectionResponse, error) {
// Check permissions - only host can test AI connection
user, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if user == nil || user.Role != store.RoleHost {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
// Validate request
if request.BaseUrl == "" {
return &v1pb.TestAiConnectionResponse{
Success: false,
Message: "Base URL is required",
}, nil
}
if request.Model == "" {
return &v1pb.TestAiConnectionResponse{
Success: false,
Message: "Model is required",
}, nil
}
// Create AI config for testing
config := &ai.Config{
Enabled: true,
BaseURL: request.BaseUrl,
APIKey: request.ApiKey,
Model: request.Model,
TimeoutSeconds: int(request.TimeoutSeconds),
}
// Default timeout if not specified
if config.TimeoutSeconds <= 0 {
config.TimeoutSeconds = 10
}
// Create AI client
client, err := ai.NewClient(config)
if err != nil {
return &v1pb.TestAiConnectionResponse{
Success: false,
Message: fmt.Sprintf("Failed to create AI client: %v", err),
}, nil
}
// Test with a simple chat request
chatRequest := &ai.ChatRequest{
Messages: []ai.Message{
{
Role: "user",
Content: "Hello, please respond with 'AI connection test successful'",
},
},
MaxTokens: 50,
Temperature: 0.1,
}
response, err := client.Chat(ctx, chatRequest)
if err != nil {
return &v1pb.TestAiConnectionResponse{
Success: false,
Message: fmt.Sprintf("AI API test failed: %v", err),
}, nil
}
// Test successful
return &v1pb.TestAiConnectionResponse{
Success: true,
Message: "AI connection test successful",
ModelInfo: fmt.Sprintf("Model: %s, Response: %s", request.Model, response.Content),
}, nil
}