Files
backend/internal/pkg/gorse/client.go
lan 4d8f2ec997 Initial backend repository commit.
Set up project files and add .gitignore to exclude local build/runtime artifacts.

Made-with: Cursor
2026-03-09 21:28:58 +08:00

287 lines
7.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gorse
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
gorseio "github.com/gorse-io/gorse-go"
)
// FeedbackType 反馈类型
type FeedbackType string
const (
FeedbackTypeLike FeedbackType = "like" // 点赞
FeedbackTypeStar FeedbackType = "star" // 收藏
FeedbackTypeComment FeedbackType = "comment" // 评论
FeedbackTypeRead FeedbackType = "read" // 浏览
)
// Score 非个性化推荐返回的评分项
type Score struct {
Id string `json:"Id"`
Score float64 `json:"Score"`
}
// Client Gorse客户端接口
type Client interface {
// InsertFeedback 插入用户反馈
InsertFeedback(ctx context.Context, feedbackType FeedbackType, userID, itemID string) error
// DeleteFeedback 删除用户反馈
DeleteFeedback(ctx context.Context, feedbackType FeedbackType, userID, itemID string) error
// GetRecommend 获取个性化推荐列表
GetRecommend(ctx context.Context, userID string, n int, offset int) ([]string, error)
// GetNonPersonalized 获取非个性化推荐(通过名称)
GetNonPersonalized(ctx context.Context, name string, n int, offset int, userID string) ([]string, error)
// UpsertItem 插入或更新物品无embedding
UpsertItem(ctx context.Context, itemID string, categories []string, comment string) error
// UpsertItemWithEmbedding 插入或更新物品带embedding
UpsertItemWithEmbedding(ctx context.Context, itemID string, categories []string, comment string, textToEmbed string) error
// DeleteItem 删除物品
DeleteItem(ctx context.Context, itemID string) error
// UpsertUser 插入或更新用户
UpsertUser(ctx context.Context, userID string, labels map[string]any) error
// IsEnabled 检查是否启用
IsEnabled() bool
}
// client Gorse客户端实现
type client struct {
config Config
gorse *gorseio.GorseClient
httpClient *http.Client
}
// NewClient 创建新的Gorse客户端
func NewClient(cfg Config) Client {
if !cfg.Enabled {
return &noopClient{}
}
gorse := gorseio.NewGorseClient(cfg.Address, cfg.APIKey)
return &client{
config: cfg,
gorse: gorse,
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
}
}
// IsEnabled 检查是否启用
func (c *client) IsEnabled() bool {
return c.config.Enabled
}
// InsertFeedback 插入用户反馈
func (c *client) InsertFeedback(ctx context.Context, feedbackType FeedbackType, userID, itemID string) error {
if !c.config.Enabled {
return nil
}
_, err := c.gorse.InsertFeedback(ctx, []gorseio.Feedback{
{
FeedbackType: string(feedbackType),
UserId: userID,
ItemId: itemID,
Timestamp: time.Now().UTC().Truncate(time.Second),
},
})
return err
}
// DeleteFeedback 删除用户反馈
func (c *client) DeleteFeedback(ctx context.Context, feedbackType FeedbackType, userID, itemID string) error {
if !c.config.Enabled {
return nil
}
_, err := c.gorse.DeleteFeedback(ctx, string(feedbackType), userID, itemID)
return err
}
// GetRecommend 获取个性化推荐列表
func (c *client) GetRecommend(ctx context.Context, userID string, n int, offset int) ([]string, error) {
if !c.config.Enabled {
return nil, nil
}
result, err := c.gorse.GetRecommend(ctx, userID, "", n, offset)
if err != nil {
return nil, err
}
return result, nil
}
// GetNonPersonalized 获取非个性化推荐
// name: 推荐器名称,如 "most_liked_weekly"
// n: 返回数量
// offset: 偏移量
// userID: 可选,用于排除用户已读物品
func (c *client) GetNonPersonalized(ctx context.Context, name string, n int, offset int, userID string) ([]string, error) {
if !c.config.Enabled {
return nil, nil
}
// 构建URL
url := fmt.Sprintf("%s/api/non-personalized/%s?n=%d&offset=%d", c.config.Address, name, n, offset)
if userID != "" {
url += fmt.Sprintf("&user-id=%s", userID)
}
// 创建请求
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// 设置API Key
if c.config.APIKey != "" {
req.Header.Set("X-API-Key", c.config.APIKey)
}
// 发送请求
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("gorse api error: status=%d, body=%s", resp.StatusCode, string(body))
}
// 解析响应
var scores []Score
if err := json.Unmarshal(body, &scores); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
// 提取ID
ids := make([]string, len(scores))
for i, score := range scores {
ids[i] = score.Id
}
return ids, nil
}
// UpsertItem 插入或更新物品
func (c *client) UpsertItem(ctx context.Context, itemID string, categories []string, comment string) error {
if !c.config.Enabled {
return nil
}
_, err := c.gorse.InsertItem(ctx, gorseio.Item{
ItemId: itemID,
IsHidden: false,
Categories: categories,
Comment: comment,
Timestamp: time.Now().UTC().Truncate(time.Second),
})
return err
}
// UpsertItemWithEmbedding 插入或更新物品带embedding
func (c *client) UpsertItemWithEmbedding(ctx context.Context, itemID string, categories []string, comment string, textToEmbed string) error {
if !c.config.Enabled {
return nil
}
// 生成embedding
var embedding []float64
if textToEmbed != "" {
var err error
embedding, err = GetEmbedding(textToEmbed)
if err != nil {
log.Printf("[WARN] Failed to get embedding for item %s: %v, using zero vector", itemID, err)
embedding = make([]float64, 1024)
}
} else {
embedding = make([]float64, 1024)
}
_, err := c.gorse.InsertItem(ctx, gorseio.Item{
ItemId: itemID,
IsHidden: false,
Categories: categories,
Comment: comment,
Timestamp: time.Now().UTC().Truncate(time.Second),
Labels: map[string]any{
"embedding": embedding,
},
})
return err
}
// DeleteItem 删除物品
func (c *client) DeleteItem(ctx context.Context, itemID string) error {
if !c.config.Enabled {
return nil
}
_, err := c.gorse.DeleteItem(ctx, itemID)
return err
}
// UpsertUser 插入或更新用户
func (c *client) UpsertUser(ctx context.Context, userID string, labels map[string]any) error {
if !c.config.Enabled {
return nil
}
_, err := c.gorse.InsertUser(ctx, gorseio.User{
UserId: userID,
Labels: labels,
})
return err
}
// noopClient 空操作客户端(用于未启用推荐功能时)
type noopClient struct{}
func (c *noopClient) IsEnabled() bool { return false }
func (c *noopClient) InsertFeedback(ctx context.Context, feedbackType FeedbackType, userID, itemID string) error {
return nil
}
func (c *noopClient) DeleteFeedback(ctx context.Context, feedbackType FeedbackType, userID, itemID string) error {
return nil
}
func (c *noopClient) GetRecommend(ctx context.Context, userID string, n int, offset int) ([]string, error) {
return nil, nil
}
func (c *noopClient) GetNonPersonalized(ctx context.Context, name string, n int, offset int, userID string) ([]string, error) {
return nil, nil
}
func (c *noopClient) UpsertItem(ctx context.Context, itemID string, categories []string, comment string) error {
return nil
}
func (c *noopClient) UpsertItemWithEmbedding(ctx context.Context, itemID string, categories []string, comment string, textToEmbed string) error {
return nil
}
func (c *noopClient) DeleteItem(ctx context.Context, itemID string) error { return nil }
func (c *noopClient) UpsertUser(ctx context.Context, userID string, labels map[string]any) error {
return nil
}
// 确保实现了接口
var _ Client = (*client)(nil)
var _ Client = (*noopClient)(nil)
// log 用于内部日志
func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
}