c949204662
借鉴 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 应用迁移并烟测。
131 lines
3.5 KiB
Go
131 lines
3.5 KiB
Go
// 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
|
||
}
|