package actions import ( "context" "fmt" "strings" "time" "git.pengzhan.dev/haopengzhan/rsspipe/internal/actions/ai" "git.pengzhan.dev/haopengzhan/rsspipe/internal/actions/readability" "git.pengzhan.dev/haopengzhan/rsspipe/internal/actions/status" "git.pengzhan.dev/haopengzhan/rsspipe/internal/actions/variants" aiClientPkg "git.pengzhan.dev/haopengzhan/rsspipe/internal/ai" "git.pengzhan.dev/haopengzhan/rsspipe/internal/db" "git.pengzhan.dev/haopengzhan/rsspipe/internal/models" "git.pengzhan.dev/haopengzhan/rsspipe/internal/worker" "go.uber.org/zap" ) // RegisterDefaultHandlers registers legacy action handlers with worker.Pool func RegisterDefaultHandlers(pool *worker.Pool, database *db.DB, log *zap.Logger) { // 1. Readability Full-Text Scraper scraper := readability.NewScraper(15 * time.Second) pool.RegisterHandler("readability_scrape", scraper.Handler()) // 2. Status Handlers (mark_read, mark_starred) pool.RegisterHandler("mark_read", status.NewMarkReadHandler()) pool.RegisterHandler("mark_starred", status.NewMarkStarredHandler()) // 3. Variant Handlers (set_variant with tag merging) pool.RegisterHandler("set_variant", variants.NewSetVariantHandler(database)) // 4. AI Subsystem Handlers aiClient := aiClientPkg.NewClient(45 * time.Second) enrichHandler := ai.NewEnrichmentHandler(database, aiClient, log) pool.RegisterHandler("ai_enrich", enrichHandler) pool.RegisterHandler("ai_summary", enrichHandler) // Backward compatibility alias pool.RegisterHandler("ai_agent_action", ai.NewTriageHandler(database, aiClient, pool, log)) } // RegisterJobEngineHandlers registers async task handlers with the persistent JobEngine func RegisterJobEngineHandlers(engine *worker.JobEngine, database *db.DB, log *zap.Logger) { scraper := readability.NewScraper(20 * time.Second) aiClient := aiClientPkg.NewClient(60 * time.Second) // 1. Readability Scrape Handler engine.RegisterHandler("readability_scrape", func(ctx context.Context, job models.Job) (*models.PatchEntryRequest, map[string]interface{}, error) { entryID, _ := job.Payload["entry_id"].(string) if entryID == "" { return nil, nil, fmt.Errorf("missing entry_id in job payload") } entry, err := database.GetEntry(entryID) if err != nil { return nil, nil, fmt.Errorf("entry not found: %w", err) } res, err := scraper.Handler()(ctx, *entry, job.Payload) if err != nil { return nil, nil, err } if res == nil || res.ResultVariant == nil { return nil, nil, nil } patch := &models.PatchEntryRequest{ Variants: map[string]*models.EntryVariant{ "readability_full_text": res.ResultVariant, }, } return patch, map[string]interface{}{ "variant_key": "readability_full_text", "length": len(res.ResultVariant.TextValue), }, nil }) // 2. AI Summary & AI Enrich Handler aiHandler := func(ctx context.Context, job models.Job) (*models.PatchEntryRequest, map[string]interface{}, error) { entryID, _ := job.Payload["entry_id"].(string) if entryID == "" { return nil, nil, fmt.Errorf("missing entry_id in job payload") } entry, err := database.GetEntry(entryID) if err != nil { return nil, nil, fmt.Errorf("entry not found: %w", err) } cfg, prompts := ai.LoadConfig(database) if !cfg.Enabled { return nil, nil, fmt.Errorf("AI features are disabled. Please enable AI in Settings > AI & Integrations") } if cfg.Provider != "ollama" && strings.TrimSpace(cfg.APIKey) == "" { return nil, nil, fmt.Errorf("AI API key is missing. Please configure an API key in Settings > AI & Integrations") } prompt, _ := job.Payload["prompt"].(string) if prompt == "" { prompt, _ = job.Payload["custom_prompt"].(string) } if prompt == "" { for _, p := range prompts { if p.IsDefault { prompt = p.Prompt break } } if prompt == "" && len(prompts) > 0 { prompt = prompts[0].Prompt } } if prompt == "" { prompt = "You are an expert technical editor. Summarize the following article in exactly 3 concise bullet points, followed by a bold 'Key Takeaway:' sentence.\n\nTitle: {{title}}\nFeed: {{feed_title}}\nContent:\n{{content}}" } vKey, _ := job.Payload["variant_key"].(string) if vKey == "" { vKey = "ai_summary" } cType, _ := job.Payload["content_type"].(string) if cType == "" { cType = "text" } resultText, err := aiClient.GenerateEnrichment(ctx, cfg, prompt, *entry) if err != nil { return nil, nil, fmt.Errorf("ai generation failed: %w", err) } variant := models.EntryVariant{ EntryID: entry.ID, VariantKey: vKey, ContentType: cType, TextValue: resultText, Metadata: map[string]interface{}{ "model": cfg.Model, "provider": cfg.Provider, "prompt_used": prompt, }, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } patch := &models.PatchEntryRequest{ Variants: map[string]*models.EntryVariant{ vKey: &variant, }, } return patch, map[string]interface{}{ "variant_key": vKey, "model": cfg.Model, "word_count": len(strings.Fields(resultText)), }, nil } engine.RegisterHandler("ai_summary", aiHandler) engine.RegisterHandler("ai_enrich", aiHandler) // 3. Add Tags Handler engine.RegisterHandler("add_tags", func(ctx context.Context, job models.Job) (*models.PatchEntryRequest, map[string]interface{}, error) { entryID, _ := job.Payload["entry_id"].(string) if entryID == "" { return nil, nil, fmt.Errorf("missing entry_id in job payload") } var tagsToAdd []string if rawTags, ok := job.Payload["tags"].([]interface{}); ok { for _, t := range rawTags { tagsToAdd = append(tagsToAdd, fmt.Sprintf("%v", t)) } } else if rawTagsStr, ok := job.Payload["tags"].(string); ok { for _, t := range strings.Split(rawTagsStr, ",") { if tr := strings.TrimSpace(t); tr != "" { tagsToAdd = append(tagsToAdd, tr) } } } entry, err := database.GetEntry(entryID) if err != nil { return nil, nil, fmt.Errorf("entry not found: %w", err) } existingTags := make(map[string]bool) if entry.Metadata != nil { if curr, ok := entry.Metadata["tags"].([]interface{}); ok { for _, t := range curr { existingTags[fmt.Sprintf("%v", t)] = true } } } for _, t := range tagsToAdd { existingTags[t] = true } var merged []string for t := range existingTags { merged = append(merged, t) } patch := &models.PatchEntryRequest{ Metadata: map[string]interface{}{ "tags": merged, }, } return patch, map[string]interface{}{"tags": merged}, nil }) // 4. Mark Read Handler engine.RegisterHandler("mark_read", func(ctx context.Context, job models.Job) (*models.PatchEntryRequest, map[string]interface{}, error) { read := true return &models.PatchEntryRequest{IsRead: &read}, map[string]interface{}{"is_read": true}, nil }) // 5. Mark Starred Handler engine.RegisterHandler("mark_starred", func(ctx context.Context, job models.Job) (*models.PatchEntryRequest, map[string]interface{}, error) { starred := true return &models.PatchEntryRequest{IsStarred: &starred}, map[string]interface{}{"is_starred": true}, nil }) }