Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func (s *Service) resolveContextCompactionPolicy(ctx context.Context, cfg config
AdminEnabled: cfg.ContextCompactEnabled,
UserEnabled: true,
}
if val, valErr := s.getUserSettingCached(ctx, userID, "chat.context_compact_auto"); valErr == nil && val == "false" {
if val, valErr := s.repo.GetUserSettingValue(ctx, userID, "chat.context_compact_auto"); valErr == nil && val == "false" {
policy.UserEnabled = false
}
return policy
Expand Down
1 change: 0 additions & 1 deletion backend/internal/application/conversation/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ type Service struct {
generationStreams *generationStreamRegistry
snapshotCache sync.Map // conversationID (uint) → *cachedSnapshot
userMemCache sync.Map // userID (uint) → *cachedUserMemories
userSettingCache sync.Map // "userID:key" (string) → *cachedUserSetting
imageContextCache *preparedConversationImageCache
}

Expand Down
38 changes: 0 additions & 38 deletions backend/internal/application/conversation/service_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package conversation

import (
"context"
"fmt"
"time"

model "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/conversation"
Expand All @@ -14,8 +13,6 @@ const (
snapshotCacheTTL = 2 * time.Minute
// userMemCacheTTL:用户记忆在会话期间极少变化,缓存 3 分钟。
userMemCacheTTL = 3 * time.Minute
// userSettingCacheTTL:用户设置在会话期间几乎不变,缓存 10 分钟。
userSettingCacheTTL = 10 * time.Minute
// inMemoryCacheSweepInterval:主动清理过期内存缓存,避免冷 key 长期驻留。
inMemoryCacheSweepInterval = time.Minute
)
Expand All @@ -30,12 +27,6 @@ type cachedUserMemories struct {
expiresAt time.Time
}

type cachedUserSetting struct {
value string
valid bool
expiresAt time.Time
}

// getCachedSnapshot 从内存缓存读取最新 Snapshot,未命中时回退到 DB 查询。
func (s *Service) getCachedSnapshot(ctx context.Context, conversationID uint) (*model.ContextSnapshot, error) {
if v, ok := s.snapshotCache.Load(conversationID); ok {
Expand All @@ -61,28 +52,6 @@ func (s *Service) invalidateSnapshotCache(conversationID uint) {
s.snapshotCache.Delete(conversationID)
}

// getUserSettingCached 从内存缓存读取用户设置,未命中时回退到 DB 查询。
func (s *Service) getUserSettingCached(ctx context.Context, userID uint, key string) (string, error) {
cacheKey := fmt.Sprintf("%d:%s", userID, key)
if v, ok := s.userSettingCache.Load(cacheKey); ok {
entry := v.(*cachedUserSetting)
if time.Now().Before(entry.expiresAt) {
if !entry.valid {
return "", fmt.Errorf("not found")
}
return entry.value, nil
}
s.userSettingCache.Delete(cacheKey)
}
val, err := s.repo.GetUserSettingValue(ctx, userID, key)
if err != nil {
s.userSettingCache.Store(cacheKey, &cachedUserSetting{valid: false, expiresAt: time.Now().Add(userSettingCacheTTL)})
return "", err
}
s.userSettingCache.Store(cacheKey, &cachedUserSetting{value: val, valid: true, expiresAt: time.Now().Add(userSettingCacheTTL)})
return val, nil
}

// getCachedUserMemories 从内存缓存读取用户长期记忆,未命中时回退到 DB 查询。
func (s *Service) getCachedUserMemories(ctx context.Context, userID uint) ([]domainmemory.UserMemory, error) {
if v, ok := s.userMemCache.Load(userID); ok {
Expand Down Expand Up @@ -136,11 +105,4 @@ func (s *Service) cleanupExpiredInMemoryCaches(now time.Time) {
}
return true
})
s.userSettingCache.Range(func(key, value interface{}) bool {
entry, ok := value.(*cachedUserSetting)
if !ok || !now.Before(entry.expiresAt) {
s.userSettingCache.Delete(key)
}
return true
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ func TestCleanupExpiredInMemoryCaches(t *testing.T) {
svc.snapshotCache.Store(uint(2), &cachedSnapshot{expiresAt: now.Add(time.Minute)})
svc.userMemCache.Store(uint(1), &cachedUserMemories{expiresAt: now.Add(-time.Second)})
svc.userMemCache.Store(uint(2), &cachedUserMemories{expiresAt: now.Add(time.Minute)})
svc.userSettingCache.Store("1:stale", &cachedUserSetting{expiresAt: now.Add(-time.Second)})
svc.userSettingCache.Store("1:fresh", &cachedUserSetting{expiresAt: now.Add(time.Minute)})

svc.cleanupExpiredInMemoryCaches(now)

Expand All @@ -30,10 +28,4 @@ func TestCleanupExpiredInMemoryCaches(t *testing.T) {
if _, ok := svc.userMemCache.Load(uint(2)); !ok {
t.Fatal("expected fresh user memory cache entry to remain")
}
if _, ok := svc.userSettingCache.Load("1:stale"); ok {
t.Fatal("expected expired user setting cache entry to be deleted")
}
if _, ok := svc.userSettingCache.Load("1:fresh"); !ok {
t.Fatal("expected fresh user setting cache entry to remain")
}
}
15 changes: 10 additions & 5 deletions backend/internal/application/conversation/service_message_send.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,18 @@ func (s *Service) reasoningContentPassbackEnabled(ctx context.Context, userID ui
if route == nil || !route.ReasoningContentPassback {
return false
}
value, err := s.getUserSettingCached(ctx, userID, reasoningContentPassbackSettingKey)
value, err := s.repo.GetUserSettingValue(ctx, userID, reasoningContentPassbackSettingKey)
return err == nil && value != "false"
}

func (s *Service) resolveMessageFileMode(ctx context.Context, userID uint) string {
fileMode := "auto"
if fm, fmErr := s.repo.GetUserSettingValue(ctx, userID, "chat.file_mode"); fmErr == nil && fm != "" {
fileMode = fm
}
return fileMode
}

func messageRouteConfig(route *channel.ResolvedRoute, attributionReferer string, attributionTitle string) llm.RouteConfig {
return llm.RouteConfig{
Protocol: route.Protocol,
Expand Down Expand Up @@ -398,11 +406,8 @@ func (s *Service) sendMessageInternal(
}()

// 读取用户的文件处理模式偏好(auto / full_context / rag)。
fileMode := "auto"
capability := s.resolveChatFileCapability(ctx)
if fm, fmErr := s.getUserSettingCached(ctx, input.UserID, "chat.file_mode"); fmErr == nil && fm != "" {
fileMode = fm
}
fileMode := s.resolveMessageFileMode(ctx, input.UserID)

// 收集并行预取结果,再规划本轮可发送的 PromptScope。
prefetch := <-prefetchCh
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package conversation

import (
"context"
"testing"

"github.com/DEEIX-AI/DEEIX-Chat/backend/internal/application/channel"
appusersettings "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/application/usersettings"
domainusersettings "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/usersettings"
"github.com/DEEIX-AI/DEEIX-Chat/backend/internal/infra/config"
"github.com/DEEIX-AI/DEEIX-Chat/backend/internal/repository"
)

type mutableUserSettingsRepository struct {
repository.ConversationRepository
values map[uint]map[string]string
}

func (r *mutableUserSettingsRepository) GetUserSettingValue(_ context.Context, userID uint, key string) (string, error) {
return r.values[userID][key], nil
}

func (r *mutableUserSettingsRepository) ListByUserID(_ context.Context, userID uint) ([]domainusersettings.UserSetting, error) {
values := r.values[userID]
items := make([]domainusersettings.UserSetting, 0, len(values))
for key, value := range values {
items = append(items, domainusersettings.UserSetting{UserID: userID, Key: key, Value: value})
}
return items, nil
}

func (r *mutableUserSettingsRepository) Upsert(_ context.Context, items []domainusersettings.UserSetting) error {
for _, item := range items {
if r.values[item.UserID] == nil {
r.values[item.UserID] = make(map[string]string)
}
r.values[item.UserID][item.Key] = item.Value
}
return nil
}

func TestConversationSettingsReadPatchedValuesImmediately(t *testing.T) {
const userID uint = 17
ctx := context.Background()
runtimeCfg := config.NewRuntime(config.Config{ContextCompactEnabled: true})
repo := &mutableUserSettingsRepository{
values: map[uint]map[string]string{
userID: {
"chat.reasoning_content_passback": "true",
"chat.context_compact_auto": "true",
"chat.file_mode": "auto",
},
},
}
conversationService := &Service{
cfg: runtimeCfg,
repo: repo,
}
settingsService := appusersettings.NewService(repo)

if !conversationService.reasoningContentPassbackEnabled(ctx, userID, &channel.ResolvedRoute{ReasoningContentPassback: true}) {
t.Fatal("expected initial reasoning passback to be enabled")
}
if !conversationService.resolveContextCompactionPolicy(ctx, runtimeCfg.Snapshot(), userID).EffectiveEnabled() {
t.Fatal("expected initial context compaction to be enabled")
}
initialFileMode := conversationService.resolveMessageFileMode(ctx, userID)
if initialFileMode != "auto" {
t.Fatalf("initial message file mode = %q, want auto", initialFileMode)
}

if _, err := settingsService.PatchSettings(ctx, userID, map[string]string{
"chat.reasoning_content_passback": "false",
"chat.context_compact_auto": "false",
"chat.file_mode": "rag",
}); err != nil {
t.Fatalf("patch settings: %v", err)
}

if conversationService.reasoningContentPassbackEnabled(ctx, userID, &channel.ResolvedRoute{ReasoningContentPassback: true}) {
t.Fatal("expected patched reasoning passback to be disabled")
}
if conversationService.resolveContextCompactionPolicy(ctx, runtimeCfg.Snapshot(), userID).EffectiveEnabled() {
t.Fatal("expected patched context compaction to be disabled")
}
updatedFileMode := conversationService.resolveMessageFileMode(ctx, userID)
if updatedFileMode != "rag" {
t.Fatalf("updated message file mode = %q, want rag", updatedFileMode)
}
}
Loading