feat: Go backend, enhanced search, new widgets, Docker deploy

Major changes:
- Add Go backend (backend/) with microservices architecture
- Enhanced master-agents-svc: reranker, content-classifier, stealth-crawler,
  proxy-manager, media-search, fastClassifier, language detection
- New web-svc widgets: KnowledgeCard, ProductCard, ProfileCard, VideoCard,
  UnifiedCard, CardGallery, InlineImageGallery, SourcesPanel, RelatedQuestions
- Improved discover-svc with discover-db integration
- Docker deployment improvements (Caddyfile, vendor.sh, BUILD.md)
- Library-svc: project_id schema migration
- Remove deprecated finance-svc and travel-svc
- Localization improvements across services

Made-with: Cursor
This commit is contained in:
home
2026-02-27 04:15:32 +03:00
parent 328d968f3f
commit 06fe57c765
285 changed files with 53132 additions and 1871 deletions

View File

@@ -0,0 +1,306 @@
package db
import (
"context"
"database/sql"
"encoding/json"
"strings"
"time"
"github.com/gooseek/backend/internal/computer"
)
type ComputerMemoryRepo struct {
db *sql.DB
}
func NewComputerMemoryRepo(db *sql.DB) *ComputerMemoryRepo {
return &ComputerMemoryRepo{db: db}
}
func (r *ComputerMemoryRepo) Migrate() error {
query := `
CREATE TABLE IF NOT EXISTS computer_memory (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
task_id UUID,
key VARCHAR(255) NOT NULL,
value JSONB NOT NULL,
type VARCHAR(50),
tags TEXT[],
created_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_computer_memory_user_id ON computer_memory(user_id);
CREATE INDEX IF NOT EXISTS idx_computer_memory_task_id ON computer_memory(task_id);
CREATE INDEX IF NOT EXISTS idx_computer_memory_type ON computer_memory(type);
CREATE INDEX IF NOT EXISTS idx_computer_memory_expires ON computer_memory(expires_at) WHERE expires_at IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_computer_memory_key ON computer_memory(key);
`
_, err := r.db.Exec(query)
return err
}
func (r *ComputerMemoryRepo) Store(ctx context.Context, entry *computer.MemoryEntry) error {
valueJSON, err := json.Marshal(entry.Value)
if err != nil {
return err
}
query := `
INSERT INTO computer_memory (id, user_id, task_id, key, value, type, tags, created_at, expires_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (id) DO UPDATE SET
value = EXCLUDED.value,
type = EXCLUDED.type,
tags = EXCLUDED.tags,
expires_at = EXCLUDED.expires_at
`
var taskID interface{}
if entry.TaskID != "" {
taskID = entry.TaskID
}
_, err = r.db.ExecContext(ctx, query,
entry.ID,
entry.UserID,
taskID,
entry.Key,
valueJSON,
entry.Type,
entry.Tags,
entry.CreatedAt,
entry.ExpiresAt,
)
return err
}
func (r *ComputerMemoryRepo) GetByUser(ctx context.Context, userID string, limit int) ([]computer.MemoryEntry, error) {
query := `
SELECT id, user_id, task_id, key, value, type, tags, created_at, expires_at
FROM computer_memory
WHERE user_id = $1
AND (expires_at IS NULL OR expires_at > NOW())
ORDER BY created_at DESC
LIMIT $2
`
rows, err := r.db.QueryContext(ctx, query, userID, limit)
if err != nil {
return nil, err
}
defer rows.Close()
return r.scanEntries(rows)
}
func (r *ComputerMemoryRepo) GetByTask(ctx context.Context, taskID string) ([]computer.MemoryEntry, error) {
query := `
SELECT id, user_id, task_id, key, value, type, tags, created_at, expires_at
FROM computer_memory
WHERE task_id = $1
AND (expires_at IS NULL OR expires_at > NOW())
ORDER BY created_at ASC
`
rows, err := r.db.QueryContext(ctx, query, taskID)
if err != nil {
return nil, err
}
defer rows.Close()
return r.scanEntries(rows)
}
func (r *ComputerMemoryRepo) Search(ctx context.Context, userID, query string, limit int) ([]computer.MemoryEntry, error) {
searchTerms := strings.Fields(strings.ToLower(query))
if len(searchTerms) == 0 {
return r.GetByUser(ctx, userID, limit)
}
likePatterns := make([]string, len(searchTerms))
args := make([]interface{}, len(searchTerms)+2)
args[0] = userID
for i, term := range searchTerms {
likePatterns[i] = "%" + term + "%"
args[i+1] = likePatterns[i]
}
args[len(args)-1] = limit
var conditions []string
for i := range searchTerms {
conditions = append(conditions, "(LOWER(key) LIKE $"+string(rune('2'+i))+" OR LOWER(value::text) LIKE $"+string(rune('2'+i))+")")
}
sqlQuery := `
SELECT id, user_id, task_id, key, value, type, tags, created_at, expires_at
FROM computer_memory
WHERE user_id = $1
AND (expires_at IS NULL OR expires_at > NOW())
AND (` + strings.Join(conditions, " OR ") + `)
ORDER BY created_at DESC
LIMIT $` + string(rune('2'+len(searchTerms)))
rows, err := r.db.QueryContext(ctx, sqlQuery, args...)
if err != nil {
return r.GetByUser(ctx, userID, limit)
}
defer rows.Close()
return r.scanEntries(rows)
}
func (r *ComputerMemoryRepo) GetByType(ctx context.Context, userID, memType string, limit int) ([]computer.MemoryEntry, error) {
query := `
SELECT id, user_id, task_id, key, value, type, tags, created_at, expires_at
FROM computer_memory
WHERE user_id = $1 AND type = $2
AND (expires_at IS NULL OR expires_at > NOW())
ORDER BY created_at DESC
LIMIT $3
`
rows, err := r.db.QueryContext(ctx, query, userID, memType, limit)
if err != nil {
return nil, err
}
defer rows.Close()
return r.scanEntries(rows)
}
func (r *ComputerMemoryRepo) GetByKey(ctx context.Context, userID, key string) (*computer.MemoryEntry, error) {
query := `
SELECT id, user_id, task_id, key, value, type, tags, created_at, expires_at
FROM computer_memory
WHERE user_id = $1 AND key = $2
AND (expires_at IS NULL OR expires_at > NOW())
ORDER BY created_at DESC
LIMIT 1
`
var entry computer.MemoryEntry
var valueJSON []byte
var taskID sql.NullString
var expiresAt sql.NullTime
var tags []string
err := r.db.QueryRowContext(ctx, query, userID, key).Scan(
&entry.ID,
&entry.UserID,
&taskID,
&entry.Key,
&valueJSON,
&entry.Type,
&tags,
&entry.CreatedAt,
&expiresAt,
)
if err != nil {
return nil, err
}
if taskID.Valid {
entry.TaskID = taskID.String
}
if expiresAt.Valid {
entry.ExpiresAt = &expiresAt.Time
}
entry.Tags = tags
json.Unmarshal(valueJSON, &entry.Value)
return &entry, nil
}
func (r *ComputerMemoryRepo) Delete(ctx context.Context, id string) error {
query := `DELETE FROM computer_memory WHERE id = $1`
_, err := r.db.ExecContext(ctx, query, id)
return err
}
func (r *ComputerMemoryRepo) DeleteByUser(ctx context.Context, userID string) error {
query := `DELETE FROM computer_memory WHERE user_id = $1`
_, err := r.db.ExecContext(ctx, query, userID)
return err
}
func (r *ComputerMemoryRepo) DeleteByTask(ctx context.Context, taskID string) error {
query := `DELETE FROM computer_memory WHERE task_id = $1`
_, err := r.db.ExecContext(ctx, query, taskID)
return err
}
func (r *ComputerMemoryRepo) DeleteExpired(ctx context.Context) (int64, error) {
query := `DELETE FROM computer_memory WHERE expires_at IS NOT NULL AND expires_at < NOW()`
result, err := r.db.ExecContext(ctx, query)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
func (r *ComputerMemoryRepo) scanEntries(rows *sql.Rows) ([]computer.MemoryEntry, error) {
var entries []computer.MemoryEntry
for rows.Next() {
var entry computer.MemoryEntry
var valueJSON []byte
var taskID sql.NullString
var expiresAt sql.NullTime
var tags []string
err := rows.Scan(
&entry.ID,
&entry.UserID,
&taskID,
&entry.Key,
&valueJSON,
&entry.Type,
&tags,
&entry.CreatedAt,
&expiresAt,
)
if err != nil {
continue
}
if taskID.Valid {
entry.TaskID = taskID.String
}
if expiresAt.Valid {
entry.ExpiresAt = &expiresAt.Time
}
entry.Tags = tags
json.Unmarshal(valueJSON, &entry.Value)
entries = append(entries, entry)
}
return entries, nil
}
func (r *ComputerMemoryRepo) Count(ctx context.Context, userID string) (int64, error) {
query := `
SELECT COUNT(*)
FROM computer_memory
WHERE user_id = $1
AND (expires_at IS NULL OR expires_at > NOW())
`
var count int64
err := r.db.QueryRowContext(ctx, query, userID).Scan(&count)
return count, err
}
func (r *ComputerMemoryRepo) UpdateExpiry(ctx context.Context, id string, expiresAt time.Time) error {
query := `UPDATE computer_memory SET expires_at = $1 WHERE id = $2`
_, err := r.db.ExecContext(ctx, query, expiresAt, id)
return err
}