Files
GovAI/server/internal/service/research/service.go
T
freedakgmail c949204662 feat(govai): 0617 优化首批 — 安全/私有化/深度研究/服务层/可观测性
借鉴 odysseus 的能力设计,全程净室实现、零 AGPL 代码、不引入 AGPL 依赖。

T1 提示注入防护: pkg/promptguard 包裹外部/知识库内容为不可信数据,buildMessages 移出 system 指令区。 T2 安全 CI: .github/workflows(ci+security: govulncheck/gitleaks/actionlint/hadolint/trivy)+dependabot+.hadolint.yaml;go.mod 加 toolchain go1.25.11 修复 20 个 stdlib CVE。 T3 管理员 2FA: 迁移 000016 + RFC6238 TOTP/备份码(pkg/auth, 零依赖) + 登录流程集成(后端)。 T4 本地模型: LLM/embedding 支持本地 vLLM/Ollama(OpenAI 兼容, 鉴权头条件发送, NoAuth) + docs/local-deploy.md。 T6 深度研究: 迁移 000017 + Python research-worker(净室多步流水线, 检索避开 SearXNG) + Go research 服务/handler/路由。 T7 service 层: 新增 internal/service/{research,twofa}, 2FA 业务逻辑从胖 handler 下沉, 接口注入可单测。 T10 缓存/可观测性: internal/cache(Redis+内存, 优雅降级) 接入 store 热点列表; Prometheus 指标+/metrics; docs/openapi.yaml。 验证: go build/vet/test ./... 全绿(8 包); research-worker 12 单测过; 真实 PG 应用迁移并烟测。
2026-06-17 17:52:47 +08:00

131 lines
3.5 KiB
Go
Raw 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 research 提供深度研究任务的业务编排(service 层)。
//
// 该层与具体存储/队列解耦:依赖 Repository(任务持久化)、Queue(任务下发)、
// Cache(快速状态)三个接口,便于单测与替换实现。HTTP handler 仅做参数解析与
// 响应,业务规则集中在这里。
package research
import (
"context"
"errors"
"strings"
"time"
"github.com/google/uuid"
)
// Task 是研究任务的领域模型(用于查询/列表返回)。
type Task struct {
ID string
Topic string
Status string
Progress int
StatusMessage *string
ErrorMessage *string
Report *string
Sources []byte // 原始 JSON[{title,url,snippet}]
TokensUsed int
CreatedAt time.Time
}
// CreateInput 创建研究任务的入参。
type CreateInput struct {
UserID string
AppID *string
Topic string
Config map[string]any
}
// CachedStatus 来自 Redis 的快速状态(worker 实时写入)。
type CachedStatus struct {
Status string
Progress int
Message string
}
// Repository 任务持久化接口。
type Repository interface {
Insert(ctx context.Context, id, userID string, appID *string, topic string, config map[string]any) error
Get(ctx context.Context, userID, taskID string) (*Task, error)
List(ctx context.Context, userID string, limit int) ([]Task, error)
// Cancel 仅取消进行中的任务;found 表示是否有可取消的任务被更新。
Cancel(ctx context.Context, userID, taskID string) (found bool, err error)
}
// Queue 任务下发接口(worker 消费)。
type Queue interface {
Enqueue(ctx context.Context, taskID string) error
}
// Cache 快速状态读取接口(可选)。
type Cache interface {
GetStatus(ctx context.Context, taskID string) (*CachedStatus, bool)
}
var (
ErrEmptyTopic = errors.New("研究题目不能为空")
ErrNotFound = errors.New("任务不存在")
)
type Service struct {
repo Repository
queue Queue
cache Cache // 可为 nil
}
func NewService(repo Repository, queue Queue, cache Cache) *Service {
return &Service{repo: repo, queue: queue, cache: cache}
}
// Create 校验入参、落库并下发到队列,返回任务 ID。
func (s *Service) Create(ctx context.Context, in CreateInput) (string, error) {
topic := strings.TrimSpace(in.Topic)
if topic == "" {
return "", ErrEmptyTopic
}
id := uuid.New().String()
if err := s.repo.Insert(ctx, id, in.UserID, in.AppID, topic, in.Config); err != nil {
return "", err
}
if err := s.queue.Enqueue(ctx, id); err != nil {
return "", err
}
return id, nil
}
// Status 返回任务(先按所有权从库中取,再用 Redis 快速状态覆盖以保证新鲜度)。
func (s *Service) Status(ctx context.Context, userID, taskID string) (*Task, error) {
t, err := s.repo.Get(ctx, userID, taskID)
if err != nil {
return nil, err
}
if s.cache != nil {
if cs, ok := s.cache.GetStatus(ctx, taskID); ok {
t.Status = cs.Status
t.Progress = cs.Progress
if cs.Message != "" {
msg := cs.Message
t.StatusMessage = &msg
}
}
}
return t, nil
}
// List 返回用户最近的研究任务。
func (s *Service) List(ctx context.Context, userID string) ([]Task, error) {
return s.repo.List(ctx, userID, 50)
}
// Cancel 取消进行中的任务;任务不存在/不可取消时返回 ErrNotFound。
func (s *Service) Cancel(ctx context.Context, userID, taskID string) error {
found, err := s.repo.Cancel(ctx, userID, taskID)
if err != nil {
return err
}
if !found {
return ErrNotFound
}
return nil
}