- Implement atomic state transitions: add EntryService.PatchEntry() with EntryDiff computation and EventEntryTransition event emission. - Add persistent SQLite task queue (JobEngine): add jobs table with crash recovery, exponential backoff retries, concurrency control, and idempotency key deduplication. - Decompose monolithic HTTP handlers: split handlers.go into domain handlers (auth, feeds, entries, jobs, settings, rules, sse). - Implement real-time Server-Sent Events (/api/events/stream) for multi-tab/device state synchronization. - Fix SQLite concurrency: apply busy_timeout(10000) and journal_mode(WAL) via DSN query parameters to resolve SQLITE_BUSY (5). - Fix AI settings & prompt loading: unmarshal JSON from ai_settings, apply configured prompt templates, and enforce strict enabled/API key validations. - Fix undo share state: remove omitempty from ShareUUID to serialize null for state resets. - Fix middle list counter: compute true unread/total stats from DB and clean switcher button labels. - Adapt mobile UI for iOS: add viewport-fit=cover in index.html and calibrate safe-area insets for the bottom navigation and article reader toolbar. - Add architectural documentation and worklog in docs/009_worklog.md.
99 lines
2.4 KiB
Go
99 lines
2.4 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/db"
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/events"
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/logger"
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/models"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type SettingsService struct {
|
|
db *db.DB
|
|
bus *events.EventBus
|
|
log *zap.Logger
|
|
}
|
|
|
|
func NewSettingsService(database *db.DB, bus *events.EventBus) *SettingsService {
|
|
return &SettingsService{
|
|
db: database,
|
|
bus: bus,
|
|
log: logger.Named("settings-service"),
|
|
}
|
|
}
|
|
|
|
func (s *SettingsService) GetPreferences(ctx context.Context, username string) (map[string]interface{}, error) {
|
|
user, err := s.db.GetUserByUsername(username)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if user.Preferences == nil {
|
|
return make(map[string]interface{}), nil
|
|
}
|
|
return user.Preferences, nil
|
|
}
|
|
|
|
func (s *SettingsService) UpdatePreferences(ctx context.Context, username string, newPrefs map[string]interface{}) (map[string]interface{}, error) {
|
|
user, err := s.db.GetUserByUsername(username)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if user.Preferences == nil {
|
|
user.Preferences = make(map[string]interface{})
|
|
}
|
|
for k, v := range newPrefs {
|
|
user.Preferences[k] = v
|
|
}
|
|
if err := s.db.UpdateUserPreferences(user.ID, user.Preferences); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if s.bus != nil {
|
|
s.bus.Publish(ctx, events.Event{
|
|
Type: events.EventPreferencesSync,
|
|
DirectAttrs: user.Preferences,
|
|
})
|
|
}
|
|
|
|
return user.Preferences, nil
|
|
}
|
|
|
|
func (s *SettingsService) GetAISettings(ctx context.Context) (*models.AISettings, error) {
|
|
raw, err := s.db.GetSetting("ai_settings")
|
|
if err != nil || raw == "" {
|
|
return &models.AISettings{
|
|
Enabled: false,
|
|
Provider: "openai",
|
|
BaseURL: "https://api.openai.com/v1",
|
|
APIKey: "",
|
|
Model: "gpt-4o-mini",
|
|
Temperature: 0.3,
|
|
MaxTokens: 1000,
|
|
Prompts: []models.PromptTemplate{},
|
|
}, nil
|
|
}
|
|
|
|
var cfg models.AISettings
|
|
if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
|
|
return nil, fmt.Errorf("failed to parse AI settings: %w", err)
|
|
}
|
|
return &cfg, nil
|
|
}
|
|
|
|
func (s *SettingsService) UpdateAISettings(ctx context.Context, cfg models.AISettings) (*models.AISettings, error) {
|
|
bytes, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal AI settings: %w", err)
|
|
}
|
|
|
|
if err := s.db.SetSetting("ai_settings", string(bytes)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &cfg, nil
|
|
}
|