- Implement atomic state transitions: add EntryService.PatchEntry() with EntryDiff computation and EventEntryTransition event emission. - Add persistent SQLite task queue (JobEngine): add jobs table with crash recovery, exponential backoff retries, concurrency control, and idempotency key deduplication. - Decompose monolithic HTTP handlers: split handlers.go into domain handlers (auth, feeds, entries, jobs, settings, rules, sse). - Implement real-time Server-Sent Events (/api/events/stream) for multi-tab/device state synchronization. - Fix SQLite concurrency: apply busy_timeout(10000) and journal_mode(WAL) via DSN query parameters to resolve SQLITE_BUSY (5). - Fix AI settings & prompt loading: unmarshal JSON from ai_settings, apply configured prompt templates, and enforce strict enabled/API key validations. - Fix undo share state: remove omitempty from ShareUUID to serialize null for state resets. - Fix middle list counter: compute true unread/total stats from DB and clean switcher button labels. - Adapt mobile UI for iOS: add viewport-fit=cover in index.html and calibrate safe-area insets for the bottom navigation and article reader toolbar. - Add architectural documentation and worklog in docs/009_worklog.md.
93 lines
2.6 KiB
Go
93 lines
2.6 KiB
Go
package ai
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"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"
|
|
)
|
|
|
|
// NewTriageHandler creates a worker action handler for AI autonomous triage decisions.
|
|
func NewTriageHandler(database *db.DB, client *ai.Client, pool *worker.Pool, log *zap.Logger) worker.ActionHandler {
|
|
return func(ctx context.Context, entry models.FeedEntry, params map[string]interface{}) (*worker.ActionResult, error) {
|
|
cfg, _ := LoadConfig(database)
|
|
if !cfg.Enabled {
|
|
log.Warn("AI triage skipped: AI features are disabled in Settings", zap.String("entry_id", entry.ID))
|
|
return nil, nil
|
|
}
|
|
if cfg.APIKey == "" && cfg.Provider != "ollama" {
|
|
log.Warn("AI triage action skipped: API key is not configured in Settings > AI & Integrations",
|
|
zap.String("entry_id", entry.ID),
|
|
)
|
|
return nil, nil
|
|
}
|
|
|
|
policyPrompt, _ := params["prompt"].(string)
|
|
var allowedActions []string
|
|
if rawAllowed, ok := params["allowed_actions"].([]interface{}); ok {
|
|
for _, a := range rawAllowed {
|
|
allowedActions = append(allowedActions, fmt.Sprintf("%v", a))
|
|
}
|
|
} else if strList, ok := params["allowed_actions"].([]string); ok {
|
|
allowedActions = strList
|
|
}
|
|
|
|
if len(allowedActions) == 0 {
|
|
allowedActions = []string{"mark_read", "mark_starred", "set_variant"}
|
|
}
|
|
|
|
decision, err := client.GenerateAgentDecision(ctx, cfg, policyPrompt, allowedActions, entry)
|
|
if err != nil {
|
|
log.Error("AI triage decision failed",
|
|
zap.String("entry_id", entry.ID),
|
|
zap.Error(err),
|
|
)
|
|
return nil, err
|
|
}
|
|
|
|
log.Info("AI Triage agent executed successfully",
|
|
zap.String("entry_id", entry.ID),
|
|
zap.String("reasoning", decision.Reasoning),
|
|
zap.Int("actions_approved", len(decision.Actions)),
|
|
)
|
|
|
|
res := &worker.ActionResult{}
|
|
for _, act := range decision.Actions {
|
|
switch act.Type {
|
|
case "mark_read":
|
|
trueVal := true
|
|
res.MarkRead = &trueVal
|
|
case "mark_starred":
|
|
trueVal := true
|
|
res.MarkStarred = &trueVal
|
|
case "set_variant":
|
|
vKey, _ := act.Params["variant_key"].(string)
|
|
if vKey == "" {
|
|
vKey, _ = act.Params["key"].(string)
|
|
}
|
|
vVal, _ := act.Params["value"].(string)
|
|
if vKey != "" {
|
|
v := models.EntryVariant{
|
|
EntryID: entry.ID,
|
|
VariantKey: vKey,
|
|
ContentType: "text",
|
|
TextValue: vVal,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
res.ResultVariant = &v
|
|
}
|
|
default:
|
|
pool.Dispatch(ctx, act, entry, 0)
|
|
}
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
}
|